/**
 * This is based on code from http://www.webreference.com/programming/javascript/ncz/column2/
 * Changed by Sam to:
 * - Use JSON (via Jquery) to get the suggestions.
 * - Also fixed a couple bugs related to the current suggestion index (this.cur).
 * - Added client-side caching of suggestions.
 * - Added support for suggestions marked up with strong tags.
 * - Added optional ability for each suggestion to have a url which 
 *   the user can be redirected to instead of submitting the form.
 *   To use this, add an array of "suggestion_urls" in the json response.
 * - Don't call init() on the controls until body.onload to avoid issues with IE.
 */

// This is a hashtable of all AutoSuggestControls by text_input_id.
// This allows us to get a handle on a specific AutoSuggestControl
// from a timeout callback.
var AutoSuggestControlRegistry = new Array();
var TIMEOUT = 100;

function initAutoSuggestControls() {
    var id;
    for (id in AutoSuggestControlRegistry) {
        var suggest = AutoSuggestControlRegistry[id];
        suggest.init();
    }
}

// Handle Firefox
if(window.addEventListener) {
    window.addEventListener("load", initAutoSuggestControls, false);
// Handle IE
} else if(document.attachEvent) {
    window.attachEvent("onload", initAutoSuggestControls);
}


function autosuggestCallback(text_input_id, bTypeAhead) {
    var suggest = AutoSuggestControlRegistry[text_input_id];
    if(! suggest) { return; }
    suggest.requestSuggestions(bTypeAhead);
}

/**
 * An autosuggest text input control.
 * @class
 * @scope public
 * suggestionSelectedCallback is an optional argument
 * It should be a function that takes 3 parms:
 * 1. the textinput object
 * 2. the suggestion value (as a plaintext string) selected by the user
 * 3. the suggestion url (undefined if there is no suggestion url)
 * The callback will be called just before this code changes the value
 * of the textinput.  So the callback can look at the textinput's value
 * to get the user's original string.
 */
// FIXME: Add another option to enable/disable type ahead?
// "Type ahead" makes sense only for classic autocompletion scenarios.
// It selects the text in a text input to the right of the cursor (for
// easy replacement).
function AutoSuggestControl(text_input_id, suggest_method_name, suggestionSelectedCallback) {
    
    /**
     * The currently selected suggestions.
     */   
    this.cur = -1;

    /**
     * The dropdown list layer.
     */
    this.layer = null;
    this.shim = null;
    
    /**
     * Name of the suggestion method (for XML-RPC requests).
     */
    this.suggest_method_name = suggest_method_name;
    
    /**
     * The text input to capture.
     */
    this.text_input_id = text_input_id;

    this.timeout = null;

    this.cache = new Array();

    // Register myself with the global registry.
    AutoSuggestControlRegistry[text_input_id] = this;

    this.suggestionSelectedCallback = suggestionSelectedCallback;
    
}

/**
 * Request suggestions from the server (or return them from our cache if available), then show the suggestions.
 * FIXME: decide whether we want to implement typeahead.
 */
AutoSuggestControl.prototype.requestSuggestions = function (bTypeAhead /*boolean*/) {
    var oThis = this;
    var inputValue = this.textinput.value;
    var caret = getCaretPosition(this.textinput);
    var cachekey = inputValue + ':' + caret;
    // Only get suggestions if there are at least 2 characters in the input.
    if(inputValue && (inputValue.length > 1)) {
        var cached = this.cache[cachekey];
        if(cached == undefined) {
            var parms = {
                'search_string': inputValue,
                'caret:int': caret
            };
            $.getJSON(this.suggest_method_name, parms, function(result, status) {
                oThis.hideSuggestions();
                if(status != "success") return;
                var search_string = result['search_string'];
                var suggestions = result['suggestions'];
                //console.log("JSON result: ", result);
                var caret = result['caret'];
                var suggestion_urls = result['suggestion_urls'];
                var total = result['total'];
                // add suggestions to cache
                var cachekey = search_string + ':' + caret;
                var cached = new Array();
                cached[0] = suggestions;
                cached[1] = suggestion_urls;
                cached[2] = total;
                oThis.cache[cachekey] = cached;

                // The textinput value may have changed by now, so let's get the latest value.
                var inputValue = oThis.textinput.value;
                if(inputValue && (inputValue.length > 1)) {
                    var caret = getCaretPosition(oThis.textinput);
                    var cachekey = inputValue + ':' + caret;
                    var cached = oThis.cache[cachekey];
                    oThis.showCachedSuggestions(cached);
                }
            });
        } else {
            oThis.showCachedSuggestions(cached);
        }
    } else {
        this.hideSuggestions();
    }
};

