/*
    Getme Ltd reserve all intellectual property rights of this source code.
    
    EXCEPT AS OTHERWISE EXPRESSLY PROVIDED IN A WRITTEN AGREEMENT BETWEEN YOU 
    AND GETME LTD, THIS SOURCE CODE IS PROVIDED "AS IS" AND WITHOUT ANY 
    WARRANTIES OF ANY KIND, INCLUDING WARRANTIES OF MERCHANTABILITY OR FITNESS 
    FOR A PARTICULAR PURPOSE.

    IN NO EVENT SHALL GETME LTD BE LIABLE FOR ANY DIRECT, INDIRECT, 
    INCIDENTIAL, SPECIAL OR CONSEQUENTIAL DAMAGES OR DAMAGES FROM BUSINESS 
    INTERRUPTION OR LOSS OF PROFITS, REVENUE, DATA OR USE, INCURRED BY YOU OR 
    ANY THIRD PARTY, WHETHER IN CONTRACT OR TORT, ARISING FROM YOUR ACCESS TO, 
    OR USE OF, THIS SOURCE CODE EVEN IF GETME LTD HAS BEEN ADVISED OF THE 
    POSSIBILITY OF SUCH DAMAGES.
    
    Copyright (c)2009 Getme Ltd.
 */

/**
 * @fileoverview    Cross-browser content selection.
 * @version         0.0.3
 * @author          Anthony Blackshaw ant@getme.co.uk
 * @alpha           MADEYE
 */


// Dependency check(s)
if(!Array.indexOf) {
    /**
     * Ensure indexOf method is supported (cheers for this IE)
     * @ignore
     */    
    Array.prototype.indexOf = function(obj) {
        for(var i=0; i<this.length; i++) {
            if(this[i]==obj) {
                return i;
            }
        }
        return -1;
    }
}


/**
 * The SELECT namespace.
 * The Select library uses SELECT to namespace it's classes and functions.
 * @final
 * @type Namespace
 */  
var SELECT = {};


/**
 * Create a new cursor selection.
 * @class An cursor selection class.
 * @constructor
 */  
SELECT.Selection = function(start, end) {
    
    /** 
     * The start (from 0) of the selection in characters.
     * @type Element
     */
    this.start = start;
    
    /** 
     * The end of the selection in characters.
     * @type Element
     */
    this.end = end;
}

/**
 * Return true if the selection is collapsed.
 * @return Whether the selection is collapsed.
 * @type Boolean
 */
SELECT.Selection.prototype.is_collapsed = function() {
    return this.start==this.end;
}

/**
 * Collapse the selection (to the start).
 */
SELECT.Selection.prototype.collapse = function() {
    this.end=this.start;
}

/**
 * Return the selection as a two item list.
 * @return An array of [start,end].
 * @type Array
 */
SELECT.Selection.prototype.as_array = function() {
    return [this.start,this.end];
}


/**
 * Find the number of characters between the parent node and the find node.
 * @param {Element} parent_node The node from which the offset will start.
 * @param {Element} find_node The node to find the offset to.
 * @return Returns a two item list the first item is the offset, the second is 
 * a boolean which indicates if the parent node contained the find node.
 * @type Array
 * @private
 */
SELECT._find_node_offset = function(parent_node, find_node) {
    var offset = 0;
    var child_node_list = parent_node.childNodes;
    for (var i=0; i<child_node_list.length; i++) {
        var child_node = child_node_list[i];
        if (child_node==find_node) {
            return [offset,true];
        }       
        switch (child_node.nodeType) {
            case Node.TEXT_NODE:
                var text = child_node.nodeValue;
                // Do not include double spaces and count line returns as one
                // space.
                offset += text.length;
                break;
            default:
                if (['br','img'].indexOf(child_node.nodeName.toLowerCase())!=-1) {
                    offset+=1;
                }
                var child_result = SELECT._find_node_offset(child_node,find_node);
                offset += child_result[0];
                if (child_result[1]) {
                    return [offset,true];
                }
        }
    }
    return [offset,false];
}

