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
		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);
		
		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) {
		if (!this.validateForm()) {
			event.stop();
		} else if (this.options.submitAjax) {
			this.form.send();
		}
	},
	
	
	/**
	 * 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) {
		return this.form.getElements('[name='+name+']');
	},
	
	
	/**
	 * Helper function to return the fields relevant label
	 * @param {Object} field
	 */	
	getFieldLabel: function(field) {
		var id = field.get('id');
		
		if(field.get('type') == "radio") {
			id = field.get('name');
		}
		
		return this.form.getElement('label[for='+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]});
		}
	}
});