AutoSuggestControl.prototype.showCachedSuggestions = function (cached) {
    this.hideSuggestions();
    if(cached == undefined) {
        return;
    }
    var suggestions = cached[0];
    var suggestion_urls = cached[1];
    var total = cached[2];
    // show cached suggestions
    if(suggestions.length) {
        this.showSuggestions(suggestions, suggestion_urls, total);
    }
}

/**
 * Creates the dropdown layer to display multiple suggestions.
 * @scope private
 */
AutoSuggestControl.prototype.createDropDown = function () {

    var oThis = this;

    //create the layer and assign styles
    this.layer = document.createElement("div");
    this.layer.className = "suggestions";
    this.layer.style.visibility = "hidden";
    this.layer.style.width = this.textinput.offsetWidth;
    
    this.layer.onmousedown = 
    this.layer.onmouseup = 
    this.layer.onmouseover = function (oEvent) {
        oEvent = oEvent || window.event;
        oTarget = oEvent.target || oEvent.srcElement;

        // The event target may be a suggestion div or a descendent.  
        // We want the suggestion div, so check parents until we get to a div.
        var divNode = oTarget;
        while(divNode.tagName != 'DIV') {
            divNode = divNode.parentNode;
        }

        if (oEvent.type == "mousedown") {
            if(oThis.cur > -1) {
                oThis.suggestionSelected(divNode, oEvent);
            }
            oThis.hideSuggestions();
        } else if (oEvent.type == "mouseover") {
            oThis.highlightSuggestion(divNode);
        } else {
            oThis.textinput.focus();
        }
    };
    document.body.appendChild(this.layer);
};

/**
 * Called when the user selects a suggestion (via click or Enter).
 * If the suggestion has a url, redirect to it.
 * Otherwise copy the suggestion into the text input.
 */
AutoSuggestControl.prototype.suggestionSelected = function (oNode, oEvent) {
    var url = oNode.getAttribute('myurl');
    if(url) {
        if(url == 'ALL') {
            this.hideSuggestions();
            this.textinput.form.submit();
        } else {
            this.hideSuggestions();
            if(this.suggestionSelectedCallback) {
                this.suggestionSelectedCallback(this.textinput, this.unformat(oNode.innerHTML), url);
            }
            if(oEvent.type == 'keypress') {
                // Prevent the default behaviour of form submission.
                // First do it IE-style
                oEvent.cancelBubble = true;
                oEvent.returnValue = false;
                // Now W3C-style 
                if(oEvent.preventDefault) {
                    oEvent.preventDefault();
                }
            }
            document.location = url;
        }
    } else {
        var suggestion = this.unformat(oNode.innerHTML);
        if(this.suggestionSelectedCallback) {
            this.suggestionSelectedCallback(this.textinput, suggestion, null);
        }
        this.textinput.value = suggestion;
        this.hideSuggestions();
        this.textinput.focus();      
    }
}

/**
 * Gets the left coordinate of the text input.
 * @scope private
 * @return The left coordinate of the text input in pixels.
 */
AutoSuggestControl.prototype.getLeft = function () /*:int*/ {

    var oNode = this.textinput;
    var iLeft = 0;
    
    while(1) {
        if(oNode.tagName == "BODY" || oNode.tagName == "HTML") { break; }
        iLeft += oNode.offsetLeft;
        if(oNode.clientLeft) {
            iLeft += oNode.clientLeft;
        }
        oNode = oNode.offsetParent;        
    }
    
    return iLeft;
};

/**
 * Gets the top coordinate of the text input.
 * @scope private
 * @return The top coordinate of the text input in pixels.
 */
AutoSuggestControl.prototype.getTop = function () /*:int*/ {

    var oNode = this.textinput;
    var iTop = 0;
    
    while(1) {
        if(oNode.tagName == "BODY" || oNode.tagName == "HTML") { break; }
        iTop += oNode.offsetTop;
        oNode = oNode.offsetParent;
    }
    
    return iTop;
};

/**
 * Keyboard event handlers.
 * We want to allow the user to hold down the up or down arrow keys
 * and have the event repeat.
 * IE does this for keydown.  FF only does it for keypress.
 * Not only that, IE doesn't even fire a keypress event for the arrow keys.
 */

/**
 * Handles three keydown events.
 * @scope private
 * @param oEvent The event object for the keydown event.
 */
AutoSuggestControl.prototype.handleKeyDown = function (oEvent /*:Event*/) {

    if(navigator.appName == "Microsoft Internet Explorer") {
        switch(oEvent.keyCode) {
            case 38: //up arrow
                this.previousSuggestion();
                break;
            case 40: //down arrow 
                this.nextSuggestion();
                break;
        }
    }
};

