/**
 * JMMDropdown plugin for jQuery
 *
 * Converts a list into a scrollable and searchable (optional) <select>-like
 * widget with smooth animations.
 *
 * requires jQuery 1.3.2+
 * requires jQuery i18n plugin 
 *
 */
(function($) {

var msie6 = $.browser.msie && $.browser.version < 7;
var msie = $.browser.msie;

/**
 * initDropdown()
 * initalize a fake dropdown, this function expects to be applied to an selected element with the following html structure:
 * <div class="gui-select gui-select-nojs menu-to-top show-search">
 * 	<a href="#" class="open-link">Title</a>
 * 	<div class="gui-select-popup">
 *		<ul>
 *			<li><a href="#"><i class="fl af"> </i><span>Link title</span></a></li
 *			[...]
 *		</ul>
 * </div>
 *
 * See CSS files for further details
 *
 *
 */
$.fn.initDropdown = function(options) {
	return this.each(function() {
		new JMMDropDown(this, options);
		return this;
	});
} 

// array holding all dropdown objects
var allJMMDropDowns = [];

var JMMDropDown = function(target, options) {	
	var s = this;
	
	this.settings = $.extend({		
		
		// defining key codes for keyboard navigation
		keys: {
			ESC: 27,
			ARROW_UP: 38,
			ARROW_DOWN: 40,
			
			PAGE_UP: 33,
			PAGE_DOWN: 34,
			HOME: 36, 
			END: 35,
			
			RETURN: 13,
			SPACEBAR: 32
		},
		
		// user settings
		animationSpeed: 250,
		pageScrollSpeed: 5,
		showSearch: false // can also be set by giving wrapper element the class "show-search"
		
	}, options);
	
	// assign array< 	
	this.allJMMDropDowns = allJMMDropDowns;
	
	// remove fix for disabled JavaScript.
	// when JS is diabled, dropdown menu should still appear using :hover pseudo-class in CSS
	this.wrapper = $(target).removeClass("gui-select-nojs");
	
	if (this.wrapper.hasClass("show-search")) {
		this.settings.showSearch = true;
	}
	
	// now using visibility instead of display: none for better performance
	this.popup = this.wrapper.find(".gui-select-popup").hide().attr("unselectable", "on").css("-moz-user-select", "none");
	
	// create text input for quick jumps in list by using keyboard
	if (this.settings.showSearch) {
		this.textInput = $("<input type='text' class='gui-select-search' />");
		this.wrapper.append(this.textInput);
		this.textInput.hide();
		this.noResult = $("<li class='no-result'>" + $.i18n._("search.noResult") + '</li>'); 
	}
	
	// store values for search puroposes
	// values will be caching on initalization so we don't
	// have to mess aroung with DOM traversing when user is searching
	// but have quick access to them instead
	this.listItems = [];
	this.labels = [];
	this.values = [];
	this.valuesLower = [];
	
	/// internal variables representing current state of the select box
	this._active = -1;
	this._activeElement = null;
	this._currentResultSet = [];

	// attach handler to open link
	this.openLink = $("a.open-link", this.wrapper);
	this.openLink.click(function() {		
		s.openLinkHandler();
		return false;
	});

	// initalize the rest all stuff
	this.init();
};

$.extend(JMMDropDown.prototype, {

	init: function() {
		
		var s = this;
		
		// loop through list elements and retrive data for search and add event handlers
		$("li", this.popup).each(function(i, v) {
			// loop through all list elements for collecting information
			var opt = $(this);
			s.listItems.push(opt);
			var l = opt.find("span");
			s.labels.push(l);
			var v = l.html();
			s.values.push(v);
			s.valuesLower.push(v.toLowerCase());
						
			
			opt.find("a")
				// when clicking on a link inside the list, close it
				.click(function() { s.close(); })
				// when hovering this link, focus it
				.mouseover(function() { s.setHighlightAbsolute(i, false); });
		});
		
		// if search is active 
		if (this.settings.showSearch) {
			this.popup.find("ul").append(this.noResult);
			this.noResult.hide();
		}

		// if menu should pop up to the upper side let's do this!
		if (this.wrapper.hasClass("menu-to-top")) {
			this.popup.css("bottom", this.wrapper.height());
		}
		
		// attach this object to global array holding all dropdowns
		this.allJMMDropDowns.push(this);
	},
	
	
	// internal function for hiding dropdown
	close: function() {
		
		var s = this;
		
		if (this.popup.is(":visible")) {
			if (!msie6) {
				// all browsers will see animation, except for MSIE6
				this.popup.fadeOut(this.settings.animationSpeed, function() { s.afterClose() });
			} else {
				this.popup.hide();
				s.afterClose();
			}
			
			this.wrapper.removeClass("gui-select-open");
			this.unbindKeyEvents();
			
			if(this.settings.showSearch) {
				this.textInput.hide();
			}
		}
	},
	
	afterClose: function() {
		for (var i in this.listItems) {
			this.listItems[i].find("span").html(this.values[i]);
		}
	},
	
	openLinkHandler: function() {
			
			var s = this;
			
			if (this.popup.is(":visible")) {
				// if link was clicked and dropdown is already visible, hide it
				this.close();
			} else {
				
				if (this.settings.showSearch) {				
					// show search input
					this.textInput
						.width(parseInt(this.openLink.width()) - parseInt(this.textInput.css("padding-left")) - parseInt(this.textInput.css("padding-right")))
						.height(parseInt(this.openLink.height()))
						.show()
				}
			
				// init variables for current result set
				// reset list by also showing all available list items
				this._activeElement = null;
				this._active = -1;
				this._currentResultSet = [];
				for (var i in this.listItems) {
					this.listItems[i].show()
					this._currentResultSet.push(i);
				}
			
				// hide find other open dropdowns and hide them
				// usually there should be one or none, but just to get shure,
				// loop through them.
				for (var i in this.allJMMDropDowns) {
					this.allJMMDropDowns[i].close();
				}
				
				// if showSearch option is set, display search above open link text
				if (this.settings.showSearch) {
					if (s._currentResultSet.length == 0) {
						s.noResult.show();
					} else {
						s.noResult.hide();
					}
				}
				
				// cleaing highlight, i.e. remove "active" class from every element and set active element attributes to defaults
				this.cleanHighlight();
				
				// finally, show dropdown
				if (!msie6) {
					// all modern browsers 'll see an animation when menu slides down,
					// based on experience, MSIE6 will get performance problems even of faster
					// machines when having hundreds of items so we hide animation from MSIE6 users  
					this.popup.slideDown(this.settings.animationSpeed, function() { s.setHighlightRelative(0, true); });
				} else {
					this.popup.show();
					this.setHighlightRelative(0, true);
				}
			
				this.wrapper.addClass("gui-select-open");
				this.bindKeyEvents();
			}
		return false;
	},
	
	// sets highlighted element based on an index relative to the current result set
	// for example we have the list items:
	// [0, 1, 2, 3, 4, 5, 6]
	// and currently visible are:
	// [2, 4, 5]
	// an index param of 2 will show us item "5"
	setHighlightRelative: function(index, scroll) {
		if (this._activeElement != null) {
			this._activeElement.removeClass("active");
		}
	
		if (this.listItems[this._currentResultSet[index]]) {
			this._activeElement = this.listItems[this._currentResultSet[index]].addClass("active");
			this._active = index;
			if (scroll == true) {
				this.adjustScoll();
			}
		}
	},
	
	// sets the highlighted item based on absolute index
	// value will be mapped to current result set
	setHighlightAbsolute: function(index, scroll) {
		for (var i in this._currentResultSet) {
			if (this._currentResultSet[i] == index) {
				this.setHighlightRelative(i, scroll);
				break;
			}
		}
	},
	
	// removed "active" class from all list elements
	// and resets internal highlight attributes
	cleanHighlight: function() {
		for (var i in this.listItems) {
			this.listItems[i].removeClass("active");
		}
		
		this._active = -1;
		this._activeElement = null;
	},
	
	// adjusts scroll so active element will be visible incurrent viewport of the
	// scroll pane containing the list
	adjustScoll: function() {
		if (this._activeElement != null) {
			var pos = this._activeElement.position();
			var height = this._activeElement.height();
			
			var pHeight = this.popup.height();
			var pScroll = this.popup.scrollTop();
			
			if (pos.top < 0) {
				this.popup.scrollTop(pScroll + pos.top);
			} else if (pos.top > pScroll + pHeight) {
				this.popup.scrollTop(pos.top);
			} else if (pos.top + height > pHeight) {
				this.popup.scrollTop(pScroll - pHeight + pos.top + height);
			}
			
		}
	},
	
	// adds key events
	bindKeyEvents: function() {
		
		var s = this;
		var filterHandler = null;
		
		if (this.settings.showSearch) {
			this.textInput.focus();
			this.lastVal = "";
			
			// handles text input and filtering of list elements
			filterHandler = function(e) {
				
				var va = this.value.toLowerCase();
								
				if (s.lastVal != va && va == "") {
					// search term is empty, show the while result set				
					
					s._currentResultSet = [];
					
					for (var i in s.listItems) {
						s.listItems[i].show();
						s.labels[i].html(s.values[i]);
						s._currentResultSet.push(i);
					}
					s.setHighlightRelative(0, true);
					
				} else if (s.lastVal != va && va.length > s.lastVal.length && va.indexOf(s.lastVal) == 0) {
					// variable has become longer, and starts with the previous value
					var newResultSet = [];
									
					for (var n = 0; n < s._currentResultSet.length; n++) {
						var i = s._currentResultSet[n];
						var pos = s.valuesLower[i].indexOf(va);
						if (pos == 0) {
							var high = '<b>' + s.values[i].substring(0, va.length) + '</b>' + s.values[i].substring(va.length);
							s.listItems[i].show();
							s.labels[i].html(high);
							newResultSet.push(i);
						} else {
							s.listItems[i].hide();
						}
					}
					
					s.setHighlightRelative(0, true);
					s._currentResultSet = newResultSet;
					s.setHighlightRelative(0, true);
					
				} else if (s.lastVal != va) {
					// all other cases
					
					s._currentResultSet = [];
				
					for (var i = 0; i < s.listItems.length; i++) {
						var pos = s.valuesLower[i].indexOf(va);
						if (pos == 0) {
							var high = '<b>' + s.values[i].substring(0, va.length) + '</b>' + s.values[i].substring(va.length);
							s.listItems[i].show();
							s.labels[i].html(high);
							s._currentResultSet.push(i);
						} else {
							s.listItems[i].hide();
						}
					}
					s.setHighlightRelative(0, true);	
				}
				
				s.lastVal = va;
				
				// display no results element, if there filter did not match any element from the list
				if (s._currentResultSet.length == 0) {
					s.noResult.show();
				} else {
					s.noResult.hide();
				}
				
			};
		}
		
		// handles key events for allowing keyboard navigation in dropdown list
		var keyboardHandler = function(e) {			
			var results = s._currentResultSet.length;
			switch(e.which || e.keyCode) {
				case (s.settings.keys.ARROW_UP):
					if (s._active > 0) {
						s.setHighlightRelative(s._active - 1, true); 
					}
					
					return false;					
					break;

				case (s.settings.keys.PAGE_UP):
					if (s._active == -1  || s._active < s.settings.pageScrollSpeed) {
						s.setHighlightRelative(0, true);
					} else {
						s.setHighlightRelative(s._active - s.settings.pageScrollSpeed, true); 
					}
					
					return false;
					break;


				case (s.settings.keys.ARROW_DOWN):
					if (s._active < results - 1) {
						s.setHighlightRelative(parseInt(s._active, 10) + 1, true); 
					}
					
					return false;					
					break;

				case (s.settings.keys.PAGE_DOWN):
					if (results < s.settings.pageScrollSpeed || parseInt(s._active, 10) + s.settings.pageScrollSpeed >= results - 1) {
						s.setHighlightRelative(results - 1, true);
					} else {
						s.setHighlightRelative(parseInt(s._active, 10) + s.settings.pageScrollSpeed, true); 
					}
					
					return false;
					break;
				
				case (s.settings.keys.HOME):
					s.setHighlightRelative(0, true);
					break;
					
				case (s.settings.keys.END):
					s.setHighlightRelative(results - 1, true);
					break;
					
				case (s.settings.keys.RETURN):
					if (s.noResult.is(":hidden")) {
						s._activeElement.find("a").click();	
					}
					//return false;
					break;
					
				case (s.settings.keys.ESC):
					s.close();
					//return false;
					break;

			}
		}
		
		// if showing search bind key events to the search field 
		if (this.settings.showSearch) {
			this.textInput.val("")
				.bind("keypress.gui-select-textinput", filterHandler)
				.bind("keydown.gui-select-textinput", filterHandler)
				.bind("keyup.gui-select-textinput", filterHandler);
		}
				
		// after showing, add behaviors for clicking and keypressing to document
		// to make sure that dropdown will also clsoe, when user is pressing ESC key 
		// or clicking anywhere outside the dropdown
		$(document.body).bind("click.gui-select", function(e) {
			var target = $(e.target);
			if (!target.is(".gui-select, .gui-select *")) {
				s.close();
			}
		}).bind("keydown.gui-select", keyboardHandler);
	},
	
	// removes key events from document and if applicable from the search field 
	unbindKeyEvents: function() {
		
		// remove dropdown specific behaviors from document when closing a dropdown
		$(document.body).unbind("keydown.gui-select").unbind("click.gui-select");
		
		if (this.settings.showSearch) {
			this.textInput
				.unbind("keypress.gui-select-textinput")
				.unbind("keydown.gui-select-textinput")
				.unbind("keyup.gui-select-textinput");
		}
	}	

});

})(jQuery);
