
/**
 * An autosuggest textbox control. This class is copied from www.wrox.com and
 * modified to improve user friendlyness and bug fixes
 * 
 * @class AutoSuggestControl
 * @scope public
 */
function AutoSuggestControl(oTextbox, oProvider) {
    
    /**  The currently selected suggestions. */
    this.cur = -1;

    /** The dropdown list layer. */
    this.layer = null;
    
    /** Suggestion provider for the autosuggest feature. */
    this.provider = oProvider;
    
    /** The textbox to capture. */
    this.textbox = oTextbox;
    
    /** Timeout ID for fast typers. */
    this.timeoutId = null;

    /** The text that the user typed. */
    this.userText = oTextbox.value;
    
    this.init();
    
}

/**
 * Autosuggests one or more suggestions for what the user has typed.
 * If no suggestions are passed in, then no autosuggest occurs.
 * 
 * @scope private
 * @param aSuggestions An array of suggestion strings.
 * @param bTypeAhead If the control should provide a type ahead suggestion.
 */
AutoSuggestControl.prototype.autosuggest = function (aSuggestions /*:Array*/,
                                                     bTypeAhead /*:boolean*/) {
    
    //re-initialize pointer to current suggestion
    this.cur = -1;
    
    //make sure there's at least one suggestion
    if (aSuggestions.length > 0) {
        if (bTypeAhead) {
           this.typeAhead(aSuggestions[0]);
        }
        
        this.showSuggestions(aSuggestions);
    } else {
        this.hideSuggestions();
    }
};

/**
 * Creates the dropdown layer to display multiple suggestions.
 * @scope private
 */
AutoSuggestControl.prototype.createDropDown = function () {

    //create the layer and assign styles
    this.layer = document.createElement("div");
    this.layer.className = "suggestions";
    this.layer.style.visibility = "hidden";
    this.textbox.parentNode.appendChild(this.layer);
};

/**
 * Highlights the next or previous suggestion in the dropdown and
 * places the suggestion into the textbox.
 * @param step Either a positive or negative number indicating whether
 *              to select the next or previous sugggestion, respectively.
 * @scope private
 */
AutoSuggestControl.prototype.goToSuggestion = function (step /*:int*/) {
    var cSuggestionNodes = this.layer.childNodes;
    
    if (cSuggestionNodes.length == 0) return;
    var oNode = null;
    
    if (step > 0) {
        this.cur = this.cur + step;
        if (this.cur >= cSuggestionNodes.length) this.cur = cSuggestionNodes.length - 1;
        oNode = cSuggestionNodes[this.cur];

        currentTop = (this.cur) * oNode.offsetHeight;
        if (currentTop > this.layer.scrollTop + this.layer.offsetHeight - oNode.offsetHeight) {
          this.layer.scrollTop = currentTop - this.layer.offsetHeight + oNode.offsetHeight;
        }

    } else if (this.cur > 0) {
        this.cur = this.cur + step;
        if (this.cur < 0) this.cur = 0;
        oNode = cSuggestionNodes[this.cur];
        
        var currentTop = (this.cur) * oNode.offsetHeight;
        if (currentTop < this.layer.scrollTop) this.layer.scrollTop = currentTop;
    }

    if (oNode) {
        this.highlightSuggestion(oNode);
        this.textbox.value = oNode.firstChild.nodeValue;
    }
};

/**
 * Handles three keydown events.
 * @scope private
 * @param oEvent The event object for the keydown event.
 */
AutoSuggestControl.prototype.handleKeyPress = function (oEvent /*:Event*/) {

    switch(oEvent.keyCode) {
        case 33: // Page up
            this.goToSuggestion(-8);
            oEvent.returnValue = false; // Prevent window scrolling
            break;
            
        case 34: // Page down
            this.goToSuggestion(8);
            oEvent.returnValue = false; // Prevent window scrolling
            break;
            
        case 38: // Up arrow
            this.goToSuggestion(-1);
            oEvent.returnValue = false; // Prevent window scrolling
            break;
            
        case 40: // Down arrow 
            this.goToSuggestion(1);
            oEvent.returnValue = false; // Prevent window scrolling
            break;
            
        case 27: // Esc
            this.textbox.value = this.userText;
            this.selectRange(this.userText.length, 0);
            /* falls through */
            
        case 9: // Tab
            this.hideSuggestions();
            break;
            
        case 13: // Enter
            this.hideSuggestions();
            oEvent.returnValue = false;
            if (oEvent.preventDefault) oEvent.preventDefault();
            break;
    }
    return true;
};

/**
 * Handles keyup events.
 * @scope private
 * @param oEvent The event object for the keyup event.
 */