AutoSuggestControl.prototype.handleKeyPress = function (oEvent /*:Event*/) {

    switch(oEvent.keyCode) {
        case 38: //up arrow
            this.previousSuggestion();
            break;
        case 40: //down arrow 
            this.nextSuggestion();
            break;
        case 13: //enter
            if(this.cur > -1) {
                var cSuggestionNodes = this.layer.childNodes;
                var oNode = cSuggestionNodes[this.cur];
                this.suggestionSelected(oNode, oEvent);
                return false;
            }
            this.hideSuggestions();
            break;
        case 27: //escape
            this.hideSuggestions();
            this.cur = -1;
            break;
    }

};

/**
 * Handles keyup events.
 * @scope private
 * @param oEvent The event object for the keyup event.
 */
AutoSuggestControl.prototype.handleKeyUp = function (oEvent /*:Event*/) {

    var iKeyCode = oEvent.keyCode;

    //for backspace (8) and delete (46), shows suggestions without typeahead
    if (iKeyCode == 8 || iKeyCode == 46) {
        //this.requestSuggestions(false);
        if(this.timeout) { clearTimeout(this.timeout); }
        this.timeout = setTimeout('autosuggestCallback("'+this.text_input_id+'", false)', TIMEOUT);
        
    //make sure not to interfere with non-character keys
    } else if (iKeyCode < 32 || (iKeyCode >= 33 && iKeyCode < 46) || (iKeyCode >= 112 && iKeyCode <= 123)) {
        //ignore
    //also ignore up and down since handleKeyPress looks for those:
    } else if (iKeyCode == 38 || iKeyCode == 40) {
        //ignore
    } else {
        //request suggestions with typeahead
        //this.requestSuggestions(true);
        if(this.timeout) { clearTimeout(this.timeout); }
        this.timeout = setTimeout('autosuggestCallback("'+this.text_input_id+'", true)', TIMEOUT);
    }
};

/**
 * Hides the suggestion dropdown.
 * @scope private
 */
AutoSuggestControl.prototype.hideSuggestions = function () {
    this.layer.style.visibility = "hidden";
    if(this.shim) {
        document.body.removeChild(this.shim);
        this.shim = null;
    }
};

/**
 * Highlights one of the suggestion divs in the dropdown.
 * @scope private
 * @param oSuggestionNode The node representing a suggestion in the dropdown.
 */
AutoSuggestControl.prototype.highlightSuggestion = function (oSuggestionNode) {
    for (var i=0; i < this.layer.childNodes.length; i++) {
        var oNode = this.layer.childNodes[i];
        if (oNode == oSuggestionNode) {
            oNode.className = "current"
            this.cur = i;
        } else if (oNode.className == "current") {
            oNode.className = "";
        }
    }
};

/**
 * Initializes the text input with event handlers for
 * auto suggest functionality.
 * @scope private
 */
AutoSuggestControl.prototype.init = function () {

    //save a reference to this object
    var oThis = this;

    this.textinput = document.getElementById(this.text_input_id);
    this.textinput.setAttribute("autocomplete", "off");
    
    //assign the onkeyup event handler
    this.textinput.onkeyup = function (oEvent) {
    
        //check for the proper location of the event object
        if (!oEvent) {
            oEvent = window.event;
        }    
        
        //call the handleKeyUp() method with the event object
        oThis.handleKeyUp(oEvent);
    };
    
    //assign onkeydown event handler
    this.textinput.onkeydown = function (oEvent) {
    
        //check for the proper location of the event object
        if (!oEvent) {
            oEvent = window.event;
        }    
        
        //call the handleKeyDown() method with the event object
        oThis.handleKeyDown(oEvent);
    };
    
    //assign onkeypress event handler
    this.textinput.onkeypress = function (oEvent) {
    
        //check for the proper location of the event object
        if (!oEvent) {
            oEvent = window.event;
        }    
        
        //call the handleKeyPress() method with the event object
        oThis.handleKeyPress(oEvent);
    };
    
    //assign onblur event handler (hides suggestions)    
    this.textinput.onblur = function () {
        oThis.hideSuggestions();
        oThis.cur = -1;
    };

    //create the suggestions dropdown
    this.createDropDown();
};

/**
 * Highlights the next suggestion in the dropdown and
 * places the suggestion into the text input.
 * @scope private
 */
AutoSuggestControl.prototype.nextSuggestion = function () {
    var cSuggestionNodes = this.layer.childNodes;

    if (cSuggestionNodes.length > 0) {
        if (this.cur < cSuggestionNodes.length-1) {
            this.cur++;
        } else {
            this.cur = 0;
        }
        var oNode = cSuggestionNodes[this.cur];
        this.highlightSuggestion(oNode);
    }
};

/**
 * Highlights the previous suggestion in the dropdown and
 * places the suggestion into the text input.
 * @scope private
 */
AutoSuggestControl.prototype.previousSuggestion = function () {
    var cSuggestionNodes = this.layer.childNodes;

    if (cSuggestionNodes.length > 0) {
        if (this.cur > 0) {
            this.cur--;
        } else {
            this.cur = cSuggestionNodes.length-1;
        }
        var oNode = cSuggestionNodes[this.cur];
        this.highlightSuggestion(oNode);
    }
};

