/* ***** BEGIN LICENSE BLOCK *****
 * Licensed under Version: MPL 1.1/GPL 2.0/LGPL 2.1
 * Full Terms at http://mozile.mozdev.org/0.8/LICENSE
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Jorgen Horstink and David Kingma's code.
 *
 * The Initial Developers of the Original Code are Jorgen Horstink and David Kingma.
 * Portions created by the Initial Developer are Copyright (C) 2005-2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *	James A. Overton <james@overton.ca>
 *
 * ***** END LICENSE BLOCK ***** */

/**
 * @fileoverview Provides a W3C Range implementation under Internet Explorer.
 * <p>History: The original code was written by Jorgen Horstink (http://jorgenhorstink.nl/2006/03/11/w3c-range-in-internet-explorer/).
 * It was extensively modified by David Kingma.
 * This version has been adapted for use with Mozile by James A. Overton.
 * Key changes include wrapping the objects in the Mozile namespace, so as to minimize the impact on other scripts in the same page.
 *
 * @link http://mozile.mozdev.org 
 * @author James A. Overton <james@overton.ca>
 * @version 0.8
 * $Id: InternetExplorerRange.js,v 1.4 2008/02/20 19:08:00 jameso Exp $
 */

mozile.require("mozile.dom");
mozile.require("mozile.xpath");
mozile.provide("mozile.dom.InternetExplorerRange");


/**
 * Range object, see http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html
 * @constructor
 */
mozile.dom.InternetExplorerRange = function(range) {
	/**
	 * A reference to an IE native TextRange object.
	 * @private
	 * @type TextRange
	 */
	this._range = null;

	if(range)
		this._range = range;
	else {
		this._range = mozile.document.body.createTextRange();
	}

	/**
	 * A boolean indicating whether the range's start and end points are at the same position.
	 * @type Boolean
	 */
	this.collapsed = null;

	/**
	 * The deepest Node that contains the startContainer and endContainer Nodes.
	 * @type Node
	 */
	this.commonAncestorContainer = null;
	
	/**
	 * The Node within which the Range starts.
	 * @type Node
	 */
	this.startContainer = null;

	/**
	 * A number representing where in the endContainer the Range starts.
	 * @type Integer
	 */
	this.startOffset = null;

	/**
	 * The Node within which the Range ends.
	 * @type Node
	 */
	this.endContainer = null;

	/**
	 * A number representing where in the endContainer the Range ends.
	 * @type Integer
	 */
	this.endOffset = null;
}


/**
 * Initializes the properties of this range according to the internal
 * IE range (_range).
 * @private
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype._init = function () {
	
	//startPoint
	var beginRange = this._range.duplicate();
	beginRange.collapse(true);
	var position = this._getPosition(beginRange);
	this.startContainer = position.node;
	this.startOffset = position.offset;

	//endPoint
	var endRange = this._range.duplicate();
	endRange.collapse(false);
	position = this._getPosition(endRange);
	this.endContainer = position.node;
	this.endOffset = position.offset;
	
	this._commonAncestorContainer();
	this._collapsed();
	
}


/**
 * Takes an Internet Explorer TextRange object and returns a W3C node and offset pair.
 * <p>The basic method is as follows:
 * <ul><li>Create a new range with its start at the beginning of the element and its end at the target position. Set the rangeLength to the length of the range's text.
 * <li>Starting with the first child, for each child:
 * <ul><li>If the child is a text node, and its length is less than the rangeLength, then move the range's start by the text node's length.
 * <li>If the child is a text node and its length is less than the rangeLength then we've found the target. Return the node and use the remaining rangeLength as the offset.
 * <li>If the child is an element, move the range's start by the length of the element's innerText.
 * </ul></ul>
 * <p>This algorithm works fastest when the target is close to the beginning of the parent element.
 * The current implementation is smart enough pick the closest end point of the parent element (i.e. the start or the end), and work forward or backward from there.
 * @private
 * @param {TextRange} textRange A TextRange object. Its start position will be found.
 * @type Object
 * @return An object with "node" and "offset" properties.
 */
