/**
 * DOM-based object that configures advanced capabilities for inputs
 *
 */
function InputConfig(input, params) 
{ 
	var This 		= this;
	this.input	= input;
	this.params = params;

	// Disable browser suggestions
	this.input.attr('autocomplete', 'off');

	// Initialize default value handling
	new InputDefault(input);

	 // Initialize tip
	new InputTip(input);
	
	// Initialize validation
	if(params['validation'])
	{
		new InputValidation(input, params['validation']['callback']);
	}
	
	// Initialize autosuggest
	if(params['autosuggest'])
	{
		new InputAutosuggest(input, params['autosuggest']['url'], params['autosuggest']['params']);
	}
}

/**
 * Initializes an input's default value
 *
 * @param object input Input
 */
function InputDefault(input)
{
	this.onfocus = function()
	{
		if(This.input.val() == This.input.attr('default_value'))
		{
			This.input.val(''); 
		}
		
		This.input.removeClass(This.getClassName('inactive'));
		This.input.addClass(This.getClassName('active'));
	}
	
	this.onblur = function(event)
	{
		if(!trim(This.input.val()))
		{
			This.input.removeClass(This.getClassName('active'));
			This.input.addClass(This.getClassName('inactive'));
			This.input.val(This.input.attr('default_value'));
		}
	};
	
	// Returns relevant class for given name
	this.getClassName = function(name)
	{
		return 'input_' + name + '_' + this.input.attr('css_size');
	};
	
	var This = this;
	this.input = $(input);

	// Default the default value to blank
	if(!input.attr('default_value'))
	{ 
		input.attr('default_value', ''); 
	}
	
	this.input.bind('focus', this.onfocus);
	this.input.bind('blur', this.onblur);
	this.input.blur();
}

/**
 * Initializes an input's tip (the text shown when input is focused)
 * 
 * @param object input Input
 */
function InputTip(input)
{
	if(!$(input).attr('tip')) { return; } // canary

	// Shows the tip on focus
	this.focus = function()
	{
		// Don't show tip if there is already a value in the field
		if(trim(This.input.val())) { return; }
		
		if(!This.tip)
		{
			This.tip = $('<div />');
			This.tip.addClass(This.getClassName());
			This.tip.attr('id', This.input.attr('id') + '_input_tip');
			
			This.tip_interior = $('<div />');
			This.tip_interior.addClass(This.getClassName('interior'));
			
			This.tip.append(This.tip_interior);
			
			var input 				= This.input;
			var input_height	= input.outerHeight();
			var input_width		= input.outerWidth();

			This.tip.css({ top: (input_height + 2)+'px' });
			This.tip.css({ width: (input_width)+'px' });
			
			This.tip.append(This.tip_interior);
			
			This.input.parent().append(This.tip);
		}
		
		This.tip_interior.html(This.input.attr('tip'));
	};
	
	// Hides the tip on blur
	this.blur = function()
	{
		if(This.tip)
		{
			This.tip.remove();
			This.tip = undefined;
		}
	};
	
	// Toggle tip on keyup based on input value
	this.keyup = function()
	{
		// Hide tip if there is a value
		if(This.tip && trim(This.input.val()))
		{
			This.tip.remove();
			This.tip = undefined;
		}
		// Show tip if there is no value
		else
		{
			This.focus();
		}
	};
	
	// Returns relevant class for given name
	this.getClassName = function(name)
	{
		if(name)
		{
			return 'input_tip_' + name + '_' + this.input.attr('css_size');
		}
		else
		{
			return 'input_tip_' + this.input.attr('css_size');
		}
	};
	
	var This = this; // closure
	
	this.input = input;
	
	// Bind event callbacks for input
	this.input.bind('focus', this.focus);
	this.input.bind('blur', this.blur);
	this.input.bind('keyup', this.keyup);
}

/**
 * Initializes an input's validation capability
 *
 * Validation provides visual indicators within the input as to whether the value provided is acceptable
 *
 * @param object input Input
 * @param function callback Custom callback to handle validation upon input value change (optional)
 */
function InputValidation(input, callback)
{
	if(typeof callback == 'function')
	{
		this.keyup = function(event)
		{
			if(event && jQuery.inArray(event.keyCode, operational_keycodes) !== -1) { return true; }
			
			$(this).removeClass('input_validated'); 
			$(this).removeClass('input_invalidated'); 
			
			This.callback(event);
		}
	}
	else
	{
		this.keyup = function()
		{
			// Show validation icon if there is value
			if(trim($(this).val())) 
			{ 
				$(this).addClass('input_validated'); 
			} 
			// Remove validation icon if there is no value
			else 
			{ 
				$(this).removeClass('input_validated'); 
			}
		}
	}
	
	var This = this; // closure
	
	this.input 		= input;
	this.callback = callback;
	this.input.bind('keyup', this.keyup);
}

