/**
 * Ensembles de fonctions et de classes prenant en charge la validation de formulaire,
 * le formatage ...
 * Dépendances : prototype (event , $H()).
 */

// ===================== A FAIRE / TODO =====================================================
// - ajouter la vérification automatique des mails.
// voici lexp reg qui va bien :
// var emailfilter=/^\w+[\+\.\w-]*@([\w-]+\.)*\w+[\w-]*\.([a-z]{2,4}|\d+)$/i
// var returnval=emailfilter.test(e.value)
// if (returnval==false){
//	alert("Please enter a valid email address.")




// =================== CLASSES =============================================================


/**
 * Classe encapsulant une erreur de validation.
 * Les instances de ValidationError sont créées par le FormValidator.
 * une liste de messages pré définie est créée à la premiere instance d'un Validationerror.
 * Eventuellement, cette liste peut être mise à jour avec ValidationError.setMessages(map);
 */
var ValidationError = Class.create();

ValidationError.prototype = {
	initialize: function(eltSrc, errorCode, values, eltName) {
		if (!this.msgMap)
		{
			this.msgMap = $H({
			'empty' : 'Vous devez renseigner {elt}.',
			'not_int' : 'Le champ {elt} doit avoir une valeur entière.', // A priori non utilisé pour CAAM ORP V2
			'not_double' : 'Cette application ne gère que des valeurs numériques.',
			'not_date' : 'Le champ {elt} doit avoir le format jj/mm/yyyy.', // A priori non utilisé pour CAAM ORP V2
			'not_in_bounds' : 'Le champ {elt} doit être compris entre {0} et {1}.' // ???
			});
		}
  		this.src = eltSrc;
  		this.code = errorCode;
  		this.values = values;
  		this.eltName = eltName;
		this.message = new MessageFormat(this.msgMap[this.code], this.values).getMessage();
		this.updateSpecialAttribute();
	},
	updateSpecialAttribute : function()
	{
		expr = eval('/\{elt\}/');
		this.message = this.message.replace(expr, this.eltName);
	},
	getMessage : function()
	{
		return this.message;
	},
	setMessages : function(errorsMsgMap)
	{
		this.msgMap = $H(errorsMsgMap);
	}
}

/**
 * Cette classe se charge de la validation d'un formulaire.
 *
* formCtrler	 = new FormController($('formulaire'));
* //formCtrler.initForm();
*
*  $Click($('btvalider'), function()
*  {
*			formCtrler.validate();
*			if (formCtrler.hasErrors())
*			{
*				errList = formCtrler.getErrors();
*				var resultString = '';
*				for (var  i=0;i<errList.length;i++)
*				{
*					resultString +='\n'+errList[i].getMessage();
*				}
*                                displayErrors(resultString);
*			}
*                        else
*                        {
*				sendRequest(null, strFormId, options);
*                        }
*			                  }
*                  );
*  }
*/
var FormController = Class.create();