/**
 * Select the contents of a node based on the specified start/end.
 * @param {Range} range The range to use for the selection.
 * @param {Element} node The node in which to make the selection.
 * @param {Number} start The start of the selection.
 * @param {Number} end The end of the selection.
 * @return Returns a two item list containf the start and end within the 
 * specified child node.
 * @type Array
 * @private
 */
SELECT._node_select = function(range, node, start, end) {
    var child_node_list = node.childNodes;
    for (var i=0; i<child_node_list.length; i++) {    
        var child_node = child_node_list[i];
        if (child_node.nodeType==Node.TEXT_NODE || (child_node.nodeName && ['br','img'].indexOf(child_node.nodeName.toLowerCase())!=-1)) {
            var length = 1;
            if (!child_node.nodeName || ['br','img'].indexOf(child_node.nodeName.toLowerCase())==-1) {
                length = child_node.nodeValue.length;
            }
            if (start>-1 && length>=start) {
                range.setStart(child_node,start);
                start=-1;
            } else {
                start-=length;
            }
            if (end>-1 && length>=end) {
                range.setEnd(child_node,end);
                end=-1;
            } else {
                end-=length;
            }
        } else {    
            var child_result = SELECT._node_select(range,child_node,start,end);
            start = child_result[0];
            end = child_result[1];;
        }
        if (end<0) {
            break;
        }
    }
    return [start,end];
}

/**
 * Return true if the specified child node is contained within the parent node.
 * @param {Element} child_node The child node to find.
 * @param {Element} parent_node The parent node to find the child node in.
 * @return Whether the child node is contained within the parent node.
 * @type Boolean
 * @private
 */
SELECT._in_node = function(child_node, parent_node) {
    if (child_node==parent_node) {
        return true;
    } else {
        if (child_node.parentNode) {
            return SELECT._in_node(child_node.parentNode,parent_node);
        }
    }
    return false;
}

/**
 * Get a list of selections within a specified node. *Important* Only selection 
 * of inline elements is supported. Whitespace should be stripped from the 
 * start and end of elements queried as behaviour is undefined between 
 * browsers).
 * @param {Element} node The node in which to search for selections.
 * @return A list of selections.
 * @type Array
 */
