/**
 * JSForm
 * @author Charles Demers
 * @version 0.1
 * @requires Core.js
 * @requires jQuery (http://www.jquery.com)
 */

/**
 * @constructor JSForm
 * @param {String} 		element 			The selector for the form itself
 * @param {Object} 		opts
 * @param {String} 		opts.lang 			The language of the default messages of the form<br />
 *											<strong>Values:</strong> (fr | en)<br />
 * 											<strong>Default:</strong> fr
 * @param {Function} 	opts.submit			The function to call after the form has been validated. Note that it has to return a boolean to know whether to submit the form or not. (For ajax calls, always return false)<br />
 * 											The function is given 1 parameter: isValid {Boolean}	Tells if the form is valid or not
 * @param {Boolean}		opts.breakOnError	If set to true, all inputs will validate until they find an error. In short, you will always receive only one error message for each input. Note that this can be overridden when you register an input.<br />
 *											<strong>Default:</strong> false<br /><br />
 *
@example 											
<strong>Example:</strong><br />
<pre>
var form = new JSForm('#myForm',{
	lang:'fr',
	breakOnError:true,
	submit:function(isValid){
		if(isValid === false){
			errorField.showErrors(); // if using UIErrorField
			return false;
		} else {
			errorField.removeErrors(); // if using UIErrorField
			return true;
		}
	}
});
</pre>
 */
