/**
 * Selection.js
 *
 * Copyright 2009, Moxiecode Systems AB
 * Released under LGPL License.
 *
 * License: http://tinymce.moxiecode.com/license
 * Contributing: http://tinymce.moxiecode.com/contributing
 */

(function(tinymce) {
	function trimNl(s) {
		return s.replace(/[\n\r]+/g, '');
	};

	// Shorten names
	var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each;

	/**
	 * This class handles text and control selection it's an crossbrowser utility class.
	 * Consult the TinyMCE Wiki API for more details and examples on how to use this class.
	 * @class tinymce.dom.Selection
	 */
	tinymce.create('tinymce.dom.Selection', {
		/**
		 * Constructs a new selection instance.
		 *
		 * @constructor
		 * @method Selection
		 * @param {tinymce.dom.DOMUtils} dom DOMUtils object reference.
		 * @param {Window} win Window to bind the selection object to.
		 * @param {tinymce.dom.Serializer} serializer DOM serialization class to use for getContent.
		 */
		Selection : function(dom, win, serializer) {
			var t = this;

			t.dom = dom;
			t.win = win;
			t.serializer = serializer;

			// Add events
			each([
				'onBeforeSetContent',
				'onBeforeGetContent',
				'onSetContent',
				'onGetContent'
			], function(e) {
				t[e] = new tinymce.util.Dispatcher(t);
			});

			// No W3C Range support
			if (!t.win.getSelection)
				t.tridentSel = new tinymce.dom.TridentSelection(t);

			// Prevent leaks
			tinymce.addUnload(t.destroy, t);
		},

		/**
		 * Returns the selected contents using the DOM serializer passed in to this class.
		 *
		 * @method getContent
		 * @param {Object} s Optional settings class with for example output format text or html.
		 * @return {String} Selected contents in for example HTML format.
		 */
		getContent : function(s) {
			var t = this, r = t.getRng(), e = t.dom.create("body"), se = t.getSel(), wb, wa, n;

			s = s || {};
			wb = wa = '';
			s.get = true;
			s.format = s.format || 'html';
			t.onBeforeGetContent.dispatch(t, s);

			if (s.format == 'text')
				return t.isCollapsed() ? '' : (r.text || (se.toString ? se.toString() : ''));

			if (r.cloneContents) {
				n = r.cloneContents();

				if (n)
					e.appendChild(n);
			} else if (is(r.item) || is(r.htmlText))
				e.innerHTML = r.item ? r.item(0).outerHTML : r.htmlText;
			else
				e.innerHTML = r.toString();

			// Keep whitespace before and after
			if (/^\s/.test(e.innerHTML))
				wb = ' ';

			if (/\s+$/.test(e.innerHTML))
				wa = ' ';

			s.getInner = true;

			s.content = t.isCollapsed() ? '' : wb + t.serializer.serialize(e, s) + wa;
			t.onGetContent.dispatch(t, s);

			return s.content;
		},

		/**
		 * Sets the current selection to the specified content. If any contents is selected it will be replaced
		 * with the contents passed in to this function. If there is no selection the contents will be inserted
		 * where the caret is placed in the editor/page.
		 *
		 * @method setContent
		 * @param {String} h HTML contents to set could also be other formats depending on settings.
		 * @param {Object} s Optional settings object with for example data format.
		 */
		setContent : function(h, s) {
			var t = this, r = t.getRng(), c, d = t.win.document;

			s = s || {format : 'html'};
			s.set = true;
			h = s.content = t.dom.processHTML(h);

			// Dispatch before set content event
			t.onBeforeSetContent.dispatch(t, s);
			h = s.content;

			if (r.insertNode) {
				// Make caret marker since insertNode places the caret in the beginning of text after insert
				h += '<span id="__caret">_</span>';

				// Delete and insert new node
				
				if (r.startContainer == d && r.endContainer ==  d) {
					// WebKit will fail if the body is empty since the range is then invalid and it can't insert contents
					d.body.innerHTML = h;
				} else {
					r.deleteContents();
					if (d.body.childNodes.length == 0) {
						d.body.innerHTML = h;
					} else {
						r.insertNode(r.createContextualFragment(h));
					}
				}

				// Move to caret marker
				c = t.dom.get('__caret');
				// Make sure we wrap it compleatly, Opera fails with a simple select call
				r = d.createRange();
				r.setStartBefore(c);
				r.setEndBefore(c);
				t.setRng(r);

				// Remove the caret position
				t.dom.remove('__caret');
			} else {
				if (r.item) {
					// Delete content and get caret text selection
					d.execCommand('Delete', false, null);
					r = t.getRng();
				}

				r.pasteHTML(h);
			}

			// Dispatch set content event
			t.onSetContent.dispatch(t, s);
		},

		/**
		 * Returns the start element of a selection range. If the start is in a text
		 * node the parent element will be returned.
		 *
		 * @method getStart
		 * @return {Element} Start element of selection range.
		 */
		getStart : function() {
			var rng = this.getRng(), startElement, parentElement, checkRng, node;

			if (rng.duplicate || rng.item) {
				// Control selection, return first item
				if (rng.item)
					return rng.item(0);

				// Get start element
				checkRng = rng.duplicate();
				checkRng.collapse(1);
				startElement = checkRng.parentElement();

				// Check if range parent is inside the start element, then return the inner parent element
				// This will fix issues when a single element is selected, IE would otherwise return the wrong start element
				parentElement = node = rng.parentElement();
				while (node = node.parentNode) {
					if (node == startElement) {
						startElement = parentElement;
						break;
					}
				}

				// If start element is body element try to move to the first child if it exists
				if (startElement && startElement.nodeName == 'BODY')
					return startElement.firstChild || startElement;

				return startElement;
			} else {
				startElement = rng.startContainer;

				if (startElement.nodeType == 1 && startElement.hasChildNodes())
					startElement = startElement.childNodes[Math.min(startElement.childNodes.length - 1, rng.startOffset)];

				if (startElement && startElement.nodeType == 3)
					return startElement.parentNode;

				return startElement;
			}
		},

		/**
		 * Returns the end element of a selection range. If the end is in a text
		 * node the parent element will be returned.
		 *
		 * @method getEnd
		 * @return {Element} End element of selection range.
		 */
		getEnd : function() {
			var t = this, r = t.getRng(), e, eo;

			if (r.duplicate || r.item) {
				if (r.item)
					return r.item(0);

				r = r.duplicate();
				r.collapse(0);
				e = r.parentElement();

				if (e && e.nodeName == 'BODY')
					return e.lastChild || e;

				return e;
			} else {
				e = r.endContainer;
				eo = r.endOffset;

				if (e.nodeType == 1 && e.hasChildNodes())
					e = e.childNodes[eo > 0 ? eo - 1 : eo];

				if (e && e.nodeType == 3)
					return e.parentNode;

				return e;
			}
		},

		/**
		 * Returns a bookmark location for the current selection. This bookmark object
		 * can then be used to restore the selection after some content modification to the document.
		 *
		 * @method getBookmark
		 * @param {Number} type Optional state if the bookmark should be simple or not. Default is complex.
		 * @param {Boolean} normalized Optional state that enables you to get a position that it would be after normalization.
		 * @return {Object} Bookmark object, use moveToBookmark with this object to restore the selection.
		 */
		getBookmark : function(type, normalized) {
			var t = this, dom = t.dom, rng, rng2, id, collapsed, name, element, index, chr = '\uFEFF', styles;

			function findIndex(name, element) {
				var index = 0;

				each(dom.select(name), function(node, i) {
					if (node == element)
						index = i;
				});

				return index;
			};

			if (type == 2) {
				function getLocation() {
					var rng = t.getRng(true), root = dom.getRoot(), bookmark = {};

					function getPoint(rng, start) {
						var container = rng[start ? 'startContainer' : 'endContainer'],
							offset = rng[start ? 'startOffset' : 'endOffset'], point = [], node, childNodes, after = 0;

						if (container.nodeType == 3) {
							if (normalized) {
								for (node = container.previousSibling; node && node.nodeType == 3; node = node.previousSibling)
									offset += node.nodeValue.length;
							}

							point.push(offset);
						} else {
							childNodes = container.childNodes;

							if (offset >= childNodes.length && childNodes.length) {
								after = 1;
								offset = Math.max(0, childNodes.length - 1);
							}

							point.push(t.dom.nodeIndex(childNodes[offset], normalized) + after);
						}

						for (; container && container != root; container = container.parentNode)
							point.push(t.dom.nodeIndex(container, normalized));

						return point;
					};

					bookmark.start = getPoint(rng, true);

					if (!t.isCollapsed())
						bookmark.end = getPoint(rng);

					return bookmark;
				};

				return getLocation();
			}

			// Handle simple range
			if (type)
				return {rng : t.getRng()};

			rng = t.getRng();
			id = dom.uniqueId();
			collapsed = tinyMCE.activeEditor.selection.isCollapsed();
			styles = 'overflow:hidden;line-height:0px';

			// Explorer method
			if (rng.duplicate || rng.item) {
				// Text selection
				if (!rng.item) {
					rng2 = rng.duplicate();

					// Insert start marker
					rng.collapse();
					rng.pasteHTML('<span _mce_type="bookmark" id="' + id + '_start" style="' + styles + '">' + chr + '</span>');

					// Insert end marker
					if (!collapsed) {
						rng2.collapse(false);
						rng2.pasteHTML('<span _mce_type="bookmark" id="' + id + '_end" style="' + styles + '">' + chr + '</span>');
					}
				} else {
					// Control selection
					element = rng.item(0);
					name = element.nodeName;

					return {name : name, index : findIndex(name, element)};
				}
			} else {
				element = t.getNode();
				name = element.nodeName;
				if (name == 'IMG')
					return {name : name, index : findIndex(name, element)};

				// W3C method
				rng2 = rng.cloneRange();

				// Insert end marker
				if (!collapsed) {
					rng2.collapse(false);
					rng2.insertNode(dom.create('span', {_mce_type : "bookmark", id : id + '_end', style : styles}, chr));
				}

				rng.collapse(true);
				rng.insertNode(dom.create('span', {_mce_type : "bookmark", id : id + '_start', style : styles}, chr));
			}

			t.moveToBookmark({id : id, keep : 1});

			return {id : id};
		},

		/**
		 * Restores the selection to the specified bookmark.
		 *
		 * @method moveToBookmark
		 * @param {Object} bookmark Bookmark to restore selection from.
		 * @return {Boolean} true/false if it was successful or not.
		 */
		moveToBookmark : function(bookmark) {
			var t = this, dom = t.dom, marker1, marker2, rng, root, startContainer, endContainer, startOffset, endOffset;

			// Clear selection cache
			if (t.tridentSel)
				t.tridentSel.destroy();

			if (bookmark) {
				if (bookmark.start) {
					rng = dom.createRng();
					root = dom.getRoot();

					function setEndPoint(start) {
						var point = bookmark[start ? 'start' : 'end'], i, node, offset, children;

						if (point) {
							// Find container node
							for (node = root, i = point.length - 1; i >= 1; i--) {
								children = node.childNodes;

								if (children.length)
									node = children[point[i]];
							}

							// Set offset within container node
							if (start)
								rng.setStart(node, point[0]);
							else
								rng.setEnd(node, point[0]);
						}
					};

					setEndPoint(true);
					setEndPoint();

					t.setRng(rng);
				} else if (bookmark.id) {
					function restoreEndPoint(suffix) {
						var marker = dom.get(bookmark.id + '_' + suffix), node, idx, next, prev, keep = bookmark.keep;

						if (marker) {
							node = marker.parentNode;

							if (suffix == 'start') {
								if (!keep) {
									idx = dom.nodeIndex(marker);
								} else {
									node = marker.firstChild;
									idx = 1;
								}

								startContainer = endContainer = node;
								startOffset = endOffset = idx;
							} else {
								if (!keep) {
									idx = dom.nodeIndex(marker);
								} else {
									node = marker.firstChild;
									idx = 1;
								}

								endContainer = node;
								endOffset = idx;
							}

							if (!keep) {
								prev = marker.previousSibling;
								next = marker.nextSibling;

								// Remove all marker text nodes
								each(tinymce.grep(marker.childNodes), function(node) {
									if (node.nodeType == 3)
										node.nodeValue = node.nodeValue.replace(/\uFEFF/g, '');
								});

								// Remove marker but keep children if for example contents where inserted into the marker
								// Also remove duplicated instances of the marker for example by a split operation or by WebKit auto split on paste feature
								while (marker = dom.get(bookmark.id + '_' + suffix))
									dom.remove(marker, 1);

								// If siblings are text nodes then merge them
								if (prev && next && prev.nodeType == next.nodeType && prev.nodeType == 3) {
									idx = prev.nodeValue.length;
									prev.appendData(next.nodeValue);
									dom.remove(next);

									if (suffix == 'start') {
										startContainer = endContainer = prev;
										startOffset = endOffset = idx;
									} else {
										endContainer = prev;
										endOffset = idx;
									}
								}
							}
						}
					};

					function addBogus(node) {
						// Adds a bogus BR element for empty block elements
						// on non IE browsers just to have a place to put the caret
						if (!isIE && dom.isBlock(node) && !node.innerHTML)
							node.innerHTML = '<br _mce_bogus="1" />';

						return node;
					};

					// Restore start/end points
					restoreEndPoint('start');
					restoreEndPoint('end');

					rng = dom.createRng();
					rng.setStart(addBogus(startContainer), startOffset);
					rng.setEnd(addBogus(endContainer), endOffset);
					t.setRng(rng);
				} else if (bookmark.name) {
					t.select(dom.select(bookmark.name)[bookmark.index]);
				} else if (bookmark.rng)
					t.setRng(bookmark.rng);
			}
		},

		/**
		 * Selects the specified element. This will place the start and end of the selection range around the element.
		 *
		 * @method select
		 * @param {Element} node HMTL DOM element to select.
		 * @param {Boolean} content Optional bool state if the contents should be selected or not on non IE browser.
		 * @return {Element} Selected element the same element as the one that got passed in.
		 */
		select : function(node, content) {
			var t = this, dom = t.dom, rng = dom.createRng(), idx;

			idx = dom.nodeIndex(node);
			rng.setStart(node.parentNode, idx);
			rng.setEnd(node.parentNode, idx + 1);

			// Find first/last text node or BR element
			if (content) {
				function setPoint(node, start) {
					var walker = new tinymce.dom.TreeWalker(node, node);

					do {
						// Text node
						if (node.nodeType == 3 && tinymce.trim(node.nodeValue).length != 0) {
							if (start)
								rng.setStart(node, 0);
							else
								rng.setEnd(node, node.nodeValue.length);

							return;
						}

						// BR element
						if (node.nodeName == 'BR') {
							if (start)
								rng.setStartBefore(node);
							else
								rng.setEndBefore(node);

							return;
						}
					} while (node = (start ? walker.next() : walker.prev()));
				};

				setPoint(node, 1);
				setPoint(node);
			}

			t.setRng(rng);

			return node;
		},

		/**
		 * Returns true/false if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
		 *
		 * @method isCollapsed
		 * @return {Boolean} true/false state if the selection range is collapsed or not. Collapsed means if it's a caret or a larger selection.
		 */
		isCollapsed : function() {
			var t = this, r = t.getRng(), s = t.getSel();

			if (!r || r.item)
				return false;

			if (r.compareEndPoints)
				return r.compareEndPoints('StartToEnd', r) === 0;

			return !s || r.collapsed;
		},

		/**
		 * Collapse the selection to start or end of range.
		 *
		 * @method collapse
		 * @param {Boolean} b Optional boolean state if to collapse to end or not. Defaults to start.
		 */
		collapse : function(b) {
			var t = this, r = t.getRng(), n;

			// Control range on IE
			if (r.item) {
				n = r.item(0);
				r = this.win.document.body.createTextRange();
				r.moveToElementText(n);
			}

			r.collapse(!!b);
			t.setRng(r);
		},

		/**
		 * Returns the browsers internal selection object.
		 *
		 * @method getSel
		 * @return {Selection} Internal browser selection object.
		 */
		getSel : function() {
			var t = this, w = this.win;

			return w.getSelection ? w.getSelection() : w.document.selection;
		},

		/**
		 * Returns the browsers internal range object.
		 *
		 * @method getRng
		 * @param {Boolean} w3c Forces a compatible W3C range on IE.
		 * @return {Range} Internal browser range object.
		 */
		getRng : function(w3c) {
			var t = this, s, r;

			// Found tridentSel object then we need to use that one
			if (w3c && t.tridentSel)
				return t.tridentSel.getRangeAt(0);

			try {
				if (s = t.getSel())
					r = s.rangeCount > 0 ? s.getRangeAt(0) : (s.createRange ? s.createRange() : t.win.document.createRange());
			} catch (ex) {
				// IE throws unspecified error here if TinyMCE is placed in a frame/iframe
			}

			// No range found then create an empty one
			// This can occur when the editor is placed in a hidden container element on Gecko
			// Or on IE when there was an exception
			if (!r)
				r = t.win.document.createRange ? t.win.document.createRange() : t.win.document.body.createTextRange();

			if (t.selectedRange && t.explicitRange) {
				if (r.compareBoundaryPoints(r.START_TO_START, t.selectedRange) === 0 && r.compareBoundaryPoints(r.END_TO_END, t.selectedRange) === 0) {
					// Safari, Opera and Chrome only ever select text which causes the range to change.
					// This lets us use the originally set range if the selection hasn't been changed by the user.
					r = t.explicitRange;
				} else {
					t.selectedRange = null;
					t.explicitRange = null;
				}
			}
			return r;
		},

		/**
		 * Changes the selection to the specified DOM range.
		 *
		 * @method setRng
		 * @param {Range} r Range to select.
		 */
		setRng : function(r) {
			var s, t = this;
			
			if (!t.tridentSel) {
				s = t.getSel();

				if (s) {
					t.explicitRange = r;
					s.removeAllRanges();
					s.addRange(r);
					t.selectedRange = s.getRangeAt(0);
				}
			} else {
				// Is W3C Range
				if (r.cloneRange) {
					t.tridentSel.addRange(r);
					return;
				}

				// Is IE specific range
				try {
					r.select();
				} catch (ex) {
					// Needed for some odd IE bug #1843306
				}
			}
		},

		/**
		 * Sets the current selection to the specified DOM element.
		 *
		 * @method setNode
		 * @param {Element} n Element to set as the contents of the selection.
		 * @return {Element} Returns the element that got passed in.
		 */
		setNode : function(n) {
			var t = this;

			t.setContent(t.dom.getOuterHTML(n));

			return n;
		},

		/**
		 * Returns the currently selected element or the common ancestor element for both start and end of the selection.
		 *
		 * @method getNode
		 * @return {Element} Currently selected element or common ancestor element.
		 */
		getNode : function() {
			var t = this, rng = t.getRng(), sel = t.getSel(), elm;

			if (rng.setStart) {
				// Range maybe lost after the editor is made visible again
				if (!rng)
					return t.dom.getRoot();

				elm = rng.commonAncestorContainer;

				// Handle selection a image or other control like element such as anchors
				if (!rng.collapsed) {
					if (rng.startContainer == rng.endContainer) {
						if (rng.startOffset - rng.endOffset < 2) {
							if (rng.startContainer.hasChildNodes())
								elm = rng.startContainer.childNodes[rng.startOffset];
						}
					}

					// If the anchor node is a element instead of a text node then return this element
					if (tinymce.isWebKit && sel.anchorNode && sel.anchorNode.nodeType == 1) 
						return sel.anchorNode.childNodes[sel.anchorOffset]; 
				}

				if (elm && elm.nodeType == 3)
					return elm.parentNode;

				return elm;
			}

			return rng.item ? rng.item(0) : rng.parentElement();
		},

		getSelectedBlocks : function(st, en) {
			var t = this, dom = t.dom, sb, eb, n, bl = [];

			sb = dom.getParent(st || t.getStart(), dom.isBlock);
			eb = dom.getParent(en || t.getEnd(), dom.isBlock);

			if (sb)
				bl.push(sb);

			if (sb && eb && sb != eb) {
				n = sb;

				while ((n = n.nextSibling) && n != eb) {
					if (dom.isBlock(n))
						bl.push(n);
				}
			}

			if (eb && sb != eb)
				bl.push(eb);

			return bl;
		},

		destroy : function(s) {
			var t = this;

			t.win = null;

			if (t.tridentSel)
				t.tridentSel.destroy();

			// Manual destroy then remove unload handler
			if (!s)
				tinymce.removeUnload(t.destroy);
		}
	});
})(tinymce);
