/*
jquery.combobox
version 0.1.2.3 alpha

ahura mazda
copyright 2007
jquery.sanchezsalvador.com

modified by marie alhomme and pierre-henri lavigne, copyright 2008
*/
jQuery.fn.combobox = function(options)
{
	// Setting class
	var settings =
	{
		comboboxContainerClass: 'comboboxContainer',
		comboboxValueContentTagName: 'h2',
		comboboxDropDownItemHeaderTagName: 'h3',
		comboboxDropDownItemHoverClass: 'comboboxItemHover',
		comboboxSelectHideClass: 'offLeft',
		animationType: "slide"
	};

	if (options)
	{
		jQuery.extend(settings, options);
	}

	//#region public events

	this.onChange =
		function()
		{
			//Intentionally left empty
		};

	//#endregion public events

	return this.each(
		function()
		{

			//#region 'private' variables

			// This class can operate of N elements depending on how ComboBox is called
			// for example jQuery('select').combobox() could return multiple Selects.
			// This variable should always be a Select JQuery element.
			// TODO: Check if select control is used
			var _originalElementJQuery = jQuery(this);
			var _originalElementJQueryId = _originalElementJQuery.attr('id');
			var _containerJQuery = null;
  			var _dropDownListJQuery = null;
			var _valueDisplayContainerJQuery = null;
			var _valueDisplayJQuery = null;
			var _dropDownButtonJQuery = null;
			var _dropDownButtonDefaultCharacter = "&#9660;";
			var _lastItemSelectedJQuery = null;
			var _lastValue = null;
			var _downdownListPositionIsInverted = false;
			var _maximumItemLength = 0;
			var _dropDownListOffset = null;

			//#endregion 'private' variables

			//#region 'private' methods

			///<summary>
			/// Function extension to String.
			///	Replaces the placeholder tags '{0}...{n}' with the parameters based on ordinal position of the parameters
			///	Example: String.format("The quick {0} fox {2} over the lazy {1}.", "brown", "Dog", "jumps");
			///	Output:	The quick brown fox jumps over the lazy Dog.
			///</summary>
			String.format =
				function()
				{
					var currentString = null;
					if (arguments.length != 0)
					{
						currentString = arguments[0];
						for (var argumentIndex = 1; argumentIndex < arguments.length; argumentIndex++)
						{
							var modifiedString = new RegExp('\\{' + (argumentIndex - 1) + '\\}','gm');
							currentString = currentString.replace(modifiedString, arguments[argumentIndex]);
						}
					}

					return currentString;
				};

			///<summary>
			/// Builds the elements that make up the always visible portion of the control.
			///	The equivalent of a Textbox-type element.
			/// Also creates the DropDownButton
			///</summary>
			function buildValueDisplay()
			{
				// A container for the Display Value and DropDownButton. A container is required as the child elements
				// are floated

                var valueDisplayContainerHTML = "";
				valueDisplayContainerHTML = String.format('<div></div>');

				// Create the equivalent of the select 'textbox' where the current selected value is shown

            	var valueDisplayHTML = "";
				valueDisplayHTML = String.format('<{0}><span></span></{0}>', settings.comboboxValueContentTagName);

                var dropdownButtonHTML = "";
				dropdownButtonHTML = String.format('<em class="dropDownLink">{0}</em>', _dropDownButtonDefaultCharacter);

 				_valueDisplayJQuery = jQuery(valueDisplayHTML);
				_dropDownButtonJQuery = jQuery(dropdownButtonHTML);
				_valueDisplayContainerJQuery = jQuery(valueDisplayContainerHTML);

				_valueDisplayContainerJQuery.appendTo(_containerJQuery);
				_valueDisplayJQuery.appendTo(_valueDisplayContainerJQuery);
				_dropDownButtonJQuery.appendTo(_valueDisplayJQuery);

			}

			///<summary>
			///	Build a drop down list element populating it will values, text, styles and class
			///	depending on the source value type. The source value (childJQuery) can be an option or
			///	and optgroup element.
			///</summary>
			function buildDropDownItem(childJQuery)
			{
				var dataItemHTML = "";
				var dataItemClass = null;
				var dataItemText = "";
				var dataItemValue = null;
				var dataItemStyle = "";
				var dataItemType = "option";

				if (childJQuery.is('option'))
				{
					dataItemText = childJQuery.text();
					dataItemValue = childJQuery.val();

                    var dataItemHTML = "";
                    dataItemHTML = String.format('<li>{0}</li>', dataItemText);

				}
				else
				{
					dataItemText = childJQuery.attr('label');
					dataItemValue = childJQuery.attr('class');
					dataItemType = "optgroup";

                    var dataItemHTML = "";
					dataItemHTML = String.format('<li><{0}>{1}</{0}></li>', settings.comboboxDropDownItemHeaderTagName, dataItemText);

				}

				var dataItemJQuery = jQuery(dataItemHTML);

				// Store the value with the DOMElement which is persisted with the Browser
				dataItemJQuery[0].dataValue = dataItemValue;
				dataItemJQuery[0].dataType = dataItemType;

				return dataItemJQuery;
			}

			///<summary>
			///	Recusively build the drop down list elements based on the options and optgroups contained
			///	with the original Select element
			///</summary>
			function recursivelyBuildList(parentJQuery, childrenOptionsJQuery)
			{
				childrenOptionsJQuery.each(
					function()
					{
						var childJQuery = jQuery(this);
						var builtDropDownItemJQuery = buildDropDownItem(childJQuery);
						parentJQuery.append(builtDropDownItemJQuery);

						if (childJQuery.is('optgroup'))
						{
							var dataGroupItemHTML = "";
                            dataGroupItemHTML = String.format('<ul></ul>');

							var dataGroupItemJQuery = jQuery(dataGroupItemHTML);
							builtDropDownItemJQuery.append(dataGroupItemJQuery);

							// If not an option, then the child of a Select must be an optgroup element
							recursivelyBuildList(dataGroupItemJQuery, childJQuery.children());
						}
					});
			}

			///<summary>
			/// Creates an unordered list of values from the original Select control
			///</summary>
			function buildDropDownList()
			{
				var originalElementChildrenJQuery = _originalElementJQuery.children();
				_lastItemSelectedJQuery = null;
				_lastValue = null;

				// If the Drop Down List container already exists, recreate only the items,
				// else create the container and the items as well.
				if (_dropDownListJQuery)
				{
					// Clear out any existing children elements
					_dropDownListJQuery.empty();
				}
				else
				{
					var dropDownHTML = "";
                    dropDownHTML = String.format('<ul{0}></ul>', ' id="'+ _originalElementJQueryId +'ListReplacement"');

					_dropDownListJQuery = jQuery(dropDownHTML);
					// Create the equivalent of the drop down list where the all the values are shown
					_dropDownListJQuery.appendTo(_containerJQuery);

					// Enable the Drop Down List to be able to receive focus and key events
					 _dropDownListJQuery.attr("tabIndex", 0);
				}

				// Create the internal list of values if they exist
				if (originalElementChildrenJQuery.length > 0)
				{
					_maximumItemLength = 0;

					recursivelyBuildList(_dropDownListJQuery, originalElementChildrenJQuery);
				}
			}

			///<summary>
			/// Repositions the display value based on height of the element.
			///	Note: the height will only have meaning if the display value element has text
			///</summary>
			function positionDisplayValue()
			{
                var displayValueHeight = _valueDisplayJQuery.outerHeight();
				var displayContainerHeight = _valueDisplayContainerJQuery.height();
				var difference = ((displayContainerHeight - displayValueHeight) / 2);

				if (difference < 0)
				{
					difference = 0;
				}

				//TODO: add other alignments for the user, such as left, top, middle, bottom, etc
				_valueDisplayJQuery.css("top", difference);
			}

			///<summary>
			/// Bind all items to mouse events except for UL elements
			/// and LI elements that are option group labels
			///</summary>
			function bindItemEvents()
			{
				jQuery("*", _dropDownListJQuery).not("ul").not("span").not("[@dataType='optgroup']").each(
					function()
					{
						var itemJQuery = jQuery(this);
						itemJQuery.click(
							function(clickEvent)
							{
								// Stops the click event propagating to the Container and the Container.onClick firing
								clickEvent.stopPropagation();

								container_onItemClick(itemJQuery);
							});

						itemJQuery.mouseover(
							function()
							{
								container_onItemMouseOver(itemJQuery);
							});

						itemJQuery.mouseout(
							function()
							{
								container_onItemMouseOut(itemJQuery);
							});
					});
			}

			///<summary>
			///		Bind the dropdown list control blur event to a function
			///</summary>
			function bindBlurEvent()
			{
				_dropDownListJQuery.blur(
					function(blurEvent)
					{
						blurEvent.stopPropagation();

						dropDownListJQuery_onBlur();
					});
			}

			///<summary>
			///	Bind the click event of the container to a function
			///</summary>
			function bindContainerClickEvent()
			{
				_containerJQuery.click(
					function(clickEvent)
					{
						container_onClick();
					});
			}

			///<summary>
			///	Remove the binding of a custom function from the container's click event
			///</summary>
			function unbindContainerClickEvent()
			{
				_containerJQuery.unbind("click");
			}

			///<summary>
			///		Bind this control to the events to custom functions
			///</summary>
			function bindEvents()
			{
				_containerJQuery.keydown(
					function(keyEvent)
					{
						keyEvent.preventDefault();container_onKeyDown(keyEvent)
					});

				bindContainerClickEvent();

				bindBlurEvent();

				bindItemEvents();
			}

			///<summary>
			///		Sets the value both internally and visually to the user
			///</summary>
			function setDisplayValue()
			{
				var valueHasChanged = false;
				var originalElement = _originalElementJQuery[0];

				if (originalElement.length > 0)
				{
					var selectedText = originalElement[originalElement.selectedIndex].text;
                    jQuery("> span",_valueDisplayJQuery).text(selectedText);

					// Reposition the display value based on height of the element after the text has changed
					positionDisplayValue();

					if (_lastValue)
					{
						if (_lastValue != _originalElementJQuery.val())
						{
							valueHasChanged = true;
						}
					}

					_lastValue = _originalElementJQuery.val();

					//  If the selected value has changed since the last click, fire the onChange event
					if (valueHasChanged)
					{
						// Check if the onChange event is being consumed, otherwise it will be undefined
						if (_originalElementJQuery.combobox.onChange)
						{
							_originalElementJQuery.combobox.onChange();
						}
					}

					// If _lastItemSelectedJQuery has been set, remove the highlight from it, before setting it to the current
					// value
					if (_lastItemSelectedJQuery)
					{
						toggleItemHighlight(_lastItemSelectedJQuery, false);
					}

					// Find the DropDown Item Element that corresponds to the current value in the Select element
					_lastItemSelectedJQuery = jQuery("li[@dataValue='" + _lastValue + "']", _dropDownListJQuery);
					toggleItemHighlight(_lastItemSelectedJQuery, true);
				}
			}

			///<summary>
			///	Highlights/Unhighlights a specific option.
			///	If a class is not set, then the background and foreground colours are inverted
			///</summary>
			function toggleItemHighlight(elementJQuery, isHighlighted)
			{
				if (elementJQuery)
				{
            		var dropdownButtonClass = settings.comboboxDropDownItemHoverClass;
					var switchHighlighted = isHighlighted ? elementJQuery.addClass(dropdownButtonClass) : elementJQuery.removeClass(dropdownButtonClass);

				}
			}

			///<summary>
			///	Builds the Outermost control and swaps out the original Select element.
			///	The Select element then becomes an hidden control within.
			///</summary>
			function buildContainer()
			{
                var containerHTML = "";
				containerHTML = String.format('<div{0}{1}></div>', ' class="' + settings.comboboxContainerClass + '"', ' id="'+ _originalElementJQueryId +'Replacement"');

				_containerJQuery = jQuery(containerHTML);
				_originalElementJQuery.before(_containerJQuery);
				_containerJQuery.append(_originalElementJQuery);
				_originalElementJQuery.addClass(settings.comboboxSelectHideClass);

				// Allow the custom jquery.combobox be able to receive focus and key events
				_containerJQuery.attr("tabIndex", 0);
			}

			///<summary>
			///	Converts an existing Select element to a JQuery.combobox.
			///	The Select element is kept and updated accordingly, but visually is represented
			///	by other richer HTML elements
			///</summary>
			function initialiseControl()
			{
				buildContainer();

				buildValueDisplay();

				buildDropDownList();

				bindEvents();

				setDisplayValue();
			}

			///<summary>
			///	Focus must be set to the DropDown list element only after it has shown.
			///	This is due to IE executing the Blur event before the list has immediately shown
			///</summary>
			function setDropDownListFocus()
			{
				_dropDownListJQuery.focus();
			}

			///<summary>
			///	Focus set to the Combobox Container
			///</summary>
			function setAndBindContainerFocus()
			{
				_containerJQuery.focus();
				bindContainerClickEvent();
			}

			///<summary>
			///	Slides up the DropDownlist when it is to be placed above the CB
			///</summary>
			function slideUp(newTop)
			{
				_dropDownListJQuery.animate(
					{
						height: "toggle",
						top: newTop
					},
					"fast",
					setDropDownListFocus);
			}

			///<summary>
			///	Slides closed the DropDownlist when it is placed above the CB.
			///	Binds the CB Container click event after the DDL is hidden to avoid a bug in IE
			///	where the click event fires re-opening the DDL.
			///</summary>
			function slideDown(newTop)
			{
				_dropDownListJQuery.animate(
					{
						height: "toggle",
						top: newTop
					},
					"fast",
					setAndBindContainerFocus);
			}

			///<summary>
			///	Hides/Shows the list of values.
			///	The method of display or hiding is specified as settings.animationType.
			///	This method also changes the button state
			///</summary>
			function toggleDropDownList(isShown)
			{
				if (isShown)
				{
					if (_dropDownListJQuery.is(":hidden"))
					{
						// Remove the click event from the container because when the dropdown list is shown
						// and the container is clicked, the dropdownlist blur event is fired which hides the control
						// and the container click is fired after which will show the list again (error);
						unbindContainerClickEvent();

						// When the DropDown list is shown, highlist the current value in the list
						toggleItemHighlight(_lastItemSelectedJQuery, true);

						//setDropDownButtonState(1);

						switch (settings.animationType)
						{
							case "slide":
								if (_downdownListPositionIsInverted)
								{
									var comboboxTop = _containerJQuery.position().top;
									var containerHeight = _containerJQuery.outerHeight();

									_dropDownListJQuery.css("top", (comboboxTop - containerHeight));

									slideUp(dropdownListTop);
								}
								else
								{
									_dropDownListJQuery.slideDown("fast", setDropDownListFocus);
								}
								break;

							case "fade":
								_dropDownListJQuery.fadeIn("fast", setDropDownListFocus);
								break;

							default:
								_dropDownListJQuery.show();
								setDropDownListFocus();
						}
					}
				}
				else
				{
					if (_dropDownListJQuery.is(":visible"))
					{

						switch (settings.animationType)
						{
							case "slide":
								if (_downdownListPositionIsInverted)
								{
									comboboxTop = _containerJQuery.position().top;
									dropdownListHeight = _dropDownListJQuery.height();

									slideDown(comboboxTop - _containerJQuery.outerHeight());
								}
								else
								{
									_dropDownListJQuery.slideUp("fast", setAndBindContainerFocus)
								}
								break;

							case "fade":
								_dropDownListJQuery.fadeOut("fast", setAndBindContainerFocus);
								break;

							default:
								_dropDownListJQuery.hide();
								setAndBindContainerFocus();
						}
					}
				}
			}

			///<summary>
			///	Selects a value from the list of options from the original Select options.
			///	Does not use JQuery Selectors ':last' and ':first' because they take optgroup elements into
			///	account.
			///</summary>
			function selectValue(subSelector)
			{
				var originalElement = _originalElementJQuery[0];
				var currentIndex = originalElement.selectedIndex;
				var newIndex = -1;
				// {select}.length returns the array size of the options. Does not count optgroup elements
				var optionCountZeroBased = originalElement.length - 1;

				switch (subSelector)
				{
					case ":next":
						newIndex = currentIndex + 1;
						if (newIndex > optionCountZeroBased)
						{
							newIndex = optionCountZeroBased;
						}
						break;

					case ":previous":
						newIndex = currentIndex - 1;
						if (newIndex < 0)
						{
							newIndex = 0;
						}

						break;

					case ":first":
						newIndex = 0;

						break;

					case ":last":
						newIndex = optionCountZeroBased;

						break;
				}

				originalElement.selectedIndex = newIndex;
				setDisplayValue();
			}

			///<summary>
			///	Returns true if the DropDownList visible on screen, else false
			///</summary>
			function isDropDownVisible()
			{
				return _dropDownListJQuery.is(":visible");
			}

			//#endregion 'private' functions

			//#region public methods

			///<summary>
			///	Updates the combobox with the current selected item.
			///	This function is called if the original Select element selection has been changed
			///</summary>
			_originalElementJQuery.combobox.updateSelection =
				function()
				{
					setDisplayValue();
				};

			///<summary>
			///	The Drop Down List Container will already have been created.
			///	This function recreates the items and binds the events to them.
			///	Thereafter, the current selection is set.
			///</summary>
			_originalElementJQuery.combobox.update =
				function()
				{
					buildDropDownList();
					bindItemEvents();
					setDisplayValue();
				};

			//#endregion public methods

			//#region private events

			function container_onClick()
			{
				if (_dropDownListJQuery.is(":hidden"))
				{
					toggleDropDownList(true);
				}
				else
				{
					toggleDropDownList(false);
				}
			}

			function dropDownListJQuery_onBlur()
			{
				if (_dropDownListJQuery.is(":visible"))
				{
					toggleDropDownList(false);
				}
			}

			function container_onItemClick(itemJQuery)
			{
				_originalElementJQuery.val(itemJQuery[0].dataValue);

				setDisplayValue();

				toggleDropDownList(false);
			}

			function container_onItemMouseOver(itemJQuery)
			{
				// An item may be selected from the previous selection and will require
				// to be set to normal.
				// TODO: find a better method of matching _lastItemSelectedJQuery to itemJQuery and optimising the removal
				// of the class, instead of removing it consistently
				toggleItemHighlight(_lastItemSelectedJQuery, false);

				toggleItemHighlight(itemJQuery, true);
			}

			function container_onItemMouseOut(itemJQuery)
			{
				toggleItemHighlight(itemJQuery, false);
			}

			function container_onKeyDown(keyEvent)
			{
				switch (keyEvent.which)
				{
					case 33:
						//Page Up
					case 36:
						//Home
						selectValue(":first");
						break;

					case 34:
						//Page Down
					case 35:
						//End
						selectValue(":last");
						break;

					case 37:
						//Left
						selectValue(":previous");
						break;

					case 38:
						//Up
						if (keyEvent.altKey)
						{
							// alt-up
							// If DDL is hidden, then it is shown and vice-versa
							toggleDropDownList(!(isDropDownVisible()));
						}
						else
						{
							selectValue(":previous");
						}
						break;

					case 39:
						//Right
						selectValue(":next");
						break;

					case 40:
						//Down
						if (keyEvent.altKey)
						{
							// alt-down
							// If DDL is hidden, then it is shown and vice-versa
							toggleDropDownList(!(isDropDownVisible()));
						}
						else
						{
							selectValue(":next");
						}
						break;

					case 27:
					case 13:
						// Escape
						toggleDropDownList(false);
						break;

					case 9:
						// Tab
						//TODO: Support alt-tab
						//TODO: Does not truly leave the Combobox if the DropDown is visible
						_dropDownListJQuery.blur();

						// This is required in Internet Explorer as the blur() order is different
						$(window)[0].focus();

						break;
				}

				keyEvent.cancelBubble = true;
			}

			//#endregion private events

			initialiseControl();
		});
}