/**
 * @class FormValidator
 * 
 * Validates form fields using a predefined object containing a bunch of
 * miscellaneous validation functions. Instantiate the object by calling:
 * <pre><code>new FormValidator();</code></pre>
 * The object takes care of selecting the forms and required fields when
 * it is instantiated.
 * The class names you can use for validation are:
 * <ul>
 * 	<li>alpha - only allow alphabetic characters</li>
 * 	<li>numeric - only allow numeric characters</li>
 * 	<li>alphanumeric - allow both alphabetic and numeric characters</li>
 * 	<li>radio - check that a radio button is selected</li>
 * 	<li>checked - check that a checkbox is checked</li>
 * 	<li>selected - check that an option has been selected</li>
 * 	<li>fileinput - check that a file has been added</li>
 * 	<li>email - check for a valid email format</li>
 * 	<li>identical - compare two values against each other</li>
 * 	<li>dob - check for a valid date of birth</li>
 * 	<li>numericmonth - check for a valid numerical month</li>
 * 	<li>cvv - check for a valid cvv number</li>
 * </ul>
 * Optional configuration options can be passed into the object when it
 * is instantiated using object notation:
 * <pre><code>new FormValidator({ foo: 'bar', lorem: 'ipsum' });</code></pre>
 * Valid config options are:
 * <ul>
 * 	<li>forms - default: document.getElementsByTagName('form') - an array of forms to validate; can be one or more form elements</li>
 * 	<li>inputParentNode - default: 'LI' - the wrapper element around your form fields</li>
 * 	<li>dobFormat - default: /\d\d\/\d\d\/\d\d\d\d/ - the regex format to use for date of birth</li>
 * 	<li>numFormat - default: /\D/g - the regex format to use for numberical values</li>
 * 	<li>strFormat - default: /\d/g - the regex format to use for alphabetical values</li>
 * 	<li>emailFormat - default: /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*\.(\w{2,})$/ - the regex format to use for emails</li>
 * 	<li>printErrorList - default: true - whether or not to print out a list of errors above the form</li>
 *		<li>defaultValues - default: {} - a key/value pair of each inputs' default values. Key has to be the name of the input and the value is the default value it contains</li>
 * </ul>
 */
