/**
 * plugin.js
 *
 * Released under LGPL License.
 * Copyright (c) 1999-2015 Ephox Corp. All rights reserved
 *
 * License: http://www.tinymce.com/license
 * Contributing: http://www.tinymce.com/contributing
 */

/*jshint smarttabs:true, undef:true, unused:true, latedef:true, curly:true, bitwise:true */
/*eslint no-labels:0, no-constant-condition: 0 */
/*global tinymce:true */

(function() {
	function isContentEditableFalse(node) {
		return node && node.nodeType == 1 && node.contentEditable === "false";
	}

	// Based on work developed by: James Padolsey http://james.padolsey.com
	// released under UNLICENSE that is compatible with LGPL
	// TODO: Handle contentEditable edgecase:
	// <p>text<span contentEditable="false">text<span contentEditable="true">text</span>text</span>text</p>
	function findAndReplaceDOMText(regex, node, replacementNode, captureGroup, schema) {
		var m, matches = [], text, count = 0, doc;
		var blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap;

		doc = node.ownerDocument;
		blockElementsMap = schema.getBlockElements(); // H1-H6, P, TD etc
		hiddenTextElementsMap = schema.getWhiteSpaceElements(); // TEXTAREA, PRE, STYLE, SCRIPT
		shortEndedElementsMap = schema.getShortEndedElements(); // BR, IMG, INPUT

		function getMatchIndexes(m, captureGroup) {
			captureGroup = captureGroup || 0;

			if (!m[0]) {
				throw 'findAndReplaceDOMText cannot handle zero-length matches';
			}

			var index = m.index;

			if (captureGroup > 0) {
				var cg = m[captureGroup];

				if (!cg) {
					throw 'Invalid capture group';
				}

				index += m[0].indexOf(cg);
				m[0] = cg;
			}

			return [index, index + m[0].length, [m[0]]];
		}

		function getText(node) {
			var txt;

			if (node.nodeType === 3) {
				return node.data;
			}

			if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) {
				return '';
			}

			txt = '';

			if (isContentEditableFalse(node)) {
				return '\n';
			}

			if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) {
				txt += '\n';
			}

			if ((node = node.firstChild)) {
				do {
					txt += getText(node);
				} while ((node = node.nextSibling));
			}

			return txt;
		}

		function stepThroughMatches(node, matches, replaceFn) {
			var startNode, endNode, startNodeIndex,
				endNodeIndex, innerNodes = [], atIndex = 0, curNode = node,
				matchLocation = matches.shift(), matchIndex = 0;

			out: while (true) {
				if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName] || isContentEditableFalse(curNode)) {
					atIndex++;
				}

				if (curNode.nodeType === 3) {
					if (!endNode && curNode.length + atIndex >= matchLocation[1]) {
						// We've found the ending
						endNode = curNode;
						endNodeIndex = matchLocation[1] - atIndex;
					} else if (startNode) {
						// Intersecting node
						innerNodes.push(curNode);
					}

					if (!startNode && curNode.length + atIndex > matchLocation[0]) {
						// We've found the match start
						startNode = curNode;
						startNodeIndex = matchLocation[0] - atIndex;
					}

					atIndex += curNode.length;
				}

				if (startNode && endNode) {
					curNode = replaceFn({
						startNode: startNode,
						startNodeIndex: startNodeIndex,
						endNode: endNode,
						endNodeIndex: endNodeIndex,
						innerNodes: innerNodes,
						match: matchLocation[2],
						matchIndex: matchIndex
					});

					// replaceFn has to return the node that replaced the endNode
					// and then we step back so we can continue from the end of the
					// match:
					atIndex -= (endNode.length - endNodeIndex);
					startNode = null;
					endNode = null;
					innerNodes = [];
					matchLocation = matches.shift();
					matchIndex++;

					if (!matchLocation) {
						break; // no more matches
					}
				} else if ((!hiddenTextElementsMap[curNode.nodeName] || blockElementsMap[curNode.nodeName]) && curNode.firstChild) {
					if (!isContentEditableFalse(curNode)) {
						// Move down
						curNode = curNode.firstChild;
						continue;
					}
				} else if (curNode.nextSibling) {
					// Move forward:
					curNode = curNode.nextSibling;
					continue;
				}

				// Move forward or up:
				while (true) {
					if (curNode.nextSibling) {
						curNode = curNode.nextSibling;
						break;
					} else if (curNode.parentNode !== node) {
						curNode = curNode.parentNode;
					} else {
						break out;
					}
				}
			}
		}

		/**
		* Generates the actual replaceFn which splits up text nodes
		* and inserts the replacement element.
		*/
		function genReplacer(nodeName) {
			var makeReplacementNode;

			if (typeof nodeName != 'function') {
				var stencilNode = nodeName.nodeType ? nodeName : doc.createElement(nodeName);

				makeReplacementNode = function(fill, matchIndex) {
					var clone = stencilNode.cloneNode(false);

					clone.setAttribute('data-mce-index', matchIndex);

					if (fill) {
						clone.appendChild(doc.createTextNode(fill));
					}

					return clone;
				};
			} else {
				makeReplacementNode = nodeName;
			}

			return function(range) {
				var before, after, parentNode, startNode = range.startNode,
					endNode = range.endNode, matchIndex = range.matchIndex;

				if (startNode === endNode) {
					var node = startNode;

					parentNode = node.parentNode;
					if (range.startNodeIndex > 0) {
						// Add `before` text node (before the match)
						before = doc.createTextNode(node.data.substring(0, range.startNodeIndex));
						parentNode.insertBefore(before, node);
					}

					// Create the replacement node:
					var el = makeReplacementNode(range.match[0], matchIndex);
					parentNode.insertBefore(el, node);
					if (range.endNodeIndex < node.length) {
						// Add `after` text node (after the match)
						after = doc.createTextNode(node.data.substring(range.endNodeIndex));
						parentNode.insertBefore(after, node);
					}

					node.parentNode.removeChild(node);

					return el;
				}

				// Replace startNode -> [innerNodes...] -> endNode (in that order)
				before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex));
				after = doc.createTextNode(endNode.data.substring(range.endNodeIndex));
				var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex);
				var innerEls = [];

				for (var i = 0, l = range.innerNodes.length; i < l; ++i) {
					var innerNode = range.innerNodes[i];
					var innerEl = makeReplacementNode(innerNode.data, matchIndex);
					innerNode.parentNode.replaceChild(innerEl, innerNode);
					innerEls.push(innerEl);
				}

				var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex);

				parentNode = startNode.parentNode;
				parentNode.insertBefore(before, startNode);
				parentNode.insertBefore(elA, startNode);
				parentNode.removeChild(startNode);

				parentNode = endNode.parentNode;
				parentNode.insertBefore(elB, endNode);
				parentNode.insertBefore(after, endNode);
				parentNode.removeChild(endNode);

				return elB;
			};
		}

		text = getText(node);
		if (!text) {
			return;
		}

		if (regex.global) {
			while ((m = regex.exec(text))) {
				matches.push(getMatchIndexes(m, captureGroup));
			}
		} else {
			m = text.match(regex);
			matches.push(getMatchIndexes(m, captureGroup));
		}

		if (matches.length) {
			count = matches.length;
			stepThroughMatches(node, matches, genReplacer(replacementNode));
		}

		return count;
	}

	function Plugin(editor) {
		var self = this, currentIndex = -1;

		function showDialog() {
			var last = {}, selectedText;

			selectedText = tinymce.trim(editor.selection.getContent({format: 'text'}));

			function updateButtonStates() {
				win.statusbar.find('#next').disabled(!findSpansByIndex(currentIndex + 1).length);
				win.statusbar.find('#prev').disabled(!findSpansByIndex(currentIndex - 1).length);
			}

			function notFoundAlert() {
				editor.windowManager.alert('Could not find the specified string.', function() {
					win.find('#find')[0].focus();
				});
			}

			var win = editor.windowManager.open({
				layout: "flex",
				pack: "center",
				align: "center",
				onClose: function() {
					editor.focus();
					self.done();
				},
				onSubmit: function(e) {
					var count, caseState, text, wholeWord;

					e.preventDefault();

					caseState = win.find('#case').checked();
					wholeWord = win.find('#words').checked();

					text = win.find('#find').value();
					if (!text.length) {
						self.done(false);
						win.statusbar.items().slice(1).disabled(true);
						return;
					}

					if (last.text == text && last.caseState == caseState && last.wholeWord == wholeWord) {
						if (findSpansByIndex(currentIndex + 1).length === 0) {
							notFoundAlert();
							return;
						}

						self.next();
						updateButtonStates();
						return;
					}

					count = self.find(text, caseState, wholeWord);
					if (!count) {
						notFoundAlert();
					}

					win.statusbar.items().slice(1).disabled(count === 0);
					updateButtonStates();

					last = {
						text: text,
						caseState: caseState,
						wholeWord: wholeWord
					};
				},
				buttons: [
					{text: "Find", subtype: 'primary', onclick: function() {
						win.submit();
					}},
					{text: "Replace", disabled: true, onclick: function() {
						if (!self.replace(win.find('#replace').value())) {
							win.statusbar.items().slice(1).disabled(true);
							currentIndex = -1;
							last = {};
						}
					}},
					{text: "Replace all", disabled: true, onclick: function() {
						self.replace(win.find('#replace').value(), true, true);
						win.statusbar.items().slice(1).disabled(true);
						last = {};
					}},
					{type: "spacer", flex: 1},
					{text: "Prev", name: 'prev', disabled: true, onclick: function() {
						self.prev();
						updateButtonStates();
					}},
					{text: "Next", name: 'next', disabled: true, onclick: function() {
						self.next();
						updateButtonStates();
					}}
				],
				title: "Find and replace",
				items: {
					type: "form",
					padding: 20,
					labelGap: 30,
					spacing: 10,
					items: [
						{type: 'textbox', name: 'find', size: 40, label: 'Find', value: selectedText},
						{type: 'textbox', name: 'replace', size: 40, label: 'Replace with'},
						{type: 'checkbox', name: 'case', text: 'Match case', label: ' '},
						{type: 'checkbox', name: 'words', text: 'Whole words', label: ' '}
					]
				}
			});
		}

		self.init = function(ed) {
			ed.addMenuItem('searchreplace', {
				text: 'Find and replace',
				shortcut: 'Meta+F',
				onclick: showDialog,
				separator: 'before',
				context: 'edit'
			});

			ed.addButton('searchreplace', {
				tooltip: 'Find and replace',
				shortcut: 'Meta+F',
				onclick: showDialog
			});

			ed.addCommand("SearchReplace", showDialog);
			ed.shortcuts.add('Meta+F', '', showDialog);
		};

		function getElmIndex(elm) {
			var value = elm.getAttribute('data-mce-index');

			if (typeof value == "number") {
				return "" + value;
			}

			return value;
		}

		function markAllMatches(regex) {
			var node, marker;

			marker = editor.dom.create('span', {
				"data-mce-bogus": 1
			});

			marker.className = 'mce-match-marker'; // IE 7 adds class="mce-match-marker" and class=mce-match-marker
			node = editor.getBody();

			self.done(false);

			return findAndReplaceDOMText(regex, node, marker, false, editor.schema);
		}

		function unwrap(node) {
			var parentNode = node.parentNode;

			if (node.firstChild) {
				parentNode.insertBefore(node.firstChild, node);
			}

			node.parentNode.removeChild(node);
		}

		function findSpansByIndex(index) {
			var nodes, spans = [];

			nodes = tinymce.toArray(editor.getBody().getElementsByTagName('span'));
			if (nodes.length) {
				for (var i = 0; i < nodes.length; i++) {
					var nodeIndex = getElmIndex(nodes[i]);

					if (nodeIndex === null || !nodeIndex.length) {
						continue;
					}

					if (nodeIndex === index.toString()) {
						spans.push(nodes[i]);
					}
				}
			}

			return spans;
		}

		function moveSelection(forward) {
			var testIndex = currentIndex, dom = editor.dom;

			forward = forward !== false;

			if (forward) {
				testIndex++;
			} else {
				testIndex--;
			}

			dom.removeClass(findSpansByIndex(currentIndex), 'mce-match-marker-selected');

			var spans = findSpansByIndex(testIndex);
			if (spans.length) {
				dom.addClass(findSpansByIndex(testIndex), 'mce-match-marker-selected');
				editor.selection.scrollIntoView(spans[0]);
				return testIndex;
			}

			return -1;
		}

		function removeNode(node) {
			var dom = editor.dom, parent = node.parentNode;

			dom.remove(node);

			if (dom.isEmpty(parent)) {
				dom.remove(parent);
			}
		}

		self.find = function(text, matchCase, wholeWord) {
			text = text.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
			text = wholeWord ? '\\b' + text + '\\b' : text;

			var count = markAllMatches(new RegExp(text, matchCase ? 'g' : 'gi'));

			if (count) {
				currentIndex = -1;
				currentIndex = moveSelection(true);
			}

			return count;
		};

		self.next = function() {
			var index = moveSelection(true);

			if (index !== -1) {
				currentIndex = index;
			}
		};

		self.prev = function() {
			var index = moveSelection(false);

			if (index !== -1) {
				currentIndex = index;
			}
		};

		function isMatchSpan(node) {
			var matchIndex = getElmIndex(node);

			return matchIndex !== null && matchIndex.length > 0;
		}

		self.replace = function(text, forward, all) {
			var i, nodes, node, matchIndex, currentMatchIndex, nextIndex = currentIndex, hasMore;

			forward = forward !== false;

			node = editor.getBody();
			nodes = tinymce.grep(tinymce.toArray(node.getElementsByTagName('span')), isMatchSpan);
			for (i = 0; i < nodes.length; i++) {
				var nodeIndex = getElmIndex(nodes[i]);

				matchIndex = currentMatchIndex = parseInt(nodeIndex, 10);
				if (all || matchIndex === currentIndex) {
					if (text.length) {
						nodes[i].firstChild.nodeValue = text;
						unwrap(nodes[i]);
					} else {
						removeNode(nodes[i]);
					}

					while (nodes[++i]) {
						matchIndex = parseInt(getElmIndex(nodes[i]), 10);

						if (matchIndex === currentMatchIndex) {
							removeNode(nodes[i]);
						} else {
							i--;
							break;
						}
					}

					if (forward) {
						nextIndex--;
					}
				} else if (currentMatchIndex > currentIndex) {
					nodes[i].setAttribute('data-mce-index', currentMatchIndex - 1);
				}
			}

			editor.undoManager.add();
			currentIndex = nextIndex;

			if (forward) {
				hasMore = findSpansByIndex(nextIndex + 1).length > 0;
				self.next();
			} else {
				hasMore = findSpansByIndex(nextIndex - 1).length > 0;
				self.prev();
			}

			return !all && hasMore;
		};

		self.done = function(keepEditorSelection) {
			var i, nodes, startContainer, endContainer;

			nodes = tinymce.toArray(editor.getBody().getElementsByTagName('span'));
			for (i = 0; i < nodes.length; i++) {
				var nodeIndex = getElmIndex(nodes[i]);

				if (nodeIndex !== null && nodeIndex.length) {
					if (nodeIndex === currentIndex.toString()) {
						if (!startContainer) {
							startContainer = nodes[i].firstChild;
						}

						endContainer = nodes[i].firstChild;
					}

					unwrap(nodes[i]);
				}
			}

			if (startContainer && endContainer) {
				var rng = editor.dom.createRng();
				rng.setStart(startContainer, 0);
				rng.setEnd(endContainer, endContainer.data.length);

				if (keepEditorSelection !== false) {
					editor.selection.setRng(rng);
				}

				return rng;
			}
		};
	}

	tinymce.PluginManager.add('searchreplace', Plugin);
})();