/**
 * Initializes autosuggestion capabilities for an input
 *
 * @param object input Input
 * @param string url URL for suggestions
 * @param Array params Various parameters to customize autosuggestion behavior (optional)
 */
function InputAutosuggest(input, url, params) 
{
	if(!input || !url) { return; } // canary
	
	if(!params) { params = new Array(); } // default

	this.query = function(q)
	{
		// Execute onquery script if available
		if(This.input.element.attr('onquery'))
		{
			eval(This.input.element.attr('onquery'));
		}

		var size 				= This.size;
		var onsuccess 	= this.query_callback_success;
		
		// Abort existing query
		if(This.query_ajax)
		{
			This.query_ajax.abort();
		}

		// Get query results
	  This.query_ajax = $.ajax({
			url: 				This.url,
			context: 		This.input,
			data: 			{ q: q, autosuggest_size: size }, 
			success: 		onsuccess
		});
	};
	
	// Show suggestions result from query
	this.query_callback_success = function(data)
	{ 
		This.suggestions.show(data.q, data.autosuggest_html);
	};
	
	// Cancel query and hide any suggestions
	this.query_cancel = function()
	{
		// Abort existing query
		if(This.query_ajax)
		{
			This.query_ajax.abort();
		}
		
		clearTimeout(This.query_timeout);
	};
	
	// Returns class for given element name
	this.getClassName = function(name)
	{
		return 'autosuggest_' + name + '_' + this.size;
	};	
	
	var This 								= this; // closure
	this.id 								= $(input).attr('id'); // ID of input field
	this.url 								= url; // resource for suggestions
	this.delay							= params['delay']; // delay in ms before querying server for suggestions after latest character typed
	this.submit_callback 		= params['submit_callback']; // callback for when user submits a new value
	this.select_callback 		= params['select_callback']; // callback for when user selects a suggestion
	this.auto_select  			= params['auto_select']; // whether to automatically select the first suggestion
	this.force_selection  	= params['force_selection']; // whether to force a selection (i.e. disallow submission of new values)
	this.size								= $(input).attr('css_size'); // size of input
	this.query_ajax					= null; // ajax object holder
	
	this.suggestions 				= new InputAutosuggestSuggestions(this);
	this.input 							= new InputAutosuggestInput(this);
	this.input_exterior 		= $('#' + this.id + '_exterior');
}

/**
 * Handles autosuggestion behavior for input element
 *
 * @param function autosuggest Parent autosuggest object
 */
function InputAutosuggestInput(autosuggest) 
{	
	this.onkeydown = function(event)
	{ 
		// Arrow down
	  if(event.keyCode == 40)
	  { 
	    This.autosuggest.suggestions.moveSelection('down');
	    return false;
	  }
	  // Arrow up
	  else if(event.keyCode == 38)
	  {
	    This.autosuggest.suggestions.moveSelection('up');
	    return false;
	  }
	  // Enter
	  else if(event.keyCode == 13)
	  {
			if(This.autosuggest.suggestions.selected_suggestion)
			{
				This.autosuggest.suggestions.selected_suggestion.click();
			}
			else
			{
				This.element.submit();
			}
		
			return false;
	  }
	};
	
	this.onkeyup = function(event)
	{
		if(event && jQuery.inArray(event.keyCode, operational_keycodes) !== -1) { return true; }
		
		// Hide any currently displayed suggestions
		This.autosuggest.suggestions.hide();

	  var q = trim(This.element.val());
		
		// Trigger query for suggestions immediately
		if(!This.autosuggest.delay)
		{
			This.autosuggest.query(q);
		}
		// Delay query based on given timeout time
		else
		{
			var delayed_query = function ()
			{
				This.autosuggest.query(q);
			}
			
			clearTimeout(This.autosuggest.query_timeout);
			This.autosuggest.query_timeout = setTimeout(delayed_query, This.autosuggest.delay);
		}
	};
	
	this.onfocus = function(event)
	{
		$('body').bind('click', This.autosuggest.input.onblur); 
	};
	
	// If user moves away from input, hide any suggestions and cancel any outstanding queries
	this.onblur = function(event)
	{
		setTimeout(function() { This.autosuggest.suggestions.hide(); }, 200);
		This.autosuggest.query_cancel();
	};
	
	// Submit new value entered into input
	this.onsubmit = function(event)
	{
		// Disallow new valuesubmissions if selection forcing is enabled
		if(This.autosuggest.force_selection) { return; }
		
		// If there is no submit callback, don't do anything but prevent event propagation
		if(!This.autosuggest.submit_callback)
		{
			event.stopPropagation();
			return;
		}

		// Trigger the submit callback and follow up depending on the returned code
		var submit_code = This.autosuggest.submit_callback(This.element.val());
		switch(submit_code)
		{
			// Refocus the input for another entry
			case 1:
				This.autosuggest.suggestions.hide();
				This.element.val('');
				This.element.blur();
				This.onblur();
				This.element.focus();
				break;
				
			// Preserve the current input valiue (do nothing)
			case 2:
				break;
				
			// Clear and blur the input
			default: 
				This.autosuggest.suggestions.hide();
				This.element.val('');
				This.element.blur();
				This.onblur();
				break;
		}
	};
	
	this.onclick = function(event)
	{
		event.stopPropagation();
	}
	
	var This = this;
	this.autosuggest 		= autosuggest;
	this.element   			= $('#' + autosuggest.id);

	this.element.bind('click', this.onclick);
	this.element.bind('keydown', this.onkeydown);
	this.element.bind('keyup', this.onkeyup);
	this.element.bind('focus', this.onfocus);
	this.element.bind('blur', this.onblur);
	this.element.bind('submit', this.onsubmit);
	this.onblur();
}