mozile.dom.InternetExplorerRange.prototype._getPosition = function(textRange) {
	var element = textRange.parentElement();
	var range = element.ownerDocument.body.createTextRange();
	range.moveToElementText(element);
	range.setEndPoint("EndToStart", textRange);
	var rangeLength = range.text.length;

	// Choose Direction
	if(rangeLength < element.innerText.length / 2) {
		var direction = 1;
		var node = element.firstChild;
	}
	else {
		direction = -1;
		node = element.lastChild;
		range.moveToElementText(element);
		range.setEndPoint("StartToStart", textRange);
		rangeLength = range.text.length;
	}

	// Loop through child nodes
	while(node) {
		switch(node.nodeType) {
			case mozile.dom.TEXT_NODE:
				nodeLength = node.data.length;
				if(nodeLength < rangeLength) {
					var difference = rangeLength - nodeLength;
					if(direction == 1) range.moveStart("character", difference);
					else range.moveEnd("character", -difference);
					rangeLength = difference;
				}
				else {
					if(direction == 1) return {node: node, offset: rangeLength};
					else return {node: node, offset: nodeLength - rangeLength};
				}
				break;

			case mozile.dom.ELEMENT_NODE:
				nodeLength = node.innerText.length;
				if(direction == 1) range.moveStart("character", nodeLength);
				else range.moveEnd("character", -nodeLength);
				rangeLength = rangeLength - nodeLength;
				break;
		}
	
		if(direction == 1) node = node.nextSibling;
		else node = node.previousSibling;
	}


	// TODO: This should throw a warning.
	//throw("Error in mozile.dom.InternetExplorerRange._getPosition: Ran out of child nodes before the range '"+ textRange.text +"' inside '"+ mozile.xpath.getXPath(element) +"' was found.");
	
	// The TextRange was not found. Return a reasonable value instead.
	return {node: element, offset: 0};
	
}


/**
 * Find the TextRange offset for a given text node and offset. Effectively the opposite of getPosition().
 * The method used is to count the innerText length for elements and the data length for text nodes.
 * @private
 * @param {Text} startNode The target text node.
 * @param {Integer} startOffset
 * @type Integer
 */
mozile.dom.InternetExplorerRange.prototype._getOffset = function (startNode, startOffset) {
	var node, moveCharacters;
	if(startNode.nodeType == mozile.dom.TEXT_NODE) {
		moveCharacters = startOffset;
		node = startNode.previousSibling;
	}
	else if(startNode.nodeType == mozile.dom.ELEMENT_NODE) {
		moveCharacters = 0;
		if(startOffset > 0) node = startNode.childNodes[startOffset - 1];
		else return 0;
	}
	else {
		mozile.debug.inform("mozile.dom.InternetExplorerRange.prototype._getOffset", "Bad node given: "+ mozile.xpath.getXPath(startNode));
		return 0;
	}

	while (node) {
		var nodeLength = 0;
		if(node.nodeType == mozile.dom.ELEMENT_NODE) {
			nodeLength = node.innerText.length;
			if(this._isChildless(node)) nodeLength = 1; // Tweak childless nodes.
			if(this._isBlock(node)) nodeLength++; // Tweak block level elements.
			//if(nodeLength == 0) nodeLength++; // minimum length is 1
		}
		else if(node.nodeType == mozile.dom.TEXT_NODE) {
			 nodeLength = node.data.length;
		}
		moveCharacters += nodeLength;
		node = node.previousSibling;
	}
	
	return moveCharacters;
}

/**
 * Internet Explorer pads certain elements with an extra space at the end. This method detects those elements.
 * TODO: This method should be smarter about detecting non-HTML or using CSS.
 * @param {Node} node The node to check.
 * @type Boolean
 */
mozile.dom.InternetExplorerRange.prototype._isBlock = function(node) {
	switch (node.nodeName.toLowerCase()) {
		case 'p':
		case 'div':
		case 'h1':
		case 'h2':
		case 'h3':
		case 'h4':
		case 'h5':
		case 'h6':
		case 'pre':
			return true;
	}
	return false;
}

/**
 * Internet Explorer sets the length of certain elements which cannot have child nodes to 1.
 * TODO: Complete this list.
 * @param {Node} node The node to check.
 * @type Boolean
 */
mozile.dom.InternetExplorerRange.prototype._isChildless = function(node) {
	switch (node.nodeName.toLowerCase()) {
		case 'img':
		case 'br':
		case 'hr':
			return true;
	}
	return false;
}


