/*
 * Super-sexy AutoComplete <select/> replacement.
 * @author Richard Assar
 */
 
// TODO Is this behind the performance issue we see when there are many tagsets? e.g. http://egan.soda.sh/stream
// TODO document the operating assumptions for this:
// - what form of html/json does it expect to work with?
// - a simple example of the html it produces
// - what's the lifecycle of these widgets?
// TODO a stand-alone test page

// NOTE: Moving to prototypical implementation -RA
function AutoCompleteWidget() {
	
};

// @static
$.extend(AutoCompleteWidget, {
	'closeAll' : function() {
		$("body").trigger("click"); // TODO: Rewrite this to invoke close on current active instance (held as static property)
	}
});

// @dynamic
AutoCompleteWidget.prototype = {
};

// TODO: Refactor! - RA
function ajaxifyAutoComplete() {
	console.log("ajaxify AutoComplete...");

	$(".AutoComplete").not(".ajaxed").each(function() {		
		var selectElement = $(this);
		
		selectElement.addClass("ajaxed");

		// Get the options/optgroup data and build flat array of values
		var promptElement = selectElement.children("option[value='']");
		var optionElements = selectElement.children("option[value!='']");
		var optgroupElements = selectElement.children("optgroup");

		var options = new Array();

		var groups = ['global'];
	
		_.each(optionElements, function(optionElement) {
			optionElement = $(optionElement);
			
			options.push({ 
				'text' : optionElement.text(),
				'value' : optionElement.val()
			});
		});

		_.each(optgroupElements, function(optgroupElement) {
			optgroupElement = $(optgroupElement);

			groups.push(optgroupElement.attr("label"));
		
			_.each(optgroupElement.children(), function(optionElement) {
				optionElement = $(optionElement);
						
				options.push({ 
					'text' : optionElement.text(),
					'value' : optionElement.val(),
					'group' : optgroupElement.attr("label")
				});
			});						
		});

		// Wrap	the select element in an autocomplete-wrapper div		
		selectElement.wrap($("<div>").addClass("AutoComplete-wrapper"));
		
		var wrapperElement = $(this).parent();

		// Append the replacement input element
		var inputElement = $("<input>")
			.val(promptElement.text())
			.attr({
				'type' : 'text',
				'autocomplete' : 'off'
			});

		inputElement.wrap(
			$("<div>")
				.addClass("input-wrapper")
				.css('width', selectElement.outerWidth())
		);		

		var inputWrapperElement = inputElement.parent();

		inputWrapperElement.append(
			$("<span>")
				.addClass("reset")
				.html("&times;")
				.click(function() {
					inputElement
						.val('')
						.trigger("focus");

					update();
				})
		);
		
		wrapperElement.append(inputWrapperElement);
								
		// Append the dropdown
		var dropdownElement = $("<ul class='dropdown'>");

		dropdownElement.wrap(
			$("<div>")
				.addClass("dropdown-wrapper")
				.css({
					'width' : selectElement.outerWidth(),
					'top' : inputWrapperElement.outerHeight()
				})
				.hide()
		);

		var dropdownWrapperElement = dropdownElement.parent();

		function hideDropdown() {
			dropdownWrapperElement.hide();
			
			wrapperElement.removeClass("open");
		};

		function showDropdown() {
			dropdownWrapperElement.show();
			
			wrapperElement.addClass("open");
		};

		wrapperElement.append(dropdownWrapperElement);	

		var selectionMade;
		var choiceMade;
		var selectedOption;
		var firstItem;
		var numAvailableOptions;

		// Updates the dropdown based on the value in the input box
		function update() {			
			numAvailableOptions = 0;
			selectionMade = false;
			firstItem = undefined;
		
			// Match the value of the input element against the options data
			var matchedOptions = new Object();
			
			_.each(options, function(option) {
				var group = option.group === undefined ? 'global' : option.group;
				
				if(matchedOptions[group] === undefined) {
					matchedOptions[group] = new Array();
				}

				// Wrap all matches (case insensitive) in STRONGs
				var numMatches = 0;

				var highlightedText;

				if(inputElement.val().length > 0) {
					// Match on word starts (including sub-words, e.g. "top" matches "off-topic").
					// This should give faster selection. 
					var regex = new RegExp("(^|[^a-z])(" + regExpEscape(inputElement.val()) + ")", "gi");
					
					highlightedText = option.text.replace(regex, function() { 
						numMatches++;

						return arguments[1] + "<strong>" + arguments[2] + "</strong>"; 
					});
				} else {
					highlightedText = option.text;
				}

				// Push matched options into group array
				if(numMatches > 0 || inputElement.val().length == 0) {
					if(firstItem === undefined) {
						firstItem = option;
					}

					numAvailableOptions++;
				
					matchedOptions[group].push({
						'value' : option.value,
						'text' : option.text,
						'highlightedText' : highlightedText						
					});
				}

				// Gather data of selected item if we find an exact match
				if(option.text == inputElement.val()) {
					selectionMade = true;
					selectedOption = option;
				}
			});

			// Clear the dropdown
			dropdownElement.children().remove();
			
			// Build the list of options/optgroups
			_.each(groups, function(group) {
				if(matchedOptions[group] === undefined || matchedOptions[group].length == 0) return;
				
				var placeHolderElement = dropdownElement;
		
				if(group != 'global') {
					dropdownElement.append(
						$("<li>")
							.append(
								$("<span>").text(group)
							)
							.append(					
								placeHolderElement = $("<ul>")
									.addClass("group")								
							)
					);
				}

				// TODO A full page of code on my screen isn't enough to 
				// see what the context is here. I.e. the context is not apparent
				// to the casual editor. This is a good point for refactoring
				// to have shorter & less nested loops. -- DBW							
				
				_.each(matchedOptions[group], function(option) {			
					placeHolderElement.append(
						$("<li>").append(
							$("<a>")
								.data('option', option)
								.html(option.highlightedText)									
								.click(function(event) {																							
									inputElement.val(option.text);
									
									selectElement
										.val(option.value)
										.trigger("change");
										// TODO without wanting to cramp your style, 10 levels deep is too deep -- DBW

									choiceMade = true;
										
									// Hide the dropdown
									close();
								})
						)
					);
				});
			});
		
			// Hide the dropdown when it is empty, show otherwise
			if(dropdownElement.children().length == 0) {
				// Hide the dropdown
				hideDropdown();
			} else {
				// Show the dropdown				
				showDropdown();

				// Highlight the first item
				highlightFirst();

				if(initialHeight === undefined) {
					initialHeight = dropdownElement.outerHeight();			
				}

				checkShouldScroll();
			}		
		};			

		// Event called back to determine whether a click event should close the widget
		function checkClose(event) {		
			if(!$(event.target).closest(".AutoComplete-wrapper").is(wrapperElement)) {			
				close();	
			}
		};

		// Checks that the dropdownElement is fully visible, sets its height and makes it scrollable if not.				
		var initialHeight;
					
		function checkShouldScroll() {
			var docViewBottom = $(window).scrollTop() + $(window).height();

			var elemTop = dropdownWrapperElement.offset().top;
			var elemBottom = elemTop + dropdownElement.outerHeight();		

			if(docViewBottom - elemTop < initialHeight) {
				dropdownWrapperElement
					.height(docViewBottom - elemTop - 1)
					.addClass("scroll");
			} else {
				resetScroll();
			}

			if(dropdownElement.outerHeight() < dropdownWrapperElement.outerHeight()) {
				resetScroll();
			}
		};

		// Remove scrolling ability from the dropdownElement
		function resetScroll() {
			dropdownWrapperElement
				.css({'height' : 'auto'})
				.removeClass("scroll");
		};

		// Opens the widget
		var previousValue;
		
		function open() {
			choiceMade = false;

			// Stash previous value
			var inputValue = inputElement.val();

			if(inputValue != '') {
				previousValue = inputValue;
			}
		
			// Clear input element
			inputElement.val('');

			// Bind close handler
			$("body").bind("click", checkClose);

			// Update the contents of the dropdown
			update();

			$(window).bind("scroll resize", checkShouldScroll);			
		};

		// Closes the widget. Event is optional, when from a click it will blur
		function close() {
			hideDropdown();

			// Clear the dropdown
			dropdownElement.children().remove();

			if(!choiceMade) {
				inputElement.val(previousValue);
			}	

			// Blur the input element
			inputElement.blur();

			// Unbind body click handler
			$("body").unbind("click", checkClose);

			// Unbind window scroll handler
			$(window).unbind("scroll resize", checkShouldScroll);

			resetScroll();
		};

		// Moves the highlight up or down
		// @param direction Accepted values up|down
		function moveHighlight(direction) {					
			var anchors = dropdownElement.find("a");

			var highlightedAnchor = dropdownElement.find("a.highlighted");

			// No highligh select first
			if(highlightedAnchor.length == 0) {
				highlightedAnchor = dropdownElement.find(direction == 'down' ? "a:first" : "a:last");

				// Still nothing, then this is an empty dropdown TODO: we can determine this a nicer way and exit at the top of this function. Low priority refactor.				
				if(highlightedAnchor.length == 0) return;
			} else {
				highlightedAnchor.removeClass("highlighted");
				
				var index = anchors.index(highlightedAnchor);

				index = (index + (direction == 'down' ? 1 : -1)).mod(anchors.length);

				highlightedAnchor = $(anchors[index]);
			}

			// Scroll to the anchor, make it appear in the middle. Unfortunately $.scrollTo doesn't support this directly, hence the "math"
			var anchorMidpoint = highlightedAnchor[0].offsetTop + highlightedAnchor.outerHeight() / 2

			var dropdownMidpoint = dropdownWrapperElement.outerHeight() / 2;			

			var scrollValue = anchorMidpoint - dropdownMidpoint;
			
			if(scrollValue < 0) {
				scrollValue = 0;
			} 
			
			dropdownWrapperElement.scrollTo(scrollValue);

			// Add highlighted class to next anchor and get the text value
			var textVal = highlightedAnchor.addClass("highlighted").text();	

			// Update input value
			inputElement.val(textVal);		

			selectionMade = true;
		};

		// Highlights the first item
		function highlightFirst() {			
			dropdownElement.find("a.highlighted").removeClass("highlighted");

			dropdownElement.find("a:first").addClass("highlighted");
			
			selectionMade = true;
		};

		// Will invoke a selection event based on the currently highlighted item.
		function selectHighlightedItem() {
			var highlightedAnchor = dropdownElement.find("a.highlighted");

			var selected = false;

			if(selectionMade && highlightedAnchor.length == 0) {								
				selectElement.val(selectedOption.value);					

				selected = true;
			} else if(selectionMade) {
				selectElement.val(highlightedAnchor.data('option').value);					

				selected = true;
			} else if(numAvailableOptions == 1) {
				selectElement.val(firstItem.value);				

				selected = true;
			}

			if(selected) {
				selectElement.trigger("change");

				inputElement.val('');

				selectionMade = true;		

				choiceMade = true;
			}
		};

		// Bind handlers
		inputElement	
			.focus(function() {
				// Hack to ensure stream row is focused, prevent bug in firefox
				var messageStreamRow = $(this).closest(".MessageStreamRow");
				
				if(messageStreamRow.length > 0) {									
					MessageStream.focus(messageStreamRow);
				}
				
				wrapperElement.addClass("focus");

				open();	
			})
			.blur(function() {
				wrapperElement.removeClass("focus");
			})		
			/*.click(function() { // On click (focus doesn't require mouseup, I've moved to a full onclick for selecing messageStream rows and so have to also follow that convention here)
				console.log("AutoCompleteWidget", "input:click");
				
							
			})*/
			.bind("mousedown", function() {
				if(!$(this).is(":focus")) return false;
			})
			.bind("mouseup", function() {
				if(!$(this).is(":focus")) {
					$(this).trigger("focus");
					
					return false;
				}
			})
			.bind("keydown", function(event) { // Some keys only register keydown (those that blur)	
				if(event.keyCode == 13) { // KEY_ENTER
					event.preventDefault();
					event.stopImmediatePropagation();
					
					selectHighlightedItem();

					return;
				} else if(event.keyCode == 27) { // KEY_ESC
					close();				
				} else if(event.keyCode == 9) { // KEY_TAB
					event.preventDefault();
				}
			})
			.bind("keyup", function(event) {
				// KEY_UP || KEY_DOWN									
				if(event.keyCode == 38 || event.keyCode == 40) {												
					event.preventDefault();
					event.stopImmediatePropagation();
								
					moveHighlight(event.keyCode == 38 ? 'up' : 'down');
					
					return;
				}			
			
				update();				
			
				checkShouldScroll();
			});
			
		selectElement		
			.change(function(event) {	
				inputElement.val($(this).find("option:selected").text());
			});

		// If an option is :select(ed) then set this as the initial value
		var initiallySelectedOption = selectElement.find("option:selected");
		
		if(initiallySelectedOption.length > 0 && initiallySelectedOption.val().length > 0) {
			inputElement.val(initiallySelectedOption.text());
		}
	});
	
	console.log("... ajaxify AutoComplete done.");
};

console.log("registering ajaxify AutoComplete...");

ajaxifyFunctions.push(ajaxifyAutoComplete);

$(ajaxifyAutoComplete);

