/**
 * jquery.autocomplete.js
 * Version 3.1
 * Copyright (c) Dylan Verheul <dylan.verheul@gmail.com>
 */
(function($) {

    /**
     * Autocompleter Object
     * @param {jQuery} $elem jQuery object with one input tag
     * @param {Object=} options Settings
     * @constructor
     */
    $.Autocompleter = function($elem, options) {

        /**
         * Cached data
         * @type Object
         * @private
         */
        this.cacheData_ = {};

        /**
         * Number of cached data items
         * @type number
         * @private
         */
        this.cacheLength_ = 0;

        /**
         * Class name to mark selected item
         * @type string
         * @private
         */
        this.selectClass_ = 'jquery-autocomplete-selected-item';

        /**
         * Handler to activation timeout
         * @type ?number
         * @private
         */
        this.keyTimeout_ = null;
        
        /**
         * Last key pressed in the input field (store for behavior)
         * @type ?number
         * @private
         */
        this.lastKeyPressed_ = null;
        
        /**
         * Last value processed by the autocompleter
         * @type ?string
         * @private
         */
        this.lastProcessedValue_ = null;
        
        /**
         * Last value selected by the user
         * @type ?string
         * @private
         */
        this.lastSelectedValue_ = null;
        
        /**
         * Is this autocompleter active?
         * @type boolean
         * @private
         */
        this.active_ = false;
        
        /**
         * Is it OK to finish on blur?
         * @type boolean
         * @private
         */
        this.finishOnBlur_ = true;

        /**
         * Assert parameters
         */
        if (!$elem || !($elem instanceof jQuery) || $elem.length !== 1 || ($elem.get(0).tagName.toUpperCase() !== 'INPUT' && $elem.get(0).tagName.toUpperCase() !== 'TEXTAREA')) {
            alert('Invalid parameter for jquery.Autocompleter, jQuery object with one element with INPUT tag expected');
            return;
        }

        /**
         * Init and sanitize options
         */
        if (typeof options === 'string') {
            this.options = { url:options };
        } else {
            this.options = options;            
        }
                this.options.maxCacheLength = parseInt(this.options.maxCacheLength);
                if (isNaN(this.options.maxCacheLength) || this.options.maxCacheLength < 1) {
                        this.options.maxCacheLength = 1;
                }
                this.options.minChars = parseInt(this.options.minChars);
                if (isNaN(this.options.minChars) || this.options.minChars < 1) {
                        this.options.minChars = 1;
                }

        /**
         * Init DOM elements repository
         */
        this.dom = {};
        
        /**
         * Store the input element we're attached to in the repository, add class
         */
        this.dom.$elem = $elem;
                if (this.options.inputClass) {
                        this.dom.$elem.addClass(this.options.inputClass);
                }

        /**
         * Create DOM element to hold results, add class, position according to $elem
         */
                this.dom.$results = $('<div></div>').hide();
                if (this.options.resultsClass) {
                        this.dom.$results.addClass(this.options.resultsClass);
                }
        var offset = this.dom.$elem.offset();
                this.dom.$results.css({
                        position: 'absolute',
                        top: offset.top + $elem.outerHeight(),
                        left: offset.left
                });
                $('body').append(this.dom.$results);

        /**
         * Shortcut to self
         */
        var self = this;

        /**
         * Attach keyboard monitoring to $elem
         */
                $elem.keydown(function(e) {
                        self.lastKeyPressed_ = e.keyCode;
                        switch(self.lastKeyPressed_) {

                                case 38: // up
                                        e.preventDefault;
                                        if (self.active_) {
                                                self.focusPrev();
                                        } else {
                                                self.activate();
                                        }
                                        return false;                                                   
                                break;
                                
                                case 40: // down
                                        e.preventDefault;
                                        if (self.active_) {
                                                self.focusNext();
                                        } else {
                                                self.activate();
                                        }
                                        return false;                                                   
                                break;
                                
                                case 9: // tab
                                case 13: // return
                                        if (self.active_) {
                                                e.preventDefault;
                                                self.selectCurrent();
                                                return false;                                                   
                                        }
                                break;

                                default:
                                        self.activate();
                                        
                        }
                });
                $elem.blur(function() {
                        if (self.finishOnBlur_) {
                                setTimeout(function() { self.finish(); }, 100);                                 
                        }
                });
        
    }
    
        $.Autocompleter.prototype.cacheRead = function(filter) {
                var filterLength, searchLength, search, maxPos, pos;
                if (this.options.useCache) {
                        filter = String(filter);
                        filterLength = filter.length;
                        if (this.options.matchSubset) {
                                searchLength = 1;
                        } else {
                                searchLength = filterLength;
                        }
                        while (searchLength <= filterLength) {
                                if (this.options.matchInside) {
                                        maxPos = filterLength - searchLength;
                                } else {
                                        maxPos = 0;
                                }
                                pos = 0;
                                while (pos <= maxPos) {
                                        search = filter.substr(0, searchLength);
                                        if (this.cacheData_[search] !== undefined) {
                                                return this.cacheData_[search];
                                        }
                                        pos++;
                                }
                                searchLength++;
                        }                               
                }
                return false;
    };

        $.Autocompleter.prototype.cacheWrite = function(filter, data) {
                if (this.options.useCache) {
                        if (this.cacheLength_ >= this.options.maxCacheLength) {
                                this.cacheFlush();
                        }
                        filter = String(filter);
                        if (this.cacheData_[filter] !== undefined) {
                                this.cacheLength_++;
                        }
                        return this.cacheData_[filter] = data;
                }
                return false;       
    };

        $.Autocompleter.prototype.cacheFlush = function() {
            this.cacheData_ = {};
            this.cacheLength_ = 0;
    };

        $.Autocompleter.prototype.callHook = function(hook, data) {
                var f = this.options[hook];
                if (f && $.isFunction(f)) {
                        return f(data, this);
                }
                return false;
        };
        
        $.Autocompleter.prototype.activate = function() {
            var self = this;
            var activateNow = function() {
                self.activateNow(); 
            };
                var delay = parseInt(this.options.delay);
                if (isNaN(delay) || delay <= 0) {
                        delay = 250;
                }
                if (this.keyTimeout_) {
                        clearTimeout(this.keyTimeout_);
                }
                this.keyTimeout_ = setTimeout(activateNow, delay);
        };

    $.Autocompleter.prototype.activateNow = function() {
                var value = this.dom.$elem.val();
                if(this.options.allowMultiples) {
                    var d, arr;
                    d = this.options.delimiter;
                    if (!d) { value = $.trim(value); }
                    arr = value.split(d);
                    value = $.trim(arr[arr.length - 1]);
                }
                if (value !== this.lastProcessedValue_ && value !== this.lastSelectedValue_) {
                        if (value.length >= this.options.minChars) {
                                this.active_ = true;
                                this.lastProcessedValue_ = value;
                                this.fetchData(value);                          
                        }                                                       
                }
        };              
        
        $.Autocompleter.prototype.fetchData = function(value) {
                if (this.options.data) {
                        this.filterAndShowResults(this.options.data, value);
                } else {
                    var self = this;
                        this.fetchRemoteData(value, function(remoteData) {
                                self.filterAndShowResults(remoteData, value);                                   
                        });
                }                       
        };

        $.Autocompleter.prototype.fetchRemoteData = function(filter, callback) {
                var data = this.cacheRead(filter);
                if (data) {
                        callback(data); 
                } else {
                        try {
                    var self = this;
                                this.dom.$elem.addClass(this.options.loadingClass);                             
                                $.get(this.makeUrl(filter), function(data) {
                                        var parsed = self.parseRemoteData(data);
                                        self.cacheWrite(filter, parsed);
                                        self.dom.$elem.removeClass(self.options.loadingClass);
                                        callback(parsed);
                                });                             
                        } catch(e) {
                                this.dom.$elem.removeClass(this.options.loadingClass);
                                callback(false);
                        }                               
                }
        };

    $.Autocompleter.prototype.setExtraParam = function(name, value) {
        var index = $.trim(String(name));
        if (index) {
            if (!this.options.extraParams) {
                this.options.extraParams = {};
            }
            if (this.options.extraParams[index] !== value) {
                this.options.extraParams[index] = value;
                this.cacheFlush();
            }
        }
    };

        $.Autocompleter.prototype.makeUrl = function(param) {
            var self = this;
                var paramName = this.options.paramName || 'q';
                var url = this.options.url;
                var params = $.extend({}, this.options.extraParams);
                var urlAppend = [];
                params[paramName] = param;
                $.each(params, function(index, value) {
                        urlAppend.push(self.makeUrlParam(index, value));
                });
                url += url.indexOf('?') == -1 ? '?' : '&';
                url += urlAppend.join('&');
                return url;
        };
        
        $.Autocompleter.prototype.makeUrlParam = function(name, value) {
                return String(name) + '=' + encodeURIComponent(value);
        }
        
        $.Autocompleter.prototype.parseRemoteData = function(remoteData) {
                var results = [];
                var text = String(remoteData).replace('\r\n', '\n');
                var i, j, data, line, lines = text.split('\n');
                var value, altvalue;
                for (i = 0; i < lines.length; i++) {
                        line = lines[i].split('|');
                        data = [];
                        for (j = 0; j < line.length; j++) {
                                data.push(unescape(line[j]));
                        }
                        value = data.shift();
                        altvalue = (data.length > 1) ? data.pop() : false;  /*************** new ***************/
                        results.push({ value: unescape(value), data: data, altvalue: altvalue});
                }
                return results;
        };
        
        $.Autocompleter.prototype.filterAndShowResults = function(results, filter) {
                this.showResults(this.filterResults(results, filter), filter);
        };
        
        $.Autocompleter.prototype.filterResults = function(results, filter) {
                
                var filtered = [];
                var value, data, i, result, type, altvalue;
                var regex, pattern, attributes = '';
                
                for (i = 0; i < results.length; i++) {
                        result = results[i];
                        type = typeof result;
                        if (type === 'string') {
                                value = result;
                                data = {};
                                altvalue = false;   /******************* new ***************/
                        } else if ($.isArray(result)) {
                                value = result.shift();
                                altvalue = (result.length > 1) ? result.pop() : false;  /****************** new ***************/
                                data = result;
                        } else if (type === 'object') {
                                value = result.value;
                                data = result.data;
                                altvalue = result.altvalue; /**************** new ******************/
                        }
                        value = String(value);
                        altvalue = (altvalue) ? String(altvalue) : false;   /**************** new *********************/
                        // Condition below means we do NOT do empty results
                        if (value) {
                                if (typeof data !== 'object') {
                                        data = {};
                                }
                                pattern = String(filter);
                                if (!this.options.matchInside) {
                                        pattern = '^' + pattern;
                                }
                                if (!this.options.matchCase) {
                                        attributes = 'i';
                                }
                                regex = new RegExp(pattern, attributes);
                                if (regex.test(value)) {
                                        filtered.push({ value: value, data: data, altvalue: altvalue });
                                }                                       
                        }
                }
                
                if (this.options.sortResults) {
                        return this.sortResults(filtered);
                }
                
                return filtered;
                
        };

        $.Autocompleter.prototype.sortResults = function(results) {
            var self = this;
                if ($.isFunction(this.options.sortFunction)) {
                        results.sort(this.options.sortFunction);
                } else {
                        results.sort(function(a, b) { return self.sortValueAlpha(a, b); });
                }
                return results;
        };
        
        $.Autocompleter.prototype.sortValueAlpha = function(a, b) {
                a = String(a.value);
                b = String(b.value);
                if (!this.options.matchCase) {
                        a = a.toLowerCase();
                        b = b.toLowerCase();
                }
                if (a > b) {
                        return 1;
                }
                if (a < b) {
                        return -1;
                }
                return 0;
        };
        
        $.Autocompleter.prototype.showResults = function(results, filter) {
            var self = this;
                var $ul = $('<ul></ul>');
                var i, result, $li, extraWidth, first = false, $first = false;
                var numResults = results.length;
                for (i = 0; i < numResults; i++) {
                        result = results[i];
                        $li = $('<li>' + this.showResult(result.value, result.data) + '</li>');
                        $li.data('value', result.value);
                        $li.data('data', result.data);
                        $li.data('altvalue', result.altvalue); /************** new *******************/
                        $li.click(function() {
                                var $this = $(this);
                                self.selectItem($this);
                        }).mousedown(function() {
                                self.finishOnBlur_ = false;
                        }).mousedown(function() {
                                self.finishOnBlur_ = true;
                        });
                        $ul.append($li);
                        if (first === false) {
                                first = String(result.value);
                                $first = $li;
                                $li.addClass(this.options.firstItemClass);
                        }
                        if (i == numResults - 1) {
                                $li.addClass(this.options.lastItemClass);
                        }
                }
                this.dom.$results.html($ul).show();
                extraWidth = this.dom.$results.outerWidth() - this.dom.$results.width();
                this.dom.$results.width(this.dom.$elem.outerWidth() - extraWidth);
                $('li', this.dom.$results).hover(
                        function() { self.focusItem(this); },
                        function() { /* void */ }
                );
                if (this.autoFill(first, filter)) {
                        this.focusItem($first);
                }
                else {
                    if(this.focusFirstItem(first, filter)) {
                            this.focusItem($first);
                    }
                }
        };
        
        $.Autocompleter.prototype.showResult = function(value, data) {
                if ($.isFunction(this.options.showResult)) {
                        return this.options.showResult(value, data);
                } else {
                        var input = this.dom.$elem.val();
                        if(this.options.allowMultiples) {
                            var d, arr;
                            d = this.options.delimiter;
                            if (!d) { input = $.trim(input); }
                            arr = input.split(d);
                            input = $.trim(arr[arr.length - 1]);
                        }
                        return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + input + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<strong>$1</strong>");
                }                       
        };
        
        $.Autocompleter.prototype.onSelectItem = function(value, data, altvalue) {
                if ($.isFunction(this.options.onSelectItem)) {
                        return this.options.onSelectItem(value, data, altvalue);
                } else {
                        return (altvalue) ? altvalue : value;
                }                       
        };
        
        $.Autocompleter.prototype.autoFill = function(value, filter) {
                var lcValue, lcFilter, valueLength, filterLength;
                if (this.options.autoFill && this.lastKeyPressed_ != 8) {
                        lcValue = String(value).toLowerCase();
                        lcFilter = String(filter).toLowerCase();
                        valueLength = value.length;
                        filterLength = filter.length;
                        if (lcValue.substr(0, filterLength) === lcFilter) {
                                this.dom.$elem.val(value);
                                this.selectRange(filterLength, valueLength);
                                return true;
                        }
                }
                return false;
        };
        
        $.Autocompleter.prototype.focusFirstItem = function(value, filter) {
                var lcValue, lcFilter, valueLength, filterLength;
                if (this.options.focusFirstItem && this.lastKeyPressed_ != 8) {
                    return true;
                }
                return false;
        };
        
        $.Autocompleter.prototype.focusNext = function() {
                this.focusMove(+1);
        };

        $.Autocompleter.prototype.focusPrev = function() {
                this.focusMove(-1);
        };

        $.Autocompleter.prototype.focusMove = function(modifier) {
                var i, $items = $('li', this.dom.$results);
                modifier = parseInt(modifier);
                for (var i = 0; i < $items.length; i++) {
                        if ($($items[i]).hasClass(this.selectClass_)) {
                                this.focusItem(i + modifier);
                                return;
                        }
                }
                this.focusItem(0);
        };
        
        $.Autocompleter.prototype.focusItem = function(item) {
                var $item, $items = $('li', this.dom.$results);
                if ($items.length) {
                        $items.removeClass(this.selectClass_).removeClass(this.options.selectClass);
                        if (typeof item === 'number') {
                                item = parseInt(item);
                                if (item < 0) {
                                        item = 0;
                                } else if (item >= $items.length) {
                                        item = $items.length - 1;
                                }
                                $item = $($items[item]);
                        } else {
                                $item = $(item);
                        }
                        if ($item) {
                                $item.addClass(this.selectClass_).addClass(this.options.selectClass);                           
                        }                               
                }
        };
        
        $.Autocompleter.prototype.selectCurrent = function() {
                var $item = $('li.' + this.selectClass_, this.dom.$results);
                if ($item.length == 1) {
                        this.selectItem($item);                         
                } else {
                        this.finish();
                }
        };
        
        $.Autocompleter.prototype.selectItem = function($li) {
                var value = $li.data('value');
                var data = $li.data('data');
                var altvalue = $li.data('altvalue');
                var displayValue = this.displayValue(value, data, altvalue);
                displayValue = displayValue.replace('&#039;', "'");
                this.lastProcessedValue_ = displayValue;
                this.lastSelectedValue_ = displayValue;
                this.dom.$elem.val(displayValue).focus();
                this.setCaret(displayValue.length);
                this.callHook('onItemSelect', { value: value, data: data, altvalue: altvalue });
                this.onSelectItem(value, data, altvalue);
                this.finish();
        };
        
        $.Autocompleter.prototype.displayValue = function(value, data, altvalue) {
                if ($.isFunction(this.options.displayValue)) {
                        return this.options.displayValue(value, data, altvalue);
                } else {
                        if(this.options.allowMultiples) {
                            var d, arr, input, i;
                            var arr_new = [];
                            d = this.options.delimiter;
                            input = this.dom.$elem.val();
                            if (!d) { return $.trim(input); }
                            arr = input.split(d);
                            value = (altvalue) ? altvalue : value;
                            if(arr.length > 1) {
                                arr.pop();
                                for(i = 0; i < arr.length; i++) {
                                    arr_new.push($.trim(arr[i]));
                                }
                                value = arr_new.join(d+' ')+d+' '+value+d+' ';
                                return value;
                            }
                            else {
                                return value+d+' ';
                            }                            
                        } else {
                            return value;
                        }
                }                       
        };
        
        $.Autocompleter.prototype.finish = function() {
                if (this.keyTimeout_) {
                        clearTimeout(this.keyTimeout_);
                }
                if (this.dom.$elem.val() !== this.lastSelectedValue_) {
                        if (this.options.mustMatch) {
                                this.dom.$elem.val('');                                 
                        }
                        this.callHook('onNoMatch');
                }
                this.dom.$results.hide();
                this.lastKeyPressed_ = null;
                this.lastProcessedValue_ = null;
                if (this.active_) {
                        this.callHook('onFinish');
                }
                this.active_ = false;                   
        };
        
        $.Autocompleter.prototype.selectRange = function(start, end) {
                var input = this.dom.$elem.get(0);
                if (input.setSelectionRange) {
                        input.focus();
                        input.setSelectionRange(start, end);
                } else if (this.createTextRange) {
                        var range = this.createTextRange();
                        range.collapse(true);
                        range.moveEnd('character', end);
                        range.moveStart('character', start);
                        range.select();
                }
        };
        
        $.Autocompleter.prototype.setCaret = function(pos) {
                this.selectRange(pos, pos);
        };
                        
    /**
     * autocomplete plugin
     */
    $.fn.autocomplete = function(options) {

        if (typeof options === 'string') {
            options = { url:options };
        }
        
        var o = $.extend({}, $.fn.autocomplete.defaults, options);

                return this.each(function() {
                    var $this = $(this);
                    var ac = new $.Autocompleter($this, o);
                    $this.data('autocompleter', ac);
                });

        };

    /**
     * Default options for autocomplete plugin
     */
        $.fn.autocomplete.defaults = {
                minChars: 1,
                allowMultiples: false,
                delimiter: ',',                 // only used when 'allowMultiples' is set to true
                loadingClass: 'acLoading',
                resultsClass: 'acResults',
                inputClass: 'acInput',
                selectClass: 'acSelect',
                mustMatch: false,                
                matchCase: false,
                matchInside: true,
                matchSubset: true,
                useCache: false,
                maxCacheLength: 10,
                autoFill: false,
                sortResults: true,
                focusFirstItem: true,
                sortFunction: false,
                onItemSelect: false,
                onSelectItem: false,
                onNoMatch: false
        };
        
})(jQuery);
