// 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({}, {
            internalId:'',
            url: url,
            queryParam: 'q',
            method: "GET",
            hintText: undefined,
            hintClass: '',
            searchDelay: 50,
            activeClass: 'active',
            listClass: '',
            minChars: 1,
            split: undefined,
            splitText: "",
            focusInput: false,
            blockInvalidInputSubmit : false,
            autoSelectFirstResult: false,
            preSubmitCallback: function(selected_value) {},
            onFocusCallback:function(self){},
            onBlurCallback:function(self){},
            autoSubmit: true,
            positionTopOffset:0,
            positionLeftOffset:0
        }, 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];
        
        var selected_value = {};
        return this.each(function() {
            var self = $(this);
            
            var results = [];
            var selected = -1;
            var cursor_pos = 0;
            var searched_term = '';
            var submit = self.siblings('input').first(); //Assume submit btn to be first submit button after input
            var list = $('<ul class="' + opt.listClass + '"></ul>');
            
            var delay_timeout;
            var cache = new $.fn.autocomplete.cache(); // Basic cache to save on db hits
            
            // Turn off browser autocomplete on the input so we don't interfere
            self.attr('autocomplete', 'off');

            // Initialize list styles based on input box and insert it into the DOM
            list.hide();
            var $frame = $('<div class="autocomplete"></div>');
            $frame.append(list);
            self.after($frame);
            
            // Wrap opt in parseInt in the event implementor passes in number wrapped in ticks/quotes into the init
            var p_top_offset = parseInt(opt.positionTopOffset, false);
            var p_left_offset = parseInt(opt.positionLeftOffset, false);
            
            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();
            
            // We get the width of the list and the font parameters from the text box,
            // however we make sure that the font-style is 
            list.css({top: p_top_offset + input_offset.top + self.outerHeight(), 
                left: p_left_offset + input_offset.left, 
                width: self.outerWidth() - list_border_width
            });

            // Initialize input, fill in hintText if needed
            if(opt.hintText && (self.val() == "" || self.val() == opt.hintText)) {
                self.addClass(opt.hintClass).val(opt.hintText);
            }

            // If told to, tigger event 'focus' on form field
            if(opt.focusInput) {
                self.focus();
            }

            // Here we define a series of functions used in various places
            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 = -1;
                cursor_pos = 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() {
                self.focus();
                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 current_text = self.val();
                var term = last_token();
                searched_term = term;
                cursor_pos = current_text.length - term.length;

                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) {
                    var list_item = null;
                    list.empty();
                    for(i in results) {
                        list_item = $('<li>'+results[i].display_name+'</li>');

                        // Add css from textbox to item
                        list_item.css({
                            'font-family': self.css('font-family'),
                            'font-size': self.css('font-size'),
                            'font-weight': self.css('font-weight'),
                            'font-style': self.css('font-style'),
                            'padding-left': self.css('padding-left'),
                            'padding-right': self.css('padding-right')
                        });

                        list.append(list_item);
                    }
                    // Add list item selction event handlers
                    list.children()
                        .on('click', function() {
                            selected = list.children().index(this);
                            self.closest("form").submit();
                            return false;
                        })
                        .on('mouseover', function() {
                            selected = list.children().index(this);
                            select();
                        });
                    list.show();
                    
                    
                    if(opt.autoSelectFirstResult) {
                        selected = 0;
                        select();
                    }
                }
            };
            
            var add_term = function() {
                if(selected != -1 && results.length > 0) {
                    var current_text = self.val();
                    var new_text = 
                        current_text.substring(0, cursor_pos)
                        + results[selected].search_name;
                    self.val(new_text);
                    selected_value = results[selected];
                    reset();
                    focus_end();
                }

            };

            var add_term_and_continue = function() {
                if(selected != -1 && results.length > 0) {
                    var current_text = self.val();
                    var new_text = 
                        current_text.substring(0, cursor_pos)
                        + results[selected].search_name
                        + opt.splitText;
                    self.val(new_text);
                }
            };
            
            var revert_term = function() {
                var current_text = self.val();
                var new_text = 
                    current_text.substring(0, cursor_pos)
                    + searched_term;
                self.val(new_text);
            };
            
            var reset_input = function(e) {
                reset();

                if(opt.hintText && self.val() == "") {
                    self.addClass(opt.hintClass).val(opt.hintText);
                }
            };
            
            // A defined function that will be used in a bind event
            // this is an important function used to unbind/bind events to list items
            // messing with this could break click selection of items in the html list
            var blur_reset_with_callback = function(){
                reset_input();
                opt.onBlurCallback(self);
            };

            // 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.on('mouseover',function() {
                    self.off("blur", blur_reset_with_callback);
                })
                .on('mouseout', function() {
                    self.on("blur", blur_reset_with_callback);
                });
                
                
            // Set up submit button event handlers
            // When the mouse is over the button, unbind the input blur event
            // because it interferes with the list item click events we need
            // to select a list item 
            if(submit) {
                submit.on('mouseover', function() {
                    self.off("blur", blur_reset_with_callback);
                })
                .on('mouseout', function() {
                    self.on("blur", blur_reset_with_callback);
                });
            }
            
            // bind the actual event for input blur
            self.on('blur', blur_reset_with_callback);
            
            // Setup up input event handlers
            self.on('keydown', function(e) {
                if(e.keyCode == KEY.TAB) {
                    if(results.length > 0) {
                        reset();
                        focus_end();
                        return false;
                    }
                    else {
                        return true;
                    }
                }
                else if(e.keyCode == KEY.ESCAPE) {
                    reset();
                }
                else if(e.keyCode == KEY.DOWN && (selected + 1 < results.length)) {
                    selected++;
                    select();
                    add_term_and_continue();
                }
                else if(e.keyCode == KEY.UP) {
                    if(selected >= 0) {
                        selected--;
                        select();
                    }
                    if(selected >= 0) {
                        add_term_and_continue();
                    }
                    else {
                        revert_term();
                    }
                }
                else if(e.keyCode == KEY.RETURN){
                    self.closest("form").trigger('submit');
                    //alert('explicit use js to trigger autocompleter submit');
                }
                else {
                    return true;
                }
                return false;
            });

            self.on('keyup', function(e) {
                if($(NAV_KEYS).index(e.keyCode) == -1 && e.keyCode != KEY.SPACE) {
                    search();
                }
            });
            
            self.on('focus', function() {
                if(opt.hintText && self.val() == opt.hintText) {
                    self.val("").removeClass(opt.hintClass);
                }
                
                // THIS CALLBACK HAS GOT TO OCCUR AFTER THE ABOVE CODE!!!!
                // AND NOT BEFORE
                // Run callback handler, serves as a hook
                opt.onFocusCallback(self);
            });
            
            self.closest("form").on('submit', function(e) {
                e.stopPropagation();
                
                add_term();
                
                opt.preSubmitCallback(self, selected_value);
                
                if(!opt.autoSubmit) {
                    return false;
                }
                
                if(opt.blockInvalidInputSubmit) {
                    if(self.val() == "" || self.val() == opt.hintText) {
                        self.focus();
                        return false;
                    }
                    return true;
                }
                else {
                    return true;
                }
            });
        });
    };
    
    
    $.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);
