// Usage:
// ------
// 
//   $("#text_field").autocomplete("http://example.com/search-complete", {split: /,\s?/});
// 
// License:
// --------
// 
// The MIT License, see README
// 
// Copyright (c) 2009 Ara Anjargolian

(function($) {
    $.fn.autocomplete = function(url, opt) {
        opt = $.extend({}, {
            url: url,
            queryParam: 'q',
            method: "GET",
            hintText: undefined,
            searchDelay: 50,
            activeClass: 'active',
            listClass: 'autocomplete',
            minChars: 1,
            split: undefined
        }, opt);

        var KEY = {
            BACKSPACE: 8,
            TAB: 9,
            RETURN: 13,
            ESCAPE: 27,
            SPACE: 32,
            LEFT: 37,
            UP: 38,
            RIGHT: 39,
            DOWN: 40
        };
        var NAV_KEYS = [KEY.TAB, KEY.RETURN, KEY.ESCAPE, KEY.LEFT, KEY.UP, KEY.RIGHT, KEY.DOWN];
    
        return this.each(function() {
            var self = $(this);
            var results = [];
            var selected = 0;
            var list = $('<ul class="' + opt.listClass + '"></ul>');
            var delay_timeout;
            var cache = new $.fn.autocomplete.cache(); // Basic cache to save on db hits
            var list
            
            // Initialize input, fill in hintText if needed
            self.attr('autocomplete', 'off');  
            if(opt.hintText && (self.val() == "" || self.val() == opt.hintText)) {
                self.addClass("hint").val(opt.hintText);
            }
            
            // Initialize list styles based on input box and insert it into the DOM
            list.hide();
            self.after(list);
            var p_top_offset = 0;
            var p_left_offset = 0;
            var positioned_parent = self.offsetParent();
            if(positioned_parent) {
                var p_offset = positioned_parent.offset();
                p_top_offset = -1 * p_offset.top;
                p_left_offset = -1 * p_offset.left;
            }
            var input_offset = self.offset();
            var list_border_width = list.outerWidth() - list.innerWidth();
            list.css({top: p_top_offset + input_offset.top + self.outerHeight(), 
                left: p_left_offset + input_offset.left, 
                width: self.outerWidth() - list_border_width});

            var last_token = function() {
                if(opt.split) {
                    var tokens = (self.val() + 'x').split(opt.split);
                    var word = tokens[tokens.length-1];
                    return word.replace(/x$/, '');
                } else {
                    return self.val();
                }
            };
                  
            var select = function() {
                list.find('li').removeClass(opt.activeClass)
                    .slice(selected, selected + 1).addClass(opt.activeClass);
            };

            var reset = function() {
                list.empty();
                selected = 0;
                results = [];
                list.hide();
            };
            
            // Bring to focus to the end of the input box, scrolling so the caret is visible
            // DOES NOT WORK ON SAFARI!
            var focus_end = function() {
                input = self.get(0);
                end = input.value.length;

                // IE end of text box focus
                if(input.createTextRange) {
                    var range = input.createTextRange();
                    range.move('character', end);
                    range.select();
                    input.focus();
                }
                else {
                    if(input.setSelectionRange) {
                        // Chrome end of text box focus
                        input.setSelectionRange(end, end);
                        input.focus();
                        
                        // Workaround for FF overflow no scroll problem
                        // Trigger a "space" keypress.
                        var evt = document.createEvent("KeyboardEvent");
                        // Only FF should have initKeyEvent, Chrome/Safari will error out!
                        if(evt.initKeyEvent) {
                            evt.initKeyEvent("keypress", true, true, null, 
                                false, false, false, false, 32, 32);
                            input.dispatchEvent(evt);
                            // Trigger a "backspace" keypress.
                            evt = document.createEvent("KeyboardEvent");
                            evt.initKeyEvent("keypress", true, true, null, 
                                false, false, false, false, 8, 0);
                            input.dispatchEvent(evt);
                        }
                    }
                    else {
                        input.focus();
                    }
                }
            };

            var search = function() {
                reset();
                var term = last_token();

                if (term.length >= opt.minChars) {
                    if(opt.searchDelay > 0) {
                        clearTimeout(delay_timeout);
                        delay_timeout = setTimeout(function(){run_search(term);}, 
                            opt.searchDelay);
                    }
                    else {
                        run_search(term);
                    }
                }
            };
            
            var run_search = function(term) {
                var cached_results = cache.get(term);
                if(cached_results) {
                    results = cached_results
                    populate_list();
                    return;
                }
                
                var callback = function(data) {
                    // If the current term in the box does not match
                    // what was searched then these results are outdated
                    // but let's cache them because they might be useful later
                    var current_term = last_token();
                    if(term != current_term) {
                        cache.add(term, data);
                        return;
                    }
                    results = data;
                    cache.add(term, results);
                    populate_list();
                }

                var query_delimiter = opt.url.indexOf("?") < 0 ? "?" : "&";              
                if(opt.method == "POST") {   
                    $.post(opt.url + query_delimiter + opt.queryParam + "=" + term, 
                        {}, callback, "json");  
                } 
                else {
                    $.get(opt.url + query_delimiter + opt.queryParam + "=" 
                        + encodeURIComponent(term), {}, callback, "json");
                }
            };

            var populate_list = function() {
                if(results.length > 0) {
                    list.empty();
                    for(i in results) {
                        list.append('<li>'+results[i].display+'</li>');
                    }
                    // Add list item selction event handlers
                    list.children()
                        .click(function() {
                            selected = list.children().index(this);
                            add_term();
                            self.bind("blur", reset_input);
                        })
                        .mouseover(function() {
                            selected = list.children().index(this);
                            select();
                        });

                    select();
                    list.show();
                }
            };

            var add_term = function() {
                var current_text = self.val();
                var last_word = last_token();
                var new_text = 
                    current_text.substring(0, current_text.length-last_word.length)
                    + results[selected].term;
                self.val(new_text);
                reset();
                self.focus();
                focus_end();
            };
            
            var reset_input = function() {
                reset();
                if(opt.hintText && self.val() == "") {
                    self.addClass("hint").val(opt.hintText);
                }
            };

            // Set up list events handlers
            // When the mouse is over the list, unbind the input blur event
            // because it interferes with the list item click events we need
            // to select a list item 
            list.mouseover(function() {
                    self.unbind("blur", reset_input);
                })
                .mouseout(function() {
                    self.bind("blur", reset_input);
                });
                
            // Setup up input event handlers
            self.keydown(function(e) {
                if(e.keyCode == KEY.TAB || e.keyCode == KEY.RETURN) {
                    if(results.length > 0) {
                        add_term();
                        return false;
                    }
                    else {
                        return true;
                    }
                }
                else if(e.keyCode == KEY.ESCAPE) {
                    reset();
                }
                else if(e.keyCode == KEY.DOWN && (selected + 1 < results.length)) {
                    selected++;
                    select();
                }
                else if(e.keyCode == KEY.UP) {
                    if(selected > 0) {
                        selected--;
                        select();
                    }
                }
                else {
                    return true;
                }
                return false;
            });

            self.keyup(function(e) {
                if($(NAV_KEYS).index(e.keyCode) == -1 && e.keyCode != KEY.SPACE) {
                    search();
                }
            });
            
            self.focus(function() {
                if(opt.hintText && self.val() == opt.hintText) {
                     self.val("").removeClass("hint");
                }
            });

            self.bind("blur", reset_input);
        });
    };
    
    $.fn.autocomplete.cache = function(opt) {
        opt = $.extend({}, {
            maxSize: 50
        }, opt);
        
        var data = {};
        var size = 0;

        this.flush = function () {
            data = {};
            size = 0;
        };
    
        this.add = function (term, results) {
            if(size > opt.max_size) {
                flush();
            }
            if(!data[term]) {
                size++;
            }
            data[term] = results;
        };

        this.get = function (term) {
            return data[term];
        };
    };
})(jQuery);