var Range = Range ? Range : document.createRange().constructor;
(function(){
	
	var EDIT_CLASS = "editingElement";
	var EDIT_ELM_TYPE = "span";

	Range.prototype.COMMAND_TYPE_PLAIN              = Range.COMMAND_TYPE_PLAIN = 0;
	Range.prototype.COMMAND_TYPE_NO_CASCADE         = Range.COMMAND_TYPE_NO_CASCADE = 1;
	Range.prototype.COMMAND_TYPE_ASYNC              = Range.COMMAND_TYPE_ASYNC = 2;
	Range.prototype.COMMAND_TYPE_INCLUDE_TEXT       = Range.COMMAND_TYPE_INCLUDE_TEXT = 4;
	Range.prototype.COMMAND_TYPE_PRESERVE_STRUCTURE = Range.COMMAND_TYPE_PRESERVE_STRUCTURE = 8;
	Range.prototype.COMMAND_TYPE_NO_SELECT    = Range.COMMAND_TYPE_NO_SELECT = 16;
	
	function createEditingElement()
	{
		var elm = document.createElement(EDIT_ELM_TYPE);
		elm.className = EDIT_CLASS;
		
		return elm;
	}
	
	function getParentNodes(start, stop)
	{
		// This method returns an array containing all the parent nodes of start, all the way up to stop
		// It exists because we need to identify which elements are being cloned as part of Range.extractContents()

		var nodes = new Array();
		var iter = start;

		if (start.nodeName != "#text")
			nodes.push(start);
		
		while (iter != stop)
		{
			if (iter.parentNode)
			{
				iter = iter.parentNode;
				nodes.push(iter);
			}
			else
			{
				break;
			}
		}

		nodes.pop();

		return nodes;
	}
	
	function getDescendantChild(elm, depth, isFirst)
	{
		var node = elm;
		
		for (var i = 0; i < depth; i++)
		{
			if (isFirst) node = node.firstChild;
			else node = node.lastChild;
		}
		
		return node;
	}
	
	function makeArray(array){
		return Array().slice.call( array );
	}
	
	function insertNodes(startParentNodes, endParentNodes, nodes)
	{
		var rangeContents = this.extractContents();
		var start = null;
		var end = null;
			
		if ((startParentNodes.length == 0 && endParentNodes.length == 0) )
		{
			var len = nodes.length;
			for (var i = 0; i < len; i++)
			{
				var node = nodes.pop();
				
				if (i == 0)
					end = node;
					
				if (i == len - 1)
					start = node;
				
				this.insertNode(node);
			}
		}
		else
		{
			var editingElm = createEditingElement();
			editingElm.appendChild(rangeContents);
			
			var contents = editingElm;
			
			var startRoot = getDescendantChild(contents.firstChild, startParentNodes.length, true);
			var endRoot = getDescendantChild(contents.lastChild, endParentNodes.length, false);
			
			var currentNode = startRoot;
						
			var results = document.createDocumentFragment();
			var startEndRef = null;
			
			if (currentNode)
				for (var i = 0; i < startParentNodes.length; i++)
				{
					//var newNode = startParentNodes[i].appendChild(node.cloneNode(false));
					
					if (currentNode == startRoot)
					{
						
						startEndRef = startParentNodes[i].appendChild(nodes.shift());
						
						start = startEndRef;
					}
					
					var siblingIter = currentNode;
					
					while (siblingIter = siblingIter.nextSibling)
					{
						startEndRef = startParentNodes[i].appendChild(nodes.shift());
					}
					
					currentNode = currentNode.parentNode;
				}
			
			var currentNode = endRoot;
			var siblingIter = null;
			
			var endOff = null;
			var rangeEndRef = null;
			
			if (currentNode)
				for (var i = 0; i < endParentNodes.length; i++)
				{	
					//var newNode = endParentNodes[i].insertBefore(node.cloneNode(false), endParentNodes[i].firstChild);
					
					if (currentNode == endRoot)
					{
						rangeEndRef = endParentNodes[i].insertBefore(nodes.pop(), endParentNodes[i].firstChild);
						
						end = rangeEndRef
					}
					
					siblingIter = currentNode;
					
					while (siblingIter = siblingIter.previousSibling)
					{
						rangeEndRef = endParentNodes[i].insertBefore(nodes.pop(), endParentNodes[i].firstChild);
					}
					
					currentNode = currentNode.parentNode;
				}
			
			var middleNodes = makeArray(contents.childNodes);
			var rootNode = null;
			var offNode = null;
			
			if (startParentNodes.length == 0)
			{
				middleNodes = middleNodes.slice(0, -1);
				rootNode = endParentNodes[endParentNodes.length - 1].parentNode;
				offNode = endParentNodes[endParentNodes.length - 1];
			}
			else if (endParentNodes.length == 0)
			{
				middleNodes = middleNodes.slice(1);
				rootNode = startParentNodes[startParentNodes.length - 1].parentNode;
				offNode = startParentNodes[startParentNodes.length - 1].nextSibling;
			}
			else
			{
				middleNodes = middleNodes.slice(1, -1)
				rootNode = this.commonAncestorContainer;
				offNode = endParentNodes[endParentNodes.length - 1];
			}
				
			var middleRefNodes = new Array();
			if (middleNodes.length)
			{
				for (var i = 0; i < middleNodes.length; i++)
				{
					var node = rootNode.insertBefore(nodes.shift(), offNode);
					
					if (startParentNodes.length == 0 && i == 0)
						start = node;
					
					if (endParentNodes.length == 0 && i == middleNodes.length - 1)
						end = node;
				}
			}
		}
		
		if (!start)
			start = end;
			
		if (!end)
			end = start;
		
		return new Array(start, end);
	}
	
	function getFlatNodeList(treesArray)
	{
		var nodeList = new Array();
		for (var i = 0; i < treesArray.length; i++)
		{
			subtreesArray = treesArray[i];
			
			for (var j = 0; j < subtreesArray.length; j++)
			{
				var subtree = subtreesArray[j];
				
				for (var k = 0; k < subtree.childNodes.length; k++)
				{
					if ((i == 0 || i == treesArray.length - 1) && treesArray.length != 1)
					{
						var nodeLevel = subtree.childNodes[k];
						for (var l = 0; l < nodeLevel.childNodes.length; l++)
						{
							nodeList.push(nodeLevel.childNodes[l]);
						}
					}
					else
					{
						nodeList.push(subtree.childNodes[k]);
					}
				}
			}
		}
		
		return nodeList;
	}
	
	function getSubtrees(startParentNodes, endParentNodes)
	{
	
		if (startParentNodes.length == 0 && endParentNodes.length == 0)
		{
			var newNode = createEditingElement();
			newNode.appendChild(this.cloneContents());
			return new Array(new Array(newNode));
		}
		else
		{
			var contents = this.commonAncestorContainer.cloneNode(false);
			contents.appendChild(this.cloneContents());
			
			var startRoot = getDescendantChild(contents.firstChild, startParentNodes.length, true);
			var endRoot = getDescendantChild(contents.lastChild, endParentNodes.length, false);
			
			var currentNode = startRoot;
						
			var startSubtrees = contents.firstChild.cloneNode(false);
			
			if (currentNode)
				for (var i = 0; i < startParentNodes.length; i++)
				{
					var tree = startSubtrees.appendChild(startParentNodes[i].cloneNode(false));
					//var newNode = startParentNodes[i].appendChild(node.cloneNode(false));
					
					if (currentNode == startRoot)
					{
						if (currentNode)
							tree.appendChild(currentNode.cloneNode(true));
					}
					
					var siblingIter = currentNode;
					
					while (siblingIter = siblingIter.nextSibling)
					{
						tree.appendChild(siblingIter.cloneNode(true));
					}
					
					currentNode = currentNode.parentNode;
				}
			
			var currentNode = endRoot;
			var siblingIter = null;
			
			var endSubtrees = contents.lastChild.cloneNode(false);
			
			if (currentNode)
				for (var i = 0; i < endParentNodes.length; i++)
				{	
					//var newNode = endParentNodes[i].insertBefore(node.cloneNode(false), endParentNodes[i].firstChild);
					
					var tree = endSubtrees.insertBefore(endParentNodes[i].cloneNode(false), endSubtrees.firstChild);
					
					if (currentNode == endRoot)
					{
							tree.insertBefore(currentNode.cloneNode(true), tree.firstChild);
					}
					
					siblingIter = currentNode;
					
					while (siblingIter = siblingIter.previousSibling)
					{
						tree.insertBefore(siblingIter.cloneNode(true), tree.firstChild);
					}
					
					currentNode = currentNode.parentNode;
				}
			
			var middleSubtree = contents.cloneNode(false);
			var middleNodes = makeArray(contents.cloneNode(true).childNodes);
			
			if (startParentNodes.length == 0)
			{
				middleNodes = middleNodes.slice(0, -1);
			}
			else if (endParentNodes.length == 0)
			{
				middleNodes = middleNodes.slice(1);
			}
			else
			{
				middleNodes = middleNodes.slice(1, -1);
			}
			
			for (var i = 0; i < middleNodes.length; i++)
			{
				middleSubtree.appendChild(middleNodes[i].cloneNode(true));
			}
			
			return new Array(new Array(startSubtrees), new Array(middleSubtree), new Array(endSubtrees));
		}
	}
	
	function surroundNode(child, parent)
	{
		
	}
	
	Range.prototype.nestedSurroundContents = function(node)
	{
		this.execCommand(function(elm) { surroundNode(elm, node) } );
	}
	
	function applyCommand(fnPtr, node, isCascading, isTextIncluded, isStructurePreserved, isAsync, args)
	{	
		if (isCascading)
		{
			args.unshift(node);
			fnPtr.apply(this, args);
			args.shift();
			
			var nextLevel = node.childNodes;
			
			for (var i = 0; i < nextLevel.length; i++)
			{
				if ((isTextIncluded && nextLevel[i].nodeName == "#text") || nextLevel[i].nodeName != "#text")
				{
					applyCommand(fnPtr, nextLevel[i], isCascading, isTextIncluded, isStructurePreserved, isAsync, args);
				}
			}
		}
		else
		{
			if (isStructurePreserved)
			{
				args.unshift(node[2]);
				args.unshift(node[1]);
				args.unshift(node[0]);
				fnPtr.apply(this, args);
				args.slice(3);
			}
			else
			{
				args.unshift(node);
				fnPtr.apply(this, args);
				args.shift();
			}

		}
	}
	
	function surroundFirstLevelTextNodes(treesArray)
	{
		for (var i = 0; i < treesArray.length; i++)
		{
			trees = treesArray[i];
			for (var j = 0; j < trees.length; j++)
			{
				var tree = trees[j];
				for (var k = 0; k < tree.childNodes.length; k++)
				{
					if ((i == 0 || i == treesArray.length - 1) && treesArray.length != 1)
					{
						var nodeLevel = tree.childNodes[k];
						for (var l = 0; l < nodeLevel.childNodes.length; l++)
						{
							var node = nodeLevel.childNodes[l];
							
							if (node.nodeName == "#text")
							{
								var newParent = createEditingElement();
								node.parentNode.insertBefore(newParent, node);
								newParent.appendChild(node);
							}
						}
					}
					else
					{
						var node = tree.childNodes[k];
						
						if (node.nodeName == "#text")
						{
							var newParent = createEditingElement();
							var root = node.parentNode;
							var offset = node.nextSibling;
							newParent.appendChild(node);
							root.insertBefore(newParent, offset);
						}
					}
				}
			}
		}
	}
	
	Range.prototype.execCommand = function(ptrToCommand, flags)
	{
		// Figure out which flags are being used
		if (!flags) flags = 0;
	
		var isCascading = (flags & Range.COMMAND_TYPE_NO_CASCADE) ^ Range.COMMAND_TYPE_NO_CASCADE;
		var isAsync = flags & Range.COMMAND_TYPE_ASYNC;
		var isTextIncluded = flags & Range.COMMAND_TYPE_INLCUDE_TEXT;
		var isStructurePreserved = flags & Range.COMMAND_TYPE_PRESERVE_STRUCTURE;
		var isRangeAdded = (flags & Range.COMMAND_TYPE_NO_SELECT) ^ Range.COMMAND_TYPE_NO_SELECT;
		
        if (3 == this.endContainer.nodeType && "" == this.endContainer.data)
        {
            var demotext = document.getElementById("demotext");
            var last = demotext.lastChild;
            while (1 == last.nodeType)
            {
                last = last.lastChild;
            }
        }
        
		// Get the user-specified arguments
		var args = makeArray(arguments).slice(2);
		
		// The the length of these lists tells us how far to look down the tree later (for removing generated parent nodes)
		var startParentNodes = getParentNodes(this.startContainer, this.commonAncestorContainer);
		var endParentNodes = getParentNodes(this.endContainer, this.commonAncestorContainer);

		var treesArray = getSubtrees.call(this, startParentNodes, endParentNodes);
		
		if (!isTextIncluded)
			surroundFirstLevelTextNodes(treesArray);
		
		var flatList = getFlatNodeList(treesArray);
		
		if (isAsync)
		{
			var tempThis = this;
			
			function callback() {
					
					if (!isStructurePreserved)
					{
						var newFlatList = new Array();
					
						var nodes = flatList[0].childNodes;
						
						for (var i = 0; i < nodes.length; i++)
						{
							newFlatList.push(nodes[i]);
						}
						
						flatList = newFlatList;
					}
					
					var boundaries = insertNodes.call(tempThis, startParentNodes, endParentNodes, flatList);
					
					// Make sure the start and end points of the range reflect the new document structure
					tempThis.setStartBefore(boundaries[0]);
					tempThis.setEndAfter(boundaries[1]);
					
					// First remove the range in case we want to de-select after the op
					// Webkit doesn't support this, so we'll fall back on removeAllRanges if we have to
					if (window.getSelection().removeRange)
						window.getSelection().removeRange(tempThis);
					else
						window.getSelection().removeAllRanges();
					
					// And re-add it if it's wanted
					if (isRangeAdded)
						window.getSelection().addRange(tempThis);
			}
			
			args.push(callback);
		}
		
		if (!isStructurePreserved)
		{
			if (!isAsync)
			{
				// If we're not preserving the structure, we can just go through the flat list and apply the function pointer
				for (var i = 0; i < flatList.length; i++)
				{
					applyCommand.call(this, ptrToCommand, flatList[i], isCascading, isTextIncluded, false, isAsync, args);
				}
			}
			else
			{
				// If we're async, just pass in the flat list all at once; no cascading is allowed in this case
				
				var newParent = createEditingElement();
				var len = flatList.length;
				
				for (var i = 0; i < len; i++)
				{
					newParent.appendChild(flatList.shift());
				}
				
				flatList[0] = newParent;
				
				applyCommand.call(this, ptrToCommand, flatList, false, isTextIncluded, false, isAsync, args);
			}
		}
		else
		{
			// If we are preserving the structure, we need to pass in the trees array; note that this precludes cascading
			applyCommand.call(this, ptrToCommand, treesArray, false, isTextIncluded, isStructurePreserved, isAsync, args);
		}
		
		// If we're synchronous, we can assume all operations have completed and just start wrapping up
		if (!isAsync)
		{
			// Now that we've made our operations on the nodes, we'll insert them back into the tree
			var boundaries = insertNodes.call(this, startParentNodes, endParentNodes, flatList);

			// Make sure the start and end points of the range reflect the new document structure
			if (boundaries[0] && boundaries[1])
			{
				this.setStartBefore(boundaries[0]);
				this.setEndAfter(boundaries[1]);
			}
			
			// First remove the range in case we want to de-select after the op
			// Webkit doesn't support this, so we'll fall back to removeAllRanges if we have to
			if (window.getSelection().removeRange)
				window.getSelection().removeRange(this);
			else
				window.getSelection().removeAllRanges();
			
			// And re-add it if it's wanted
			if (isRangeAdded)
				window.getSelection().addRange(this);
		}
	}
	
})();