FormController.prototype = {
	/**
	 * Le constructeur lie l'objet de validation au formulaire de validation.
	 */
	initialize : function(formId)
	{
		this.formId = formId;
		this.form = $(this.formId);

		this.initForm();
	},
	getFormId:function()
	{
		return this.formId;
	},
	isFormId:function()
	{
		return this.formId && this.formId!=null;
	},
	updateForm: function(strFormId, formElt)
	{
		this.formId = strFormId;
		this.form = formElt;
		this.initForm();
	},
	/**
	 * @return true si le formulaire a été modifié.
	 */
	isDirty:function()
	{
		return this.dirty;
	},
	/**
	 * permet de spécifier si le formulaire a été modifié ou non
	 */
	setDirty : function(isFormDirty)
	{
		this.dirty = isFormDirty;
	},
	/**
	 * retourne vrai si une erreur a été détectée.
	 * cette méthode doit être appelée après la méthode validate.
	 */
	hasErrors : function()
	{
		return this.errors.length >0;
	},
	/**
	 * retourne un tableau d'objets ValidationError
	 */
	getErrors : function()
	{
		return this.errors;
	},
	/**
	 * met à jour les champs du formulaire si besoin est.
	 * (par exemple, cette méthode effectue un formatage automatique des données.
	 */
	initForm:function()
	{
		this.dirty = false;
      this.errors = new Array();
		if (!this.form || this.form==null)
		{
			return;
		}
		for(var i=0; i<this.form.length; i++)
		{
			var element = this.form[i];
			if (element.type && element.type=='text' && element.value != "" && element.value.length > 0)
			{
				var formatType2 = element.getAttribute('format');
				if (formatType2 != null)
				{
					var format2 = null;
					if (formatType2 == 'date')
					{
						format2 =new DateFormat();
					}
					else
					{
						format2 = new NumberFormat();
					}
					var resFormat2 = format2.parse(element.value, formatType2);
					var displayValue2 = 0.0;
					if (resFormat2)
					{
						displayValue2 = format2.format(resFormat2, formatType2);
					}
					element.value = displayValue2;
				}
			}
			if (element.type=='text')
			{
				Event.observe(element , 'blur',
					function(evt)
					{
						// à la perte de focus, on applique le formatage
                  // et la vérification unitaire du champ.
						var element = Event.element(evt);
						var formatType = element.getAttribute('format');
						if (formatType)
						{
							var format = null;
							if (formatType == 'date')
							{
								format = new DateFormat();
							}
							else
							{
								format = new NumberFormat();
							}
							//CMR : Suppression des 0 en début de chaine
							var valeur = element.value;
//							if(valeur.indexOf(".") == -1 || valeur.indexOf(",") == -1)
//							{
								while(valeur.indexOf("0") != -1 && valeur.indexOf("0") == 0)
								{
									valeur = valeur.substr(1, valeur.length);
								}
//							}

							resFormat = format.parse(valeur, formatType);

							if (resFormat)
							{
								var displayValue = format.format(resFormat, formatType);
								if (displayValue)
								{
									element.value = displayValue;
									if (Element.hasClassName(element, 'fielderror'))
									{
										Element.removeClassName(element, 'fielderror');
									}
								}
								else
								{	if (formatType == 'date')
									{
										element.value='jj/mm/aaaa';
									}
									Element.addClassName(element, 'fielderror');
								}
							}
							else
							{
								Element.addClassName(element, 'fielderror');
							}
						}

					}
				);

				Event.observe(element , 'keyup',
				this.componentKeyPressed.bindAsEventListener(this)

				);

			}
		}
	},
	componentKeyPressed : function(evt)
	{
		if (this.isDirty() == false)
		{
			this.setDirty(true);
		}
						var element = Event.element(evt);
                                                // gère le transfert de focus automatique de champ à champ.
                                                // si la touche enter est pressée.
						var keyCode = evt.keyCode ? evt.keyCode : evt.which ? evt.which : evt.charCode;
						if (keyCode == 13) {
							var i;
							for (i = 0; i < element.form.elements.length; i++)
							if (element == element.form.elements[i])
							break;
							i = (i + 1) % element.form.elements.length;
							//element.form.elements[i].focus();
							return false;
						}
						// effectue une vérification du format à la frappe.
                                                // le formatage n'est pas appliquée.
                                                // par contre, s'il y a une erreur, elle est signalée en rouge.
						var formatType = element.getAttribute('format');
						if (formatType)
						{
							var format = null;
							if (formatType == 'date')
							{
								format =new DateFormat();
							}
							else
							{
								format = new NumberFormat();
							}
							resFormat = format.parse(element.value, formatType);

							if (resFormat)
							{
								if (Element.hasClassName(element, 'fielderror'))
								{
									Element.removeClassName(element, 'fielderror');
								}
							}
							else
							{
								Element.addClassName(element, 'fielderror');
							}
						}
	},
	/**
	 * lance la validation et le formatage du formulaire.
	 */
	validate:function()
	{
		this.errors = new Array();
		if (!this.form || this.form==null)
		{
			 return;
		}
		for(var i=0; i<this.form.length; i++)
		{
			var element = this.form[i];
			if(Element.visible(element) )
			{
				if(element.tagName=='SELECT')
				{
					if($F(element) == -1)
					{
						this.errors.push(new ValidationError(element, 'empty', null, this.getLabel(element)));
						break;
					}
				}
				else if(element.tagName=='INPUT')
				{
					if(element.type ==  'text')
					{
						this.affError(element);
						if (this.errors.length >0)
							break;
					}
					else if(element.type ==  'radio')
					{
						if (isRequired(element))
						{
							var strName = element.name;
							var arrayOfNameElement = $A(this.form).findAll(
							function(otherElement)
							{
								return (otherElement.name == strName);
							}
							);
							var isChecked = false;
							for (var k=0; k < arrayOfNameElement.length; k++)
							{
								isChecked = isChecked || (arrayOfNameElement[k].checked == true);
							}
							if (!isChecked && element == arrayOfNameElement[0])
							{
								this.errors.push(new ValidationError(element, 'empty', null, this.getLabel(element)));
								break;
							}
						}
					}
					else if(element.type ==  'checkbox')
					{
						this.affError(element);
						if (this.errors.length >0)
							break;
					}
					else if(element.type ==  'password')
					{
						this.affError(element);
						if (this.errors.length >0)
							break;
					}
				}
			}
		}
	},
	affError:function(element)
	{
		var formatType = element.getAttribute('format');
		// si c'est un champ requis
		if (isRequired(element) && element.disabled!=true)
		{
			if (element.value == null || element.value.length==0)
			{
				this.errors.push(new ValidationError(element, 'empty', null, this.getLabel(element)));
			}
			else
			{
				this.evalFormat(element, formatType);
			}
		}
		else
		// si le champ n'est pas obligatoire
		{
			this.evalFormat(element, formatType);
		}
	},
	getLabel:function(element)
	{
		var label = findLabelTextFor(element);
		if(element.getAttribute('altLbl') != null)
			label = element.getAttribute('altLbl');
		return label;
	},
	evalFormat:function(element, formatType)
	{
		// si aucun format n'est défini, on ne fait aucun controle
		// la vérification n'est faite également que si le champ a une valeur.
		if (formatType!=null && element.value != null && element.value.length>0)
		{
			var resFormat = null;
			var typeOfError = null;
			if (formatType == 'date')
			{
				resFormat = new DateFormat().parse(element.value);
				if (resFormat == null)
				{
					typeOfError = 'not_date';
					this.errors.push(new ValidationError(element, typeOfError, null, this.getLabel(element)));
				}
			}
			else
			{
				var format = new NumberFormat();
				resFormat = format.parse(element.value);
				if (resFormat == null)
				{
					typeOfError = 'not_double';
					if (format.isInteger())
						typeOfError = 'not_int';
					this.errors.push(new ValidationError(element, typeOfError, null, this.getLabel(element)));
				}
			}
		}
	}

}