function JSForm(element,opts){
	this.constructor = JSForm;
	this.constructor.name = "JSForm";
	
	var sel = jQuery(element);
	if(sel.length !== 0){
		this.form = sel;
	} else {
		throw new Error("[JSForm] '"+element+"' could not be found");
	}
	if(opts){
		this.lang = (opts.lang) ? opts.lang : "fr";
		this.breakOnError = (isDefined(opts.breakOnError)) ? opts.breakOnError : false;
		this.submitCallback = (opts.submit) ? opts.submit : null;
	}
	var _this = this;
	this.form.submit(function(){
		var ret = _this.submit();

		if(ret !== undefined){
			return ret;
		}
	});
	this.inputs = new Hash();
	this.errors = new Hash();
}
JSForm.prototype = {
	/**
	 * Registers given inputs with the validation process of the form
	 * @param {String} 											element			A jQuery selector, it can select multiple elements at once
	 * @param {String | RegExp | Function | Object | Array} 	validation 		The validation you want to apply to the input.<br />
	 * 																			If it's a string, it will only be checked if it represents a validation described in JSFormValidationRegExp.<br />
	 * 																			If it's a regular expression, the value of the input will be checked against it.<br />
	 * 																			If it's a function, it will be given the value of the input as a parameter. Remember that it has to return a boolean.<br />
	 *																			If it's an object, you must specify a function (fn) and an array of arguments (args) to validate the input. Your function will receive as parameters (value,args1,args2,argsN). And again, it has to return a boolean.<br />
	 *																			If it's an array, this means that the input is gonna be validated against multiple validation types.<br /><br />
	 * 																			<strong>Examples:</strong><br />
	 * 																			<pre>form.registerInput('#date',"date","Invalid date buddy!");</pre>
	 * 																			<pre>form.registerInput('#letter',/[a-z]/,"This field has to be a letter!");</pre>
	 * 																			<pre>form.registerInput('#number',{fn:"isBetween",args:[1,10]},"This value has to be between 1 and 10!");</pre>
	 * 																			<strong>Example with an array of validations:</strong><br />
@example
<strong>Example:</strong>
<pre>
form.registerInput(jQuery('#myInput'),[
	['required'],
	[myFunc, "This does not validate against my rule."],
	[/[0-9]/, "This is not a digit"]
],true);
</pre>
	 * 																			
	 * @param {String} 											message			The error message in case the input is not valid. This argument can be left blank in the case of default validation type, or in the case of multiple validations (validation of type Array). Note that for default validation type, a default message (with the language specified for the form) will be added
	 * @param {Boolean}											setBreak		If set to true, all matching inputs will validate until they find an error. In short, you will always receive only one error message for each input.<br />
	 * 																			<strong>Default:</strong> Same value as the form, which is false if you didn't change it
	 */
	
	
	registerInput:function(element,validation,message,setBreak){
		var _this = this;
		jQuery(this.form).find(element+":input").each(function(){
			var name = jQuery(this).attr('name');
			var type = jQuery(this).attr('type');
			if(type == "radio"){
				if(validation == "required"){
					if(_this.inputs.hasKey(name) === false){
						_this.inputs.add(name, new JSFormRadioButton(jQuery(this)));
					} else {
						_this.inputs.get(name).addInput(jQuery(this));
					}
					messageSent = (message && typeOf(message) == "string") ? message : {lang:_this.lang};
					willBreak = (isDefined(setBreak)) ? setBreak : (typeOf(message) == "boolean") ? message : _this.breakOnError;
					_this.inputs.get(name).addValidation(validation,messageSent,willBreak);
				}
			} else {
				if(_this.inputs.hasKey(name) === false){
					_this.inputs.add(name,new JSFormInput(jQuery(this)));
				}
				var messageSent, willBreak;
				if(typeOf(validation) == "array"){
					var thisInput = _this.inputs.get(name);
					willBreak = (isDefined(message)) ? message : _this.breakOnError;
					for(var i=0, l=validation.length; i<l; i++){
						messageSent = (validation[i][1]) ? validation[i][1] : {lang:_this.lang};
						thisInput.addValidation(validation[i][0],messageSent,willBreak);
					}
				} else {
					messageSent = (message && typeOf(message) == "string") ? message : {lang:_this.lang};
					willBreak = (isDefined(setBreak)) ? setBreak : (typeOf(message) == "boolean") ? message : _this.breakOnError;
					_this.inputs.get(name).addValidation(validation,messageSent,willBreak);
				}
			}
		});
	},
	/**
	 * Calls the validation code of all the inputs registered and determines if the form is valid
	 * @private
	 * @returns Whether the form is valid or not
	 * @type Boolean
	 */
	validate:function(){
		var ret = true;
		this.inputs.forEach(function(name,jsFormInput){
			if(jsFormInput.validate() === false){
				ret = false;
			}
		});
		return ret;
	},
	/**
	 * Is executed at the submit event of the form. Calls the validate function and if the form is valid, submits it or calls the callback method specified
	 * @private
	 * @returns Whether the form should be submitted or not
	 * @type Boolean
	 */
	submit:function(){
		this.errors.empty();
		var isValid = this.validate();
		
		if(this.submitCallback != null){
			try{
				var ret = this.submitCallback.call(this,isValid);
				if(ret !== undefined){
					return ret;
				}
			} catch(e){
				alert("[JSForm submitHandler] "+e);
				return false;
			}
		}
		if(isValid === false){
			return false;
		}
	},
	/**
	 * Returns the errors collected in the validation process
@example
<strong>Example:</strong>
<pre>
{
	myInputName:["My first error message","My second error message"],
	mySecondInputName:["My first error message"]
}
</pre>
	 * @returns An object containing the error messages<br />
	 * @type Object
	 */
	getErrors:function(){
		var _this = this;
		this.inputs.forEach(function(name,jsFormInput){
			_this.errors.add(name,jsFormInput.errors);
		});
		return this.errors.getClean();
	},
	/**
	 * Returns the errors collected in the validation process for a specific input
	 * @param {String} name 	The name of the input you want to get the errors from
	 * @returns An array containing the error messages for the input
	 * @type Array
	 */
	getErrorsForName:function(name){
		return this.inputs.get(name).errors;
	}
};


/**
 * Provides an instance of JSFormInput; to use with JSForm, it will work alone but no form will bind the input to itself, so function calls will have to be made on each instances
 * @author Charles Demers
 * @version 0.1
 * @private
 * @requires JSForm
 * @see JSForm#registerInput
 */
