/********************************************************************************
 *   Copyright (C) 2009 by Vladimir Kadalashvili				*
 *   Kadalashvili.Vladimir@gmail.com						*
 *										*
 *   This program is free software; you can redistribute it and/or modify	*
 *   it under the terms of the GNU General Public License as published by	*
 *   the Free Software Foundation; either version 2 of the License, or		*
 *   (at your option) any later version.					*
 *										*
 *   This program is distributed in the hope that it will be useful,		*
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of		*
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the		*
 *   GNU General Public License for more details.				*
 *										*
 *   You should have received a copy of the GNU General Public License		*
 *   along with this program; if not, write to the				*
 *   Free Software Foundation, Inc.,						*
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.			*
 ********************************************************************************/


;(function() {
	$.fn.sexyCombo = function(config) {
		return this.each(function() {
			new $sc(this, config);
		});  
	};
	
	//default config options
	var defaults = {
		//skin name
		skin: "sexy",
	
		//this suffix will be appended to the selectbox's name and will be text input's name
		suffix: "__sexyCombo",
	
		//initial / default field value.
		//Also applied when user types something that is not in the options list
		initialValue: "",
	
		//if provided, will be the value of the text input when it has no value and focus
		emptyText: "",
	
		//if true, autofilling will be enabled
		autoFill: false,
	
		//if true, selected option of the selectbox will be the initial value of the combo
		triggerSelected: false,
	
		//function for options filtering
		filterFn: null,
	
		//if true, the options list will be placed above text input
		dropUp: false,
	
		//separator for values of multiple combos
		separator: ","
	};
	
	//constructor
	//creates initial markup and does some initialization
	$.sexyCombo = function(selectbox, config) {
		if (selectbox.nodeName != "SELECT")
		return;
		
		this.config = $.extend({}, defaults, config || {}); 
		
		this.selectbox = selectbox;
		this.$selectbox = $(selectbox);

		this.wrapper = this.$selectbox.wrap("<div>").
		hide().
		parent().
		addClass("combo").
		addClass(this.config.skin); 
		
		this.input = $("<input type='text' />").
		appendTo(this.wrapper).
		attr("autocomplete", "off").
		attr("value", this.config.initialValue).
		attr("name", this.$selectbox.attr("name") + this.config.suffix);
	
		this.icon = $("<div />").
		appendTo(this.wrapper).
		addClass("icon"); 
	
		this.listWrapper = $("<div />").
		appendTo(this.wrapper).
		addClass("invisible").
		addClass("list-wrapper"); 
		this.updateDrop();
	
		this.list = $("<ul />").appendTo(this.listWrapper); 
		var self = this;
		$options = $(this.selectbox).children("option");
		$options.each(function() {
			var optionText = $(this).text();
			var optionValue = $(this).val();
			$("<li />").
			appendTo(self.list).
			text(optionText).
			attr("id", self.$selectbox.attr("id") + "-" + optionValue).
			addClass("visible");
			
		});  
	
		this.listItems = this.list.children();

		if ($.browser.opera) {
			this.wrapper.css({position: "relative", left: "0", top: "0"});
		} 
	
		this.filterFn = ("function" == typeof(this.config.filterFn)) ? this.config.filterFn : this.filterFn;
	
		this.lastKey = null;
		this.overflowCSS = $.browser.opera ? "overflow" : "overflowY";

		this.multiple = this.$selectbox.attr("multiple");	
	
		this.triggerSelected();
		//this.initialValue();

		this.initEvents();
	};
	
	//shortcuts
	$sc = $.sexyCombo;
	$sc.fn = $sc.prototype = {};
	$sc.fn.extend = $sc.extend = $.extend;
	
	$sc.fn.extend({
		//TOC of our plugin
		//initializes all event listeners
		//it would be more correct to call it initEvents
		initEvents: function() {
			var self = this;
		
			this.icon.bind("click", function() {
				self.iconClick();
			}); 
		
			this.listItems.bind("mouseover", function(e) {
				self.highlight(e.target);
			});
		
			this.listItems.bind("click", function(e) {
				self.listItemClick($(e.target));
			});
			
			this.input.bind("keyup", function(e) {
				self.keyUp(e);
			});
		
			this.input.bind("keypress", function(e) {
				if ($sc.KEY.RETURN == e.keyCode)
					e.preventDefault();
				if ($sc.KEY.TAB == e.keyCode)
					self.hideList();	
			});
		
			$(document).bind("click", function(e) {
				if ((self.icon.get(0) == e.target) || (self.input.get(0) == e.target))
					return;
				self.hideList();	
			});
		
			//this.triggerSelected();
			//this.initialValue();
			this.applyEmptyText();
		},
	
	
		getTextValue: function() {
			return this.__getValue("input");
		},
	
		getCurrentTextValue: function() {
			return this.__getCurrentValue("input");
		},	
	
		__getValue: function(prop) {
			prop = this[prop];
			if (!this.multiple)
				return prop.val();
		
			var tmpVals = prop.val().split(this.config.separator);
			var vals = [];
		
			for (var i = 0, len = tmpVals.length; i < len; ++i) {
				vals.push(tmpVals[i]);
			}
			vals = $sc.normalizeArray(vals);
			return vals;
		},
	
		__getCurrentValue: function(prop) {
			 prop = this[prop];
			 if (!this.multiple)
				 return prop.val();
			 return prop.val().split(this.config.separator).pop();		 
		},
	
		//icon click event listener
		iconClick: function() {
			if (this.listVisible()) 
				this.hideList();
			else 
				this.showList();
			this.input.focus();
		},
	
		//returns true when dropdown list is visible
		listVisible: function() {
			return this.listWrapper.hasClass("visible");
		},
	
		//shows dropdown list
		showList: function() {
			if (!this.listItems.filter(".visible").length)
				return;
		
			this.listWrapper.removeClass("invisible").
			addClass("visible");
			this.wrapper.css("zIndex", "99999");
			this.listWrapper.css("zIndex", "99999");
			this.setOverflow();
			this.setListHeight();
			this.highlightFirst();
			this.listWrapper.scrollTop(0);
		},
	
		//hides dropdown list
		hideList: function() {
			this.listWrapper.removeClass("visible").
			addClass("invisible");
			this.wrapper.css("zIndex", "0");
			this.listWrapper.css("zIndex", "99999");
		},
	
		//returns sum of all visible items height
		getListItemsHeight: function() {
			return this.listItems.height() * this.liLen();
		},
	
		//changes list wrapper's overflow from hidden to scroll and vice versa (depending on list items height))
		setOverflow: function() {
			if (this.getListItemsHeight() > this.getListMaxHeight())
				this.listWrapper.css(this.overflowCSS, "scroll");
			else
				this.listWrapper.css(this.overflowCSS, "hidden");	
		},
	
		//highlights active item of the dropdown list
		highlight: function(activeItem) {
			if (($sc.KEY.DOWN == this.lastKey) || ($sc.KEY.UP == this.lastKey))
				return;
		
			this.listItems.removeClass("active");   
			$(activeItem).addClass("active");
		},
		
		//sets text inputs value
		setComboValue: function(val, pop, hideList) {

			if(val == '') val = this.input.val();
		
			$options = $(this.selectbox).children('option');

			var v = '';
			var set = false;
			if (this.multiple) {
				var comboVals = this.getTextValue();
				var hiddenVals = [];
				for (var i = 0, len = comboVals.length; i < len; ++i) {
					for (var j = 0, len1 = $options.length; j < len1; ++j) {
						var $opt = $options.eq(j);
						if (comboVals[i] == $opt.text()) {
							$opt.selected = true;
						}	  
					}
				}
				v = this.getTextValue();

				if (pop) 
					v.pop();

				v.push(val);
				v = $sc.normalizeArray(v);
				v = v.join(this.config.separator) + this.config.separator;   
			}
			else {
				for (var i = 0, len = $options.length; i < len; ++i) {
					var $opt = $options.eq(i);
					//alert('val: <' + val + '>, opt: <' + $opt.text() + '>, opt elem: ' + $opt.toString());
					if ($opt.text() == val) {
						$opt.selected = true;
						this.selectbox.selectedIndex = i;
						set = true;
						break;
					}
				}
			}
			this.input.val(val);
			this.filter();
			if (hideList)
				this.hideList();

			this.input.removeClass("empty");

			if (this.multiple)
				this.input.focus();

			if (!set) {
				//alert('adding value: ' + val);
				// add an element to selectbox
				$("<option />").appendTo(this.$selectbox).
				attr("value", val).
				text(val).
				attr("selected", "selected");
			}
		
		},
	
		listItemClick: function(item) {
			this.setComboValue(item.text(), true, true);
			this.inputFocus();
		},
	
		//adds / removes items to / from the dropdown list depending on combo's current value
		filter: function() {
			var comboValue = this.input.val();
			var self = this;

			this.listItems.each(function() {
				var $this = $(this);
				var itemValue = $this.text();
				if (self.filterFn.call(self, self.getCurrentTextValue(), itemValue, self.getTextValue())) {
					$this.removeClass("invisible").
					addClass("visible");
				} else {
					$this.removeClass("visible").
					addClass("invisible");
				}
			});
		
			this.setOverflow();
			this.setListHeight();
		},
	
		//default dropdown list filtering function
		filterFn: function(currentComboValue, itemValue, allComboValues) {
			if (!this.multiple) {
				return itemValue.toLowerCase().search(currentComboValue.toLowerCase()) == 0;
			}
			else {
				//exclude values that are already selected
				for (var i = 0, len = allComboValues.length; i < len; ++i) {
					if (itemValue == allComboValues[i]) {
						return false;
					}
				}
				return itemValue.toLowerCase().search(currentComboValue.toLowerCase()) == 0;
			}
		},
	
		//just returns integer value of list wrapper's max-height property
		getListMaxHeight: function() {
			return parseInt(this.listWrapper.css("maxHeight"), 10);
		},
	
		//corrects list wrapper's height depending on list items height
		setListHeight: function() {
			var liHeight = this.getListItemsHeight();
			var maxHeight = this.getListMaxHeight();
			var listHeight = this.listWrapper.height();
			if (liHeight < listHeight) {
				this.listWrapper.height(liHeight);   
			}
			else if (liHeight > listHeight) {
				this.listWrapper.height(Math.min(maxHeight, liHeight));
			}
		},
	
		//returns active (hovered) element of the dropdown list
		getActive: function() {
			return this.listItems.filter(".active");
		},
	
		keyUp: function(e) {
			this.lastKey = e.keyCode;
			var k = $sc.KEY;
			switch (e.keyCode) {
			case k.RETURN:
				this.setComboValue(this.getActive().text(), true, true);
				if (!this.multiple)
					this.input.blur();
				break;
			case k.DOWN:
				this.highlightNext();
				break;
			case k.UP:
				this.highlightPrev();
				break;
			case k.ESC:
				this.hideList();
				break;
			default:
				this.inputChanged();
				break;
			}
		},
	
		//returns number of currently visible list items
		liLen: function() {
			return this.listItems.filter(".visible").length;
		},
	
		//triggered when the user changes combo value by typing
		inputChanged: function() {
			this.filter();

			if (this.liLen()) {
				this.showList();
				this.setOverflow();
				this.setListHeight();
			} else {
				this.hideList();
			}
		},
	
		//highlights first item of the dropdown list
		highlightFirst: function() {
			this.listItems.removeClass("active").filter(".visible:eq(0)").addClass("active");
			this.autoFill();
		},
	
		//highlights item of the dropdown list next to the currently active item
		highlightNext: function() {
			var $next = this.getActive().next();
		
			while ($next.hasClass("invisible") && $next.length) {
				$next = $next.next();
			}
		
			if ($next.length) {
				this.listItems.removeClass("active");
				$next.addClass("active");
				this.scrollDown();
			}
		},
	
		//scrolls list wrapper down when needed
		scrollDown: function() {
			if ("scroll" != this.listWrapper.css(this.overflowCSS))
				return;
		
			var beforeActive = this.getActiveIndex() + 1;
			if ($.browser.opera)
				++beforeActive;
			
			var minScroll = this.listItems.height() * beforeActive - this.listWrapper.height();
			
			if ($.browser.msie)
				minScroll += beforeActive;
			
			if (this.listWrapper.scrollTop() < minScroll)
				this.listWrapper.scrollTop(minScroll);
		},
		
		
		//highlights list item before currently active item
		highlightPrev: function() {
			var $prev = this.getActive().prev();
			
			while ($prev.length && $prev.hasClass("invisible"))
				$prev = $prev.prev();
			
			if ($prev.length) {
				this.getActive().removeClass("active");
				$prev.addClass("active");
				this.scrollUp();
			}
		},
		
		//returns index of currently active list item
		getActiveIndex: function() {
			return $.inArray(this.getActive().get(0), this.listItems.filter(".visible").get());
		},
		
		
		//scrolls list wrapper up when needed
		scrollUp: function() {
			
			if ("scroll" != this.listWrapper.css(this.overflowCSS))
				return;
			
			var maxScroll = this.getActiveIndex() * this.listItems.height();
			
			if (this.listWrapper.scrollTop() > maxScroll) {
				this.listWrapper.scrollTop(maxScroll);
			}	 
		},
		
		//emptyText stuff
		applyEmptyText: function() {
			if (!this.config.emptyText.length)
				return;
			
			var self = this;	
			this.input.bind("focus", function() {
				self.inputFocus();
			}).
			bind("blur", function() {
				self.inputBlur();
			});	
			
			if ("" == this.input.val()) {
				this.input.addClass("empty").val(this.config.emptyText);
			}
		},
		
		inputFocus: function() {
			if (this.input.hasClass("empty")) {
				this.input.removeClass("empty").
				val("");
			}
		},
		
		inputBlur: function() {
			if ("" == this.input.val()) {
				this.input.addClass("empty").
				val(this.config.emptyText);
			}
		},
		
		/*
		//initialValue stuff
		initialValue: function() {
			if (!this.config.initialValue)
				return;
			this.setComboValue(this.config.initialValue, false, true);
		},
		*/
		
		//triggerSelected stuff
		triggerSelected: function() {
			if (!this.config.triggerSelected)
				return;
			
			var self = this;
			this.$selectbox.children("option:selected").each(function() {
				self.setComboValue($(this).text(), false, true);
			});
		},
		
		//autofill stuff
		autoFill: function() {
			if (!this.config.autoFill || ($sc.KEY.BACKSPACE == this.lastKey) || this.multiple)
				return;
			var curVal = this.input.val();
			var newVal = this.getActive().text();
			this.input.val(newVal);
			this.selection(this.input.get(0), curVal.length, newVal.length);
		},
		
		//provides selection for autofilling
		//borrowed from jCarousel
		selection: function(field, start, end) {
			if( field.createTextRange ) {
				var selRange = field.createTextRange();
				selRange.collapse(true);
				selRange.moveStart("character", start);
				selRange.moveEnd("character", end);
				selRange.select();
			} else if( field.setSelectionRange ){
				field.setSelectionRange(start, end);
			} else {
				if( field.selectionStart ){
					field.selectionStart = start;
					field.selectionEnd = end;
				}
			}
			// field.focus();	
		},
		
		//for internal use
		updateDrop: function() {
			if (this.config.dropUp)
				this.listWrapper.addClass("list-wrapper-up");
			else 
				this.listWrapper.removeClass("list-wrapper-up");		
		},
		
		//updates dropUp config option
		setDropUp: function(drop) {
			this.config.dropUp = drop;   
			this.updateDrop(); 
		}
	});
	
	$sc.extend({
		//key codes
		//from jCarousel
		KEY: {
			UP: 38,
			DOWN: 40,
			DEL: 46,
			TAB: 9,
			RETURN: 13,
			ESC: 27,
			COMMA: 188,
			PAGEUP: 33,
			PAGEDOWN: 34,
			BACKSPACE: 8	
		},
		
		//for debugging
		log: function(msg) {
			var $log = $("#log");
			$log.html($log.html() + msg + "<br />");
		},
		
		createSelectbox: function(config) {
			var $selectbox = $("<select />").
			appendTo(config.container).
			attr({name: config.name, id: config.id, size: "1"});
			
			if (config.multiple)
				$selectbox.attr("multiple", true);
			
			var data = config.data;
			var selected = false;
			
			for (var i = 0, len = data.length; i < len; ++i) {
				selected = data[i].selected || false;
				$("<option />").appendTo($selectbox).
				attr("value", data[i].value).
				text(data[i].text).
				attr("selected", selected);
			}
			
			return $selectbox.get(0);
		},
		
		create: function(config) {
			var defaults = {
			//the name of the selectbox
			name: "",
			//the ID of the selectbox
			id: "",
			//data for the options
			/*
			This is an array of objects. The objects should contain the following properties:
			(string)value - the value of the <option>
			(string) text - text of the <option>
			(bool) selected - if set to true, "selected" attribute of this <option> will be set to true
			*/
			data: [],
			
			//if true, combo with multiple choice will be created
			multiple: false,
			
			//an element that will contain the widget
			container: $(document),
			//url that contains JSON object for options data
			//format is the same as in data config option
			//if passed, "data" config option will be ignored
			url: "",
			//params for AJAX request
			ajaxData: {}
			};
			config = $.extend({}, defaults, config || {});
			
			if (config.url) {
				return $.getJSON(config.url, config.ajaxData, function(data) {
					delete config.url;
					delete config.ajaxData;
					config.data = data;
					return $sc.create(config);
				});
			}
			
			config.container = $(config.container);
			
			var selectbox = $sc.createSelectbox(config);
			return new $sc(selectbox, config);
			
		},
		
		normalizeArray: function(arr) {
			var result = [];
			for (var i = 0, len =arr.length; i < len; ++i) {
				if ("" == arr[i]) continue;
				result.push(arr[i]);	
			}
			return result;
		}
	});
})(jQuery); 