/**
 * Cette classe se charge de simuler le comportement de
 * façon minimaliste de la classe MessageFormat de Java.
 * <br />
 * De façon basique, elle remplace les éléments {n} où n est
 * un nombre entier à l'intérieur d'une chaîne 'srcString'.
 * Pour cela, elle s'appuie sur le tableau d'éléments 'args'
 */

var MessageFormat = Class.create();
MessageFormat.prototype = {
	initialize: function(srcString, args) {
		this.message = srcString;
		if (args)
		{
			for (var i = 0; i<args.length; i++)
			{
				expr = eval('/\{['+i+']\}/');
				this.message = this.message.replace(expr, args[i]);
			}
		}
	},
	getMessage: function()
	{
		return this.message;
	}
}

/**
 * Cette classe permet de manipuler Date et chaine.
 * il n'y a qu'un seul format applicable / appliqué : dd/MM/yyyy
 */
var DateFormat = Class.create();
DateFormat.prototype = {
	initialize : function ()
	{
	},
	format : function(dateArg)
	{
		if (dateArg)
		{
			var day = dateArg.getDate();
			if (isNaN(day))
				return null;
			if (day < 10)
				day = '0' + day;
			var month = dateArg.getMonth() + 1;
			if (isNaN(month))
				return null;
			if (month <10)
				month = '0' + month;
			var year = dateArg.getFullYear();
			if (isNaN(year))
				return null;
			return ''+day+'/'+month+'/'+year;
		}
		else
		{
			return null;
		}
	},
	parse : function(strArg)
	{
		if (!strArg || strArg.length==0)
			return null;

		strArg = strArg.replace(/(-|\.)/g, "/") ;
		strArg = strArg.replace(/^(\d{2})(\d{2})(\d{2,4})$/g, "$1/$2/$3") ; /* cas où il n'y a aucun séparateur.*/
		var elements = strArg.split(/\//g);
		if (elements == null || elements.length !=3)
			return null;
		var day = elements[0];
		var month = elements[1] - 1; /*les mois vont de 0 à 11 en js*/
		var year = elements[2];
		if (!year || isNaN(year))
		{
			return null;
		}
		else
		{
			year = parseInt(year);
		}
		// prise en compte de la saisie sur 2 chiffres pour les années.
		var currentYear = new Date().getFullYear() - 2000;
		if (year <= currentYear)
			year += 2000;

		// vérification du domaine de valeur pour chaque champ de la date.
		daysInMonths = [31,28,31,30,31,30,31,31,30,31,30,31];
		var maxDays = daysInMonths[month];
		if (isLeapYear(year) && month==1)
			maxDays = 29;

		if (year <0)
			return null;
		if (month <0 || month >11)
			return null;
		if (day < 0 || day > maxDays)
			return null;

		return new Date(year,month, day );
	}
}

/**
 * Retourne vrai (true) si l'année passée en argument est bissextile.
 */
function isLeapYear(aYear)
{
	/* A year will be a leap year if it is divisible by 4 but not by 100.
	If a year is divisible by 4 and by 100, it is not a leap year
	unless it is also divisible by 400.*/

	if (aYear % 4 == 0)
	{
		if (aYear % 100 !=0)
		{
			return true;
		}
		else
		{
			if (aYear % 400 == 0)
			{
				return true;
			}
		}
	}
	return false;

	// autre solution :
	// return new Date(yr,2-1,29).getDate()==29;
}

/**
 * Simule le comportement de la classe NumberFormat issue de Java.
 * version minimaliste :
 * ne supporte que des formats variant autour de #,###.##.
 * attention: pour le format :
 *	- ',' = séparateur de milliers
 *	- '.' = séparateur décimal
 *	- '#' = n'importe quel chiffre
 *
 * à partir de la chaine qui est passée, 2 choses sont conservées et utilisées :
 * 	- le nombre de digits de la partie décimale
 * 	- si le séparateur de groupe existe, le nombre de caractères à sa droite.
 *
 * à la récupération de la chaine, on regarde s'il y a des
 *
 * au formatage du nombre
 * A utiliser pour le formatage de nombres entiers et réels.
 */
var NumberFormat = Class.create();
NumberFormat.prototype = {
	initialize : function (strPattern)
	{
		this.pattern = strPattern;
	},
	/**
	 * lance le formatage d'une valeur numérique
	 * en fonction d'un pattern défini.
	 */
	format : function(value, strPattern)
	{
		this.setFormat(strPattern);

		var result = null;

		// application de l'arrondi
		value = roundUp(value, this.digits);

		// ajustement des groupes
		var valueAsStr = ''+value;
		var iSep = valueAsStr.indexOf('.');
		result = '';
		var digitsStr = '';
		if (iSep ==-1)
		{
			iSep = valueAsStr.length;
		}
		else
		{
			digitsStr = '.'+valueAsStr.substr(iSep+1, valueAsStr.length);
		}
		var iCpt = 0;
		for (var iDec = iSep-1; iDec >=0; iDec--)
		{
			if (this.grouping > 0 && iCpt % this.grouping == 0 && iCpt!=0)
			{
				result =' ' + result;
				iCpt = 0;
			}
			result = valueAsStr.charAt(iDec) + result;
			iCpt++;
		}

		// ajustement du séparateur décimal
		result += digitsStr;

		return result;

	},
	isInteger : function()
	{
		this.setFormat(null);
		return this.digits==0;
	},
	/**
	 * Cette méthode parse une chaine et retourne la valeur numérique
	 * associée en fonction du pattern (défini en argument ou spécifié par défaut)
	 * @return la valeur réelle (int ou double), null en cas d'erreur lors du parsing
	 */
	parse : function(strArg, strPattern)
	{

		this.setFormat(strPattern);
		if (strArg)
		{
			strArg = strArg.replace(',','.');
			strArg = strArg.replace(new RegExp("[\\s]+", "gm"),'');

			// Supprime les espaces insécables, code caractère 160
			var insecableSpace = String.fromCharCode(160);
			var expr = "[" + insecableSpace + "]+";
			strArg = strArg.replace(new RegExp(expr, "gm"),'');

			if (isNaN(strArg))
			{
				return null;
			}
			if (this.digits==0)
				return parseInt(strArg);
			else
			{
				var realValue = parseFloat(strArg);
				realValue = roundUp(realValue, this.digits);
				return realValue;
			}
		}
		return null;
	},
	/**
	 * Cette méthode détermine automatiquement les grandeurs caractéristiques
	 * liées au format.
	 * Elle crée un pattern par défaut si aucun n'est défini.
	 */
	setFormat:function (strPattern)
	{
		if (strPattern==null)
		{
			strPattern = '#,###.##';
		}
		this.pattern = strPattern;
		this.grouping = 0;
		this.digits = 0;
		var strGroup = this.pattern.match(/,#+[\.]{0,1}/);
		if (strGroup)
		{
			var str = strGroup[0].substr(1, strGroup[0].length-1);
			var endPoint = str.indexOf('.');
			if (endPoint>-1)
			{
				str = str.substr(0, str.length-1);
			}
			if (str != null)
			{
				this.grouping = str.length;
			}
		}
		var strDigits = this.pattern.match(/\.#+/);
		if (strDigits)
		{
			var str = strDigits[0].substr(1, strDigits[0].length-1);
			if (str != null)
			{
				this.digits = str.length;
			}
		}
	}
}


/**
 * fonction dont le rôle est d'arrondir une valeur passée en argument en fonction du nombre de digits
 * passé en argument.
 * si le nombre de digits est de 0, on applique le traitement simple Math.round.
 */
function roundUp(value, nbDigits)
{
	if (value!="" && !isNaN(value))
	{
		if (nbDigits!=-1 )
		{
			value = Math.round(value * Math.pow(10.0,nbDigits)) / Math.pow(10.0,nbDigits);
		}
		else
		{
			value = Math.round(value);
		}
	}

	return value;
}

//====================== Méthodes globales ===============================

/**
 * retourne le libellé associé à champ (à partir du tag label)
 */
function findLabelTextFor(element)
{
	// 2 solutions :
	// soit le champ input est sous un label
	// soit le chp label a un attribut for défini pour ce champ input.

	// méthode 1
	parentNode = element.parentNode;
	if (parentNode && parentNode.tagName=='LABEL')
	{
		return getInnerText(parentNode);
	}

	// méthode 2
	var allLbls = (document.body).getElementsByTagName('LABEL');
	if (allLbls)
	{
		for (var itLbls = 0; itLbls < allLbls.length;itLbls++)
		{
			curLbl = allLbls[itLbls];
			var forAttribute = curLbl.htmlFor;

			if (forAttribute == element.id)
			{
				return getInnerText(curLbl);
			}
		}
	}

	// sinon
	return null;
}

/**
 * retourne le text directement sous un élément.
 * gère les différences entre IE et Mozilla.
 */
function getInnerText(element)
{
	if (element.innerText)
	{
		return element.innerText;
	}
	return element.textContent;
}
/**
 * retourne vrai si un champ est obligatoire :
 *  - attribut required spécifié sur le champ
 *  - class required pour cet élément
 */
function isRequired(element)
{
	var classRequired = Element.hasClassName(element, 'required');
	var myElt = element;
	return element.getAttribute('required')!=null || Element.hasClassName(element, 'required');
}

