/**
 * jQuery.DropdownReplacement
 * Copyright (c) 2010 Mikhail Koryak - http://notetodogself.blogspot.com
 * Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
 * Date: 02/03/10
 *
 * @projectDescription Full featured dropdown replacement
 *
 * Dependancies:
 * jquery.scrollTo v1.4.2+ [required]
 * jquery.support.windowsTheme v0.2 [optional]
 *
 * http://notetodogself.blogspot.com
 * Works with jQuery 1.4.2. Tested on FF3.6, FF3.0, IE6, IE7, IE8 on WinXP.
 *
 * @author Mikhail Koryak
 * @version 0.4.5 - dev
 */

;(function($){

$.fn.dropdownReplacement = function(opts){
	opts = $.extend({}, {
		options: null, //can be a JSON list ex: [{"v":"val1", "t":"test"},{"v":"v2", "t":"text2"}], prebuilt options html (see demos), or null if 'this' is a select
		selectClass: "dropdown", //class to give to the element that will act as the <select>
		optionsClass: "dropdownOpts", //the options body class
		optionsDisplayNum: 5, //number of options to display before making a scrollbar
		optionClass: "dropdownOpt",  //every option will have this class
		optionSelectedClass:"selectedOpt", //class to give to the selected option
		resizeOptionsToFitSelect: true, //set this to false, and if your options are 20px, and select 200px, the options will stay 20px
		resizeSelectToFitOptions: false,//if false - the options will extend beyond the select box. this may be preferable
		useHiddenInput:false, //when selected value is changed copy it to a hidden input with same name as the original element. otherwise use onSelect function to do it yourself
		ellipsisSelectText: false, //if select is resized smaller then dropdown text, will we add a "..." to end of it to indicate overflow
		selectWidth: "auto", //set width of the dropdown to something else.
		
		//advanced options:
		optionsWidthOffset: 3, //magic number that you can fiddle with if the width of the options body is wrong
		debounceLookupMs: 200, //when user types in order to find the option, this is the timeout between keystrokes before doing the search
		debounceArrowsMs: 50, //timeout between arrow keydowns before firing onSelect
		lookupMaxWordLength: 3, //higher the number, more you can look up by typing at the select, more memory used/slower load time.
		fastEllipsisBuilding: false, //(only matters if ellipsisSelectText=true) recalculate text width on every selection if set to false. set this to true if you experience performance problems or weirdness
		ellipsisText: "...",
		
		//on select callback:
		onSelect : function($select, value, text, selectIndex){
			$("#"+$source_id).val(value);
							//this is fired when user selects something
		}
	}, opts);

	var $self = $(this);
	var $body = $("body");
	var $window = $(window);
	var msie8 = $.browser.msie && parseInt($.browser.version, 10) == 8;
	var $source_id = $($self[0]).attr('id');

	var textList = []; 
	var textToOption = {};//used for selecting the correct option when clicking on a select
	var textToValue = {};
	var optionLookup = {};
	var winHeight, winWidth, optionHeight, optionHeight, optionsHeight, optionsWidth, selectHeight, selectWidth;

	var $options;
	var $hiddenInputs = [];

	var $selectedOption = $([]); //
	var $selectedSelect = $([]); //which select is currently droppped down

  var selectedSelectIndex = null;
  var selectText = []; //each select's text indexed by select index
	var enableBlur = true;
	var optionsShowing = [];//list of booleans indexed
	var charWidth = null; //how wide is one char. if null, char width is not used
	var ellipsisWidth = null;
	var detectedCharWidths = {};
	
	var event = { //events
	  lastLookupWord : null,
	  lastLookupIndex: 0,
	  
		options: function(e){ //options box - this is triggered when user clicks in the options box
			var $option = $(e.target);
			if($option.is("a")){
				setOptionsVisible(false);
				hightlightSelectedOption($option); 
				setSelection();
				e.stopPropagation();
				return false;
			}
		},
		select : function(e){ //this = select box - this is trigged when user clicks the select box
			this.selectionStart=this.selectionEnd=-1;
			selectedSelectIndex = e.data.index;
			if(e.data.showOptions && isOptionsVisible()){
				setOptionsVisible(false);
				return;
			}
			$selectedSelect = $(this);
			var text = getSelectText();
			var $option = textToOption[text];
			setOptionsVisible(e.data.showOptions && true);
		  hightlightSelectedOption($option);
		},
		unselect: function(e){ // - this is triggered when user clicks outside the select box, or tabs out
			if(enableBlur){
				setOptionsVisible(false);
			}
		},
		optionsOver: function(){// - this is triggered when user mouses over the options box
			enableBlur = false;
			$selectedOption.removeClass(opts.optionSelectedClass);
		},
		optionsOut: function(){// - this is triggered when user mouses out of the options box
			enableBlur = true;
		},
		selectLookup: function(word){ // select an option by a <= 3 char sequence typed at the select - triggered on key up over select box/input (debounced)
			var optionList = null;
			var chop = opts.lookupMaxWordLength > word.length ? opts.lookupMaxWordLength : word.length;
			for(var i = 0; (!optionList && i < chop); i++) {//match longest first, then back down
				word = word.substring(0, opts.lookupMaxWordLength - i);
				optionList = optionLookup[word]; 
			}
			if(!optionList){
				return;
			}
			if(event.lastLookupWord === word){
				event.lastLookupIndex = event.lastLookupIndex + 1;
			} else {
				event.lastLookupIndex = 0;
				event.lastLookupWord = word;
			}
			
			if(optionList && optionList.length){
				if(optionList.length <= event.lastLookupIndex){
					event.lastLookupIndex = 0;
				}
				var $option = optionList[event.lastLookupIndex];
				hightlightSelectedOption($option);
				setSelection();
			}
		}
  };
  
  /*
  Whenever the hidden input value changes this function is called. 
  fireEvent = TRUE when actual selection by user is made
  						FALSE when widget is loaded and inital selected values are shoved into the hidden input(s)
  */
  var onSelect = function($select, value, text, index, fireEvent){
  	if(fireEvent){
  		opts.onSelect($select, value, text, index);
  	}
  }
  
  var detectCharWidth = function(testText){
	    var val = testText || "a b c d e f 1 2 3 4 5 6 A B C D E F ! ! %"; //correct detection depends on this more then anything
	  	if(!detectedCharWidths[val]){
				var $inp = $("<span>", {
					"text":val,
					"class":opts.selectClass,
					"css": {"background":"none", "margin":0, "padding":0, "overflow":"visible", "width":"auto", "color":"#FFF"}
				});
				$body.append($inp);
				detectedCharWidths[val] = ($inp.width() / val.length);
				$inp.remove();	 
	  	}
	  	return detectedCharWidths[val];
  }
  
	var setOptionsVisible = function(visible){
		optionsShowing[selectedSelectIndex] = visible;
		$options[(visible ? "show" : "hide")]();
		if(visible){
			repositionOptions();
		}
	};
	var isOptionsVisible = function(){
		return optionsShowing[selectedSelectIndex];
	};

	var setSelection = function(){
		var text = $selectedOption.text();
		setSelectText($selectedSelect, text);
		onSelect($selectedSelect, textToValue[text], text, selectedSelectIndex, true);
		if(opts.useHiddenInput){
			$hiddenInputs[selectedSelectIndex].val(textToValue[getSelectText()]);
		}
	};
	
	var getSelectText = function(index){
		return selectText[arguments.length ? index : selectedSelectIndex];
	}
	
	var setSelectText = function($select, text){
		selectText[selectedSelectIndex] = text;
//		console.log("index:"+selectedSelectIndex+ " text:"+text);
		if(opts.ellipsisSelectText){
			charWidth = detectCharWidth(text);
			var selectWidth = $select.width();
			var maxChars = ~~(selectWidth / charWidth);
			if(maxChars < text.length) {
				maxChars -= ~~((ellipsisWidth + 5)  / charWidth);
				text = text.substring(0, maxChars) + opts.ellipsisText;
			}
		}
		$select.val(text);
	}

	var hightlightSelectedOption = function($option){
		$selectedOption.removeClass(opts.optionSelectedClass);
		if($option){
			$selectedOption = $option;
			$selectedOption.addClass(opts.optionSelectedClass);
		}
		if(isOptionsVisible() && jQuery.scrollTo){
			$option && $options.scrollTo($option);
		} 
	};

	var constructOptions = function($select){ //this is done once: order #1
		selectedSelectIndex = 0;
		var constructWithPrebuildOptions = function(){
			$options = opts.options;
			var json = [];
			var l = $options.find("a");
			for(var i = 0; i < l.length; i++){
				var $option = $(l[i]);
				json[i] = {"t": $option.text(), "v": $option.attr("name")};
				$option.addClass(opts.optionClass); 
				textToOption[json[i].t] = $option;
			}
			opts.options = json;
		};
		var constructWithJSONOptions = function(){
			var l = opts.options;
			$options = $("<div>");
			for(var i = 0; i < l.length; i++){
				var $option = $("<a>", {
					href:"#",
					name: l[i].v,
					text: l[i].t,
					"class":opts.optionClass
				});
				$options.append($option);
				textToOption[l[i].t] = $option;
			}
			$body.append($options);
		};
		constructWithNativeSelect = function(){
			if($self.length > 1){
				throw exception("trying to widgetize more then ONE 'select' is not supported. You can widgetize multiple 'input' elements.");
			}
			var l = $select.find("option");
			if(l.length === 0){
				throw exception("'select' must have ONE or more options elements as children in order to widgetize the select");
			}
			opts.options = [];
			var $newSelect = $("<input>", {
				"css": {"width": $self.width()}
			});
			for(var i = 0; i < l.length; i++){
				var $option = $(l[i]);
				var value = $option.val();
				var text = $option.text();
				opts.options[i] = {"t":text, "v":value};
				if($option.is(":selected")){
					$newSelect.val(text);
				}
			}
			$select.after($newSelect);
			//$select.remove();
			$select.hide();
			$select = $self = $newSelect;
			constructWithJSONOptions();
		};
		
	  if($select.is("select")){ //the element widgetized was a select
			constructWithNativeSelect();			
		} else if(opts.options instanceof jQuery){ //input was widgetized, options are prebuilt
			constructWithPrebuildOptions();
		} else { //input widgetized, options are a json list
			constructWithJSONOptions();
		}
		var l = opts.options; //at this point options should be json options
		for(var i = 0; i < l.length; i++){
			textList[textList.length] = l[i].t;
			textToValue[l[i].t] = l[i].v;
		}
		$options.addClass(opts.optionsClass);
		$options.click(event.options);
		$options.mouseover(event.optionsOver);
		$options.mouseleave(event.optionsOut);
	};

	var buildTextLookupMap = function(){  //this is done once: order #2
		for(var i = 0; i < textList.length; i++){
			for(var j = 1; j < (opts.lookupMaxWordLength + 1); j++){
				if(textList[i].length >= j){
					var letters = textList[i].substring(0, j).toUpperCase();
					if(!optionLookup[letters]){
						optionLookup[letters] = [];
					}
					optionLookup[letters].push(textToOption[textList[i]]);
				}
			}
		}
	};

	var resizeOptions = function($select){ // this is done once: order #3
		var $firstOption = textToOption[textList[0]];
		$options.show();
	  optionHeight = $firstOption.outerHeight(true); //we can only get height if its visible
	  var requestedHeight = opts.optionsDisplayNum * optionHeight ;
		var preferedHeight = $options.height();
		var selectWidth = $select.width();
		var preferedWidth = $options.width();
		$options.hide();
		
		if(opts.resizeSelectToFitOptions && preferedWidth > selectWidth){
		//	$self.each(function(){ 
		    //$(this)
				$select.width(preferedWidth + opts.optionsWidthOffset);
		//	});
			selectWidth = preferedWidth ;
		}
		if(opts.resizeOptionsToFitSelect && preferedWidth < selectWidth){
			selectWidth -= opts.optionsWidthOffset;
		}
		$options.css({
			width:  (preferedWidth > selectWidth ? preferedWidth : selectWidth),
			height: (preferedHeight < requestedHeight ? preferedHeight : requestedHeight) + (msie8 ? 2 : 0)
		});
	};

	var repositionOptions = function(){
		var offset = $selectedSelect.offset();
		var top = offset.top;
		var left = offset.left;
		
		if(top + optionsHeight > winHeight + $window.scrollTop() && offset.top - optionsHeight > 0){
			top = offset.top - optionsHeight;
		} else {
			top = top + selectHeight;
		}
		if(!opts.resizeSelectToFitOptions && left + optionsWidth  > winWidth){
			left -= (optionsWidth - selectWidth);
		}
		$options.css({
			"top": top ,
			"left": left
		});
		
	};

	var getDebouncedKeyUp = function() {
		var timer;
		var word = [];
		return function(e) {
			var args = arguments;
			word.push(String.fromCharCode(e.keyCode));
			clearTimeout(timer);
			timer = setTimeout(function() {
				event.selectLookup(word.join(""));
				timer = null;
				word = [];
			}, opts.debounceLookupMs);
		};
	};

	var arrowMove =	function(direction){ //direction is "prev" or "next"
		var $newSelected;
		if(direction !== "first" && direction !== "last"){
			if($selectedOption.length > 0) {
				$newSelected = $selectedOption[direction]("a");
			} else {
				$newSelected = textToOption[textList[0]]; //select first on the list if initial selection not on the list
			}
		} else {
			if(direction === "first"){
				$newSelected = textToOption[textList[0]];
			} else {
				$newSelected = textToOption[textList[textList.length - 1]];
			}
		}
		if($newSelected.length === 0){
			return false; //cant move there
		} else {
			hightlightSelectedOption($newSelected);
			return true;
		}
	};

	var debounceArrows = function() {
			var timer;
			var directions = [];
			directions[38] = "prev"; //up
			directions[40] = "next"; //down
			directions[33] = "first"; //page up
			directions[34] = "last"; //page down
			return function(e) {
				var direction = directions[e.keyCode];
				if(direction){
					if(arrowMove(direction)){
						clearTimeout(timer);
						timer = setTimeout(function() {
							setSelection();
							timer = null;
						}, opts.debounceArrowsMs); 
					}
				}
			};
	};

	var calculateWindowBounds = function(){
		winWidth = $window.width();
		winHeight = $window.height();
	};
	
	var calculateDimensions = function($select){
		selectWidth = $select.outerWidth(true);
		selectHeight = $select.outerHeight(true);
		optionsHeight = $options.outerHeight(true);
		optionsWidth = $options.outerWidth(true);
	};
	
	var skinSelectBox = function($select){
		$select.addClass(opts.selectClass);
		if($.support.windowsTheme && $.support.windowsTheme.name){
			$select.addClass("dd-theme-"+$.support.windowsTheme.name);
			$options.addClass("opt-theme-"+$.support.windowsTheme.name);
		} else {
			//$select.addClass("dd-all");
		}
	}
	
	var browserSpecificSettings = function(){
		if(msie8){
			opts.optionsWidthOffset -= 3;
		}
	}
	
	var exception = function(str){
		return "jquery.dropdownReplacement exception: "+str;
	}

	var init = function(){
		if(!jQuery.scrollTo){
			throw exception("jquery.scrollTo plugin is required for this plugin. http://plugins.jquery.com/project/ScrollTo");
		}
		browserSpecificSettings();
		var $firstSelect = $($self[0]);
		if(opts.ellipsisSelectText){
			charWidth = detectCharWidth();
			ellipsisWidth = detectCharWidth(opts.ellipsisText) * opts.ellipsisText.length;
		}		
		constructOptions($firstSelect);
		buildTextLookupMap(); 
		calculateWindowBounds();
		
		$self.each(function(index){
			var $select = $(this);
			selectedSelectIndex = index;
			if(opts.selectWidth !== "auto"){
				$select.css({"width": opts.selectWidth}); //TODO: this might want to be right before 'ellipsisSelectText'
			}
			if(!$select.is("input") && !$select.is("select")){
				throw exception("root element must be an 'input' or 'select'");
			}
			skinSelectBox($select);
			if(index === 0){
				resizeOptions($select);
				calculateDimensions($select);
			}
			setSelectText($select, $select.val());
			
			if(opts.useHiddenInput){
			  var $hiddenInput = $("<input>", {
			  	name: $select.attr("name")
			  });
			  var text = getSelectText();
			  $hiddenInput.val(textToValue[text] || "");
			  $select.attr("name", "");
			  $hiddenInput.attr("hidden","true");
			  $select.after($hiddenInput);
			  $hiddenInput.hide();
				$hiddenInputs[index] = $hiddenInput;
				onSelect($select, textToValue[text], text, index, false)
			}
			
			$select.attr("readonly", "true");
			$select.bind("click", {"index":index, "showOptions": true}, event.select)
						 .bind("blur",{"index":index},event.unselect)
						 .bind("focus",{"index":index, "showOptions": false}, event.select)
						 .keyup(getDebouncedKeyUp())
						 .keydown(debounceArrows())
						 .keydown(function(e){
								if(e.keyCode == 13 || e.keyCode == 27){
									setOptionsVisible(false);
									$select.blur();
								}
							});
		});
		$window.resize(function(){
			calculateWindowBounds();
			setOptionsVisible(false);
		});
	};
	init();
	return $self;
};
}(jQuery));
