(function ($) {

	$.suggest = function (input, options) {

		var $input = $(input).attr("autocomplete", "off");
		var $results = $(document.createElement("ul"));

		var timeout = false; 	// hold timeout ID for suggestion results to appear	
		var prevLength = -1; 		// last recorded length of $input.val()
		var cache = []; 			// cache MRU list
		var cacheLength = 0; 		// length of cache in chars (bytes?)
		var isFocused = false;

		$results.addClass(options.resultsClass).appendTo('body');


		resetPosition();
		$(window)
			.load(resetPosition)		// just in case user is changing size of page while loading
			.resize(resetPosition);

		$input.blur(function () {
			setTimeout(function () { $results.hide() }, 200);
			isFocused = false;
		});
		$input.focus(function () {
			isFocused = true;
		});

		// help IE users if possible
		try {
			//$results.bgiframe();
		} catch (e) { }


		// I really hate browser detection, but I don't see any other way
		if ($.browser.mozilla)
			$input.keypress(processKey); // onkeypress repeats arrow keys in Mozilla/Opera
		else
			$input.keydown(processKey); 	// onkeydown repeats arrow keys in IE/Safari



		function resetPosition() {
			// requires jquery.dimension plugin
			var offset = $input.offset();
			$results.css({
				top: (offset.top + input.offsetHeight) + 'px',
				left: offset.left + 'px'
			});
		}


		function processKey(e) {

			// handling up/down/escape requires results to be visible
			// handling enter/tab requires that AND a result to be selected
			if ((/27$|38$|40$/.test(e.keyCode) && $results.is(':visible')) ||
				(/^13$|^9$/.test(e.keyCode) && getCurrentResult())) {

				if (e.preventDefault)
					e.preventDefault();
				if (e.stopPropagation)
					e.stopPropagation();

				e.cancelBubble = true;
				e.returnValue = false;

				switch (e.keyCode) {

					case 38: // up
						prevResult();
						break;

					case 40: // down
						nextResult();
						break;

					case 9:  // tab
					case 13: // return
						selectCurrentResult();
						break;

					case 27: //	escape
						$results.hide();
						break;

				}

			} else if ($input.val().length != prevLength) {

				if (timeout)
					clearTimeout(timeout);
				timeout = setTimeout(suggest, options.delay);
				prevLength = $input.val().length;

			}


		}


		function suggest() {
			var q = $.trim($input.val());
			var params = $.extend({}, options.params, { search: q });
			for (dyn in options.dynamicParams) {
				params[dyn] = options.dynamicParams[dyn]();
			}

			if (q.length >= options.minchars) {

				cached = checkCache(q);

				if (cached) {
					displayItems(cached['items'], q);
				} else {

					$.getJSON(options.source, params, function (items) {

						$results.hide();

						displayItems(items, q);

						addToCache(q, items);

					});

				}

			} else {

				$results.hide();

			}

		}

		function checkCache(q) {

			for (var i = 0; i < cache.length; i++)
				if (cache[i]['q'] == q) {
					cache.unshift(cache.splice(i, 1)[0]);
					return cache[0];
				}

			return false;

		}

		function addToCache(q, items) {
			length = items.length
			while (cache.length && (cacheLength + length > options.maxCacheLength)) {
				var cached = cache.pop();
				cacheLength -= cached.legth;
			}

			cache.push({
				q: q,
				items: items
			});

			cacheLength += length;

		}

		function displayItems(items, q) {

			if (!items || !isFocused)
				return;

			if (!items.length) {
				$results.hide();
				return;
			}

			html = '';
			$.each(items, function (i, item) {
				var val = item.replace(new RegExp(q, 'ig'),
					function (q) { return '<span class="' + options.matchClass + '">' + q + '</span>' });
				html += '<li>' + val + '</li>';

			});

			$results.html(html).show();

			$results
				.children('li')
				.mouseover(function () {
					$results.children('li').removeClass(options.selectClass);
					$(this).addClass(options.selectClass);
				})
				.click(function (e) {
					e.preventDefault();
					e.stopPropagation();
					selectCurrentResult();
				});

		}

		function getCurrentResult() {

			if (!$results.is(':visible'))
				return false;

			var $currentResult = $results.children('li.' + options.selectClass);

			if (!$currentResult.length)
				$currentResult = false;

			return $currentResult;

		}

		function selectCurrentResult() {

			var currentResult = getCurrentResult();
			if (currentResult) {

				$input.val($(currentResult).text());
				$results.hide();

				if (options.onSelect)
					options.onSelect.apply($input[0]);

			}

		}

		function nextResult() {

			$currentResult = getCurrentResult();

			if ($currentResult)
				$currentResult
					.removeClass(options.selectClass)
					.next()
						.addClass(options.selectClass);
			else
				$results.children('li:first-child').addClass(options.selectClass);

		}

		function prevResult() {

			$currentResult = getCurrentResult();

			if ($currentResult)
				$currentResult
					.removeClass(options.selectClass)
					.prev()
						.addClass(options.selectClass);
			else
				$results.children('li:last-child').addClass(options.selectClass);

		}

	}

	$.fn.suggest = function (source, options) {

		if (!source)
			return;

		options = options || {};
		options.source = source;
		options.delay = options.delay || 200;
		options.resultsClass = options.resultsClass || 'ac_results';
		options.selectClass = options.selectClass || 'ac_over';
		options.matchClass = options.matchClass || 'ac_match';
		options.minchars = options.minchars || 0;
		options.delimiter = options.delimiter || '\n';
		options.onSelect = options.onSelect || false;
		options.maxCacheLength = options.maxCacheLength || 1000;
		options.params = options.params || {};
		options.dynamicParams = options.dynamicParams || {};

		this.each(function () {
			new $.suggest(this, options);
		});

		return this;

	};

})(jQuery);