function JSFormInput(element){
	var sel = jQuery(element);
	if(sel.length != 0){
		this.input = sel;
	} else {
		throw new Error("[JSFormInput] '"+element+"' could not be found;");
	}
	this.validations = {
		regexp:[],
		func:[]
	};
	this.errors = [];
	return this;
}
JSFormInput.prototype = {
	/**
	 * Adds a validation type to the appropriate stack of validation (regexp or func) and sets breakOnError for this input
	 * @private
	 * @see JSForm#registerInput
	 */
	addValidation:function(validation,message,breakOnError){
		if(typeOf(validation) == "string" && JSFormValidationRegExp.hasKey(validation)){
			message = (typeOf(message) == "object" && message.lang) ? JSFormErrors.get(message.lang).get(validation) : message;
			this.validations.regexp.push([JSFormValidationRegExp.get(validation), message]);
		} else if(typeOf(validation) == "object"){
			message = (typeOf(message) == "object" && message.lang) ? JSFormErrors.get(message.lang).get(validation) : message;
			if(JSFormValidationFunctions.hasKey(validation.fn)){
				this.validations.func.push([JSFormValidationFunctions.get(validation.fn),validation.args,message]);
			} else {
				this.validations.func.push([validation.fn,validation.args,message]);
			}
		} else {
			if(isDefined(message) === false){
				throw new Error("[JSFormInput addValidation] custom validation requires specifying a custom error message");
			}
			if(typeOf(validation) == "regexp"){
				this.validations.regexp.push([validation, message]);
			} else if(typeOf(validation) == "function"){
				this.validations.func.push([validation, message]);
			}
		}
		this.breakOnError = (breakOnError) ? breakOnError : false;
	},
	/**
	 * Gets the current value of the input
	 * @private
	 * @returns The current value of the input
	 * @type String
	 */
	getValue:function(){
		if(window.CKEDITOR && CKEDITOR.instances[jQuery(this.input).attr("id")]){
			return CKEDITOR.instances[jQuery(this.input).attr("id")].getData();
		} else if(jQuery(this.input).attr("type") == "checkbox"){
			return (jQuery(this.input).attr("checked") == true) ? "checked" : "";
		}
		return jQuery(this.input).val();
	},
	/**
	 * Validates the input against all regular expression and functions in its stacks
	 * @private
	 * @returns Whether the input is valid or not
	 * @type Boolean
	 */
	validate:function(){
		this.errors.empty();
		var value = this.getValue();
		var regexps = this.validations.regexp;
		var funcs = this.validations.func;
		
		var i;
		if(value == ""){
			for(i=0, l=regexps.length; i<l; i++){
				if(regexps[i][0] == "required"){
					this.errors.push(regexps[i].getLast());
					return false;
				}
			}
			return true;
		} else {
			for(i=0, l=regexps.length; i<l; i++){
				if(regexps[i][0] != "required"){
					if(regexps[i][0].test(value) === false){
						this.errors.push(regexps[i].getLast());
						if(this.breakOnError){ return false; }
					}
				}
			}
			var ret;
			for(i=0, l=funcs.length; i<l; i++){
				ret = false;
				if(typeOf(funcs[i][1]) == "array"){
					var args = funcs[i][1].clone();
					args.splice(0,0,value);
					ret = funcs[i][0].apply(this,args);
				} else {
					ret = funcs[i][0].call(this,value);
				}
				if(ret === false){
					this.errors.push(funcs[i].getLast());
					if(this.breakOnError){ return false; }
				}
			}
			return (this.errors.length == 0);
		}
	}
};

function JSFormRadioButton(element){
	this.els = [jQuery(element)];
	
	this.validations = {
		regexp:[]
	};
	this.errors = [];
}
JSFormRadioButton.prototype = {
	addInput:function(element){
		this.els.push(jQuery(element));
	},
	addValidation:function(validation,message,breakOnError){
		this.validations.regexp.push([validation,message]);
	},
	getValue:function(){
		return $(this.els[0]+":checked").val();
	},
	validate:function(){
		this.errors.empty();
		var value = this.getValue();
		if(value == undefined){
			this.errors.push(this.validations.regexp[0][1]);	
		}
		return (this.errors.length == 0);
	}
};

/**
 * @constructor JSFormValidationRegExp
 * @desc 	Describes all the validation regular expressions for JSForm
 * 
 * 
<pre>
required
email
url
phone
zip
usZip
date
creditCard 
alphaNumeric
alphaNumericWithSpaces
letters
digits
</pre>
 */
