/**
 * checkForm : jQuery form's fields validation plug-in.
 *
 * Copyright (c) 2007 Université Lumière Lyon 2 (www.univ-lyon2.fr)
 * Dual licensed under the MIT and GPL licenses:
 *   http://www.opensource.org/licenses/mit-license.php
 *   http://www.gnu.org/licenses/gpl.html
 */
 /**
 * checkForm may used to check form's fields. Not only you can define the type, the format of each field, but also if it is required or optional. 
 * The submission of the form won't be possible while the constraint aren't validate.
 * Here are some features of checkForm :
 * 		- two formats of displaying errors ;
 * 		- two modes of checking form, one immediate and one global ;
 * 		- translation of error messages ;
 * 
 * 
 * @example $(function() {
 * 	$("#form1").checkForm({
 * 		lang: 'fr',
 * 		displayErrorType: 'wrap',
 * 		cssError: "erreur",
 * 		fields:{
 * 			"requis": "text, required",
 * 			"passConfirm": "password, pass, required",
 * 			"numerique": "numeric, required",
 * 			"mail": "email, univ-lyon2.fr, required",
 * 			"date": "date, 00/00/0000",
 * 			"telephone": "phone",
 * 			"texteGrand": "text, required",
 * 			"liste": "select",
 * 			"checkbox[]": "checkbox, 2, required",
 * 			"radio[]": "radio, required"
 * 		}
 * 	});
 * @desc Check the fields of form "#form1" given as argument.
 * 
 * @author Mehdi Kabab mehdi.kabab@univ-lyon2.fr
 * @copyright (c) 2007 Université Lumière Lyon 2 (www.univ-lyon2.fr)
 * @version 1.0 (16/01/2007)
 * @requires jQuery v1.1
 * @name checkForm
 * @type jQuery
 * @cat Plugins/Forms
 * @return jQuery
 * 
 * @param {Hash} options Additional options.
 * @param {String} options[lang] Language of error messages (optional. Default: en).
 * @param {Boolean} options[onSubmit] Immediate validation (false) or at the submission (true) ? (optional. Default: false).
 * @param {Boolean} options[focusFirstElement] Give the focus to the first wrong element when onSubmit is true (optional. Default: true).
 * @param {Boolean} options[displayError] Display errors if true (optional. Default: true).
 * @param {String} options[displayErrorType] Display mode of error messages : 'nomal' or 'wrap' (optional. Default: normal).
 * @param {String} options[cssError] Name of the CSS class applied to error messages (optional. Default: error).
 * @param {Hash} options[fields] List of the fields to be checkec.
 * 
 * @todo check group of checkbox/radio.
 * @todo password type to be review.
 */
