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

(function() {
	function Selection(selection) {
		var t = this, invisibleChar = '\uFEFF', range, lastIERng, dom = selection.dom, TRUE = true, FALSE = false;

		// Returns a W3C DOM compatible range object by using the IE Range API
		function getRange() {
			var ieRange = selection.getRng(), domRange = dom.createRng(), element, collapsed;

			// If selection is outside the current document just return an empty range
			element = ieRange.item ? ieRange.item(0) : ieRange.parentElement();
			if (element.ownerDocument != dom.doc)
				return domRange;

			// Handle control selection or text selection of a image
			if (ieRange.item || !element.hasChildNodes()) {
				domRange.setStart(element.parentNode, dom.nodeIndex(element));
				domRange.setEnd(domRange.startContainer, domRange.startOffset + 1);

				return domRange;
			}

			collapsed = selection.isCollapsed();

			function findEndPoint(start) {
				var marker, container, offset, nodes, startIndex = 0, endIndex, index, parent, checkRng, position;

				// Setup temp range and collapse it
				checkRng = ieRange.duplicate();
				checkRng.collapse(start);

				// Create marker and insert it at the end of the endpoints parent
				marker = dom.create('a');
				parent = checkRng.parentElement();

				// If parent doesn't have any children then set the container to that parent and the index to 0
				if (!parent.hasChildNodes()) {
					domRange[start ? 'setStart' : 'setEnd'](parent, 0);
					return;
				}

				parent.appendChild(marker);
				checkRng.moveToElementText(marker);
				position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng);
				if (position > 0) {
					// The position is after the end of the parent element.
					// This is the case where IE puts the caret to the left edge of a table.
					domRange[start ? 'setStartAfter' : 'setEndAfter'](parent);
					dom.remove(marker);
					return;
				}

				// Setup node list and endIndex
				nodes = tinymce.grep(parent.childNodes);
				endIndex = nodes.length - 1;
				// Perform a binary search for the position
				while (startIndex <= endIndex) {
					index = Math.floor((startIndex + endIndex) / 2);

					// Insert marker and check it's position relative to the selection
					parent.insertBefore(marker, nodes[index]);
					checkRng.moveToElementText(marker);
					position = ieRange.compareEndPoints(start ? 'StartToStart' : 'EndToEnd', checkRng);
					if (position > 0) {
						// Marker is to the right
						startIndex = index + 1;
					} else if (position < 0) {
						// Marker is to the left
						endIndex = index - 1;
					} else {
						// Maker is where we are
						found = true;
						break;
					}
				}

				// Setup container
				container = position > 0 || index == 0 ? marker.nextSibling : marker.previousSibling;

				// Handle element selection
				if (container.nodeType == 1) {
					dom.remove(marker);

					// Find offset and container
					offset = dom.nodeIndex(container);
					container = container.parentNode;

					// Move the offset if we are setting the end or the position is after an element
					if (!start || index > 0)
						offset++;
				} else {
					// Calculate offset within text node
					if (position > 0 || index == 0) {
						checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange);
						offset = checkRng.text.length;
					} else {
						checkRng.setEndPoint(start ? 'StartToStart' : 'EndToEnd', ieRange);
						offset = container.nodeValue.length - checkRng.text.length;
					}

					dom.remove(marker);
				}

				domRange[start ? 'setStart' : 'setEnd'](container, offset);
			};

			// Find start point
			findEndPoint(true);

			// Find end point if needed
			if (!collapsed)
				findEndPoint();

			return domRange;
		};

		this.addRange = function(rng) {
			var ieRng, ieRng2, doc = selection.dom.doc, body = doc.body, startPos, endPos, sc, so, ec, eo, marker, lastIndex, skipStart, skipEnd;

			this.destroy();

			// Setup some shorter versions
			sc = rng.startContainer;
			so = rng.startOffset;
			ec = rng.endContainer;
			eo = rng.endOffset;
			ieRng = body.createTextRange();

			// If document selection move caret to first node in document
			if (sc == doc || ec == doc) {
				ieRng = body.createTextRange();
				ieRng.collapse();
				ieRng.select();
				return;
			}

			// If child index resolve it
			if (sc.nodeType == 1 && sc.hasChildNodes()) {
				lastIndex = sc.childNodes.length - 1;

				// Index is higher that the child count then we need to jump over the start container
				if (so > lastIndex) {
					skipStart = 1;
					sc = sc.childNodes[lastIndex];
				} else
					sc = sc.childNodes[so];

				// Child was text node then move offset to start of it
				if (sc.nodeType == 3)
					so = 0;
			}

			// If child index resolve it
			if (ec.nodeType == 1 && ec.hasChildNodes()) {
				lastIndex = ec.childNodes.length - 1;

				if (eo == 0) {
					skipEnd = 1;
					ec = ec.childNodes[0];
				} else {
					ec = ec.childNodes[Math.min(lastIndex, eo - 1)];

					// Child was text node then move offset to end of text node
					if (ec.nodeType == 3)
						eo = ec.nodeValue.length;
				}
			}

			// Single element selection
			if (sc == ec && sc.nodeType == 1) {
				// Make control selection for some elements
				if (/^(IMG|TABLE)$/.test(sc.nodeName) && so != eo) {
					ieRng = body.createControlRange();
					ieRng.addElement(sc);
				} else {
					ieRng = body.createTextRange();

					// Padd empty elements with invisible character
					if (!sc.hasChildNodes() && sc.canHaveHTML)
						sc.innerHTML = invisibleChar;

					// Select element contents
					ieRng.moveToElementText(sc);

					// If it's only containing a padding remove it so the caret remains
					if (sc.innerHTML == invisibleChar) {
						ieRng.collapse(TRUE);
						sc.removeChild(sc.firstChild);
					}
				}

				if (so == eo)
					ieRng.collapse(eo <= rng.endContainer.childNodes.length - 1);

				ieRng.select();
				ieRng.scrollIntoView();
				return;
			}

			// Create range and marker
			ieRng = body.createTextRange();
			marker = doc.createElement('span');
			marker.innerHTML = ' ';

			// Set start of range to startContainer/startOffset
			if (sc.nodeType == 3) {
				// Insert marker after/before startContainer
				if (skipStart)
					dom.insertAfter(marker, sc);
				else
					sc.parentNode.insertBefore(marker, sc);

				// Select marker the caret to offset position
				ieRng.moveToElementText(marker);
				marker.parentNode.removeChild(marker);

				// Move if we need to, moving it 0 characters actually moves it!
				if (so > 0)
					ieRng.move('character', so);
			} else {
				ieRng.moveToElementText(sc);

				if (skipStart)
					ieRng.collapse(FALSE);
			}

			// If same text container then we can do a more simple move
			if (sc == ec && sc.nodeType == 3) {
				try {
					ieRng.moveEnd('character', eo - so);
					ieRng.select();
					ieRng.scrollIntoView();
				} catch (ex) {
					// Some times a Runtime error of the 800a025e type gets thrown
					// especially when the caret is placed before a table.
					// This is a somewhat strange location for the caret.
					// TODO: Find a better solution for this would possible require a rewrite of the setRng method
				}

				return;
			}

			// Set end of range to endContainer/endOffset
			ieRng2 = body.createTextRange();
			if (ec.nodeType == 3) {
				// Insert marker after/before startContainer
				ec.parentNode.insertBefore(marker, ec);

				// Move selection to end marker and move caret to end offset
				ieRng2.moveToElementText(marker);
				marker.parentNode.removeChild(marker);
				ieRng2.move('character', eo);
				ieRng.setEndPoint('EndToStart', ieRng2);
			} else {
				ieRng2.moveToElementText(ec);
				ieRng2.collapse(!!skipEnd);
				ieRng.setEndPoint('EndToEnd', ieRng2);
			}

			ieRng.select();
			ieRng.scrollIntoView();
		};

		this.getRangeAt = function() {
			// Setup new range if the cache is empty
			if (!range || !tinymce.dom.RangeUtils.compareRanges(lastIERng, selection.getRng())) {
				range = getRange();

				// Store away text range for next call
				lastIERng = selection.getRng();
			}

			// IE will say that the range is equal then produce an invalid argument exception
			// if you perform specific operations in a keyup event. For example Ctrl+Del.
			// This hack will invalidate the range cache if the exception occurs
			try {
				range.startContainer.nextSibling;
			} catch (ex) {
				range = getRange();
				lastIERng = null;
			}

			// Return cached range
			return range;
		};

		this.destroy = function() {
			// Destroy cached range and last IE range to avoid memory leaks
			lastIERng = range = null;
		};

		// IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode
		if (selection.dom.boxModel) {
			(function() {
				var doc = dom.doc, body = doc.body, started, startRng;

				// Make HTML element unselectable since we are going to handle selection by hand
				doc.documentElement.unselectable = TRUE;

				// Return range from point or null if it failed
				function rngFromPoint(x, y) {
					var rng = body.createTextRange();

					try {
						rng.moveToPoint(x, y);
					} catch (ex) {
						// IE sometimes throws and exception, so lets just ignore it
						rng = null;
					}

					return rng;
				};

				// Fires while the selection is changing
				function selectionChange(e) {
					var pointRng;

					// Check if the button is down or not
					if (e.button) {
						// Create range from mouse position
						pointRng = rngFromPoint(e.x, e.y);

						if (pointRng) {
							// Check if pointRange is before/after selection then change the endPoint
							if (pointRng.compareEndPoints('StartToStart', startRng) > 0)
								pointRng.setEndPoint('StartToStart', startRng);
							else
								pointRng.setEndPoint('EndToEnd', startRng);

							pointRng.select();
						}
					} else
						endSelection();
				}

				// Removes listeners
				function endSelection() {
					dom.unbind(doc, 'mouseup', endSelection);
					dom.unbind(doc, 'mousemove', selectionChange);
					started = 0;
				};

				// Detect when user selects outside BODY
				dom.bind(doc, 'mousedown', function(e) {
					if (e.target.nodeName === 'HTML') {
						if (started)
							endSelection();

						started = 1;

						// Setup start position
						startRng = rngFromPoint(e.x, e.y);
						if (startRng) {
							// Listen for selection change events
							dom.bind(doc, 'mouseup', endSelection);
							dom.bind(doc, 'mousemove', selectionChange);

							startRng.select();
						}
					}
				});
			})();
		}
	};

	// Expose the selection object
	tinymce.dom.TridentSelection = Selection;
})();
