(function (window, undefined) {
	"use strict";
	var jQuery = window.alohaQuery || window.jQuery,
		$ = jQuery,
		//		GENTICS = window.GENTICS,
		//		Aloha = window.Aloha,
		DOMUtils, TextRangeUtils, selection, DOMRange, RangeIterator, DOMSelection;

	/*
	 * Only execute the following code if we are in IE (check for
	 * document.attachEvent, this is a microsoft event and therefore only available
	 * in IE).
	 */

	if (document.attachEvent && document.selection) {
		/*!
		 *   DOM Ranges for Internet Explorer (m2)
		 *
		 *   Copyright (c) 2009 Tim Cameron Ryan
		 *   Released under the MIT/X License
		 *   available at http://code.google.com/p/ierange/
		 */

		/*
	  Range reference:
	    http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html
	    http://mxr.mozilla.org/mozilla-central/source/content/base/src/nsRange.cpp
	    https://developer.mozilla.org/En/DOM:Range
	  Selection reference:
	    http://trac.webkit.org/browser/trunk/WebCore/page/DOMSelection.cpp
	  TextRange reference:
	    http://msdn.microsoft.com/en-us/library/ms535872.aspx
	  Other links:
	    http://jorgenhorstink.nl/test/javascript/range/range.js
	    http://jorgenhorstink.nl/2006/07/05/dom-range-implementation-in-ecmascript-completed/
	    http://dylanschiemann.com/articles/dom2Range/dom2RangeExamples.html
	*/

		//[TODO] better exception support


		/*
	  DOM functions
	 */

		DOMUtils = {
			findChildPosition: function (node) {
				for (var i = 0; node = node.previousSibling; i++)
				continue;
				return i;
			},
			isDataNode: function (node) {
				return node && node.nodeValue !== null && node.data !== null;
			},
			isAncestorOf: function (parent, node) {
				return !DOMUtils.isDataNode(parent) && (parent.contains(DOMUtils.isDataNode(node) ? node.parentNode : node) || node.parentNode == parent);
			},
			isAncestorOrSelf: function (root, node) {
				return DOMUtils.isAncestorOf(root, node) || root == node;
			},
			findClosestAncestor: function (root, node) {
				if (DOMUtils.isAncestorOf(root, node)) while (node && node.parentNode != root)
				node = node.parentNode;
				return node;
			},
			getNodeLength: function (node) {
				return DOMUtils.isDataNode(node) ? node.length : node.childNodes.length;
			},
			splitDataNode: function (node, offset) {
				if (!DOMUtils.isDataNode(node)) return false;
				var newNode = node.cloneNode(false);
				node.deleteData(offset, node.length);
				newNode.deleteData(0, offset);
				node.parentNode.insertBefore(newNode, node.nextSibling);
			}
		};

		/*
	  Text Range utilities
	  functions to simplify text range manipulation in ie
	 */

		TextRangeUtils = {
			convertToDOMRange: function (textRange, document) {
				var domRange, adoptBoundary;

				adoptBoundary = function (domRange, textRange, bStart) {
					// iterate backwards through parent element to find anchor location
					var cursorNode = document.createElement('a'),
						cursor = textRange.duplicate(),
						parent;

					cursor.collapse(bStart);
					parent = cursor.parentElement();
					do {
						parent.insertBefore(cursorNode, cursorNode.previousSibling);
						cursor.moveToElementText(cursorNode);
					} while (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) > 0 && cursorNode.previousSibling);

					// when we exceed or meet the cursor, we've found the node
					if (cursor.compareEndPoints(bStart ? 'StartToStart' : 'StartToEnd', textRange) == -1 && cursorNode.nextSibling) {
						// data node
						cursor.setEndPoint(bStart ? 'EndToStart' : 'EndToEnd', textRange);
						domRange[bStart ? 'setStart' : 'setEnd'](cursorNode.nextSibling, cursor.text.length);
					} else {
						// element
						domRange[bStart ? 'setStartBefore' : 'setEndBefore'](cursorNode);
					}
					cursorNode.parentNode.removeChild(cursorNode);
				};

				// return a DOM range
				domRange = new DOMRange(document);
				adoptBoundary(domRange, textRange, true);
				adoptBoundary(domRange, textRange, false);
				return domRange;
			},

			convertFromDOMRange: function (domRange) {
				function adoptEndPoint(textRange, domRange, bStart) {
					// find anchor node and offset
					var container = domRange[bStart ? 'startContainer' : 'endContainer'],
						offset = domRange[bStart ? 'startOffset' : 'endOffset'],
						textOffset = 0,
						anchorNode = DOMUtils.isDataNode(container) ? container : container.childNodes[offset],
						anchorParent = DOMUtils.isDataNode(container) ? container.parentNode : container,
						cursorNode, cursor;

					// visible data nodes need a text offset
					if (container.nodeType == 3 || container.nodeType == 4) {
						textOffset = offset;
					}

					// create a cursor element node to position range (since we can't select text nodes)
					cursorNode = domRange._document.createElement('a');
					anchorParent.insertBefore(cursorNode, anchorNode);
					cursor = domRange._document.body.createTextRange();
					cursor.moveToElementText(cursorNode);
					cursorNode.parentNode.removeChild(cursorNode);
					// move range
					textRange.setEndPoint(bStart ? 'StartToStart' : 'EndToStart', cursor);
					textRange[bStart ? 'moveStart' : 'moveEnd']('character', textOffset);
				}

				// return an IE text range
				var textRange = domRange._document.body.createTextRange();
				adoptEndPoint(textRange, domRange, true);
				adoptEndPoint(textRange, domRange, false);
				return textRange;
			}
		};

		/*
	  DOM Range
	 */
		DOMRange = function (document) {
			// save document parameter
			this._document = document;

			// initialize range
			//[TODO] this should be located at document[0], document[0]
			this.startContainer = this.endContainer = document.body;
			this.endOffset = DOMUtils.getNodeLength(document.body);
		};

		DOMRange.START_TO_START = 0;
		DOMRange.START_TO_END = 1;
		DOMRange.END_TO_END = 2;
		DOMRange.END_TO_START = 3;

		DOMRange.prototype = {
			// public properties
			startContainer: null,
			startOffset: 0,
			endContainer: null,
			endOffset: 0,
			commonAncestorContainer: null,
			collapsed: false,
			// private properties
			_document: null,

			// private methods
			_refreshProperties: function () {
				// collapsed attribute
				this.collapsed = (this.startContainer == this.endContainer && this.startOffset == this.endOffset);
				// find common ancestor
				var node = this.startContainer;
				while (node && node != this.endContainer && !DOMUtils.isAncestorOf(node, this.endContainer))
				node = node.parentNode;
				this.commonAncestorContainer = node;
			},

			// range methods
			//[TODO] collapse if start is after end, end is before start
			setStart: function (container, offset) {
				this.startContainer = container;
				this.startOffset = offset;
				this._refreshProperties();
			},
			setEnd: function (container, offset) {
				this.endContainer = container;
				this.endOffset = offset;
				this._refreshProperties();
			},
			setStartBefore: function (refNode) {
				// set start to beore this node
				this.setStart(refNode.parentNode, DOMUtils.findChildPosition(refNode));
			},
			setStartAfter: function (refNode) {
				// select next sibling
				this.setStart(refNode.parentNode, DOMUtils.findChildPosition(refNode) + 1);
			},
			setEndBefore: function (refNode) {
				// set end to beore this node
				this.setEnd(refNode.parentNode, DOMUtils.findChildPosition(refNode));
			},
			setEndAfter: function (refNode) {
				// select next sibling
				this.setEnd(refNode.parentNode, DOMUtils.findChildPosition(refNode) + 1);
			},
			selectNode: function (refNode) {
				this.setStartBefore(refNode);
				this.setEndAfter(refNode);
			},
			selectNodeContents: function (refNode) {
				this.setStart(refNode, 0);
				this.setEnd(refNode, DOMUtils.getNodeLength(refNode));
			},
			collapse: function (toStart) {
				if (toStart) this.setEnd(this.startContainer, this.startOffset);
				else this.setStart(this.endContainer, this.endOffset);
			},

			// editing methods
			cloneContents: function () {
				// clone subtree
				return (function cloneSubtree(iterator) {
					for (var node, frag = document.createDocumentFragment(); node = iterator.next();) {
						node = node.cloneNode(!iterator.hasPartialSubtree());
						if (iterator.hasPartialSubtree()) node.appendChild(cloneSubtree(iterator.getSubtreeIterator()));
						frag.appendChild(node);
					}
					return frag;
				})(new RangeIterator(this));
			},
			extractContents: function () {
				// cache range and move anchor points
				var range = this.cloneRange();
				if (this.startContainer != this.commonAncestorContainer) this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer));
				this.collapse(true);
				// extract range
				return (function extractSubtree(iterator) {
					for (var node, frag = document.createDocumentFragment(); node = iterator.next();) {
						if (iterator.hasPartialSubtree()) {
							node = node.cloneNode(false);
						} else {
							iterator.remove();
						}
						if (iterator.hasPartialSubtree()) node.appendChild(extractSubtree(iterator.getSubtreeIterator()));
						frag.appendChild(node);
					}
					return frag;
				})(new RangeIterator(range));
			},
			deleteContents: function () {
				// cache range and move anchor points
				var range = this.cloneRange();
				if (this.startContainer != this.commonAncestorContainer) this.setStartAfter(DOMUtils.findClosestAncestor(this.commonAncestorContainer, this.startContainer));
				this.collapse(true);
				// delete range
				(function deleteSubtree(iterator) {
					while (iterator.next()) {
						if (iterator.hasPartialSubtree()) {
							deleteSubtree(iterator.getSubtreeIterator());
						} else {
							iterator.remove();
						}
					}
				})(new RangeIterator(range));
			},
			insertNode: function (newNode) {
				// set original anchor and insert node
				if (DOMUtils.isDataNode(this.startContainer)) {
					DOMUtils.splitDataNode(this.startContainer, this.startOffset);
					this.startContainer.parentNode.insertBefore(newNode, this.startContainer.nextSibling);
				} else {
					this.startContainer.insertBefore(newNode, this.startContainer.childNodes[this.startOffset]);
				}
				// resync start anchor
				this.setStart(this.startContainer, this.startOffset);
			},
			surroundContents: function (newNode) {
				// extract and surround contents
				var content = this.extractContents();
				this.insertNode(newNode);
				newNode.appendChild(content);
				this.selectNode(newNode);
			},

			// other methods
			compareBoundaryPoints: function (how, sourceRange) {
				// get anchors
				var containerA, offsetA, containerB, offsetB;
				switch (how) {
				case DOMRange.START_TO_START:
				case DOMRange.START_TO_END:
					containerA = this.startContainer;
					offsetA = this.startOffset;
					break;
				case DOMRange.END_TO_END:
				case DOMRange.END_TO_START:
					containerA = this.endContainer;
					offsetA = this.endOffset;
					break;
				}
				switch (how) {
				case DOMRange.START_TO_START:
				case DOMRange.END_TO_START:
					containerB = sourceRange.startContainer;
					offsetB = sourceRange.startOffset;
					break;
				case DOMRange.START_TO_END:
				case DOMRange.END_TO_END:
					containerB = sourceRange.endContainer;
					offsetB = sourceRange.endOffset;
					break;
				}

				// compare
				return containerA.sourceIndex < containerB.sourceIndex ? -1 : containerA.sourceIndex == containerB.sourceIndex ? offsetA < offsetB ? -1 : offsetA == offsetB ? 0 : 1 : 1;
			},
			cloneRange: function () {
				// return cloned range
				var range = new DOMRange(this._document);
				range.setStart(this.startContainer, this.startOffset);
				range.setEnd(this.endContainer, this.endOffset);
				return range;
			},
			detach: function () {
				//[TODO] Releases Range from use to improve performance.
			},
			toString: function () {
				return TextRangeUtils.convertFromDOMRange(this).text;
			},
			createContextualFragment: function (tagString) {
				// parse the tag string in a context node
				var
				content = (DOMUtils.isDataNode(this.startContainer) ? this.startContainer.parentNode : this.startContainer).cloneNode(false),
					fragment;

				content.innerHTML = tagString;
				// return a document fragment from the created node
				for (fragment = this._document.createDocumentFragment(); content.firstChild;)
				fragment.appendChild(content.firstChild);
				return fragment;
			}
		};

		/*
	  Range iterator
	 */
		RangeIterator = function (range) {
			this.range = range;
			if (range.collapsed) {
				return;
			}

			//[TODO] ensure this works
			// get anchors
			var root = range.commonAncestorContainer;
			this._next = range.startContainer == root && !DOMUtils.isDataNode(range.startContainer) ? range.startContainer.childNodes[range.startOffset] : DOMUtils.findClosestAncestor(root, range.startContainer);
			this._end = range.endContainer == root && !DOMUtils.isDataNode(range.endContainer) ? range.endContainer.childNodes[range.endOffset] : DOMUtils.findClosestAncestor(root, range.endContainer).nextSibling;
		};

		RangeIterator.prototype = {
			// public properties
			range: null,
			// private properties
			_current: null,
			_next: null,
			_end: null,

			// public methods
			hasNext: function () {
				return !!this._next;
			},
			next: function () {
				// move to next node
				var current = this._current = this._next;
				this._next = this._current && this._current.nextSibling != this._end ? this._current.nextSibling : null;

				// check for partial text nodes
				if (DOMUtils.isDataNode(this._current)) {
					if (this.range.endContainer == this._current)
					(current = current.cloneNode(true)).deleteData(this.range.endOffset, current.length - this.range.endOffset);
					if (this.range.startContainer == this._current)
					(current = current.cloneNode(true)).deleteData(0, this.range.startOffset);
				}
				return current;
			},
			remove: function () {
				var end, start;
				// check for partial text nodes
				if (DOMUtils.isDataNode(this._current) && (this.range.startContainer == this._current || this.range.endContainer == this._current)) {
					start = this.range.startContainer == this._current ? this.range.startOffset : 0;
					end = this.range.endContainer == this._current ? this.range.endOffset : this._current.length;
					this._current.deleteData(start, end - start);
				} else this._current.parentNode.removeChild(this._current);
			},
			hasPartialSubtree: function () {
				// check if this node be partially selected
				return !DOMUtils.isDataNode(this._current) && (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer) || DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer));
			},
			getSubtreeIterator: function () {
				// create a new range
				var subRange = new DOMRange(this.range._document);
				subRange.selectNodeContents(this._current);
				// handle anchor points
				if (DOMUtils.isAncestorOrSelf(this._current, this.range.startContainer)) subRange.setStart(this.range.startContainer, this.range.startOffset);
				if (DOMUtils.isAncestorOrSelf(this._current, this.range.endContainer)) subRange.setEnd(this.range.endContainer, this.range.endOffset);
				// return iterator
				return new RangeIterator(subRange);
			}
		};

		/*
	  DOM Selection
	 */

		//[NOTE] This is a very shallow implementation of the Selection object, based on Webkit's
		// implementation and without redundant features. Complete selection manipulation is still
		// possible with just removeAllRanges/addRange/getRangeAt.

		DOMSelection = function (document) {
			// save document parameter
			this._document = document;

			// add DOM selection handler
			var selection = this;
			document.attachEvent('onselectionchange', function () {
				selection._selectionChangeHandler();
			});
		};

		DOMSelection.prototype = {
			// public properties
			rangeCount: 0,
			// private properties
			_document: null,

			// private methods
			_selectionChangeHandler: function () {
				// check if there exists a range
				this.rangeCount = this._selectionExists(this._document.selection.createRange()) ? 1 : 0;
			},
			_selectionExists: function (textRange) {
				// checks if a created text range exists or is an editable cursor
				return textRange.compareEndPoints('StartToEnd', textRange) !== 0 || textRange.parentElement().isContentEditable;
			},

			// public methods
			addRange: function (range) {
				// add range or combine with existing range
				var selection = this._document.selection.createRange(),
					textRange = TextRangeUtils.convertFromDOMRange(range);
				if (!this._selectionExists(selection)) {
					// select range
					textRange.select();
				} else {
					// only modify range if it intersects with current range
					if (textRange.compareEndPoints('StartToStart', selection) == -1) if (textRange.compareEndPoints('StartToEnd', selection) > -1 && textRange.compareEndPoints('EndToEnd', selection) == -1) selection.setEndPoint('StartToStart', textRange);
					else if (textRange.compareEndPoints('EndToStart', selection) < 1 && textRange.compareEndPoints('EndToEnd', selection) > -1) selection.setEndPoint('EndToEnd', textRange);
					selection.select();
				}
			},
			removeAllRanges: function () {
				// remove all ranges
				this._document.selection.empty();
			},
			getRangeAt: function (index) {
				// return any existing selection, or a cursor position in content editable mode
				var textRange = this._document.selection.createRange();
				if (this._selectionExists(textRange)) return TextRangeUtils.convertToDOMRange(textRange, this._document);
				return null;
			},
			toString: function () {
				// get selection text
				return this._document.selection.createRange().text;
			}
		};

		/*
	  scripting hooks
	 */

		document.createRange = function () {
			return new DOMRange(document);
		};

		selection = new DOMSelection(document);
		window.getSelection = function () {
			return selection;
		};

		//[TODO] expose DOMRange/DOMSelection to window.?
	}

})(window);
