(
	function ($)
	{
		$.fn.setAutocomplete = function (options)
		{
			options = $.extend({}, $.fn.setAutocomplete.defaults, options); 
			
			// check for no values.
			if (!options.values.length)
			{
				return this;
			}
			
			return this.each(
					function ()
					{
						SetFieldAutoCompletion($(this), options);
					});
		}
		
		$.fn.setAutocomplete.defaults = 
		{
			multiValue:false,
			uniqueValues:true,
			separator:",",
			maxItems:5,
			values:[]
		};
		
		// key codes
		var KEY_CODE = 
		{
			UP: 38,
			DOWN: 40,
			DEL: 46,
			TAB: 9,
			RETURN: 13,
			ESC: 27,
			COMMA: 188, 
			OPERA_COMMA: 44,
			PAGEUP: 33,
			PAGEDOWN: 34,
			BACKSPACE: 8
		};
		
		function SetFieldAutoCompletion(field, options)
		{
			// create list for that field
			var list = new List(options.values, field, options.maxItems);
			
			// turn off standard autocompletion
			field.attr("autocomplete", "off");
			
			field.bind($.browser.isOpera ? "keypress" : "keydown", function (event) { return OnKeyDown(event, field, list); });
			field.bind("keyup", function (event) { return OnKeyUp(event, field, list); });
			field.focus(function () { OnChange(field, list); });
			field.blur(function () { list.Hide(); });
		}
		
		/**
		 * Key down event handler.
		 * 
		 * @param event
		 * @param field
		 * @param list
		 */
		function OnKeyDown(event, field, list)
		{	
			switch (event.keyCode)
			{
			case KEY_CODE.UP:
				event.preventDefault();
				if (list.IsVisible())
				{
					list.Prev();
				}
				return false;
				break;
			case KEY_CODE.DOWN:
				event.preventDefault();
				if (list.IsVisible())
				{
					list.Next();
				}
				return false;
				break;
			
			case KEY_CODE.ESC:
				event.preventDefault();
				list.Hide();
				return false;
				break;
			
			case KEY_CODE.RETURN:
				// write selected to field
				event.preventDefault();
				WriteSelectedItem(field, list);
				return false;
				break;
			
			case KEY_CODE.TAB:
				WriteSelectedItem(field, list);
				return true;
				break;
				
			case KEY_CODE.COMMA:
			case KEY_CODE.OPERA_COMMA:
				// write current value to field
				if (WriteSelectedItem(field, list))
				{
					event.preventDefault();
					field.val(field.val() + ", ");
					MoveCaretToEnd(field.get(0));
					return false;
				}
				break;
			}
		}
		
		function OnKeyUp(event, field, list)
		{
			switch (event.keyCode)
			{
			case KEY_CODE.UP:
			case KEY_CODE.DOWN:
			case KEY_CODE.ESC:
				event.preventDefault();
				break;
			case KEY_CODE.COMMA:
			case KEY_CODE.RETURN:
				event.preventDefault();
				break;
			case KEY_CODE.PAGEUP:
				event.preventDefault();
				if (list.IsVisible())
				{
					list.SelectFirst();
				}
				break;
			case KEY_CODE.PAGEDOWN:
				event.preventDefault();
				if (list.IsVisible())
				{
					list.SelectLast();
				}
				break;
			default:
				OnChange(field, list);
			}
		}
		
		/**
		 * Checks field value, and filters list according to it.
		 * 
		 * @param Element field
		 * @param List list
		 */
		function OnChange(field, list)
		{
			var emails = field.val().split(",");
			var str = emails[emails.length - 1].replace(/^\s+/, '').replace(/\s+$/, '');
			
			if (!str)
			{
				list.Hide();
				return;
			}
			
			// filter list
			if (!list.Filter(str))
			{
				list.Hide();
			}
			else if (!list.IsVisible())
			{
				list.Show();
			}
		}
		
		/**
		 * Writes selected item to field.
		 * 
		 * @param Element field
		 * @param list
		 * @param Boolean checkForVisibility (optional, default true)
		 * @return true if added value to field, false in other cases
		 */
		function WriteSelectedItem(field, list, checkForVisibility)
		{
			checkForVisibility = checkForVisibility == undefined ? true : checkForVisibility;
			
			if (checkForVisibility && !list.IsVisible())
			{
				return false;
			}
			
			var value = list.GetSelectedValue();
			
			if (value)
			{
				// process
				var emails = field.val().split(",");
				var newValue = "";
				
				for (var i = 0; i < (emails.length - 1); ++i)
				{
					newValue += emails[i].replace(/^\s+/, '').replace(/\s+$/, '') + ", ";
				}
				
				field.val(newValue + value);
				
				// move caret to end
				MoveCaretToEnd(field.get(0));
				list.Hide();
				return true;
			}
			
			return false;
		}
		
		/**
		 * Moves caret to end of input field.
		 * @param Element field
		 */
		function MoveCaretToEnd(field)
		{
			var fieldLength = field.value.length;
			
			if (field.setSelectionRange)
			{
				field.focus();
				field.setSelectionRange(fieldLength, fieldLength);
				
				var evt = document.createEvent("KeyboardEvent");
				evt.initKeyEvent(
		                 "keypress",        //  in DOMString typeArg,
		                  true,             //  in boolean canBubbleArg,
		                  true,             //  in boolean cancelableArg,
		                  null,             //  in nsIDOMAbstractView viewArg,  Specifies UIEvent.view. This value may be null.
		                  false,            //  in boolean ctrlKeyArg,
		                  false,            //  in boolean altKeyArg,
		                  false,            //  in boolean shiftKeyArg,
		                  false,            //  in boolean metaKeyArg,
		                  KEY_CODE.ESC,     //  in unsigned long keyCodeArg,
		                  0);               //  in unsigned long charCodeArg);
				field.dispatchEvent(evt);

			}
			else if (field.createTextRange)
			{
				var range = field.createTextRange();
				range.collapse(true);
				range.moveEnd('character', fieldLength);
				range.moveStart('character', fieldLength);
				range.select();
			}
		}
		
		/**
		 * List class
		 * 
		 * @param Array values 
		 * @param Element field
		 */
		function List(values, field, maxItems)
		{
			this.container = $("<div />").addClass("list-container")
										 .css("position", "absolute")
										 .appendTo(document.body)
										 .hide();
			
			this.list = $("<div />").appendTo(this.container).addClass("autocompletion-list");
			this.current = -1;
			this.values = values || [];
			this.field = field;
			this.maxItems = maxItems || 5;
			
			this.BuildList();
		}
		
		/**
		 * Positions list to field.
		 */
		List.prototype.UpdatePositionAndSize = function ()
		{
			var position = this.field.offset();
			this.container.css("left", position.left + "px");
			this.container.css("top", position.top + this.field.outerHeight() + "px");
			this.container.css("width", this.field.outerWidth() + "px");
		}

		/**
		 * Checks if list is visible
		 */
		List.prototype.IsVisible = function ()
		{
			return this.container.is(":visible");
		}

		/**
		 * Shows list
		 */
		List.prototype.Show = function ()
		{
			this.container.show();
			this.UpdatePositionAndSize();
		}

		/**
		 * Hides list
		 */
		List.prototype.Hide = function ()
		{
			this.container.hide();
		}

		/**
		 * Selects next item in the list
		 */
		List.prototype.Next = function ()
		{
			if (this.IsVisible())
			{
				this.list.find("div.selected").removeClass("selected");
				
				++this.current;
				
				var itemsList = this.list.find("div:visible");
				if (this.current >= itemsList.length)
				{
					this.current = 0;
				}
				
				var item = $(itemsList[this.current]);
				if (item && !item.hasClass("selected"))
				{
					this.ScrollToItem(item.addClass("selected"));
				}
			}
		}

		/**
		 * Selects previous item int the list
		 */
		List.prototype.Prev = function ()
		{
			if (this.IsVisible())
			{
				this.list.find("div.selected").removeClass("selected");
				
				--this.current;
				
				var itemsList = this.list.find("div:visible");
				if (this.current < 0)
				{
					this.current = itemsList.length - 1;
				}
				
				var item = $(itemsList[this.current]);
				if (item && !item.hasClass("selected"))
				{
					this.ScrollToItem(item.addClass("selected"));
				}
			}
		}

		/**
		 * 
		 */
		List.prototype.SelectFirst = function ()
		{
			if (this.IsVisible())
			{
				this.list.find("div.selected").clear();
				
				this.current = 0;
				var item = $(this.list.find("div:visible")[0]);
				if (item && !item.hasClass("selected"))
				{
					this.ScrollToItem(item.addClass("selected"));
				}
			}
		}

		/**
		 * 
		 */
		List.prototype.SelectLast = function ()
		{
			if (this.IsVisible())
			{
				this.list.find("div.selected").removeClass("selected");
				
				var itemsList = this.list.find("div:visible");
				if (itemsList.length && itemsList[itemsList.length - 1])
				{
					this.current = itemsList.length - 1;
					var item = $(itemsList[this.current]);
					
					if (item && !items.hasClass("selected"))
					{
						this.ScrollToItem(item.addClass("selected"));
					}
				}
			}
		}

		/**
		 * Builds list
		 */
		List.prototype.BuildList = function ()
		{	
			this.list.empty();
			
			var thisPtr = this;
			if (this.values.length > 0)
			{
				for (var i = 0; i < this.values.length; ++i)
				{
					var listItemElement = $("<div />").mouseover(function () { HighlightItem(thisPtr, this); })
												  	  .data("value", this.values[i]).appendTo(this.list);
					
					var listItemHref = $("<a />").text(this.values[i])
												 .appendTo(listItemElement)
												 .bind("mousedown", function (event)
														{
													 		return false;
														})
												 .click(function (event)
														{
													 		WriteSelectedItem(thisPtr.field, thisPtr, false);
														});
					
				}
				
				this.list.css("overflow-x", "hidden").css("overflow-y", "auto");
			}
			else
			{
				this.hide();
			}
		}

		/**
		 * Filters list by str
		 * 
		 * @param str
		 */
		List.prototype.Filter = function (str)
		{
			str = str.toLowerCase();
			var field = this.field;
			var numItems = 0;
			var isFirst = true;
			var maxItems = this.maxItems;
			
			this.current = 0;
			this.list.find("div.selected").removeClass("selected");
			this.list.find("div").each
			(
					function ()
					{
						var item = $(this);
						var itemValue = item.data("value").toLowerCase();
						
						if (!itemValue)
						{
							return;
						}
						
						if (itemValue.indexOf(str) != -1 && field.val().indexOf(itemValue) == -1 && numItems < maxItems)
						{
							// set item index in visbile items list
							item.data("index", numItems);
							
							item.show();
					 		
							if (isFirst)
							{
								if (!item.hasClass("selected"))
								{
									item.addClass("selected");
								}
								
								isFirst = false;
							}
							
							++numItems;
						}
						else
						{
							// remove item index in visible items list
							item.removeData("index");
							item.hide();
						}
					}
			);
			
			return numItems;
		}

		/**
		 * Scrolls list to item
		 */
		List.prototype.ScrollToItem = function (item)
		{
		}

		/**
		 * Get selected value
		 * 
		 * @return selected value or null if nothing is selected
		 */
		List.prototype.GetSelectedValue = function (value)
		{
			var element = this.list.find("div.selected");
			if (element)
			{
				return element.data("value");
			}
			return false;
		}

		/**
		 * Selects current element in the list
		 * 
		 * @param List list
		 * @param Element element
		 */
		function HighlightItem(list, element)
		{
			element = $(element);
			element.siblings("div.selected").removeClass("selected");
			element.addClass("selected");
			list.current = element.data("index");
		}
	}
)(jQuery);