// == positioning ==
/**
 * Sets the start position of a Range
 * If the startNode is a Node of type Text, Comment, or CDATASection, then startOffset 
 * is the number of characters from the start of startNode. For other Node types, 
 * startOffset is the number of child nodes between the start of the startNode.
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.setStart = function (startNode, startOffset) {
	var container = startNode;
	if(startNode.nodeType == mozile.dom.TEXT_NODE || 
		startNode.nodeType == mozile.dom.COMMENT_NODE || 
		startNode.nodeType == mozile.dom.CDATA_SECTION_NODE) {
		container = container.parentNode;
	}

	var copyRange = this._range.duplicate();
	copyRange.moveToElementText(container);
	copyRange.collapse(true);
	copyRange.move('Character', this._getOffset(startNode, startOffset));
	this._range.setEndPoint('StartToStart', copyRange);

	//update object properties
	this.startContainer = startNode;
	this.startOffset    = startOffset;
	if (this.endContainer == null && this.endOffset == null) {
		this.endContainer = startNode;
		this.endOffset    = startOffset;
	}
	this._commonAncestorContainer();
	this._collapsed();
}


/**
 * Sets the end position of a Range.
 * Creates a clone of the current range, moves it to the desired spot
 * and the we move the endPoint of the current range to the clones endpoint
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.setEnd = function (endNode, endOffset) {
	// Store the start of the range
	var copyRange = this._range.duplicate();
	copyRange.collapse(true);
	
	var container = endNode;
	if(endNode.nodeType == mozile.dom.TEXT_NODE || 
		endNode.nodeType == mozile.dom.COMMENT_NODE || 
		endNode.nodeType == mozile.dom.CDATA_SECTION_NODE) {
		container = container.parentNode;
	}

	copyRange = this._range.duplicate();
	copyRange.moveToElementText(container);
	copyRange.collapse(true);
	copyRange.move('Character', this._getOffset(endNode, endOffset));
	this._range.setEndPoint('EndToEnd', copyRange);
	
	//update object properties
	this.endContainer = endNode;
	this.endOffset    = endOffset;
	if (this.startContainer == null && this.startOffset == null) {
		this.startContainer = endNode;
		this.startOffset    = endOffset;
	}
	this._commonAncestorContainer();
	this._collapsed();
}


/**
 * Sets the start position of a Range relative to another Node.
 * The parent Node of the start of the Range will be the same as 
 * that for the referenceNode.
 * @param {Node} referenceNode
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.setStartBefore = function (referenceNode) {
	this.setStart(referenceNode.parentNode, mozile.dom.getIndex(referenceNode));
};

/**
 * Sets the start position of a Range relative to another Node.
 * @param {Node} referenceNode
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.setStartAfter = function (referenceNode) {
	this.setStart(referenceNode.parentNode, mozile.dom.getIndex(referenceNode) + 1);
};

/**
 * Sets the end position of a Range relative to another Node.
 * @param {Node} referenceNode
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.setEndBefore = function (referenceNode) {
	this.setEnd(referenceNode.parentNode, mozile.dom.getIndex(referenceNode));
};

/**
 * Sets the end position of a Range relative to another Node.
 * @param {Node} referenceNode
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.setEndAfter = function (referenceNode) {
	this.setEnd(referenceNode.parentNode, mozile.dom.getIndex(referenceNode) + 1);
};

/**
 * Sets the Range to contain the node and its contents.
 * The parent Node of the start and end of the Range will be the same as
 * the parent of the referenceNode.
 * @param {Node} referenceNode
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.selectNode = function (referenceNode) {
	this.setStartBefore(referenceNode);
	this.setEndAfter(referenceNode);
};

/**
 * Sets the Range to contain the contents of a Node.
 * The parent Node of the start and end of the Range will be the referenceNode. The 
 * startOffset is 0, and the endOffset is the number of child Nodes or number of characters 
 * contained in the reference node.
 * @param {Node} referenceNode
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.selectNodeContents = function (referenceNode) {
	this.setStart(referenceNode, 0);
	if(referenceNode.nodeType == mozile.dom.TEXT_NODE)
		this.setEnd(referenceNode, referenceNode.data.length);
	else
		this.setEnd(referenceNode, referenceNode.childNodes.length);
};

/**
 * Collapses the Range to one of its boundary points.
 * @param {Boolean} toStart When true the Range is collapsed to the start position, when false to the end position.
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.collapse = function (toStart) {
	this._range.collapse(toStart);
	
	//update the properties
	if(toStart) {
		this.endContainer = this.startContainer;
		this.endOffset = this.startOffset;
	} else {
		this.startContainer = this.endContainer;
		this.startOffset = this.endOffset;
	}
	this._commonAncestorContainer();
	this._collapsed();
};

// == editing ==
/**
 * Returns a document fragment copying the nodes of a Range.
 * Partially selected nodes include the parent tags necessary to make the 
 * document fragment valid.
 * @type Range
 */
