/**
 * [ USO ]
 *  - Para otorgar la funcionalidad de autovalidación al formulario, basta con emplear el prefijo "frm" en el atributo "name"
 *  - Para otorgar funcionalidad a los elementos del formulario, basta con emplear la nomenclatura "tipodatos_obligatoriedad_persistencia" en el atributo "id"
 *
 * [ EJEMPLO ]
 *   <form method="post" name="frmTest">
 *     <input type="text" name="nombre" id="string_1_0" />
 *     <input type="text" name="email" id="email_1_1" value="test@test.com" />
 *   </form> 
 *
 *  - Crea un formulario con 2 campos:
 *    - Campo "nombre", cuyo tipo de datos es "string" y es obligatorio
 *    - Campo "email", cuyo tipo de datos es "email", es obligatorio, y posee valor por defecto persistente, lo que significa
 *      que si no se introduce ningún valor, carga el valor por defecto ( atributo "value" ) al retirar el foco
 *
 * [ IMPORTANTE ] 
 * Por defecto, los errores derivados de la validación, se volcarán en una capa cuyo identificativo será el valor
 * de la variable de configuración 'validator.config.id.errorBlock' ( errors ), aunque dicha capa se puede cambiar añadiendo el sufijo
 * "_" + "nombrecapa" al atributo 'name' del formulario.
 *
 * Es decir, '<form name="frmTest_divErrors" />' se autovalidaría volcando los errores en la capa 'divErrors'
 * Las capas de autovalidación se crean automáticamente a continuación del formulario al que están asociadas, aunque se pueden
 * declarar manualmente en caso de querer aplicar estilos u otros atributos personalizados
 */

/**
 * @class validator
 * @access public
 */