/**
 * Selects a range of text in the text input.
 * @scope public
 * @param iStart The start index (base 0) of the selection.
 * @param iLength The number of characters to select.
 */
AutoSuggestControl.prototype.selectRange = function (iStart /*:int*/, iLength /*:int*/) {

    //use text ranges for Internet Explorer
    if (this.textinput.createTextRange) {
        var oRange = this.textinput.createTextRange(); 
        oRange.moveStart("character", iStart); 
        oRange.moveEnd("character", iLength - this.textinput.value.length);      
        oRange.select();
        
    //use setSelectionRange() for Mozilla
    } else if (this.textinput.setSelectionRange) {
        this.textinput.setSelectionRange(iStart, iLength);
    }     

    //set focus back to the text input
    this.textinput.focus();      
}; 

/**
 * Builds the suggestion layer contents, moves it into position,
 * and displays the layer.
 * @scope private
 * @param aSuggestions An array of suggestions for the control.
 * @param aSuggestionUrls An array of urls (one for each suggestion).
 *        If this param is not undefined, then selecting a suggestion
 *        will redirect the user to the corresponding url.
 * @param iTotal The total number of available suggestions.  May be undefined.
 */
AutoSuggestControl.prototype.showSuggestions = function (aSuggestions /*array*/, aSuggestionUrls /*array*/, iTotal /* int */) {
    
    var oDiv = null;
    this.layer.innerHTML = "";  //clear contents of the layer
    for (var i=0; i < aSuggestions.length; i++) {
        oDiv = document.createElement("div");

        // If there's a url, stash it in the div in an attribute named "myurl".
        if(aSuggestionUrls != undefined) {
            oDiv.setAttribute('myurl', aSuggestionUrls[i]);
        }
        oDiv.innerHTML = aSuggestions[i];
        this.layer.appendChild(oDiv);
        oDiv.style.width = this.textinput.offsetWidth;
    }
    if(iTotal != undefined) {
        if(iTotal > aSuggestions.length) {
            oDiv = document.createElement("div");
            oDiv.setAttribute('myurl', 'ALL');
            //oDiv.innerHTML = "See all "+iTotal+" results";
            oDiv.innerHTML = "<em>See all results</em>";
            this.layer.appendChild(oDiv);
            oDiv.style.width = this.textinput.offsetWidth;
        }
    }
    this.cur = -1;
    this.layer.style.left = this.getLeft() + "px";
    this.layer.style.top = (this.getTop()+this.textinput.offsetHeight) + "px";

    // Add an iframe shim so select boxes won't show thru in IE.
    this.shim = document.createElement('iframe');
    this.shim.style.position='absolute';
    this.shim.style.width=this.layer.offsetWidth + "px";
    this.shim.style.height=this.layer.offsetHeight + "px";
    this.shim.style.top=this.layer.style.top;
    this.shim.style.left=this.layer.style.left;
    this.shim.style.zIndex='9';
    this.shim.setAttribute('frameborder','0');
    this.shim.setAttribute('src','about:blank');
    document.body.appendChild(this.shim);

    this.layer.style.visibility = "visible";
};

/**
 * Inserts a suggestion into the text input, highlighting the 
 * suggested part of the text.
 * @scope private
 * @param sSuggestion The suggestion for the text input.
 */
AutoSuggestControl.prototype.typeAhead = function (sSuggestion /*:String*/) {

    //check for support of typeahead functionality
    if (this.textinput.createTextRange || this.textinput.setSelectionRange){
        var iLen = this.textinput.value.length; 
        this.textinput.value = sSuggestion; 
        this.selectRange(iLen, sSuggestion.length);
    }
};

/**
 * Given a suggestion marked up with <strong> tags, return a plaintext string.
 * @scope private
 * @param sSuggestion The HTML-formatted suggestion for the text input.
 */
AutoSuggestControl.prototype.unformat = function (sSuggestion /*:String*/) {
    var result = sSuggestion.replace(/<strong>/gi, "");
    result = result.replace(/<\/strong>/gi, "");
    result = result.replace(/&amp;/gi, "&");
    return result;
};

function getCaretPosition(oTextinput) {
    var result = 0;
    if (navigator.appVersion.indexOf("MSIE")!=-1) {
        var range = document.selection.createRange();
        var isCollapsed = range.compareEndPoints("StartToEnd", range) == 0;
        if (!isCollapsed) range.collapse(false);
        var b = range.getBookmark();
        result = b.charCodeAt(2) - 2;
    } else {
        result = oTextinput.selectionEnd;
    }
    // If we can't determine the position, assume it's at the end of the string.
    if(result < 0) result = oTextinput.length;
    return result;
}