mozile.dom.InternetExplorerRange.prototype.cloneContents = function () {
	var df = mozile.document.createDocumentFragment();
	
	var container = this.commonAncestorContainer;
	if(container.nodeType == mozile.dom.TEXT_NODE) {
		df.appendChild(mozile.document.createTextNode(this._range.text));
		return df;
	}
	
	var startNode = this.startContainer;
	if(this.startContainer.nodeType != mozile.dom.TEXT_NODE)
		startNode = this.startContainer.childNodes[this.startOffset];
	var endNode = this.endContainer;
	if(this.endContainer.nodeType != mozile.dom.TEXT_NODE)
		endNode = this.endContainer.childNodes[this.endOffset - 1];

	if(startNode == endNode) {
		df.appendChild(startNode.cloneNode(true));
		return df;
	}

	// Walk the tree.
	var current = container.firstChild;
	var parent = null;
	var clone;
	while(current) {
		//alert(current.nodeName +"\n"+ df.innerHTML);
		// Watch for the start node, then start adding nodes.
		if(!parent) {
			if(mozile.dom.isAncestorOf(current, startNode, container)) {
				parent = df;
			}
			// Skip this node.
			else {
				current = current.nextSibling;
				continue;
			}
		}

		// Clone the node.
		if(current == startNode && this.startContainer.nodeType == mozile.dom.TEXT_NODE) {
			content = this.startContainer.data.substring(this.startOffset);
			parent.appendChild(mozile.document.createTextNode(content));
		}
		else if(current == endNode) {
			if(this.endContainer.nodeType == mozile.dom.TEXT_NODE) {
				content = this.endContainer.data.substring(0, this.endOffset);
				parent.appendChild(mozile.document.createTextNode(content));
			}
			else parent.appendChild(endNode.cloneNode(false));
			// We're done.
			break; 
		}
		else {
			clone = current.cloneNode(false);
			parent.appendChild(clone);
		}
		
		// Move
		if(current.firstChild) {
			parent = clone;
			current = current.firstChild;
		}
		else if(current.nextSibling) {
			current = current.nextSibling;
		}
		// Climb the tree
		else while(current) {
			if(current.parentNode) {
				parent = parent.parentNode;
				current = current.parentNode;
				if(current.nextSibling) {
					current = current.nextSibling;
					break;
				}
			}
			else current = null;
		}
	}
	
	return df;
};

/**
 * Removes the contents of a Range from the document.
 * Unlike extractContents, this method does not return a documentFragment 
 * containing the deleted content. 
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.deleteContents = function () {
	this._range.pasteHTML('');//This is incorrect, it might also delete the container

	//update properties
	this.endContainer = this.startContainer;
	this.endOffset = this.startOffset;
	this._commonAncestorContainer();
	this._collapsed();
};

/**
 * Moves contents of a Range from the document tree into a document fragment.
 * @type DocumentFragment
 */
mozile.dom.InternetExplorerRange.prototype.extractContents = function () {
	var fragment = this.cloneContents();
	this.deleteContents();
	return fragment;
};