jQuery.fn.checkForm = function( options ) {
	
	/** Default parameters of the script. @type {Hash} */
	var	settings = {
		lang: 'en',
		immediate: (!options.onSubmit),
		focusFirstElement: false,
		displayError: true,
		displayErrorType: "normal",
		cssError: "error"
	};
	
	// Add user options
	if( options )
	{
		jQuery.extend( settings, options );
	};
	
	var rules = jQuery.fn.checkForm.rules;
	var locale = new jQuery.fn.checkForm.locales( settings.lang );

	// Loop on each calling jQuery element
	return this.each( function() {
		
		/** Workspace. @type {Element} */
		jQuery.context = this;
		
		/** Fields of the form. @type {jQuery}  */
		var jFields = jQuery('input:not(:reset):not(:submit):not(:hidden),select,textarea', jQuery.context);
		
		/* Clean all fields at the beginning. */
		jFields.each( function(){
			var sType = this.type;
			var sTagName = this.tagName.toLowerCase();
		//	if (sType == 'text' || sType == 'password' || sTagName == 'textarea')
		//		this.value = '';
		//	else if (sType == 'checkbox' || sType == 'radio')
		//		this.checked = false;
		//	else if (sTagName == 'select')
			if (sTagName == 'select')
				this.selectedIndex = -1;
		});
		
		/** Fields container. @type {Object} @id elements*/
		window.elements = new Object();
		var e = window.elements[index];
		
		/** Name of the current item, used as index in {@link #elements}. @type {String} */
		var	index = new String();

		
		//
		// Creation of the stack error.
		//
		/**
		 * Stack error.
		 * The submission of the form won't be possible while the stack isn't empty.
		 * @type {Array}
		 */
		window.errorList = new Array();
		for( var field in settings.fields )
		{
			if( jQuery.browser.safari && settings.fields[field].search('checkbox|radio') == 0 ) 
				continue;
			errorList.push( field );
		};
		
		// If reset, we erare the error messages.
		jQuery( 'input:reset', jQuery.context ).click( function(){
			for( var field in elements )
			{
				hideError(elements[field]);
			}
		});

		// Immediate validation of the fields
		if( settings.immediate )
		{
			jSubmitBtn = jQuery( 'input:submit', jQuery.context ).attr( 'disabled', 'disabled' );
			jQuery( 'input:reset', jQuery.context ).click( function(){ jSubmitBtn.attr( 'disabled', 'disabled' ); } );
			jFields.each( function(){
				if( (jQuery(this).val() == '') || ( this.type == 'checkbox' && jQuery(this).is(':not(:checked)') ) )
				{
					jQuery(this).blur( function(){
						return validate( jQuery(this) );
					});
				}
				else
				{
					var aaa = validate( jQuery(this) );
					if( aaa == true  )
					{
						var _nL = errorList.length;
						for( i = 0; i != _nL; i++ )
						{
							if( errorList[i] == jQuery(this).attr('name') )
							{
								errorList.splice( i, 1 );
								break;
							}
						}
					}
				}
			});
		};
		
		// Global validation of the fields
		if( settings.onSubmit )
		{
			jQuery(this).submit( function(){
				jFields.each( function(){
					validate( jQuery(this) );
				});
				if( errorList.length == 0 )
					return true;
				else
				{
					if( settings.focusFirstElement )
						jQuery('.' + settings.cssError + ':first', jQuery.context).find('input:not(:submit):not(:reset),select,textarea').focus();
					return false;
				}
			});
		};
		
	});
	
	
	/**
	 * Check the field given as argument.
	 * 
	 * @param {jQuery} field Field to check.
	 * @return {Boolean}
	 */
	function validate( field ){
	
		// Initialization of the course index in the elements object.
		_t = field.attr( 'type' );
		if( _t === "checkbox" || _t === "radio" )
		{
			index = field.attr('name');
		}
		else
		{
			index = ( _h = field.attr('id') ) ? _h : field.attr('name');
		};
		delete _h;
		delete _t;

		/** Instance of the current element. @type {jQuery} */
		e = window.elements[index] = field;
				
		/** Identifier of the field. @type {String} */
		e.id = index;
		
		/** If the element is a checkbox or a radio button. @type {String} */
		e.idSlice = ( index.indexOf('[') != -1 ) ? index.slice( 0, -2 ) : false;
		
		/** Value of the field. @type {String} */
		e.value = e.val();
		
		/** Required field but empty ? @type {Boolean} */
		e.requiredButEmpty = false;
		
		/** Empty field ? @type {Boolean} */
		e.empty = ( e.value === "" ) ? true : false;
				
		/** Is the element matching with the constraints ? @type {Boolean} */
		e.ok = checkElement();

		// Manager of the stack error.
		// If the field isn't valid, we add its identifier to the stack error.
		var _nL = errorList.length;
		if( !e.ok )
		{
			var _bI = false;
			for( i = 0; i != _nL; i++ )
			{
				if( errorList[i] == e.id )
				{
					_bI = true;
					break;
				}
			}
			if( !_bI ) errorList.push( e.id );
			showError();
			if( settings.immediate ) jSubmitBtn.attr( 'disabled', 'disabled' );
			return false;
		}
		else
		{
			if( _nL > 0 )
			{
				for( i = 0; i != _nL; i++ )
				{
					if( errorList[i] == e.id )
					{
						errorList.splice( i, 1 );
						break;
					}
				}
				hideError();
				
				// If the stack error is empty, we active the submit button of the form.
				_nL = errorList.length;
				if( _nL == 0 )
					jQuery( 'input:disabled', jQuery.context ).removeAttr('disabled');
				delete _nL;
						
				return true;
			};
		};
	};
	
	
	/**
	 * Check if the constraints of the field are matching.
	 * 
	 * @return {Boolean}
	 */
	function checkElement(){
		// If the field is defined in the options.
		if( settings.fields.hasOwnProperty( e.id ) )
		{
			var _aOpt =  settings.fields[e.id].split(',');
			var _nL = _aOpt.length;
			for( var i = 0; i != _nL; i++ )
				_aOpt[i] = jQuery.trim( _aOpt[i] );
			delete _nL;
			
			/** Type of the element. @type {String} */
			e.type = _aOpt[0];
			
			/** List of the element's options (type, parameters). @type {Array} */
			e.param = _aOpt.slice( 1, _aOpt.length );
			
			delete _aOpt;
		}
		else
		{
			return true;
		};
		
		/** Required field ? @type {Boolean} */
		e.required = isRequired();
		if( e.required && e.empty ) e.requiredButEmpty = true;
		
		// If the field is required but empty, return to the calling function.
		if( e.requiredButEmpty ) return false;

		return  ( e.value == '' && !e.required ) ? true : rules[e.type]();
	};
	
	
	/**
	 * Check if the current field is required (see option « required »). 
	 * 
	 * @param {Object} jElem
	 * @return {Boolean} Return true if the element is required, false otherwise.
	 */
	function isRequired() {
		var _bTmp = false;
		var _nTmp = e.param.length;
		for( var i=0; i < _nTmp; i++ )
		{
			if( e.param[i] === "required" )
			{
				_bTmp = true;
				break;
			};
		};
		delete _nTmp;
		return _bTmp;
	};


	/**
	 * Display error message.
	 * 
	 * @private
	 */
	function showError() {
		if( !settings.displayError ) return;
		
		// See bug #769 (http://jquery.com/dev/bugs/bug/769/) - part 1/4
		if( jQuery.browser.msie && ( e.type == "checkbox" || e.type == "radio" ) )
		{
			sdisplayErrorTypeBackup = settings.displayErrorType; 
			settings.displayErrorType = 'normal';
		};
		
		var _sId = e.idSlice || e.id;
		
		var _jElemError = jQuery( '#c' + _sId, jQuery.context );
		var _sMsg = getErrorMsg();
		var _sMsg = ( e.idSlice ) ? _sMsg.replace( "%nb%", e.param[0] ) : _sMsg;
		
		switch( settings.displayErrorType )
		{
			case 'normal':
				if( _jElemError.length == 0 )
				{
					e.parents().not("label").eq(0)
					.after('<p  id="c' + _sId + '" class="' + settings.cssError + '">' + _sMsg + '</p>' );
				}
				else
				{
					if( _sMsg != _jElemError.html() )
					{
						_jElemError.html( _sMsg );
					}
				}
				break;
			case 'wrap':
				if( _jElemError.length == 0 )
				{
					e.parents().not("label").eq(0)
					.wrap( '<div id="c' + _sId + '" class="' + settings.cssError + '" style="display:none;"></div>' )
					.parent().append( '<p>' + _sMsg + '</p>' )
					.fadeIn(500);
				}
				else
				{
					c = _jElemError.find( 'p:last' );
					if( _sMsg != c.html() )
					{
						c.html( _sMsg );
					}
				};
				break;
		};
		
		// See bug #769 (http://jquery.com/dev/bugs/bug/769/) - part 2/4
		if( jQuery.browser.msie && ( e.type == "checkbox" || e.type == "radio" ) )
		{
			settings.displayErrorType = sdisplayErrorTypeBackup;
		};
	};
	
	
	/**
	 * Clean error message.
	 * 
	 * @private
	 */
	function hideError( field ) {
		e = field || e;
		if( !settings.displayError ) return;
		
		// See bug #769 (http://jquery.com/dev/bugs/bug/769/) - part 3/4
		if( jQuery.browser.msie && ( e.type == "checkbox" || e.type == "radio" ) )
		{
			settings.displayErrorType = sdisplayErrorTypeBackup; 
			settings.displayErrorType = 'normal';
		};
		
		switch( settings.displayErrorType )
		{
			case 'normal':
				jQuery( '#c' + ( e.idSlice || e.id ), jQuery.context ).fadeOut(300, function(){ jQuery(this).remove() } );
				break;
			case 'wrap':
				var sId = e.idSlice || e.id;
				var jElemError = jQuery( '#c' + sId, jQuery.context );
//				e.parents().not("label").eq(0).insertAfter( '#c' + sId );
				jElemError.children( ':not(p)' ).insertAfter( jElemError ).end().fadeOut( 200, function(){ jQuery(this).remove() } );
				break;
		};
		
		// See bug #769 (http://jquery.com/dev/bugs/bug/769/) - part 4/4
		if( jQuery.browser.msie && ( e.type == "checkbox" || e.type == "radio" ) )
		{
			settings.displayErrorType = sdisplayErrorTypeBackup;
		};
		
		delete window.elements[e.id];
		delete e;
	};
	
	
	/**
	 * Return the error message of the type defined by the constraint.
	 * 
	 * @private
	 * @return {String}
	 */
	function getErrorMsg() {
		if( e.requiredButEmpty )
		{
			return locale.errorMsg.required;
		}
		else
		{
			return locale.errorMsg[e.type];
		}
	};
};


/**
 * Get locales.
 * 
 * @param {Object} lang
 */
jQuery.fn.checkForm.locales = function( lang ) {
	this.errorMsg = jQuery.fn.checkForm.locales[lang];
	if( this.errorMsg == undefined )
	{
		this.errorMsg = false;
	};
};