/**
 * xSelect control. Emulates HTML <select> element using <div>'s.
 * Compability tested with Opera 9.62, 9.64, Firefox 3.0.5, 3.0.7 & 2.0.0.16,
 * Internet Explorer 6.0.2900 & 7.0.5730 & 8.0.6801.
 * 
 * v. 2.1 beta
 * 
 * Uses JQuery framework.
 * 
 * Copyright (c) 2009, Profigroup Company
 * 
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 * - Redistributions of source code must retain the above copyright notice, this list
 *   of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice, this list
 *   of conditions and the following disclaimer in the documentation and/or other
 *   materials provided with the distribution.
 * - Neither the name of the Profigroup Company nor the names of its contributors may be
 *   used to endorse or promote products derived from this software without specific
 *   prior written permission.
 *   
 *   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
 *   EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 *   OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
 *   SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 *   SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
 *   OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 *   HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 *   TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 *   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *   
 *   Made in Belarus.
 *   
 *   Usage examples:
 *   1) Array options:
 *   var xselect = new xSelect('xxx', new Array("The <b>first</b>", "The <b>second</b>"), "first", {optionsAsHtml: true});
 *   xselect.render($('#host'), 250);
 *   2) Hash options:
 *   var xselect = new xSelect('yyy', {first: "The first", {second: "The second"}, "The first", {optionsAsHtml: false});
 *   xselect.render($('#host'), 250);
 *   
 *   @param id string Unique ID for xSelect container and ID-prefix for xSelect subelements
 *   @param options Array|Object [Optional] Elements collection
 *   @param selected string [Optional] Value of initially selected item. It can be an elements
 *   value (label), key or undefined/null
 *   @param settings Object [Optional] Hash-object.
 *   Structure: {optionsAsHtml: true[|false], clickEvents: true[|false]}
 *   - optionsAsHtml true value indicates that Labels will be used as HTML-contents,
 *     but not simple text.
 *   - clickEvents true value indicates that dropdown box should appear/disappear on click events,
 *     but not on mouse ones.
 */
