/*
 * NAME
 * jquery.autocompleter
 *
 * SYNOPSIS
 * $(selector).autocompleter(hash);
 *
 * DESCRIPTION
 * Associates an autocompletion dropdown menu to an input field. Data are retrieved
 * by an XmlHttpRequest with the following parameters:
 *
 * q : the query string (the first letters of the items to retrieve)
 * o : the offset of the first item to display
 * n : the maximum number of expected items
 *
 * OPTIONS
 * options are passed to the autocomplete constructor in an hash array. The
 * available options are:
 *
 * autofill (default: false)
 * 	Whether or not the first match should be used to autofill in the input
 *	element.
 *
 * count (default: 10)
 * 	Number of results that will be showed in the dropdown menu.
 *
 * delay (default: 400)
 * 	The delay in milliseconds the autocompleter waits after a keystroke to
 * 	activate itself.
 *
 * minchars (default: 3)
 * 	The minimum number of characters to type before the autocompleter activates.
 *
 * selectfirst (default: false)
 * 	The first autocomplete value will be automatically selected on tab/return.
 * 
 * source (required)
 *		The URL to request.
 */
(function($) {
	$.autocompleter = function(input, options) {
		var hasFocus = false; // Input field has focus
		var jInput;		   // Holds the input field DOM object
		var jDropdown;		// Holds the dropdown menu DOM object
		var lastKey = null;   // Last typed key
		var lastQuery = null; // Last requested string
		var offset = 0;	   // Offset of first displayed item
		var results = [];	 // Resulting items
		var selected = -1;	// Currently selected item
		var timeout = null;   // Holds a timer
		var total = 0;		// Total number of the current request

		// Create the drowdown element
		jDropdown = $('<div class="' + options.className + '"></div>').hide();
		$("body").append(jDropdown);

		// Disable Mozilla autocompletion
		jInput = $(input).attr('autocomplete', 'off');

		// Keep track of focus to avoid to show autocompletion when neither input nor menu has focus.
		jInput.blur(function () {hasFocus = false;});
		jInput.focus(function () {hasFocus = true;});

		// Keydown event handler
		jInput.keydown(function(e) {
			if (!options.source)
				return true;
			lastKey = e.keyCode;
			switch(lastKey) {
				case 9:  // Tab key
				case 13: // Return key
					if (selectCurrentItem()) {
						jInput.blur();
						e.preventDefault();
					}
				break;
				case 38: // Up key
				case 40: // Down key
					e.preventDefault();
					moveSelection((lastKey == 38) ? -1 : 1);
				break;
				default:
					selected = -1;
					if (timeout) {
						clearTimeout(timeout);
					}
					timeout = setTimeout(function(){ onChange(); }, options.delay);
				break;
			}
		});

		/**
		 * Fills the input field with the first matching result
		 */
		function autofill(value) {
			if (lastKey != 8) {
				jInput.val(jInput.val() + value.substring(lastQuery.length));
				createSelection(lastQuery.length, value.length);
			}
		};

		/**
		 * Fills the dropdown menu
		 */
		function buildDropdown() {
			jDropdown.html("");

			if ($.browser.msie) {
				jDropdown.append($('<iframe src="javascript:\'&lt;html&gt;&lt;/html&gt;\';" frameborder="0"></iframe>'));
			}

			// Add the previous link
			if (offset > 0) {
				var jLink = $('<a class="prev" href="javascript:;" title="R\u00e9sultats pr\u00e9c\u00e9dents"><span>\u00a0</span></a>');
				jLink.click(function(e) {
					e.preventDefault();
					e.stopPropagation();
					offset = Math.max(0, offset - options.count);
					requestData(lastQuery);
					jInput.focus(); // Set focus back to input field
				});
				jDropdown.append(jLink);
			}

			// Add list items
			var jUl = $('<ul></ul>');
			for (var i = 0; i < results.length; i++) {
				var jLi = $('<li>' + results[i] + '</li>');
				jLi.hover(
					function() { $(this).addClass('over'); },
					function() { $(this).removeClass('over'); }
				).click(function(e) {
					e.preventDefault();
					e.stopPropagation();
					selectItem(this);
				});
				jUl.append(jLi);
			}
			jDropdown.append(jUl);

			// Add the next link
			if (offset + options.count < total) {
				var jLink = $('<a class="next" href="javascript:;" title="R\u00e9sultats suivants"><span>\u00a0</span></a>');
				jLink.click(function(e) {
					e.preventDefault();
					e.stopPropagation();
					offset = Math.min(total, offset + options.count);
					requestData(lastQuery);
					jInput.focus();
				});
				jDropdown.append(jLink); // Set focus back to input field
			}
			selected = -1;
		};

		/**
		 * Selects a portion of the input string
		 *
		 * @param integer start The first character to select
		 * @param integer end The last character to select
		 */
		function createSelection(start, end){
			var el = jInput.get(0);
			if (el.createTextRange) {
				var selRange = el.createTextRange();
				selRange.collapse(true);
				selRange.moveStart("character", start);
				selRange.moveEnd("character", end);
				selRange.select();
			} else if (el.setSelectionRange) {
				el.setSelectionRange(start, end);
			} else {
				if(el.selectionStart) {
					el.selectionStart = start;
					el.selectionEnd = end;
				}
			}
			el.focus();
		};

		/**
		 * Returns the top left position of the given element
		 *
		 * @param object obj A DOM object
		 */
		function findPosition(obj) {
			var top = obj.offsetTop || 0;
			var left = obj.offsetLeft || 0;

			while (obj = obj.offsetParent) {
				top += obj.offsetTop;
				left += obj.offsetLeft;
			}
			return {top:top, left:left}
		};

		/**
		 * Hides the dropdown menu
		 */
		function hideDropdown() {
			jInput.removeClass("loading");
			if (timeout) {
				clearTimeout(timeout);
			}
			if (jDropdown.is(":visible")) {
				jDropdown.hide();
				$().unbind("click", onClick);
			}
		};

		/**
		 * Moves selection in the given direction
		 *
		 * @param integer dir Direction (-1=move up, 1=move down)
		 */
		function moveSelection(dir) {
			var jLis = $("li", jDropdown);
			if (jLis) {
				selected += dir;

				if (selected < 0) {
					selected = 0;
				} else if (selected >= jLis.size()) {
					selected = jLis.size() - 1;
				}
				jLis.removeClass("selected");
				$(jLis[selected]).addClass("selected");
			}
		};

		/**
		 * Detects changes in the input field
		 */
		function onChange() {
			var value = $.trim(jInput.val());
			if (value != lastQuery || !jDropdown.is(":visible")) {
				offset = 0;
				lastQuery = value;
				if (value.length >= options.minchars) {
					requestData(value);
				} else {
					hideDropdown();
				}
			}
		};

		/**
		 * Global click event handler
		 *
		 * @param event e The click event descriptor
		 */
		function onClick(e) {
			var target = $(e.target);
			if ((target != jInput) && (target.parents("." + options.className).size() == 0)) {
				hideDropdown();
			}
		};

		/**
		 * Issues an HttpRequest
		 *
		 * @param string query The first letters of the requested data
		 */
		function requestData(query) {
			jInput.addClass("loading");
			var separator = options.source.indexOf('?') >= 0 ? '&' : '?';
			var url = options.source + separator + 'q=' + encodeURI(query) + '&o=' + offset 
				+ '&n=' + options.count + '&format=autocompleter';
			if (options.source.indexOf(':') >= 0)
				// Call to a remote site => Use jsonp.
				url += '&jsonp=?';
			$.getJSON(url, function(data, textStatus) {
				if (!data) { // No data
					hideDropdown();
					return;
				}
				if ($.trim(jInput.val().toLowerCase()) != $.trim(data['q'].toLowerCase()))
					// Response doesn't match query => ignore it; the right response will come later.
					return;
				jInput.removeClass("loading");
				if (hasFocus) {
					total = data['n'];
					results = data['results'];
					buildDropdown();
					if (options.autofill)
						autofill(results[0]);
					selected = -1;
					showDropdown();
				}
			});
		};

		/**
		 * Selects the current list item
		 *
		 * @return boolean True if there was an item to select, false otherwise
		 */
		function selectCurrentItem() {
			var jLi = $("li.selected", jDropdown).get(0);
			if (!jLi) {
				var jLis = $("li", jDropdown);
				if (options.selectfirst) {
					jLi = jLis[0];
				}
			}
			if (jLi) {
				selectItem(jLi);
				return true;
			} else {
				return false;
			}
		};

		/**
		 * Selects the given list item
		 *
		 * @param object li The item to select
		 */
		function selectItem(li) {
			var value = $(li).text();
			lastQuery = value;
			jInput.val(value);
			hideDropdown();
		};

		/**
		 * Displays the dropdown menu
		 */
		function showDropdown() {
			var pos = findPosition(jInput.get(0));
			// findPosition returns 0, 0 when object doesn't exist anymore.
			if (pos.left == 0 && pos.top == 0) {
			    return
			}

			jDropdown.css({
				top: (pos.top + input.offsetHeight - 1) + "px",
				left: pos.left + "px",
				width: width = jInput.width() + "px"
			}).show();

			if ($.browser.msie) {
				var iframe = $('iframe', jDropdown);
				iframe.css({
					border: 0,
					width: jDropdown.width() + "px",
					height: jDropdown.height() + "px"
				});
			}
			$().bind("click", onClick);
		};
	};

	/**
	 * This public function instantiates one or more autocompleter widgets
	 *
	 * @param hash options An hash list of optional parameters
	 */
	$.fn.autocompleter = function(options) {
		options = options || {};

		// Note: The URL of web-service (ie options.source) may be given later...
		if (options.source)
			options.source = encodeURI(options.source);
		options.autofill = options.autofill || false;
		options.delay = options.delay || 400;
		options.className = options.className || "autocompleter";
		options.count = options.count || 10;
		options.minchars = options.minchars || 3;
		options.selectfirst = options.selecfirst || false;

		this.each(function() {
			if ($(this).is("input")) {
				new $.autocompleter(this, options);
			}
		});
		return this;
	};
})(jQuery);