var FormValidator = (function() {
	var config = {
		// which forms to validate
		forms: document.getElementsByTagName('form'),
		
		// the element that wraps around each form field
		inputParentNode: 'LI',
		
		// regex format for date of birth
		dobFormat: /\d\d\/\d\d\/\d\d\d\d/,
		
		// regex for numerical values
		numFormat: /\D/g,
		
		// regex for alphabetical values
		strFormat: /\d/g,
		
		// regex for email format
		emailFormat: /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*\.(\w{2,})$/,
		
		// whether or not to print out a list of errors above the form
		printErrorList: true,
		
		// easiest way to define the default values of input elements
		// defining it this way ensures you can also use php to validate the form
		// without getting weird issues when checking input.defaultValue
		defaultValues: {}
	},
	// gets the title to display in the error list - defining a title on the element
	// is much faster than letting the javascript figure out the text to put in
	getTitle = function(element) {
		return (element.title) ? element.title : element.name.charAt(0).toUpperCase() + element.name.substr(1).replace('_', ' ');
	},
	validationFunctions = {
		/**
		 * Checks if a value is en empty string, null, or undefined.
		 * 
		 * @param {String} value
		 * 
		 * @return {Boolean}
		 */
		required: function(element) {
			element.value = ajil.trim(element.value);
			if(element.value == '' || element.value == null || element.value == undefined || config.defaultValues[element.name] == element.value) {
				return getTitle(element) + ' is required';
			}
			return true;
		},
		
		/**
		 * Performs a regex search for alphabetic characters.
		 * 
		 * @param {HTMLElement} element
		 * 
		 * @return {Boolean}
		 */
		alpha: function(element) {
			if(element.value.search(strFormat) != -1) { 
				return getTitle(element) + ' can only contain alphabetic characters';
			}
			return true;
		},
		
		/**
		 * Performs a regex search for numeric characters.
		 * 
		 * @param {HTMLElement} element 
		 * 
		 * @return {Boolean}
		 */
		numeric: function(element) {
			if(element.value.search(numFormat) != -1) {
				return getTitle(element) + ' can only contain numeric characters';
			}
			return true;
		},
		
		/**
		 * Checks a group of radio buttons to see if one has been selected
		 * 
		 * @param {Array} radioButtons an array containing a group of radio button elements
		 * 
		 * @return {Boolean}
		 */
		radio: function(radioButtons, title) {
			for(var i = 0; i < radioButtons.length; i++) {
				if(radioButtons[i].element.checked) {
					return true;
				}
			}
			return false;
		},
		
		/**
		 * Checks a group of checkboxes to see if one has been checked. Optionally you
		 * can also supply a minimum or maximum number of checkboxes that need to be
		 * checked.
		 * 
		 * @param {HTMLElement} element
		 * @param {Integer} [numberChecked] minimum amount that need to be checked
		 * @param {Integer} [maxChecked] maximum amount that can be checked
		 * 
		 * @return {Boolean}
		 */
		checked: function(element, title, numberChecked, maxChecked) {
			numberChecked = numberChecked || 1;
			maxChecked = maxChecked || 0; // 0 = unlimited
			
			var msg = 'You must check at least ' + numberChecked + ' checkbox(es).',
			max_msg = 'You cannot check more than ' + maxChecked + ' checkboxes.',
			checked = 0,
			input = element.getElementsByTagName('input')[0],
			inputs = element.getElementsByTagName('input'),
			radios = (input.type == 'radio');
			
			if(!radios) { // CHECKBOXES
				for(var i = 0; i < inputs.length; i++) {
					if(inputs[i].checked) {
						numberChecked--;
						
						if(maxChecked !== 0) {
							checked++;
						}
					}
				}
				
				if(numberChecked <= 0 && maxChecked == 0) {
					return true;
				} else if(numberChecked <= 0 && maxChecked == checked) {
					return true;
				}
				
				return { element: element, message: title + ' - ' + msg };
			} else {
				for(var i = 0; i < inputs.length; i++) {
					if(inputs[i].checked) {
						return true;
					}
				}
				return false;
			}
		},
		
		/**
		 * Check if an option has been chosen from a select box. Invalid option
		 * value is -1.
		 * 
		 * @param {HTMLElement} element
		 * 
		 * @return {Boolean}
		 */
		selected: function(element) {
			var selected = element.options[element.selectedIndex];
			if(selected.value == -1) {
				return 'Please select ' + getTitle(element);
			}
			return true;
		},
		
		/**
		 * Perform a regex search for a valid email address format. Email format regex
		 * can be defined in the configuration options.
		 * 
		 * @param {HTMLElement} element
		 * 
		 * @return {Boolean}
		 */
		email: function(element) {
			if(element.value.search(config.emailFormat) == -1) {
				return getTitle(element) + ' is not in the correct format';
			}
			return true;
		},
		
		/**
		 * Loops over two values and compares them to each other.
		 * 
		 * @param {HTMLElement} element
		 * 
		 * @return {Boolean}
		 */
		identical: function(element) {
			var fields = site.getParentNode(element, config.inputParentNode).getElementsByTagName('input');
			// I know that this is very inefficient, but time dictates the quick and obvious solution. <sigh />
			for(var i = 0; n = fields.length, i < n; i++) {
				for(var j = 0; j < n; j++) {
					if(fields[i].value != fields[j].value) {
						//return { element: site.getParentNode(element, config.inputParentNode), message: 'Please make sure that your email addresses match' };
						return false;
					}
				}
			}
			return true;
		},
		
		/**
		 * Performs a regex match for a valid date of birth value. Date of birth regex
		 * can be defined in the configuration options.
		 * 
		 * @param {HTMLElement} element
		 * 
		 * @return {Boolean}
		 */
		dob: function(element) {
			var strDate = element.value.toString();
			if(validationFunctions.empty(element.value)) {
				return 'You must provide a date of birth';
			} else if(!strDate.match(dobFormat)) {
				return 'Please make sure your date of birth is in the correct format';
			}
			return true;
		},
		
		/**
		 * Performs a numeric regex search and also checks that the value
		 * is between 1 and 12.
		 * 
		 * @param {HTMLElement} element
		 * 
		 * @return {Boolean}
		 */
		numericmonth: function(element, title) {
			if(element.value.search(numFormat) != -1) {
				//return { element: element, message: title + ' - Only numeric characters allowed' };
				return false;
			} else if(element.value > 12 || element.value < 1) {
				//return { element: element, message: title + ' - Please enter a valid month' };
				return false;
			}
			return true;
		},
		
		/**
		 * Performs a check for a valid CVV number. Valid means 3 or 4
		 * numerical values.
		 */
		cvv: function(element, title) {
			if(element.value.length < 3 || element.value.length > 4 || element.value.search(numFormat) != -1) {
				return 'CVV number must be three or four digits';
			}
			return true;
		}
	};
	
	/**
	 * Loops through the required fields and validates them using the
	 * validationFunctions object
	 * 
	 * @return {Boolean}
	 */
	function validate(form) {
		var errors = [], parent, response;
		
		ajil.foreach(form.requiredFields, function(requiredField) {
			// we will be adding the error class to the parent node
			//parent = ajil.getParentNode(requiredField, config.inputParentNode);
			
			ajil.foreach(requiredField.validate, function(fieldValidate) {
				// call the validate function
				response = validationFunctions[fieldValidate](requiredField, requiredField.name);
				
				if(response === true) {
					ajil.removeClass(requiredField, 'validation-error');
				} else {
					// error!
					ajil.addClass(requiredField, 'validation-error');
					errors[errors.length] = response;
				}
			});
		});
		
		// if there are errors then return false to the form submit
		if(errors.length > 0) {
			// print out a list of error messages
			if(config.printErrorList) {
				if(form.errorList == null) {
					// check to see if there is a list created by php
					var errorList = form.getElementsByTagName('ul')[0];
					if(errorList != null && errorList.className == 'error-list') {
						form.errorList = errorList;
						form.errorList.className = 'error-list';
						form.errorList.innerHTML = '<li>' + errors.join('</li><li>') + '</li>';
					} else {
						form.errorList = document.createElement('ul');
						form.errorList.className = 'error-list';
						form.errorList.innerHTML = '<li>' + errors.join('</li><li>') + '</li>';
						form.parentNode.insertBefore(form.errorList, form);
					}
				} else {
					form.errorList.innerHTML = '<li>' + errors.join('</li><li>') + '</li>';
				}
			}
			return false;
		}
		
		return true;
	}
	
	/**
	 * Setup everything
	 */
	function initialise() {
		// loop through all the forms
		ajil.foreach(config.forms, function(form) {
			form.requiredFields = [];
			
			// get form field parents
			var parentNodes = form.getElementsByTagName(config.inputParentNode),
			fields;
			
			ajil.foreach(parentNodes, function(parentNode) {
				
				// find all of our form fields
				fields = {
					inputs: parentNode.getElementsByTagName('input'),
					textareas: parentNode.getElementsByTagName('textarea'),
					selects: parentNode.getElementsByTagName('select')
				};
				
				if(fields.inputs.length > 0) {
					ajil.foreach(fields.inputs, function(input) {
						// dont include submit or reset buttons
						if(input.type != 'submit' && input.type != 'reset') {
							input.validate = [];
							
							ajil.foreach(input.className.split(' '), function(className) {
								if(className in validationFunctions) {
									input.validate[input.validate.length] = className;
								}
							});
							
							form.requiredFields[form.requiredFields.length] = input;
						}
					});
				}
				
				if(fields.textareas.length > 0) {
					ajil.foreach(fields.textareas, function(textarea) {
						textarea.validate = [];
						
						ajil.foreach(textarea.className.split(' '), function(className) {
							if(className in validationFunctions) {
								textarea.validate[textarea.validate.length] = className;
							}
						});
						
						form.requiredFields[form.requiredFields.length] = textarea;
					});
				}
				
				if(fields.selects.length > 0) {
					ajil.foreach(fields.selects, function(select) {
						select.validate = [];
						
						ajil.foreach(select.className.split(' '), function(className) {
							if(className in validationFunctions) {
								select.validate[select.validate.length] = className;
							}
						});
						
						form.requiredFields[form.requiredFields.length] = select;
					});
				}
			});
			
			// Bind form submission to the validate function
			form.onsubmit = function() {
				return validate(this);
			}
			
			// If the form is reset we should reset the validation as well
			form.onreset = function() {
				if(this.errorList != null) {
					this.errorList.innerHTML = '';
				}
				
				ajil.foreach(this.requiredFields, function(requiredField) {
					ajil.removeClass(requiredField, 'validation-error');
				});
			}
		});
	}
	
	/**
	 * Setup config options and call the initialise function
	 */
	return function(userConfig) {
		for(var key in userConfig) {
			if(config.hasOwnProperty(key)) {
				config[key] = userConfig[key];
			}
		}
		
		initialise();
	}
})();
