var Autocompleter = Class.create({

// Constructor
    initialize: function(divId, options) {
        // Update the options hash
        this.options = $H({
            displayLimit: 8,
            defaultInputText: "",
            data: null,
            ajax: null
        });
        this.options.update(options);

        // Class variables
        this.data = this.options.get('data');
        this.highlightedLi = null;
        this.liArray = null;

        // Identify the autocompleter div
        this.div = $(divId);

        // Create an input element and add it to the div
        this.input = new Element('input', {
            'type': 'text',
            'id': "" + this.div.id.replace("-autocompleter", "") + "-input"
        }).addClassName('autocompleter-input');
        this.div.insert(this.input);
        // Create the default input text if the option is supplied
        if(!this.options.get('defaultInputText').blank()) {
            this.input.value = this.options.get('defaultInputText');
            this.input.addClassName('autocompleter-default-input-text');
            Event.observe(this.input, 'focus', this.setDefaultInputText.bindAsEventListener(this));
            Event.observe(this.input, 'blur', this.setDefaultInputText.bindAsEventListener(this));
        }

        // Add a keyboard handler to the input box
        this.input.observe('keydown', function(event) {
            switch(event.keyCode) {
                case Event.KEY_UP:
                    event.stop();
                    this.move('up');
                    break;
                case Event.KEY_DOWN:
                    event.stop();
                    this.move('down');
                    break;
                case Event.KEY_RETURN:
                    event.stop();
                    if(this.highlightedLi != null) {
                        this.lockInputValue(this.highlightedLi.id.replace(new RegExp('.*-'), ""), this.highlightedLi.innerHTML);
                        //this.input.value = this.highlightedLi.innerHTML.stripTags();
                        //this.input.blur();
                        //this.hideUl();
                    }
                    break;
                case Event.KEY_ESC:
                    this.hideUl();
                    break;
            }
        }.bind(this));

        // Create a new UL and insert it after the input element
        this.ul = new Element('ul', {
            'id': "" + this.div.id.replace("-autocompleter", "") + "-list"
        }).addClassName('autocompleter-ul').hide();
        this.div.insert(this.ul, {
            'after': this.input
        });
        this.autoSizeUl();

        // Initialize a list passed in the options
        if(this.options.get('data') != null) {
            this.updateData(this.data);
            Event.observe(this.input, 'keyup', this.filterUl.bindAsEventListener(this));
            Event.observe(this.input, 'focus', function() { this.showUl(); }.bind(this));
            //Event.observe(this.input, 'blur', function() { this.hideUl(); }.bind(this));
        }
        // Or use an AJAX request as a source for a new list
        else if(this.options.get('ajax') != null) {
            // AJAX variables
            this.ajaxRequest = new Ajax.Request('');
            this.ajaxTimeout = null;
            this.timeOfLastKeyStroke = null;
            this.timeOfCurrentKeyStroke = null;
            this.lastSearchString = null;

            Event.observe(this.input, 'keyup', this.updateAndShowListUsingAjax.bindAsEventListener(this));
            Event.observe(this.input, 'focus', this.showUl.bindAsEventListener(this));
            //Event.observe(this.input, 'blur', function() { this.hideList(); }.bind(this));
        }

        //this.input.focus();
    },

    // Used to display or clear the default input text if specified in the options
    setDefaultInputText: function() {
        if(this.input.getValue() == this.options.get('defaultInputText')) {
            this.input.value = "";
            this.input.removeClassName('autocompleter-default-input-text');
        }
        else if(this.input.getValue().blank()) {
            this.input.value = this.options.get('defaultInputText');
            this.input.addClassName('autocompleter-default-input-text');
        }
    },

    updateData: function(data) {
        // Update the list as a new Hash object
        this.data = $H(data);

        this.updateUl();
    },

    // Create LI items and insert them into the UL based on what's in this.data
    updateUl: function() {
        // Used to maintain scope
        var that = this;

        // Clear the list
        this.clearUl();

        // Update the UL with the new LI items
        if(this.data.size() == 0) { // If there is nothing in this.data
            var li = new Element('li').update("No results found.").addClassName('autocompleter-li');
            this.ul.insert(li);
        }
        else { // If there is something in this.data
            this.data.each(function(pair) {
                var li = new Element('li', {
                    'id': "" + that.div.id.replace("-autocompleter", "") + "-" + pair.key
                }).update(pair.value).addClassName('autocompleter-li');

                Event.observe(li, 'click', function() { this.lockInputValue(pair.key, pair.value); }.bind(that));
                Event.observe(li, 'mouseover', function() {
                    that.highlight(li);
                    that.highlightedLi = li;
                });

                that.ul.insert(li);
            });
        }

        this.liArray = this.ul.childElements();
    },

    autoSizeUl: function() {
        this.ul.setStyle({
            'width': this.input.getWidth() + "px",
            'height': this.input.getHeight() * this.options.get('displayLimit') + "px" // Set the default UL height based on the displayLimit
        });
    },

    filterUl: function(event) {
        if(event.keyCode == Event.KEY_UP || event.keyCode == Event.KEY_DOWN || event.keyCode == Event.KEY_LEFT || event.keyCode == Event.KEY_RIGHT || event.keyCode == Event.KEY_RETURN) {
            return null;
        }

        // Used to maintain scope
        var that = this;

        // Bold the text of each matching character
        this.liArray.each(function(li) {
            var html = li.innerHTML.stripTags(); // Strip the tags from the LI

            if(!that.input.getValue().blank()) {
                html = that.boldMatchingText(html, that.input.getValue().strip().gsub(/\s+/, " "));
            }

            li.update(html);
        });

        // Hide every LI that doesn't match a portion of the input text
        if(!this.input.getValue().blank()) {
            this.showUl();
            var mostMatchingCharacters = 0;
            this.liArray.each(function(li) {
                var matchingCharacters = 0;
                li.getElementsBySelector('b').each(function(element) {
                    matchingCharacters += element.innerHTML.length;
                })
                if(matchingCharacters == 0) {
                    li.hide();
                }
                // Select (highlight) the LI that has the most character matches
                else if(matchingCharacters > mostMatchingCharacters) {
                    that.highlight(li);
                    mostMatchingCharacters = matchingCharacters;
                }
            });
        }
        else {
            this.highlight(null);
            this.showUl();
        }
    },

    boldMatchingText: function(html, highlight) {
        return html.gsub(new RegExp(highlight,'i'), function(match) {
        //return html.gsub(new RegExp("(^|\\s)"+highlight,'i'), function(match) {
            return '<b>' + match[0] + '</b>';
        });
    },

    // Show the UL
    showUl: function() {
        // Show the UL
        if(this.data != null && this.data.size() != 0) {
            this.ul.show();
            this.ul.childElements().each(function(element) {
                element.show();
            });
        }
    },

    // Hide the UL
    hideUl: function() {
        // Hide the UL
        //this.ul.hide();
        var effect = new Effect.Fade(this.ul, {duration: .1});
    },

    // Remove all of the LI items from the UL
    clearUl: function() {
        this.ul.descendants().each(function(element) {
            element.remove()
        });
    },

    // Highlight's the currently selected item in the UL
    highlight: function(li) {
        // Remove the selected class from all elements
        this.liArray.each(function(element) {
            element.removeClassName('autocompleter-highlighted');
        });
        // Add the highlighted class to the selected item
        if(li != null) {
            li.addClassName('autocompleter-highlighted');
        }
    },

    // Use the keyboard to move through items in the list
    move: function(direction) {
        if(this.highlightedLi == null && direction == "down") { // Special starting case
            this.highlightedLi = this.getNextVisibleElement(this.liArray[0], this.liArray);
        }
        else if(direction == "down") {
            this.highlightedLi = this.getNextVisibleElement(this.highlightedLi.next(), this.liArray);
        }
        else if(this.highlightedLi == null && direction == "up") { // Special starting case
            this.highlightedLi = this.getPreviousVisibleElement(this.liArray[this.liArray.length - 1], this.liArray);
        }
        else if(direction == "up") {
            this.highlightedLi = this.getPreviousVisibleElement(this.highlightedLi.previous(), this.liArray);
        }

        // Highlight the current selection
        this.highlight(this.highlightedLi);

        // Scroll to the current selection
        this.ul.scrollTop = this.highlightedLi.positionedOffset()[1] - this.highlightedLi.getHeight();
    },

    getNextVisibleElement: function(element, elementArray) {
        if(element == null) {
            return this.getNextVisibleElement(elementArray[0], elementArray);
        }
        if(!element.visible()) {
            return this.getNextVisibleElement(element.next(), elementArray);
        }
        return element;
    },

    getPreviousVisibleElement: function(element, elementArray) {
        if(element == null) {
            return this.getPreviousVisibleElement(elementArray[elementArray.length - 1], elementArray);
        }
        if(!element.visible()) {
            return this.getPreviousVisibleElement(element.previous(), elementArray);
        }
        return element;
    },

    // Update list using AJAX
    updateAndShowListUsingAjax: function(event) {
        // THIS BREAKS IE7 - FIX THIS!!! =]
        if(event.keyCode == Event.KEY_UP || event.keyCode == Event.KEY_DOWN || event.keyCode == Event.KEY_LEFT || event.keyCode == Event.KEY_RIGHT || event.keyCode == Event.KEY_RETURN) {
            return null;
        }

        // Used to maintain scope
        var that = this;

        // Get the value of the input
        var string = this.input.getValue().strip();

        // Clear the list and show the loader LI
        if(string != this.lastSearchString && !string.blank()) {
            this.clearUl();
            this.ul.insert(new Element('li').update("Searching...").addClassName('autocompleter-li')).show();
            // Resize the UL if it isn't displayed (could have height or width of zero if it was initialized when display: none
            if(this.ul.getHeight() == 0 || this.ul.getWidth() == 0) {
                this.autoSizeUl();
            }
            // Show the loader
            this.input.addClassName('autocompleter-ajax-loader');
        }
        else if(string.blank()) {
            this.hideUl();
        }

        // Abort the previous request to prevent overlap
        //this.ajaxRequest.transport.abort();

        // Special starting case, reset the last keystroke if it has been more than 500 ms since they've typed
        if(this.timeOfLastKeyStroke == null || this.timeOfLastKeyStroke < new Date().getTime() - 500) {
            this.timeOfLastKeyStroke = new Date().getTime();
        }

        // Get the current keystroke time
        this.timeOfCurrentKeyStroke = new Date().getTime();

        // If it has been less than 300 ms since the last keystroke delay the request
        if(this.timeOfLastKeyStroke > this.timeOfCurrentKeyStroke - 300) {
            // Wait for them to finish typing
            clearTimeout(this.ajaxTimeout);
            this.ajaxTimeout = setTimeout(function() { that.updateAndShowListUsingAjax(event); }, 350);
        }
        else if(string != this.lastSearchString && !string.blank()) {
            var url = that.options.get('ajax').url;
            var parameters = $H({"string": string }).update(that.options.get('ajax').parameters);
            this.ajaxRequest = new Ajax.Request(url, {
                method: 'post',
                parameters: parameters,
                onSuccess: function(transport) {
                    that.input.removeClassName('autocompleter-ajax-loader');
                    that.lastSearchString = string;
                    that.updateData(transport.responseText.evalJSON(true));
                    that.showUl();
                }
            });
        }

        this.timeOfLastKeyStroke = this.timeOfCurrentKeyStroke;
    },

    lockInputValue: function(key, value) {
        this.input.fire(this.input.id + ':inputStateChange', {'inputState': 'locked'}); // Must come before the input is disabled
        this.input.name = key;
        this.input.value = value.stripTags(); // Strip any tags
        this.input.disable();
        this.hideUl();

        var p = new Element('a', {'id': 'unlockInputValue'}).update("(Change)");
        Event.observe(p, 'click', this.unlockInputValue.bindAsEventListener(this));
        this.div.insert(p, {
            'after': this.input
        });
    },

    unlockInputValue: function() {
        this.input.enable();
        this.input.fire(this.input.id + ':inputStateChange', {'inputState': 'unlocked'}); // Must come after the input is enabled
        this.input.focus();
        this.input.value = "";
        this.ul.hide();
        $('unlockInputValue').remove();
    }
});