var JSFormValidationRegExp = new Hash({
	required : "required",
	email : /^[a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,4}$/,
	url : /^((http|https|ftp):\/\/)?([[a-zA-Z0-9]\-\.])+(\.)([[a-zA-Z0-9]]){2,4}([[a-zA-Z0-9]\/\+=%&_\.~?\-]*)$/,
	phone : /^(([0-9]{1})*[- .(]*([0-9]{3})[- .)]*[0-9]{3}[- .]*[0-9]{4})+$/,
	zip : /^[ABCEGHJKLMNPRSTVXY][0-9][A-Z] [0-9][A-Z][0-9]$/i,
	usZip : /^[0-9]{5}(?:-[0-9]{4})?$/,
	date : /^(19|20)?[0-9]{2}[- \/\.](0?[1-9]|1[012])[- \/\.](0?[1-9]|[12][0-9]|3[01])$/,
	creditCard : /^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6011[0-9]{12}|3(?:0[0-5]|[68][0-9])[0-9]{11}|3[47][0-9]{13})$/,
	alphaNumeric : /^[a-zA-Z0-9]+$/,
	alphaNumericWithSpaces : /^[a-zA-Z0-9\ ]+$/,
	letters : /^[a-zA-Z]+$/,
	digits : /^[0-9]+$/
});
/**
 * @constructor JSFormValidationFunctions
 * @desc 	Describes all the validation functions for JSForm
 * 
 * 
<pre>
isBetween(min,max)
isNotDefault()
</pre>
 */
var JSFormValidationFunctions = new Hash({
	isBetween:function(value,min,max){
		if(value >= min && value <= max){ return true; }
		return false;
	},
	isNotDefault:function(value){
		return (this.input.get(0).defaultValue != value);
	}
});
/**
 * @constructor JSFormErrors
 * @desc 	Describes all the default error messages for JSForm
<pre>
fr {
	required                : "Ce champ ne peux être vide.",
	email                   : "Veuillez entrer un courriel valide.",
	url                     : "Veuillez entrer un url valide.",
	phone                   : "Veuillez entrer un numéro de téléphone valide.",
	zip                     : "Veuillez entrer un code postal valide.",
	usZip                   : "Veuillez entrer un code postal valide.",
	date                    : "Veuillez entrer une date valide.",
	creditCard              : "Veuillez entrer un numéro de carte de crédit valide.",
	alphaNumeric            : "Veuillez entrer des caractères alpha-numériques seulement.",
	alphaNumericWithSpaces  : "Veuillez entrer des caractères alpha-numériques et des espaces seulement.",
	letters                 : "Veuillez entrer des lettres seulement.",
	digits                  : "Veuillez entrer des chiffres seulement."
},
en {
	required                : "This field cannot be empty.",
	email                   : "Please enter a valid e-mail.",
	url                     : "Please enter a valid url.",
	phone                   : "Please enter a valid phone number.",
	zip                     : "Please enter a valid zip.",
	usZip                   : "Please enter a valid zip.",
	date                    : "Please enter a valid date.",
	creditCard              : "Please enter a valid credit card number.",
	alphaNumeric            : "Please enter only alphanumeric caracters.",
	alphaNumericWithSpaces  : "Please enter only alphanumeric caracters and spaces.",
	letters                 : "Please enter only letters.",
	digits                  : "Please enter only digits."
}
</pre>
 */
var JSFormErrors = new Hash({
	fr : new Hash({
		required	: "Ce champ ne peux être vide.",
		number		: "Ce champ n'accepte que des caractères numériques.",
		email		: "Veuillez entrer un courriel valide.",
		url			: "Veuillez entrer un url valide.",
		phone		: "Veuillez entrer un numéro de téléphone valide.",
		zip			: "Veuillez entrer un code postal valide.",
		usZip		: "Veuillez entrer un code postal valide.",
		date 		: "Veuillez entrer une date valide."
	}),
	en : new Hash({
		required	: "This field cannot be empty.",
		number		: "This field only accepts numeric caracters.",
		email		: "Please enter a valid e-mail.",
		url			: "Please enter a valid url.",
		phone		: "Please enter a valid phone number.",
		zip			: "Please enter a valid zip.",
		usZip		: "Please enter a valid zip.",
		date		: "Please enter a valid date."
	})
});


/**
 * @constructor UIForm
 * @desc Static class to enhance form elements
 */
var UIForm = {
	/**
	 * If applied to a text input, it will empty the field if its value is equal to its defaut value on focus, and put back its default value if its value is blank on blur.
	 * @param {String} element	A jQuery selector
	 */
	makeSmartInput : function(element){		
		jQuery(element).not("input[type=submit], input[type=button]").blur(function(){
			if(jQuery(this).val() == ""){
				jQuery(this).val(this.defaultValue);
			}
		});
		jQuery(element).not("input[type=submit], input[type=button]").focus(function(){
			if(jQuery(this).val() == this.defaultValue){
				jQuery(this).val("");
			}
		});
	}
};
