(function($j) {
	$j.fn.extend( {
		typeahead : function(options) {
			return this.each(function() {
				new $j.typeaheadz(this, options);
			});
		},
		typeahead_configure : function(options) {
			return this.trigger("typeahead_configure", [ options ]);
		},
		typeahead_load : function() {
			return this.trigger("typeahead_load");
		},
		typeahead_clear : function() {
			return this.trigger("typeahead_clear");
		}
	});
	$j.typeaheadz = function(input, options) {
		var inputbox = input;
		var defaults = {
			field : 'tags' // id of input control (textbox or text area)
			,url : 'jsontags.php' // the remote url to get the suggestion list from
			,tagsep : '' // multi-value delimiter of field
			,enclose : '' // character to enclose multi-word filters
			,max : 10 // maximum number of results to show in the suggestion list
			,cache : true // cache results from suggestion list or not
			,delay : 500 // pause after which the suggestion list is loaded
			,charMin : 1 // minimum number of chars for filter before a lookup is done
			,dblClick : true // activate suggestion list on double click?
			,postData : null // extra post data specified in object notation
			,visible : true // indicates whether the lookup list will be shown when there are suggestions
			,dataType : 'jsonp' // datatype of return results
			,jsonp : 'jsonp_callback'
			,method : 'GET'
			,onRenderItem : function(row) {
				return decodeURIComponent(row);
			}
			,onSelectItem : function(val) {
				return true;
			}
			,onSelectedItem : function(index, char_count, chars_typed, val, e) {
				return true;
			}
			,onDefaultEnter : function(e){
				return true;
			}
			,onLoadList : function(filter) {
				return true;
			}
			,onLoadedList : function(results) {
				return true;
			}
		};
		var options = $j.extend(defaults, options);
		var input = $j('#' + options.field);
		var char_count = 0;
		$j(input).attr("autocomplete", "off");

		var lkup = $j('<div />');
		lkup.attr( {'id' : 'inputbox-lkup'});
		$j(lkup, inputbox).show();
		input.after(lkup);

		var lkuplst = $j('<ol />');
		$j(lkup, inputbox).append(lkuplst);

		var cursor = -1; // keyboard arrow cursor in suggestion list (0=first position, -1 = no position)
		var length = 0; // length of last suggestion list
		var loading = false; // loading indicator of suggestion list
		var loaded = false; // loaded indicator of suggestion list
		var cacheLst = null; // in-memory suggestion list, used for containing the rich objects inside
		var inserted = false; // state variable to prevent double suggestion list after inserting a value

		var preg_escape = function(str) {
			return (str + '').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!<>\|\:])/g, "\\$1");
		};

		var hideLkup = function() {
			$j(lkuplst, inputbox).empty();
			$j(lkup, inputbox).hide();
			loaded = false;
			cacheLst = null;
			inserted = false;
		};

		var insertTag = function(filter, tag) {
			var cur = input.val();
			var words = tag.split(' ').length;
			var enclose = (words > 1) ? options.enclose.length > 0 ? options.enclose: '': '';
			cur = cur.replace(eval('/' + preg_escape(filter) + '$/i'), enclose + tag + enclose);
			input.val(cur);
		};

		var addItem = function(val, filter, index) {
			if (!options.visible) return;

			var row = val;
			var val = options.onRenderItem(val, index, length, filter);

			var li = $j('<li/>');
			li.bind('mouseover', function(e){
				var e = e || window.event;
				if (cursor > -1)
					$j('li:eq(' + cursor + ')', inputbox).removeClass('hl');
				cursor = index;
				$j('li:eq(' + cursor + ')', inputbox).addClass('hl');
			});
			lkuplst.append(li);

			var aLink = $j('<a/>');
			aLink.attr( {'href' : '#'});
			$j(aLink, inputbox).text(val);
			$j(aLink, inputbox).addClass(index % 2 == 0 ? 'td-odd' : 'td-even');
			$j(aLink, inputbox).html($j(aLink, inputbox).text().replace(eval('/(' + preg_escape(filter) + ')/gi'),"<em>$1</em>"));
			li.append(aLink);

			aLink.click(function(e) {
				inserted = true;
				var cur = input.val();
				options.onSelectItem(row);
				insertTag(filter, val);
				options.onSelectedItem(index, cur.length, cur, row, e);
				e.preventDefault();
				hideLkup();
				inserted = true;
			});
		};

		var loadList = function() {
			inserted = false;

			var filter = parseFilter(input.val());
			if (options.onLoadList(filter)){
				$j(lkuplst, inputbox).empty();
				$j.ajax( {
					type		: options.method
					,url		: options.url
					,data		: $j.extend({
										ahead : encodeURIComponent(filter),
										total : options.max
									}, options.postData)
					,dataType	: options.dataType
					,jsonp		: options.jsonp
					,cache		: options.cache
					,success	: function(json) {
										if (filter != parseFilter(input.val())) {
											loadList();
										} else {
											$j(lkuplst, inputbox).empty();
											length = json.length;
											cacheLst = json;
											cursor = -1;
											for (i = 0; i < json.length && i < options.max; i++) {
												addItem(json[i], filter, i);
											}
											if (options.visible) {
												$j(lkup, inputbox).show();
											}
											loading = false;
											loaded = true;
											options.onLoadedList(json);
										}
									}
					,error		: function(XMLHttpRequest, textStatus, errorThrown) {
										length = 0;
										cacheLst = null;
										loading = false;
										loaded = false;
										options.onLoadedList(false);
									}
				});
			};
		};

		var parseFilter = function(val) {
			if (options.tagsep.length == 0)
				return val;

			if (val.indexOf(options.tagsep) > -1) {
				if (options.tagsep == ' ')
					val = val.substring(val.lastIndexOf(options.tagsep) + 1,val.length);
				else
					val = jQuery.trim(val.substring(val.lastIndexOf(options.tagsep) + 1, val.length));
			}
			return val;
		};

		var triggerLoad = function() {
			if (inserted) return false;
			else {
				var filter = parseFilter(input.val());

				if (filter.length >= options.charMin) {
					loading = true;
					setTimeout(function() {loadList()},options.delay);
				} else { hideLkup(); }
			}
		}

		input.dblclick(function(e){
			if (options.dblClick && !loading) triggerLoad();
		});

		$j(lkuplst,inputbox).blur(function(e) {
			hideLkup();
		});

		var handleSpecials = function(e) {
			var e = e || window.event;
			var key = e.charCode || e.keyCode;
			var cur = input.val();

			if (!loaded){
				switch (key) {
					case 40: { //Down key pressed
							triggerLoad();
						}
						break;
				}
				return true;
			}

			switch (key) {
				case 9: {// TAB key pressed
						cursor = ((cursor + 1) < length) ? cursor++ : cursor;
						if (cursor < length) {
							$j('li:eq(' + cursor + ')', inputbox).addClass('hl');
							if ((cursor - 1) > -1)
								$j('li:eq(' + (cursor - 1) + ')', inputbox).removeClass('hl');
								e.preventDefault();
						}
					}
					break;
				case 40: {// DOWN key pressed
						if ((cursor+1) < length) {
							cursor++;
							$j('li:eq(' + cursor + ')', inputbox).addClass('hl');
							if ((cursor - 1) > -1)
								$j('li:eq(' + (cursor - 1) + ')', inputbox).removeClass('hl');
							//insertTag(parseFilter(input.val()), $j('li:eq(' + (cursor) + ')', inputbox).text());
							e.preventDefault();
						}else if((cursor+1) == length){
							$j('li:eq(' + cursor + ')', inputbox).removeClass('hl');
							cursor = 0;
							$j('li:eq(' + cursor + ')', inputbox).addClass('hl');
							//insertTag(parseFilter(input.val()), $j('li:eq(' + (cursor) + ')', inputbox).text());
							e.preventDefault();
						}
					}
					break;
				case 38: {// UP key pressed
						if ((cursor - 1) >= 0) {
							cursor--;
							$j('li:eq(' + cursor + ')', inputbox).addClass('hl');
							$j('li:eq(' + (cursor + 1) + ')', inputbox).removeClass('hl');
							//insertTag(parseFilter(input.val()), $j('li:eq(' + (cursor) + ')', inputbox).text());
							e.preventDefault();
						}else if (cursor == 0){
							hideLkup();
						}
					}
					break;
				case 13: {// ENTER key pressed
						if (cursor >= 0 && cursor < length) {
							var row = cacheLst[cursor];
							options.onSelectItem(row);
							insertTag(parseFilter(input.val()), $j('li:eq(' + (cursor) + ')', inputbox).text());
							options.onSelectedItem(cursor, cur.length, cur, row, e);
							e.preventDefault();
							hideLkup();
						}else{
							options.onDefaultEnter(e);
							e.preventDefault();
						}
					}
					break;
				case 27: {// ESC key pressed
						hideLkup();
						e.preventDefault();
					}
					break;
				case 39: {// Right arrow
						if (typeof(input[0].selectionEnd) != "undefined"){
							if (input[0].selectionEnd == input[0].selectionStart && input[0].selectionEnd == input[0].textLength){
								if (input[0].type != "textarea")
									e.preventDefault();
								if (cursor >= 0 && cursor < length) {
									var row = cacheLst[cursor];
									options.onSelectItem(row);
									insertTag(parseFilter(input.val()), $j('li:eq(' + (cursor) + ')', inputbox).text());
									options.onSelectedItem(cursor, cur.length, cur, row, e);
									e.preventDefault();
									hideLkup();
								}
							}
						}else{
							var end = caretEnd();
							var start = caretStart();
							//alert('caretStart ('+start+') == caretEnd ('+end+') && caretEnd ('+end+') == input.val().length ('+input.val().length+')');
							if (start == end && end == input.val().length){
								if (input[0].type != "textarea")
									e.preventDefault();
								if (cursor >= 0 && cursor < length) {
									var row = cacheLst[cursor];
									options.onSelectItem(row);
									insertTag(parseFilter(input.val()), $j('li:eq(' + (cursor) + ')', inputbox).text());
									options.onSelectedItem(cursor, cur.length, cur, row, e);
									e.preventDefault();
									hideLkup();
								}
							}
						}
					}
					break;
			}
		};

		var caretStart = function () {
			var bookmark = document.selection.createRange().getBookmark();
			input[0].selection = input[0].createTextRange();
			input[0].selection.moveToBookmark(bookmark);
			input[0].selectLeft = input[0].createTextRange();
			input[0].selectLeft.collapse(true);
			input[0].selectLeft.setEndPoint("EndToStart", input[0].selection);
			return input[0].selectLeft.text.length + 1;
		}

		var caretEnd = function () {
			var bookmark = document.selection.createRange().getBookmark();
			input[0].selection = input[0].createTextRange();
			input[0].selection.moveToBookmark(bookmark);
			input[0].selectLeft = input[0].createTextRange();
			input[0].selectLeft.collapse(true);
			input[0].selectLeft.setEndPoint("EndToStart", input[0].selection);
			return input[0].selectLeft.text.length + ((input[0].selection.text.length == 0) ? 1 : input[0].selection.text.length);
		}

		var handleKey = function(e) {
			var e = e || window.event;
			var key = e.charCode || e.keyCode;

			if (key == 13) return true;
			if (key > 8 && key < 46 && key != 32) { return false; }

			if (loading == false) { triggerLoad(); }
			if (options.visible) { $j(lkup, inputbox).show(); }
		};

		$j(input).keyup(handleKey);
		$j(input).keydown(handleSpecials);
		$j(inputbox).bind("typeahead_configure", function() { $j.extend(options, arguments[1]); });
		$j(inputbox).bind("typeahead_load", function() { triggerLoad(); });
		$j(inputbox).bind("typeahead_clear", function() { hideLkup(); });
	};
})(jQuery);