(function($) {
    $.Autocompleter = function($elem, options) {
        this.cacheData_ = {};
        this.cacheLength_ = 0;
    	this.selectClass_ = 'jquery-autocomplete-selected-item';
        this.keyTimeout_ = null;
        this.lastKeyPressed_ = null;
        this.lastProcessedValue_ = null;
        this.lastSelectedValue_ = null;
        this.active_ = false;
        this.finishOnBlur_ = true;
        if (!$elem || !($elem instanceof jQuery) || $elem.length !== 1 || $elem.get(0).tagName.toUpperCase() !== 'INPUT') {
            alert('Invalid parameter for jquery.Autocompleter, jQuery object with one element with INPUT tag expected');
            return;
        }
        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;}
        this.dom = {};
        this.dom.$elem = $elem;
		if (this.options.inputClass) {this.dom.$elem.addClass(this.options.inputClass);}
		this.dom.$results = $('<div style="float:left"></div>').hide();
		if (this.options.resultsClass) {this.dom.$results.addClass(this.options.resultsClass);}
		this.dom.$results.css({position: 'absolute'});
		$('body').append(this.dom.$results);
        var self = this;
		$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;

				case 27: // escape
					if (self.active_) {
						e.preventDefault();
						self.finish();
						return false;
					}
				break;
				default:
					self.activate();
			}
		});
		$elem.blur(function() {
			if (self.finishOnBlur_) {
				setTimeout(function() { self.finish(); }, 200);
			}
		});
    };
    $.Autocompleter.prototype.position = function() {
        var offset = this.dom.$elem.offset();
		this.dom.$results.css({
			top: offset.top + this.dom.$elem.outerHeight(),
			left: offset.left
		});
    };
	$.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 (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 {
		    var self = this;
			this.dom.$elem.addClass(this.options.loadingClass);
		    var ajaxCallback = function(data) {
		        var parsed = false;
		        if (data !== false) {
    				parsed = self.parseRemoteData(data);
    				self.cacheWrite(filter, parsed);
		        }
				self.dom.$elem.removeClass(self.options.loadingClass);
				callback(parsed);
		    };
			$.ajax({
                url: this.makeUrl(filter),
                success: ajaxCallback,
				error: function() {
				    ajaxCallback(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);
		if (this.options.paramName === false) {url += encodeURIComponent(param);} else {params[paramName] = param;}
		var urlAppend = [];
		$.each(params, function(index, value) {urlAppend.push(self.makeUrlParam(index, value));	});
		if (urlAppend.length) {
    		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;
		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();
			results.push({ value: unescape(value), data: data });
		}
		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;
		var regex, pattern, attributes = '';
		for (i = 0; i < results.length; i++) {
			result = results[i];
			type = typeof result;
			if (type === 'string') {
				value = result;
				data = {};
			} else if ($.isArray(result)) {
				value = result.shift();
				data = result;
			} else if (type === 'object') {
				value = result.value;
				data = result.data;
			}
			value = String(value);
			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 });}
			}}
		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.click(function() {
				var $this = $(this);
				self.selectItem($this);
			}).mousedown(function() {
				self.finishOnBlur_ = false;
			}).mouseup(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);
			}
		}

		// Alway recalculate position before showing since window size or
		// input element location may have changed. This fixes #14
		this.position();

		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);
		}
	};

	$.Autocompleter.prototype.showResult = function(value, data) {
		if ($.isFunction(this.options.showResult)) {
			return this.options.showResult(value, data);
		} else {
			return 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.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 displayValue = this.displayValue(value, data);
		this.lastProcessedValue_ = displayValue;
		this.lastSelectedValue_ = displayValue;
		this.dom.$elem.val(displayValue).focus();
		this.setCaret(displayValue.length);
		this.callHook('onItemSelect', { value: value, data: data });
		this.finish();
	};

	$.Autocompleter.prototype.displayValue = function(value, data) {
		if ($.isFunction(this.options.displayValue)) {
			return this.options.displayValue(value, data);
		} 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);
	};
    $.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);
		});

	};
	$.fn.autocomplete.defaults = {
	    paramName: 'q',
		minChars: 1,
		loadingClass: 'acLoading',
		resultsClass: 'acResults',
		inputClass: 'acInput',
		selectClass: 'acSelect',
		mustMatch: false,
		matchCase: false,
		matchInside: true,
		matchSubset: true,
		useCache: true,
		maxCacheLength: 10,
		autoFill: false,
		sortResults: true,
		sortFunction: false,
		onItemSelect: false,
		onNoMatch: false
	};

})(jQuery);