SELECT.get = function(node) {
    var selection_list = new Array();
    // Which browser method can we use for selection?
    if (window.getSelection) {
        selection = window.getSelection();
        if (selection.rangeCount>0) {
            for (var i=0; i<selection.rangeCount; i++) {
                var range = selection.getRangeAt(i);
                if (SELECT._in_node(range.startContainer,node) && SELECT._in_node(range.endContainer,node)) {
                    var start = 0;
                    var end = 0;
                    if (node!=range.startContainer) {
                        start += SELECT._find_node_offset(node,range.startContainer)[0];
                    }
                    if (node!=range.endContainer) {
                        end = SELECT._find_node_offset(node,range.endContainer)[0];
                    }
                    // Firefox fixes. These fixes handle out of range by one
                    // issues in FF selection method.
                    var start_type = range.startContainer.nodeType;
                    if (start_type==Node.TEXT_NODE||start_type==Node.COMMENT_NODE||start_type==Node.CDATA_SECTION_NODE){
                        start += range.startOffset;
                    } else {
                        if (range.startOffset==0) {
                            start = 0;
                        } else {
                            start = this._find_node_offset(node,range.startContainer)[0];
                        }
                    }
                    var end_type = range.endContainer.nodeType;
                    if (end_type==Node.TEXT_NODE||end_type==Node.COMMENT_NODE||end_type==Node.CDATA_SECTION_NODE){
                        end += range.endOffset;
                    } else {
                        if (range.startOffset==0) {
                            end = 0;
                            if (node!=range.startContainer) {
                                start = this._find_node_offset(node,range.startContainer);
                                end = start;
                            }
                        } else {
                            end = this._find_node_offset(node,range.endContainer)[0];
                            if (start==end && !range.collapsed) {
                                // This resolves double clicking on links
                                start = this._find_node_offset(node,range.startContainer.childNodes[range.startOffset])[0];
                                end = start + range.toString().length;
                            }
                        }
                    }
                    selection_list.push(new SELECT.Selection(start,end));
                }
            }
        }
    } else {
        // Currently this code is purely to handle IE's selection method. IE's
        // selection method is limited to single selections.
        if(document.selection) {
            var range = document.selection.createRange();
            if (SELECT._in_node(range.parentElement(),node)) {
	            var outer_range = range.duplicate();
	            outer_range.moveToElementText(node);
	            var find_range = range.duplicate();
                find_range.collapse();
                var offset = 0;
                var select_length = Math.max(1,Math.min(500,node.innerText.length/2));
	            var in_range = true;
	            // Slower (too slow) brute force way
	            /*while (in_range) {
	                find_range.moveStart('character',-1);
	                in_range = outer_range.inRange(find_range);
	            }*/
	            // Binary chop method
	            var count = 0;
                while (select_length>1||in_range) {
	                // Restore for the find range
                    find_range = range.duplicate();	
	                find_range.collapse(true);                
	                find_range.moveStart('character',-(select_length+offset));
	                in_range = outer_range.inRange(find_range);
	                if (in_range) {
	                    offset+=select_length;
	                } else {
                        // Half the find distance
                        if (select_length%2>0) {
                            select_length++;
                        }
                        select_length = select_length/2;
	                }
	                count++;
	            }
	                
                var start = find_range.text.replace(/\r\n/g,'').length;
                var end = start+range.text.length;
                // Get IE to count images as a character
                var start_imgs = find_range.htmlText.match(/<img.+?>/ig);
                var start_img_count = 0;
                if (start_imgs) {
                    start_img_count = start_imgs.length;
                }
                start += start_img_count;
                var end_imgs = range.htmlText.match(/<img.+?>/ig);
                var end_img_count = 0;
                if (end_imgs) {
                    end_img_count = end_imgs.length;
                }
                end += end_img_count;
                if (start>outer_range.text.length+start_img_count) {
                    start = outer_range.text.length+start_img_count;
                    end = start;
                }
                selection_list.push(new SELECT.Selection(start,end));
            }
        }
    }
    return selection_list;
}

/**
 * Set one or more selections within a specified node. *Important* Only 
 * selection of inline elements is supported.)
 * @param {Element} The node in which to make the selection.
 * @param {Selection|Array} selection_list The list of one or more selections.
 * @param {Boolean} clear If true then on browsers that support multiple 
 * selection, all existing selections are removed.
 * to set.
 */
SELECT.set = function(node, selection_list, clear) {
    if (selection_list.constructor!=Array.constructor) {
        selection_list = [selection_list,];
    }
    if (window.getSelection) {
        // Clear all current selections
        if (clear){
            window.getSelection().removeAllRanges();
        }
        // Make selection
        for (var i=0; i<selection_list.length; i++) {
            var selection = selection_list[i];
            var range = document.createRange();
            SELECT._node_select(range,node,selection.start,selection.end);
            window.getSelection().addRange(range);
        }
    } else {
        // IE only supports single selection
        selection = selection_list[0];
        // Reset the range to the start of the document
        var range = document.selection.createRange();
        range.moveToElementText(node);
        range.collapse(true);
        range.moveStart('character',selection.start);
        range.moveEnd('character',selection.end-selection.start);
        range.select();
    }
}

/**
 * Clear all selections.
 */
SELECT.clear = function() {
    if (window.getSelection) {
        window.getSelection().removeAllRanges();
    } else {
        document.selection.empty();
    }
}
