var Autocomplete = function() {
    var LEFT_BORDER_WIDTH = RIGHT_BORDER_WIDTH = 1;

    function AutocompleteControl(textbox, provider, delay) {
        this.selectionIndex = -1;
        this.provider = provider;
        this.textbox = textbox;
        this.delay = delay;
        this.isRegistered = false;

        var self = this;
        this.textbox.onkeyup = function(event) {
            event = event || window.event;
            self.handleKeyUp(event);
        };
        this.textbox.onkeydown = function(event) {
            event = event || window.event;
            self.handleKeyDown(event);
        };
        this.textbox.onblur = function() {
            self.hideSuggestions();
        };
        this.createDropDown();
    };

    AutocompleteControl.prototype.createDropDown = function() {
        // Create the suggestions drop down
        this.suggestDropDown = document.createElement("div");
        this.suggestDropDown.className = "suggestions";
        this.suggestDropDown.style.visibility = "hidden";
        // offsetWidth is the total width of an element (including borders)
        // element.style.width is width of element without the borders
        // (refer to content-box model at http://www.w3.org/TR/css3-ui/#box-sizing)
        // Internet Explorer uses border-box in quirks mode and content-box in
        // strict mode and doesn't support specifying the box-sizing in CSS
        // when using strict mode (tested in IE7). Therefore currently have
        // to use hard coded values for consistent behaviour
        // in Internet Explorer and Firefox
        this.suggestDropDown.style.width = (this.textbox.offsetWidth -
            (LEFT_BORDER_WIDTH + RIGHT_BORDER_WIDTH)) + "px";

        // when the user clicks on a suggestion, get its value
        // and place it into the textbox
        var self = this;
        this.suggestDropDown.onmousedown = function(event) {
            event = event || window.event;
            target = event.target || event.srcElement;
            self.textbox.value = target.firstChild.nodeValue;
            self.hideSuggestions();
        };
        this.suggestDropDown.onmouseup = function(event) {
            event = event || window.event;
            target = event.target || event.srcElement;
            self.textbox.focus();
        };
        this.suggestDropDown.onmouseover = function(event) {
            event = event || window.event;
            target = event.target || event.srcElement;
            self.highlightSuggestion(target);
        };

        document.body.appendChild(this.suggestDropDown);
    };

    /**
     * Gets the left coordinate of the textbox.
     */
    AutocompleteControl.prototype.getLeft = function() {
        var node = this.textbox;
        var left = 0;
        
        while(node !== null) {
            left += node.offsetLeft;
            node = node.offsetParent;
        }

        return left;
    };

    /**
     * Gets the top coordinate of the textbox.
     */
    AutocompleteControl.prototype.getTop = function() {
        var node = this.textbox;
        var top = 0;

        while(node !== null) {
            top += node.offsetTop;
            node = node.offsetParent;          
        }

        return top;
    };

    /**
     * Handles selecting suggestions with keyboard
     */
    AutocompleteControl.prototype.handleKeyDown = function(event) {
        switch(event.keyCode) {
            case 40: // down arrow
                this.nextSuggestion();
                break;
            case 38: // up arrow
                this.previousSuggestion();
                break;
            case 13: // enter
                if (this.hideSuggestions() && event.preventDefault) {
                    // Prevent the enter for selecting from drop down
                    // from submitting the form
                    event.preventDefault();
                }
                break;
        }
    };

    /**
     * Handles updating suggestions in response to changes in textbox
     */
    AutocompleteControl.prototype.handleKeyUp = function(event) {
        var keyCode = event.keyCode;
    	if (isInputKey(keyCode)) {
    		this.lastKeyCode = keyCode;

    		// Register function to handle updates
    		if (!this.isRegistered) {
    		    var self = this;

    			// Register updateSuggestions to be called
    			// We uses a timeout callback function to avoid constantly requesting
    			// the provider for suggestions. Therefore it is friendly to AJAX
    			// suggestion providers that need to perform opeartions such as database queries
    			setTimeout(function() {
                    self.updateSuggestions();
                    self.isRegistered = false;
                }, this.delay);

    			this.isRegistered = true;
    		}
    	}
    };

    /**
     * Hides the suggestion dropdown.
     */
    AutocompleteControl.prototype.hideSuggestions = function() {
        if (this.suggestDropDown.style.visibility === "hidden") {
            return false;
        }

        this.selectionIndex = -1;
        this.suggestDropDown.style.visibility = "hidden";
        return true;
    };

    /**
     * Highlights the given node in the suggestions dropdown.
     */
    AutocompleteControl.prototype.highlightSuggestion = function(targetSuggestionNode) {
        for (var i = 0, n = this.suggestDropDown.childNodes.length; i < n; i++) {
            var suggestionNode = this.suggestDropDown.childNodes[i];
            if (suggestionNode === targetSuggestionNode) {
                suggestionNode.className = "current";
                this.selectionIndex = i;
            } else if (suggestionNode.className === "current") {
                suggestionNode.className = "";
            }
        }
    };

    /**
     * Return true if keyCode affects the input
     */
    function isInputKey(keyCode) {
        if (keyCode === 8 || keyCode === 46) { // Backspace, Delete
            return true;
        } else if (keyCode < 32) { // Tab, Enter, Shift, Ctrl, Alt, CapsLock, Pause/Break, Esc
            return false;
        } else if (keyCode >= 33 && keyCode < 46) {  // Page Up/Down, Insert, Arrow Keys
            return false;
        } else if (keyCode >= 112 && keyCode <= 123) { // F1 - F12 keys
            return false;
        } else if (keyCode === 144 || keyCode === 145) { // Numlock, Scroll Lock
            return false;
        } else {
            return true;
        }
    };

    /**
     * Highlights the next suggestion in the dropdown and
     * places the suggestion into the textbox.
     */
    AutocompleteControl.prototype.nextSuggestion = function() {
        var suggestionNodes = this.suggestDropDown.childNodes;

        if (suggestionNodes.length > 0 && this.selectionIndex < suggestionNodes.length - 1) {
            var nextSuggestionNode = suggestionNodes[++this.selectionIndex];
            this.highlightSuggestion(nextSuggestionNode);
            this.textbox.value = nextSuggestionNode.firstChild.nodeValue;
        }
    };

    /**
     * Highlights the previous suggestion in the dropdown and
     * places the suggestion into the textbox.
     */
    AutocompleteControl.prototype.previousSuggestion = function () {
        var suggestionNodes = this.suggestDropDown.childNodes;

        if (suggestionNodes.length > 0 && this.selectionIndex > 0) {
            var prevSuggestionNode = suggestionNodes[--this.selectionIndex];
            this.highlightSuggestion(prevSuggestionNode);
            this.textbox.value = prevSuggestionNode.firstChild.nodeValue;
        }
    };

    /**
     * Builds the suggestion dropdown contents, moves it into position,
     * and displays the layer.
     */
    AutocompleteControl.prototype.showSuggestions = function(suggestions) {
        this.suggestDropDown.innerHTML = ""; // Clear the drop down

        var suggestionNode = null;
        for (var i = 0, n = suggestions.length; i < n; i++) {
            suggestionNode = document.createElement("div");
            suggestionNode.appendChild(document.createTextNode(suggestions[i]));
            this.suggestDropDown.appendChild(suggestionNode);
        }

        this.suggestDropDown.style.left = this.getLeft() + "px";
        this.suggestDropDown.style.top = (this.getTop() + this.textbox.offsetHeight) + "px";
        this.suggestDropDown.style.visibility = "visible";
    };

    function trim(str) {
        return str.replace(/^\s*|\s*$/g, "");
    }

    /**
     * Update list of suggestions
     */
    AutocompleteControl.prototype.updateSuggestions = function(){
        var value = trim(this.textbox.value);
        if (value === '') {
            this.hideSuggestions();
            return;
        }

        // Request suggestions from the suggestion provider
    	var suggestions = this.provider(value);

        if (suggestions.length > 0) {
            this.selectionIndex = -1; // Clear selection
            this.showSuggestions(suggestions);
        } else {
            this.hideSuggestions();
        }
    }

    return {
        /**
         * Setup autocomplete on textbox.
         *
         * @param textbox HTMLInputElement The text input element to attach autocomplete to
         * @param suggestionProvider Function A function that returns the possible suggestions
         * @param delay Integer The frequency in ms to check for updates to the textbox
         */
        setup: function(textbox, suggestionProvider, delay) {
            new AutocompleteControl(textbox, suggestionProvider, delay);
        }
    }
}();