function validator()
{
	
	// ==================== Atributos ==========================
	
	/**
	 * Listado de etiquetas necesarias para la validación
	 * 
	 * @var tags ( Array )
	 * @access public
	 */
	
	this.tags = new Array();
	
	/**
	 * Listado de configuración del objeto
	 *  - id: configuración relativa a identificativos
	 *  - class: configuración de los estilos CSS
	 *  - controls: nombres de los controles de sistema
	 *  - prefix: prefijos empleados para las etiquetas y nombres de controles
	 * 
	 * @var config ( Array )
	 * @access public
	 */
	
	this.config = 
	{
	 css: { ok: "input", error: "error" },
	 id: { separator: "_", errorBlock: "errors" },
	 controls: { tags: "TAGS", required: "REQUIRED" },
	 prefix: { form: "frm", mode: "mode_", field: "field_", message: "message_", pattern: "pattern_" }
	}
	
	/**
	 * Métodos de callback
	 * 
	 * @var callback
	 * @access public
	 */
	
	this.callback =
	{
	 glue: function( text, index ){ return( text + "<br />" ); }
	}
	
	// ===================== Métodos ===========================	
	
	/**
	 * Comprueba si el tipo de datos solicitado en el 2º parámetro
	 * soporta el elemento especificado en el primer parámetro
	 * 
	 * @function check
	 * @access public
	 * @param oFormElement ( HTMLElement )
	 * @param sDataType ( String )
	 * @return ( String )
	 */
	
	this.check = function( oFormElement, sDataType )
	{
	 /**
	  * @var sPattern ( RegExp ) Patrón de comparación asociado al tipo de datos
	  * @var bIsValid ( Boolean ) Bandera que indica si la validación es correcta ( por defecto SI )
	  */ 
	 
	 var sPattern = new String();
	 var bIsValid = new Boolean( true );
	 
	 /**
	  * Carga el tipo de datos si el elemento ha sido precargado
	  */
	 
	 if( !sDataType && oFormElement._validationConfig )
	  sDataType = oFormElement._validationConfig[ 0 ];
	 
	 /**
	  * Trata de cargar el patrón asociado al tipo de datos
	  */
	 
	 if( typeof( this.tags[ this.config.prefix.pattern + sDataType ] ) == "string" )
	  sPattern = this.tags[ this.config.prefix.pattern + sDataType ];
		 
	 /**
	  * Procede a validar según el tipo de datos
	  */
	 
	 switch( sDataType.toLowerCase() )
	 {
	  case "bool":

	   bIsValid = new RegExp( sPattern, "i" ).test( oFormElement.value ) == true;
	  
	   if( oFormElement.getAttribute( "type" ) == "checkbox" && oFormElement._validationConfig[ 1 ] == 1 )
	    bIsValid = new Boolean( oFormElement.checked );
	   
	  break;
	    
	  default: 
		  
	   if( sPattern.length > 0 )
		bIsValid = new RegExp( sPattern, "i" ).test( oFormElement.value );
	   
	  break;
	 } 
	 
	 return( bIsValid );		
	}
	
	/**
	 * Efectúa una carga de etiquetas a partir de un documento xml
	 * 
	 * @function load
	 * @access public
	 * @param sUrl ( String )
	 * @return ( undefined )
	 */
	
	this.load = function( sUrl )
	{	
	 var oValidator = this;
		
	 jQuery.ajax( {
		 
	  /**
	   * Carga correcta
	   * Procede a volcar las etiquetas
	   */
		 
	  success: function( xml ) 
	  {
		jQuery( xml ).find( 'tag' ).each( function(){
		oValidator.tags[ jQuery( this ).attr( "id" ) ] = jQuery( this ).text().replace(/^\s*|\s*$/g,""); // trim() !!			
	   } );
	  },
	    
	  /**
	   * Parámetros de carga por ajax
	   */
	    
	  type: "GET",
	  url: sUrl,
      dataType: "xml"    
	 });
	}
	
	/**
	 * Prepara todos los formularios existentes en la página
	 * 
	 * @function initialize
	 * @access public
	 * @return ( undefined )
	 */
	
	this.initialize = function()
	{
	 /**
	  * Recorre los formularios existentes en la página, y precarga solamente aquellos
	  * cuyo nombre comience con el prefijo 'frm' ( this.config.prefix.form )
	  */

	 for( var i = 0; i < document.forms.length; i++ )
	 {
	  /**
	   * Ignora los formularios ya validados
	   */
		 
	  if( typeof( document.forms[ i ]._validator ) == "object" )
	   continue;
		 
	  /**
	   * Aplica la autovalidación?
	   */
	  
	  if( String( document.forms[ i ].getAttribute( "name" ) ).indexOf( this.config.prefix.form ) == 0 )
	   this.prepare( document.forms[ i ] );
	 }		
	}
	
	/**
	 * Método encargado de precargar el sistema de validación para el 
	 * formulario especificado en el primer parámetro
	 * Devuelve el formulario precargado
	 *
	 * @function prepare
	 * @access public
	 * @param oForm ( HTMLElement )
	 * @return ( HTMLElement )
	 */	
	
	this.prepare = function( oForm )
	{
	 oForm._validator = this;
	 oForm._validatorErrorBlockName = this.config.id.errorBlock;
	 
	 /**
	  * Extrae el nombre de la capa sobre la que se volcarán los errores, a partir del identificativo
	  * del formulario, siempre que éste posea al menos un separador de identificativo ( this.config.id.separator )
	  */
	 
	 if( oForm.getAttribute( "name" ).indexOf( this.config.id.separator ) != -1 )
	  oForm._validatorErrorBlockName = oForm.getAttribute( "name" ).split( this.config.id.separator ).pop();

	 /**
	  * Nombre del control que contiene la ruta hacia el fichero de etiquetas
	  * @var sTagControlName ( String )
	  */
	 
	 var sTagControlName = oForm._validator.config.controls.tags;
	 
	 /**
	  * Intenta cargar las etiquetas de forma automática
	  */
	 
	 if( typeof( oForm.elements[ sTagControlName ] ) != "undefined" )
	  this.load( oForm.elements[ sTagControlName ].value );
	 
	 /**
	  * Precarga los elementos del formulario
	  */
	    
	 for( var i = 0; i < oForm.elements.length; i++ )
	 {
	  /** 
	   * Ignora el elemento actual si no posee atributo 'id'
	   */
		 
	  if( !oForm.elements[ i ].getAttribute( "id" ) )
	   continue;
	 
	  /**
	   * Configuración del elemento del formulario según la estructura del atributo 'id'
	   * Empleamos el separador '_' para obtener los índices del array resultante, que son los siguientes:
	   *  - 0: Tipo de datos del elemento ( string, number, phone, email, dni, .. )
	   *  - 1: Los posibles valores para este índice son:
	   *	  0: El campo es opcional
	   *      1: El campo es obligatorio
	   *  - 2: Los posibles valores para este índice son:
	   *	  0: El campo no mantiene su valor inicial si el usuario no introduce un valor ( por defecto )
	   *      1: El campo mantiene su valor inicial si el usuario no introduce un valor
	   
	   * [ EJEMPLO ] Genera un campo obligatorio de tipo 'dni'
	   * <input type="text" name="campo1" id="dni_1_0" value="Campo 1" />
	   * 
	   * @var oElementConfig ( Array )
	   */
		 
	  var oElementConfig = oForm.elements[ i ].getAttribute( "id" ).split( this.config.id.separator );
	  
	  /**
	   * El elemento tiene un valor asignado, por defecto le aplicamos la funcionalidad de reiniciar
	   * automáticamente el valor al retirar el foco, si el usuario no ha definido un valor manualmente
	   */

	  if( typeof( oElementConfig[ 2 ] ) == "undefined" && String( oForm.elements[ i ].value ).length == 0 )
	   oElementConfig[ 2 ] = 1;
	  
	 /**
	   * Fuerza el formato de la configuración al de un array de 3 elementos
	   * ( tipo_datos, obligatoriedad, persistencia )
	   */
	  
	  while( oElementConfig.length <= 2 )
	   oElementConfig.push( "0" );
	   
	  /**
	   * Copia la configuración con el formato esperado
	   */ 
	   
	  oForm.elements[ i ]._validationConfig = oElementConfig;
	    
	  /**
	   * Evento lanzado al enviar el formulario
	   * Este método se encarga de comprobar si el campo contiene el tipo de datos correcto o
	   * si se trata de un campo obligatorio sin valor definido
	   *
	   * Devuelve los siguientes valores, en función del resultado de la validación:
	   *  - 0: El campo es obligatorio y no se ha definido ningún valor
	   *  - 1: El campo no posee el formato deseado
	   *  - 2: El campo posee el formato correcto
	   */   
	  
	  oForm.elements[ i ].onsubmit = function()
	  {
	   /**
	    * Define si el elemento se considera vacío
	    * @var bIsEmpty ( Boolean ) 
	    */
	       
	   var bIsEmpty = ( !this.value || this.value == this._default && this._validationConfig[ 2 ] == true );
	   
	   /**
	    * Campo obligatorio vacío
	    */
	   
	   if( bIsEmpty == true && this._validationConfig[ 1 ] == true )
	    return( 0 );

	   /**
	    * Formato del campo no válido
	    */

	   else if( bIsEmpty == false && this.form._validator.check( this ) == false )
	    return( 1 );   

	   /**
	    * Formato correcto
	    */

	   else
	    return( 2 );
	  }
	  
	  /**
	   * Aplica la funcionalidad para autocompletar el valor si el usuario no ha introducido nada
	   * Este método aplica los siguientes eventos al elemento actual:
	   *  - onclick: encargado de vaciar el valor al hacer click en el control
	   *  - onblur: encargado de restaurar, si es preciso, el valor del control al retirar el foco
	   */
	  
	  if( oElementConfig[ 2 ] == 1 )
	  {
	   oForm.elements[ i ]._default = oForm.elements[ i ].value;
	   oForm.elements[ i ].onblur = function(){ if( this.value.length == 0 ) this.value = this._default; }
	   oForm.elements[ i ].onclick = oForm.elements[ i ].onfocus = function(){ if( this.value == this._default ) this.value = ""; }
	  }
	 } 	

	 /**
	  * Forzamos la existencia del control de formulario encargado de informar
	  * al script receptor sobre los campos obligatorios
	  */
	 
	 if( typeof( oForm.elements[ this.config.controls.required ] ) == "undefined" )
	 {
          if( navigator.userAgent.indexOf( "MSIE" ) != -1 )
           oRequiredFieldsControl = oForm.appendChild( document.createElement( "<input type=\"hidden\" name=\"" + this.config.controls.required + "\" />" ) );

          else
          {
           oRequiredFieldsControl = oForm.appendChild( document.createElement( "input" ) );
           oRequiredFieldsControl.setAttribute( "type", "hidden" );
	   oRequiredFieldsControl.setAttribute( "name", this.config.controls.required );
          }
	 }
	 
	 /**
	  * Respetamos el método, si se ha definido manualmente
	  */
	 
	 if( typeof( oForm.onsubmit ) == "function" )
	  oForm._onsubmit = oForm.onsubmit;
	 
	 /**
	  * Evento lanzado antes de enviar el formulario
	  * Esta función se encarga de validar todos sus subelementos mediante la llamada al método 'onsubmit', que sólo
	  * existirá para aquellos elementos que hayan sido precargados mediante el bucle superior
	  */
	 
	 oForm.onsubmit = function()
	 {
	  /**
	   * Ejecuta el método 'onsubmit' definido en mediante html
	   * Si éste devuelve 'false', cancela la validación del formulario
	   */
		 
	  if( typeof( this._onsubmit ) == "function" && this._onsubmit() == false )
	   return( false );

	  /**
	   * @var oValidator ( validator ) Puntero hacia el objeto validador de formularios
	   * @var nValidationResponse ( Number ) Resultado obtenido tras ejecutar el método 'onsubmit' de cada elemento
	   * @var oErrorLayer ( DOMElement ) Capa sobre la que se volcar el resultado de la validación, en caso de ser negativo
	   */
	 
	  var oValidator = this._validator;
	  var nValidationResponse = new Number();
	  var oErrorLayer = null, oErrorList = new Array();
	  
	  /**
	   * Reinicializa el valor del campo encargado de informar al script receptor sobre
	   * cuales son los campos de valor obligatorio
	   */
	  
	  this.elements[ oValidator.config.controls.required ].value = new String();		
	  
	  for( var i = 0; i < this.elements.length; i++ )
	  { 
	   /**
	    * Ignora los elementos NO precargados
	    */
		  
	   if( typeof( this.elements[ i ].onsubmit ) != "function" )
	    continue;
		
	   /**
	    * Autovalida el campo invocando al método 'onsubmit'
	    * Si el elemento está deshabilitado, lo trata como un campo validado correctamente
	    */
	     
	   nValidationResponse = ( this.elements[ i ].disabled == true ) ? 2 : this.elements[ i ].onsubmit();
	     
	   switch( nValidationResponse )
	   {
	    /**
	     * Campo obligatorio vacío
		 */
		   
		case 0:
		  
		  this.elements[ i ].className = oValidator.config.css.error;
		  var sErrorMessage = new String( oValidator.tags[ oValidator.config.prefix.mode + "mandatory" ] );
		   
		  oErrorList.push( sErrorMessage.sprintf( oValidator.tags[ oValidator.config.prefix.field + this.elements[ i ].getAttribute( "name" ) ] ) ); 
		  
		break;
		
		/**
		 * Campo con formato incorrecto
		 */
		
		case 1: 
		  
		  this.elements[ i ].className = oValidator.config.css.error;
		  var sErrorMessage = new String( oValidator.tags[ oValidator.config.prefix.message + this.elements[ i ]._validationConfig[ 0 ] ] );
		      
		  oErrorList.push( sErrorMessage.sprintf( oValidator.tags[ oValidator.config.prefix.field + this.elements[ i ].getAttribute( "name" ) ] ) );
		  
	 	break;
		
		/** 
		 * Campo correcto
		 */
		
		default:
		  
		  this.elements[ i ].className = oValidator.config.css.ok;
		   
		  /**
		   * Validación correcta de un campo obligatorio
		   * Procede a agregarlo a la lista de campos cuyos valor debe ser comprobado por el script receptor
		   */
		  
		  if( this.elements[ i ]._validationConfig[ 1 ] == true )
		   this.elements[ oValidator.config.controls.required ].value += this.elements[ i ].getAttribute( "name" ) + ":" + this.elements[ i ]._validationConfig[ 0 ] + ",";
		 
		break;
	   }
	  }
	  
	  /**
	   * Envía el formulario si no se han generado errores
	   */
	    
	  if( oErrorList.length == 0 )
	   return( true );
	   
	  /**
	   * Se han producido errores
	   * Busca la capa sobre la que volcar el resultado, o la crea si ésta no existe
	   */
	   
	  else if( !( oErrorLayer = document.getElementById( this._validatorErrorBlockName ) ) )
	  {
	   oErrorLayer = this.appendChild( document.createElement( "div" ) );
	   oErrorLayer.setAttribute( "id", this._validatorErrorBlockName );
	  }
	   
	  oErrorLayer.innerHTML = "";
	  oErrorLayer.style.display = "block";
	   
	  /**
	   * Vuelca el listado de errores invocando la función de usuario encargada
	   * de preparar la salida de los errores con el formato deseado
	   */
	   
	  for( var i = 0; i < oErrorList.length; i++ )
	   oErrorLayer.innerHTML += oValidator.callback.glue( oErrorList[ i ], i );
	   
	  return( false );
	 } // oForm.onsubmit

	 return( oForm );	
	}		
	
}

// ==================== Funciones ==========================

/**
 * Reemplaza los elementos '%s' por los valores indicados en los parámetros
 * @function String.sprintf
 * @return ( String )
 */

String.prototype.sprintf = function()
{
 var sString = this.toString();

 for( var i = 0; sString.indexOf( "%s" ) != -1; i++ )
  sString = sString.replace( "%s", arguments[ i ] );
  
 return( sString );
}

// ================= Comprobaciones ========================

if( !jQuery )
 throw new Error( "jQuery not found!" );