AutoSuggestControl.prototype.handleKeyUp = function (oEvent /*:Event*/) {

    var iKeyCode = oEvent.keyCode;
    var oThis = this;
    
    //get the currently entered text
    this.userText = this.textbox.value;
    
    clearTimeout(this.timeoutId);

    //for backspace (8) and delete (46), shows suggestions without typeahead
    if (iKeyCode == 8 || iKeyCode == 46) {
        
        this.timeoutId = setTimeout( function () {
            oThis.provider.requestSuggestions(oThis, false);
        }, 700);
        
    //make sure not to interfere with non-character keys
    } else if ((iKeyCode != 16 && iKeyCode < 32) || (iKeyCode >= 33 && iKeyCode < 46) || (iKeyCode >= 112 && iKeyCode <= 123)) {
        //ignore
    } else {
        //request suggestions from the suggestion provider with typeahead
        this.timeoutId = setTimeout( function () {
            oThis.provider.requestSuggestions(oThis, true);
        }, 700);
    }
};

/**
 * Hides the suggestion dropdown.
 * @scope private
 */
AutoSuggestControl.prototype.hideSuggestions = function () {
    this.layer.style.visibility = "hidden";
    if (isIE) unHideSelects();
};

/**
 * Highlights the given node in the suggestions 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; // From mouse over

        } else if (oNode.className == "current") {
            oNode.className = "";
        }
    }
};

/**
 * Initializes the textbox with event handlers for
 * auto suggest functionality.
 * @scope private
 */
AutoSuggestControl.prototype.init = function () {

    // Save a reference to this object
    var oThis = this;
    
    // Assign the onkeyup event handler
    this.textbox.onkeyup = function (oEvent) {
        if (! oEvent) oEvent = window.event;
        oThis.handleKeyUp(oEvent);
    };
    
    // Assign onkeydown event handler
    var keypressHandler = function (oEvent) {
        if (! oEvent) oEvent = window.event;
        oThis.handleKeyPress(oEvent);
    };
    
    if (isIE) {
        this.textbox.onkeydown = keypressHandler;
    } else {
        this.textbox.onkeypress = keypressHandler;
    }
    
    // Create the suggestions dropdown
    this.createDropDown();
};

/**
 * Selects a range of text in the textbox.
 * @scope public
 * @param iStart The start index (base 0) of the selection.
 * @param iEnd The end index of the selection.
 */
AutoSuggestControl.prototype.selectRange = function (iStart /*:int*/, iEnd /*:int*/) {

    // Use text ranges for Internet Explorer
    if (this.textbox.createTextRange) {
        var oRange = this.textbox.createTextRange(); 
        oRange.moveStart("character", iStart); 
        oRange.moveEnd("character", iEnd - this.textbox.value.length);      
        oRange.select();
        
    // Use setSelectionRange() for Mozilla
    } else if (this.textbox.setSelectionRange) {
        this.textbox.setSelectionRange(iStart, iEnd);
    }     

    // Set focus back to the textbox
    this.textbox.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.
 */
AutoSuggestControl.prototype.showSuggestions = function (aSuggestions /*:Array*/) {
    
    var oDiv = null;
    this.layer.innerHTML = "";  //clear contents of the layer
    
    // Add entries
    for (var i = 0; i < aSuggestions.length; i++) {
        oDiv = document.createElement("div");
        oDiv.appendChild(document.createTextNode(aSuggestions[i]));
        this.layer.appendChild(oDiv);
    
      // When the user clicks on the a suggestion, get the text (innerHTML)
      // and place it into a textbox
      var oThis = this;

      oDiv.onmouseover = function(event) {
          oThis.highlightSuggestion(getTarget(event));
      }

      oDiv.onmousedown = function(event) {
          oThis.textbox.value = getTarget(event).firstChild.nodeValue;
      }

      oDiv.onmouseup = function () {
          oThis.textbox.focus();
          oThis.hideSuggestions();
      }
    }
    
    // Set layer position
    this.layer.style.left = getRealLeft(this.textbox) + "px";
    this.layer.style.top = getRealTop(this.textbox) + this.textbox.offsetHeight + "px";
    
    // For better height matching of entries we re-set the maxHeight here.
    this.layer.style.maxHeight = (8 * oDiv.offsetHeight) + "px";

    var scrollBarWidth = 20;
    this.layer.style.minWidth = this.textbox.clientWidth + scrollBarWidth + "px";
    
    // Do not break lines at white spaces, only at line breaks
    this.layer.style.whiteSpace = "pre";
    this.layer.style.visibility = "visible";
    
    if (isIE) hideSelects();
};

/**
 * Inserts a suggestion into the textbox, highlighting the 
 * suggested part of the text.
 * @scope private
 * @param sSuggestion The suggestion for the textbox.
 */
AutoSuggestControl.prototype.typeAhead = function (sSuggestion /*:String*/) {

    //check for support of typeahead functionality
    if (this.textbox.createTextRange || this.textbox.setSelectionRange){
        var iLen = this.textbox.value.length; 
        this.textbox.value = sSuggestion; 
        this.selectRange(iLen, sSuggestion.length);
    }
};