xSelect = function(id, options, selected, settings)
{
	// Properties
	this.timeout = null;
	this.container = null;
	this.containerId = id;
	this.options = null; // text labels (values)
	this.keys = null; // keys
	this.value = null; // selected text label
	this.key = null; // selected key

	// Settings
	this.settings = {optionsAsHtml: false, clickEvents: false};
	if (typeof(settings) == 'object')
	{
		if (settings.optionsAsHtml)
			this.settings.optionsAsHtml = settings.optionsAsHtml;
		if (settings.clickEvents)
			this.settings.clickEvents = settings.clickEvents;
	}
	
	this.rendered = false;
	this.expanded = false;
	
	// Self-link for clousures
	var self = this;
	
	// Notice: Look for constructor finalization in the bottom
	
	/**
	 * Render this control. Control will be appended to the contents of parentElement
	 * 
	 * @param parentElement DomElement The Dom-tree element to host xSelect box.
	 * xSelect container will be appended to the end of parentElement contents
	 * @param width int Fixed width of the xSelect box in pixels
	 * @return Object Given the object self-reference
	 */
	this.render = function(parentElement, width)
	{
		// create DOM-element
		this.container = $('<div class="xSelect_Contaner" id="' + this.containerId + '"><div class="xSelect_Label" id="' + this.containerId + '__Label">' + this.value + '</div><div class="xSelect_Button">&nbsp;</div><div class="xSelect_Clear"></div><div class="xSelect_Options" id="' + this.containerId + '__Options"></div></div>');
		// Bind events
		if (!this.settings.clickEvents)
		{
			this.container.bind('mouseover', self.expand);
			this.container.bind('mouseout', self.collapse);
		}
		else
		{
			this.container.bind('click', self._clickHandler);
			$(document.body).bind('click', function(){if (self._c) {self.collapseReal();} self._c++;});
		}
		if (!$.browser.msie) // IE bug
			this.container.bind('mouseover mouseout', function(){$('.xSelect_Button', this).toggleClass('xSelect_ButtonHover');});
		else
		{
			$('.xSelect_Label', this.container).bind('mouseover mouseout', function(){$('.xSelect_Button', this.parentNode).toggleClass('xSelect_ButtonHover');});
			$('.xSelect_Button', this.container).bind('mouseover mouseout', function(){$(this).toggleClass('xSelect_ButtonHover');});
		}
		
		// Set width
		this.container.css('width', width);
		
		$('#' + this.containerId + '__Label', this.container).css('width', width - 20 - 4);
		$('#' + this.containerId + '__Options', this.container).css('width', width - 4);
		
		// Append element
		$(parentElement).append(this.container);
		
		this.rendered = true;
		
		// Draw dropdown container
		this.renderDropdownBox();
		
		// Set initial value
		if (this.value)
		{
			this.setValue(this.value);
		}
		
		return this;
	}
	
	this._clickHandler = function(e)
	{
		if (!self.expanded)
		{
			self.toggle();
			self._c = 0; // to prevent momental closing
		}
	}
	
	/**
	 * Show or hides the options dropdown box (with effects) depending on it's current status
	 * @return Object Given the object self-reference
	 */
	this.toggle = function()
	{
		if (!self.rendered)
			return this;
		
		if (self.expanded)
			self.collapseReal();
		else
			self.expand();
		
		return self;
	}
	
	/**
	 * Show the options dropdown box (with effects)
	 * @return Object Given the object self-reference
	 */
	this.expand = function()
	{
		if (!self.rendered)
			return self;
		
		if (self.timeout)
		{
			clearTimeout(self.timeout);
		}
		
		if (self.expanded)
			return self;
		
		var el = $('#' + self.containerId + '__Options', this.container);
		el.fadeIn('fast');
		var targetOffset = 0;
		// markup selected element
		$('.xSelect_Option', el).each(function(){
			var it = $(this);
			//if ((self.settings.optionsAsHtml && it.html()==self.value) || ((!self.settings.optionsAsHtml && it.text()==self.value)))
			if (jQuery.data(this, "xSelectKey") == self.key)
			{
				it.addClass('xSelect_OptionActive');
				targetOffset = it.offset().top - el.offset().top - el.height() + it.height() - 2;
			}
			else
			{
				if (it.hasClass('xSelect_OptionActive'))
				{
					it.removeClass('xSelect_OptionActive');
				}
			}
		});
		if (el.get(0).scrollTop == 0)
		{
			el.get(0).scrollTop = targetOffset;
			//el.animate({scrollTop: targetOffset}, 100);
		}
		
		self.expanded = true;
		return self;
	}
	
	/**
	 * Request to hide the options dropdown box (with effect)
	 * @return Object Given the object self-reference
	 */
	this.collapse = function()
	{
		if (!self.rendered || !self.expanded)
			return this;
		
		self.timeout = setTimeout(self.collapseReal, 100);
		
		return self;
	}
	
	/**
	 * Hide the options dropdown box (with effect)
	 * @return Object Given the object self-reference
	 */
	this.collapseReal = function()
	{
		if (!self.rendered || !self.expanded)
			return this;
		
		$('#' + self.containerId + '__Options', this.container).fadeOut('fast');
		self.expanded = false;
		return self;
	}
	
	/**
	 * Hide options dropdown box without effects (applied on option click)
	 * @return Object Given the object self-reference
	 */
	this.collapseMomental = function()
	{
		$('#' + self.containerId + '__Options', this.container).hide();
		self.expanded = false;
		return self;
	}
	
	/**
	 * Set the key & selected option text (value) by value
	 * @param value string Value (label) to set active
	 * @return bool True if the value was found in current values collection and
	 * set as active. False otherwise
	 */
	this.setValue = function(value)
	{
		// search for value
		for (var i=0; i < this.options.length; i++)
		{
			if (this.options[i] == value)
			{
				// set value
				this.value = value;
				// set key
				this.key = this.keys[i];
				
				// draw new text label
				this.drawValue();
				
				// fire event
				if (this.change)
				{
					this.change();
				}
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Internal operation to draw current selected value as controls label
	 * @return Object Given the object self-reference
	 */
	this.drawValue = function()
	{
		if (!this.rendered)
			return this;
		
		if (this.settings.optionsAsHtml)
			$('#' + this.containerId + '__Label', this.container).html(this.value);
		else
			$('#' + this.containerId + '__Label', this.container).text(this.value);
		
		return this;
	}
	
	/**
	 * Returns the value (text) of control
	 * @return string Current selected value (label)
	 */
	this.getValue = function()
	{
		return this.value;
	}
	
	/**
	 * Set the key & selected option text by key
	 * @param value string Key to set active
	 * @return bool True if the key was found in current keys collection and set as
	 * active. False otherwise
	 */
	this.setKey = function(value)
	{
		// search for key
		for (var i=0; i < this.keys.length; i++)
		{
			if (this.keys[i] == value)
			{
				// set value
				this.value = this.options[i];
				// set key
				this.key = value;
				
				// draw new text label
				this.drawValue();
				
				// fire event
				if (this.change)
				{
					this.change();
				}
				return true;
			}
		}
		return false;
	}
	
	/**
	 * Set the key & selected option text by DOM element
	 * @param value DomElement Option element (DIV) to get new active key and value
	 */
	this.setSelectedElement = function(el)
	{
		if (self.settings.optionsAsHtml)
			self.value = $(el).html();
		else
			self.value = $(el).text();
		self.key = jQuery.data(el, "xSelectKey");
		
		// draw new text label
		this.drawValue();
		
		// fire event
		if (this.change)
		{
			this.change();
		}
	}
	
	/**
	 * Get the active key
	 * @return string Selected key
	 */
	this.getKey = function()
	{
		return this.key;
	}
	
	/**
	 * Set values array of controls
	 * @param options Array|Object Elements collection
	 * @return Object The object self-reference
	 */
	this.setOptions = function(options)
	{
		// Use passed values or local saved?
		if (options != undefined)
		{
			// Process keys
			var keysArray = new Array();
			var opetionsArray = new Array();
			// if Array given
			if (options.constructor == Array.prototype.constructor)
			{
				// Dublicate options Array elements to keys Array
				for (var i=0; i < options.length; i++)
				{
					keysArray.push(options[i]);
				}
				opetionsArray = options;
			}
			// if Hash given
			else
			{
				// Convert Hash into two Arrays
				for (var key in options)
				{
					if ('function' == typeof options[key])
						continue;
					keysArray.push(key);
					opetionsArray.push(options[key]);
				}
			}
			
			// Set internal arrays
			this.options = opetionsArray;
			this.keys = keysArray;
		}
		else
		{
			// Options was not set and wasn't passed
			if (this.options == null && this.keys == null)
				return this;
		}
		
		// Flush selected value & key
		if (this.options.length > 0)
		{
			this.setValue(this.options[0]);
		}
		else
		{
			this.value = this.key = undefined;
			this.drawValue();
		}
		
		if (!this.rendered)
			return this;
		
		if (this.expanded)
		{
			this.collapseMomental();
		}
		
		this.renderDropdownBox();
		
		return this;
	}
	
	/**
	 * Renders dropdown elements Div
	 * @return Object The object self-reference
	 */
	this.renderDropdownBox = function()
	{
		// Draw dropdown box container elements
		var y = $('#' + this.containerId + '__Options', this.container);
		y.empty();
		for (var i=0; i < this.options.length; i++)
		{
			if (!this.options[i])
				continue;
			var x = $('<div class="xSelect_Option" onmouseout="if(!$(this).hasClass(\'xSelect_OptionActive\'))$(this).removeClass(\'xSelect_OptionHover\');" onmouseover="if(!$(this).hasClass(\'xSelect_OptionActive\'))$(this).addClass(\'xSelect_OptionHover\');"></div>');
			if (this.settings.optionsAsHtml)
				x.html(this.options[i]);
			else
				x.text(this.options[i]);
			jQuery.data(x.get(0), "xSelectKey", this.keys[i]);
			x.bind('click', function(){self.collapseMomental();self.setSelectedElement(this);return false;});
			y.append(x);
		}
		
		if (this.options.length > 17)
		{
			y.height(120);
		}
		
		return this;
	}
	
	/**
	 * Returns values array of control
	 * @return Array Options Array
	 */
	this.getOptions = function()
	{
		return this.options;
	}
	
	/**
	 * Events
	 * Fired on selected element changed
	 */
	this.change = null;
	
	// Constructor finalization
	if (typeof(options) == 'object')
	{
		this.setOptions(options);
	}
	
	// Set initially selected element
	if (!selected || !this.setValue(selected))
		if (!selected || !this.setKey(selected))
			if (this.keys.length > 0)
				this.setKey(this.keys[0]);
}