/**
 * Handles autosuggestion behavior for suggestions display
 *
 * @param function autosuggest Parent autosuggest object
 */
var InputAutosuggestSuggestions = function(autosuggest)
{
	this.show = function(q,html)
	{
		// Hide the suggestions if nothing to work with
		if(!html || !This.autosuggest.input.element.val() || trim(This.autosuggest.input.element.val()) == This.autosuggest.input.element.attr('default_value')) 
		{ 
			This.hide();
			return; 
		}
		// Skip if the suggestions don't match the current input value
		else if(q != trim(This.autosuggest.input.element.val()))
		{
			return;
		}
		
		This.selected_suggestion = null;
		
		// Create the suggestions element if needed
		if(!This.element)
		{
			This.element = $('<div />');
			This.element.addClass(This.autosuggest.getClassName('suggestions'));
			This.element.attr('id', This.autosuggest.id+'_suggestions');
			
			This.element_interior = $('<div />');
			This.element_interior.addClass(This.autosuggest.getClassName('suggestions_interior'));
			
			This.element.append(This.element_interior);
			
			var input 				= This.autosuggest.input.element;
			var input_height	= input.outerHeight();
			var input_width		= input.outerWidth();

			This.element.css({ top: (input_height+2)+'px', width: (input_width)+'px' });
			
			This.autosuggest.input_exterior.append(This.element);
		}

	  This.element_interior.html(html);
		
		var lis = This.element.find('.autosuggest_suggestions_item');
		lis.bind('click', This.autosuggest.select_callback);
		
		if(This.autosuggest.auto_select)
		{
			This.moveSelection('down');
		}
		
		This.element.bind('mouseover',This.maskSelection);
		This.element.bind('mouseout',This.unmaskSelection);
	};
	
	this.hide = function()
	{ 
		if(This.element)
		{
			This.element.remove();
			This.element = undefined;
		}
		
		This.selected_suggestion = null;
	};
	
	this.moveSelection = function(direction)
	{
		var items = this.element.find('.autosuggest_suggestions_item');
		
		if(this.selected_suggestion)
		{
			if(direction == 'up')
			{
				var previous_sibling = this.selected_suggestion.prev();
				
				if(previous_sibling.length)
				{
					this.selected_suggestion.removeClass(this.autosuggest.getClassName('selected_suggestion'));
					this.selected_suggestion = previous_sibling;
					this.selected_suggestion.addClass(this.autosuggest.getClassName('selected_suggestion'));
				}
				else if(!this.autosuggest.force_selection)
				{
					this.selected_suggestion.removeClass(this.autosuggest.getClassName('selected_suggestion'));
					this.selected_suggestion = null;
				}
			}
			else if(direction == 'down')
			{
				var next_sibling = this.selected_suggestion.next();
				
				if(next_sibling.length)
				{
					this.selected_suggestion.removeClass(this.autosuggest.getClassName('selected_suggestion'));
					this.selected_suggestion = next_sibling;
					this.selected_suggestion.addClass(this.autosuggest.getClassName('selected_suggestion'));
				}
			}
		}
		else
		{
			if(direction == 'down')
			{
				$(items[0]).addClass(this.autosuggest.getClassName('selected_suggestion'));
				this.selected_suggestion = $(items[0]);
			}
		}
	};
	
	this.maskSelection = function()
	{
		if(This.selected_suggestion)
		{
			This.selected_suggestion.removeClass(This.autosuggest.getClassName('selected_suggestion'));
		}
	};
	
	this.unmaskSelection = function()
	{
		if(This.selected_suggestion)
		{
			This.selected_suggestion.addClass(This.autosuggest.getClassName('selected_suggestion'));
		}
	};
	
	var This = this;
	this.autosuggest = autosuggest;
}