/**
 * Insert a node at the start of a Range.
 * newNode is inserted at the start boundary point of the Range. If the newNodes 
 * is to be added to a text Node, that Node is split at the insertion point, and 
 * the insertion occurs between the two text Nodes
 * 
 * If newNode is a document fragment, the children of the document fragment are  
 * inserted instead.
 * @param {Node} newNode The node to insert.
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.insertNode = function (newNode) {
	//salert('mozile.dom.InternetExplorerRange.insertNode() is not implemented yet');
	if(this.startContainer.nodeType == mozile.dom.TEXT_NODE){
		this.startContainer.splitText(this.startOffset);
		this.startContainer.parentNode.insertBefore(newNode, this.startContainer.nextSibling);
		this.setStart(this.startContainer, this.startOffset);
		//Mozilla collapses, is this needed?
		//this.collapse(true);
		//this._range.select();
		return;
	} else { //Element node
		var parentNode = this.startContainer.parentNode;
		if(this.startContainer.childNodes.length == this.startOffset) {
			parentNode.appendChild(newNode);
		}else {
			this.startContainer.insertBefore(newNode, this.startContainer.childNodes.item(this.startOffset));
			this.setStart(this.startContainer, this.startOffset+1);
			//this._range.select();
			return;
		}
	}
};

/**
 * Moves content of a Range into a new node.
 * SurroundContents is equivalent to newNode.appendChild(range.extractContents());
 * range.insertNode(newNode). After surrounding, the boundary points of the Range 
 * include newNode.
 * @param {Node} newNode The node to select.
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.surroundContents = function (newNode) {
	newNode.appendChild(this.extractContents());
	this.insertNode(newNode);
};

// == Other ==
/**
 * NOT IMPLEMENTED. Compares the boundary points of two Ranges
 * Returns a number, 1, 0, or -1, indicating whether the corresponding boundary-point of range is respectively before, equal to, or after the corresponding boundary-point of sourceRange
 * Any of the following constants can be passed as the value of how parameter:
 * Range.END_TO_END compares the end boundary-point of sourceRange to the end boundary-point of range.
 * Range.END_TO_START compares the end boundary-point of sourceRange to the start boundary-point of range.
 * Range.START_TO_END compares the start boundary-point of sourceRange to the end boundary-point of range.
 * Range.START_TO_START compares the start boundary-point of sourceRange to the start boundary-point of range
 * @param {Integer} how A code for the comparison method.
 * @param {Range} sourceRange
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.compareBoundaryPoints = function (how, sourceRange) {
	alert('mozile.dom.InternetExplorerRange.compareBoundaryPoints() is not implemented yet');
};

/**
 * Returns a Range object with boundary points identical to the cloned Range.
 * @type Range
 */
mozile.dom.InternetExplorerRange.prototype.cloneRange = function () {
	var r = new mozile.dom.InternetExplorerRange(this._range.duplicate());
	var properties = ["startContainer", "startOffset", "endContainer", "endOffset", "commonAncestorContainer", "collapsed"];
	for(var i=0; i < properties.length; i++) {
		r[properties[i]] = this[properties[i]];
	}
	return r;
};

/**
 * Releases Range from use to improve performance.
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.detach = function () {};

/**
 * Returns the text of the Range.
 * @type String
 */
mozile.dom.InternetExplorerRange.prototype.toString = function () {
	return this._range.text;
};

/**
 * Finds the commonAncestorComtainer.
 * @private
 * @type Element
 */
mozile.dom.InternetExplorerRange.prototype._commonAncestorContainer = function () {
	if(this.startContainer == null || this.endContainer == null){
		this.commonAncestorContainer = null;
		return;
	}
	if(this.startContainer == this.endContainer) {
		this.commonAncestorContainer = this.startContainer;	
	}
	else {
		this.commonAncestorContainer = mozile.dom.getCommonAncestor(this.startContainer, this.endContainer);
	}
}

/**
 * Check to see if this selection is collapsed, and assign a value to this.collapsed.
 * @private
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype._collapsed = function () {
	this.collapsed = (this.startContainer == this.endContainer && this.startOffset == this.endOffset);
}



/**
 * Store the details about this range in an object which can be used to restore the range.
 * @type Object
 */
mozile.dom.InternetExplorerRange.prototype.store = mozile.dom.Range.prototype.store;

/**
 * Takes a stored range object and creates a new range with the same properties.
 * @param {Object} state A state object from Selection.store() or Range.store().
 * @type Void
 */
mozile.dom.InternetExplorerRange.prototype.restore = mozile.dom.Range.prototype.restore;

