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

	options: {
		fieldSelectors: 'input, textarea, select',

		validateOnBlur: false,

		failOnFirst: false,
		reportFirstError: true,

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

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

		submitAjax: false,

		customErrors: {},

		// 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.validateOnBlur) {this.observeFields();}
	},


	/**
	 * onSubmit event handler, also submits form via ajax if required
	 * @param {Object} event
	 */
	onSubmit: function(event) {
		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('rel'); // Get the validation string
		var pass = true;
		var errors = [];
		var errorReported = false;

		if ($defined(valStr)) {
			var vals = valStr.split('|'); // Split the valStr into induvidual validators

			// If validators are required
			if (vals.length > 0) {

				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;
					}

					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('text', he.error);
				}

				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]);
			}
		}
	},


	/**
	 * 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();
				},
				error: 'This field is required!'
			},
			'minLength': {
				test: function() {
					this.errorArr.include(options);
					return (field.get('value').length >= options);
				},
				error: 'This field must be a minimum of {%0} long!',
				errorArr: []
			},
			'maxLength': {
				test: function() {
					this.errorArr.include(options);
					return (field.get('value').length <= options);
				},
					error: 'This field cannot be over {%0} long!',
				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);
						this.errorArr.include(split[1]);
					} else {
						this.errorArr.include(targF);
					}

					return result;
				},
				error: 'This field requires {%0} {to be %1}!',
				errorArr: []
			},
			'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}$/);
				},
				error: 'This field is not a valid email address!'
			},
			'isUrl': {
				test: function() {
					return field.get('value').test(/^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?$/);
				},
				error: 'This field requires a valid URL!'
			},
			'isNumber': {
				test: function() {
					return field.get('value').test(/^\-?[0-9]+(\.[0-9]+)?$/);
				},
				error: 'This field is not a valid number!'
			},
			'isInt': {
				test: function() {
					return field.get('value').test(/^[0-9]+$/);
				},
				error: 'This field is not a valid number!'
			},
			'isFloat': {
				test: function() {
					return field.get('value').test(/^[0-9]+\.[0-9]+$/);
				},
				error: 'This field is not a valid float!'
			},
			'isValue': {
				test: function() {
					var split = options.split(',');

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

					return result;
				},
				error: 'This field must be {0}',
				errorArr: []
			},
			'isFileFormat': {
				test: function() {

				},
				error: ''
			}
		};

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