/*
	FormTest Class
	This class is for validating form inputs

	TO DO:
		+ Custom error reporting callback
		+ Custom error messages

*/
var FormTest = new Class({
	Implements: [Options, Events],

	options: {
		// Field element selectors
		fieldSelectors: 'input, textarea, select',

		valClass: 'vals',

		// When page loads validate form
		onLoad: false,
		// Validate form on field blur
		onBlur: false,

		// Validator options format = valname[options]
		valOptReg: /^([^\[^\{]+)(?:\[([^\]]+)\])?(?:\{([^\}]+)\})?/,

		// Errors options
		error: {
			lang: 'en',
			reportErrors: true,
			elem: 'span',
			elemClass: 'error-msg',
			fieldClass: 'field-error',
			labelClass: 'label-error',
			position: 'after',
			injectInto: false,
			showText: true
		},

		submitAjax: false,

		// Class Events
		onFormSubmit: $empty,
		onFormValidate: $empty,
		onDisplayError: $empty,
		onRemoveError: $empty
	},


	/**
	 * @constructor
	 * @param {Object} The form element to validate
	 * @param {Object} The instance options
	 */
	initialize: function(form, options) {

		// If a form is not passed exit
		if (!$defined(form)) {
			return false;
		}

		this.form = form;
		this.setOptions(options);

		// Setup errors
		this.errors = this.errors();

		this.form.addEvent('submit', this.onSubmit.bind(this));

		// If the form should be sent via ajax set form send property
		if (this.options.submitAjax) {this.form.set('send', this.options.submitAjax);}

		// If validate on blur is set observe fields for blur event
		if (this.options.onBlur) {this.observeFields();}

		// If validate on load is set run validation now
		if (this.options.onLoad) {this.validateForm();}
	},


	/**
	 * onSubmit event handler, also submits form via ajax if required
	 * @param {Object} event
	 */
	onSubmit: function(event) {
		// Inoccent until proven guilty
		var valid = true;

		if (!this.validateForm()) {
			event.stop();
			valid = false;
		} else if (this.options.submitAjax) {
			this.form.send();
		}

		// Fire event to give user chance to cancel event
		this.fireEvent('onFormSubmit', [valid, event]);
	},


	/**
	 * Observes form fields for blur event, when blur event fires validate field
	 */
	observeFields: function() {
		this.form.getElements(this.options.fieldSelectors).each(function(field) {
			field.addEvent('blur', this.validateField.bind(this, field));
		}, this);
	},


	/**
	 * Validates form and returns result
	 * @return {Boolean} Returns true if form passes validation or false if form fails
	 */
	validateForm: function() {
		this.fireEvent('onFormValidate');

		var result = this.form.getElements(this.options.fieldSelectors).map(function(field) {
			return this.validateField(field);
		}, this).every(function(res){return res;});

		return result;
	},


	/**
	 * Helper function to return fields by name
	 * @param {String} The field names required
	 * @return {Array} Returns an array of fields matching the passed in name
	 */
	getFieldByName: function(name) {
		// If field has square bracket (PHP ARRAY) we need to get the fields manually
		if (name.contains('[')) {
		    return this.form.getElements(this.options.fieldSelectors).filter(function(elem) {return (elem.getAttribute('name') === name);});
		} else {
		    return this.form.getElements('[name='+name+']');
		}
	},


	/**
	 * Helper function to return the fields relevant label
	 * @param {Object} field
	 */
	getFieldLabel: function(field) {
		return this.form.getElement('label[for='+field.get('id')+']');
	},


	/**
	 * Validates a field and reports any errors
	 * @param {Object} field
	 */
	validateField: function(field) {
		var result = this.testField(field);

		if (!result) {
			if (this.errors.hasError(field)) {
				this.errors.reportError(field);
			}
		}

		return result;
	},


	/**
	 * Tests a field against validators assigned to it
	 * @param {Object} field - The field to validate
	 * @return {Array}
	 */
	testField: function(field) {
		field = ($type(field) === 'array') ? field.getLast() : field;

		var valStr = field.get('class'); // Get the validation string

		var pass = true;
		var errorReported = false;
		var visible = (field.getStyle('display') === 'none' || field.getStyle('visibility') === 'hidden') ? false : true;

		if (valStr.length > 0) {
			var vals = valStr.split(' '); // Split the valStr into induvidual validators

			// If validators are required and the field is visible
			if (vals.length > 0 && visible) {

				if (vals[0] === 'required' || this.getValidator('isSet', field).test()) {
					pass = vals.map(function(val){
						var result = true;

						var validator = this.getValidator(val, field); // Get the validator

						if (validator) {
							result = validator.test();

							if (!result && !errorReported) {
								this.errors.addError(field, validator);

								errorReported = true;
							}
						}

						return result;
					}, this).every(function(res){return res;});
				}

				if (pass && this.errors.hasError(field)) {
					this.errors.removeError(field);
				}
			}
		}

		return pass;
	},

	/**
	 * Error handling closure
	 */
	errors: function() {
		var errors = $H(), opt = this.options, self = this;

		return {
			/**
			 * Adds the error object to the errors hash if there are no errors for the current field or
			 * the error is different to existing error.
			 * @param {Object} The field that has thrown the error
			 * @param {Object} The error object
			 */
			addError: function(field, error) {
				var fn = field.get('name');
				var en = error.name;

				// If label class is to be added include label in hash
				if (opt.error.labelClass) {
					var label = self.getFieldLabel(field);

					if ($defined(label))
						error.label = label;
				}

				if (!errors.has(fn) || errors.get(fn).get('name') !== error.name) {
					if (errors.has(fn)) {
						this.removeError(field);
					}
					errors.set(fn, $H(error));
				}
			},

			/**
			 * Tests if the field has any errors set
			 * @param {Object} The field to search
			 * @return {Boolean} If the field has an error return true. Otherwise return false.
			 */
			hasError: function(field) {
				return errors.has(field.get('name'));
			},

			/**
			 * Removes the error hash for the field and any dom elements
			 * @param {Object} The field to remove the error for
			 */
			removeError: function(field) {
				var fn = field.get('name');

				if (this.hasError(field)) {
					var he = errors.get(fn);

					if (he.has('elem')) {
						self.fireEvent('onRemoveError', [field, he]);
						he.get('elem').destroy();
					}

					// Remove errors from label
					if (he.has('label')) {
						he.get('label').removeClass(opt.error.labelClass);
					}

					errors.erase(fn);
				}

				if (field.hasClass(opt.error.fieldClass)) {
					field.removeClass(opt.error.fieldClass);
				}
			},


			/**
			 * Reports any errors for the field and updates existing errors
			 * @param {Object} The field to report errors for
			 */
			reportError: function(field) {
				var fn = field.get('name'), he = errors.get(fn), elem, ie = field, ip = opt.error.position;

				// If text is to be shown inject or reset existing error messages
				if (opt.error.showText) {
					if (opt.error.injectInto) {
						ie = opt.error.injectInto;
						ip = 'bottom';
					}

					if (!he.has('elem')) {
						elem = new Element(opt.error.elem);

						if (opt.error.elemClass)
							elem.addClass(opt.error.elemClass);

						elem.inject(ie, ip);

						he.set('elem', elem);
					} else {
						elem = he.get('elem');
					}

					elem.set('html', this.buildError(he));
				}

				if (he.has('label')) {
					var label = he.get('label');
					label.addClass(opt.error.labelClass);
				}

				if (opt.error.fieldClass) {
					field.addClass(opt.error.fieldClass);
				}

				self.fireEvent('onDisplayError', [field, elem]);
			},

			/**
			 * Builds up error messages from error array
			 */
			buildError: function (error) {
				var errLang = FormTest.lang[self.options.error.lang];
				var errS = errLang[error.name];
				var errSN = ''; // New error string

				if ($defined(error.errorArr)) {
					var errA = error.errorArr;

					// Get patterns
					var pats = errS.match(/(\{[^\{^\}]*\})/g).length;

					for (var i = 0; i < pats; i++) {
						var pReg = new RegExp('([^\{^\}]*)\{([^%]*)(%'+i+')([^}]*)\}([^\{^\}]*)');
						var parts = errS.match(pReg);

						errSN+= parts[1];

						if (errA[i]) {
							if ($type(errA[i]) === 'string') {
								errSN+= errA[i];
							} else if ($type(errA[i]) === 'element' && self.options.fieldSelectors.contains(errA[i].get('tag'))) {
								errSN+= '"'+self.getFieldLabel(errA[i]).get('text')+'"';
							}
							errSN+= parts[4];
						}

						errSN+= parts[5];
					}
				} else {
					errSN+= errS;
				}

				errSN = errLang.preFix+errSN+errLang.postFix;

				return (errSN.length > 0) ? errSN : errS;
			}
		}
	},


	/**
	 * Returns a field validator
	 * @param {Object} The validator string with options
	 * @param {Object} The field that the validator is for
	 * @return {Object} The validator object
	 */
	getValidator: function(val, field) {
		val = this.options.valOptReg.exec(val);
		var options = val[2] || null;

		var self = this;

		// Validators object
		var validators = {
			'isSet': {
				test: function() {
					if (field.get('type') === 'radio' || field.get('type') === 'checkbox') {
						return self.getFieldByName(field.get('name')).some(function(field){return field.get('checked')});
					} else {
						return field.get('value').length > 0;
					}
				}
			},
			'required': {
				test: function() {
					return validators['isSet'].test();
				}
			},
			'minLength': {
				test: function() {
					this.errorArr.include(options);
					return (field.get('value').trim().length >= options);
				},
				errorArr: []
			},
			'maxLength': {
				test: function() {
					this.errorArr.include(options);
					return (field.get('value').trim().length <= options);
				},
				errorArr: []
			},
			'requires': {
				test: function() {
					var result = true;

					var split = options.split('=');

					var targF = self.getFieldByName(split[0]);
					result = self.testField(targF);

					if (result && $defined(split[1])) {
						result = targF.get('value') == split[1];

						this.errorArr.include(targF[0]);
						this.errorArr.include(split[1]);
					} else {
						this.errorArr.include(targF[0]);
					}

					return result;
				},
				errorArr: []
			},
			'match': {
				test: function () {
					var targF = self.getFieldByName(options)[0];

					var result = targF.get('value') == field.get('value');

					if (!result)
						this.errorArr.include(targF);

					return result;
				},
				errorArr: []
			},
			'diff': {
				test: function () {
					var result = !validators['match'].test();

					if (!result)
						this.errorArr.include(self.getFieldByName(options)[0]);

					return result;
				},
				errorArr: []
			},
			// 'captcha': {
			// 	test: function() {
			// 		field.get('value');
			// 		new Request({'url'})
			// 	}
			// },
			'isEmail': {
				test: function() {
					return field.get('value').test(/^([a-zA-Z0-9\+_\-]+)(\.[a-zA-Z0-9\+_\-]+)*@([a-zA-Z0-9\-]+\.)+[a-zA-Z]{2,6}$/);
				}
			},
			'isUrl': {
				test: function() {
					return field.get('value').test(/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/);
				}
			},
			'isAlphaNum': {
				test: function () {
					return field.get('value').test(/^[a-zA-Z0-9]+$/);
				}
			},
			'isNumber': {
				test: function() {
					return field.get('value').test(/^\-?[0-9]+(\.[0-9]+)?$/);
				}
			},
			'isInt': {
				test: function() {
					return field.get('value').test(/^[0-9]+$/);
				}
			},
			'isFloat': {
				test: function() {
					return field.get('value').test(/^[0-9]+\.[0-9]+$/);
				}
			},
			'isValue': {
				test: function() {
					var split = options.split(',');

					var result = split.some(function(val){
						val = val.trim();
						return (field.get('value') == val);
					});

					if (!result)
						this.errorArr.include(options);

					return result;
				},
				errorArr: []
			},
			'isExt': {
				test: function() {
		
					var split = null;

					if (options === 'image') {
						split = ['png', 'gif', 'jpg', 'jpeg', 'tiff'];
					} else {
						split = options.split(',');
					}

					var value = field.get('value');

					var result = split.some(function(val){
						val = val.trim();

						var regexp = new RegExp('\.'+val+'$');
						return value.test(regexp);
					});

					return result;
				}
			}
		};

		if ($defined(validators[val[1]])) {
			return $extend(validators[val[1]], {name: val[1]});
		}
	}
});

FormTest.lang = {
	'en': {
		'preFix': 'This field ',
		'postFix': '!',
		'required': 'is required',
		'match': 'must match <strong>{%0}</strong>',
		'diff': 'must <strong>NOT</strong> match <strong>{%0}</strong>',
		'minLength': 'must be a <strong>minimum</strong> of <strong>{%0}</strong> characters long',
		'maxLength': 'cannot be over <strong>{%0}</strong> characters long',
		'requires': 'requires <strong>{%0}</strong>{ to be %1 just}',
		'isEmail': 'is not a valid email address',
		'isUrl': 'requires a valid URL',
		'isAlphaNum': 'must contain only alphanumeric characters',
		'isNumber': 'is not a valid number',
		'isInt': 'is not a valid integer',
		'isFloat': 'is not a valid float',
		'isValue': 'must be <strong>{%0}</strong>',
		'isExt': 'this file is not an accepted format',
		'marks': 'this is not marks'
	}
};