/**
 * ------------------------------------------------------------
 * FormValidator.js
 * ------------------------------------------------------------
 * Automatic validating of HTML forms by type using class names
 * as validating options. (required, email, numeric, notNull)
 * Grouping and validating of form elements by name.
 * Extensible with custom element and group validating functions.
 * ------------------------------------------------------------
 * version 1.0.0 @ 2008-06-01
 * version 1.0.1 @ 2008-06-02
 * ------------------------------------------------------------
 * require: prototype.js
 * ------------------------------------------------------------
 * Example:
 * ------------------------------------------------------------
 * <script type="text/javascript" src="js/prototype.js"></script>
 * <script type="text/javascript" src="js/formvalidator.js"></script>
 *
 *	FormValidatorOptions.errorMessages = Object.extend(
 *		FormValidatorOptions.errorMessages,
 * 		{
 * 			'testForm' : {
 *				'name' : {
 *					'prefix' : 'Please type Your name'
 *				},
 *				'products[]' : {
 *					'prefix' : 'Please select a product'
 *				}
 *			}
 *		});
 *
 * function validate(formId) {
 * 		var validator = new FormValidator(formId);
 *
 * 		validator.addElementFunction(validator.e.name, function() {
 *			if (validator.e.name.value == 'bush') {
 *				validator.e.name.showError('You can\'t be Bush...');
 *				return false;
 *			}
 *			return true;
 *		});
 *
 * 		validator.addGroupFunction(validator.g.products, function() {
 *			if (validator.g.products.e[1].empty) {
 *				validator.g.products.e[1].showError('Product "1" is required');
 *				return false;
 *			}
 *			return true;
 *		});
 *
 * 		if (validator.validateForm()) {
 *			validator.submitForm();
 * 			return true;
 *		} else {
 *			validator.showDebugInfo();
 *			return false;
 *		}
 * }
 */

/**
 * General FormValidator options
 */
FormValidatorOptions = Object.extend({
	debug : true,

	classNames : {
		'required' : 'fvRequired',
		'email' : 'fvEmail',
		'numeric' : 'fvNumeric',
		'notNull' : 'fvNotNull'
	},

	defaultErrorMessage : {},

	errorMessages : {},

	errorObjParams : {
		'prefix' : '',
		'postfix' : 'Error',
		'showMessage' : true
	},

	debugMessages : {
		'HEAD' : 'FormValidator DEBUG',
		'SEPARATOR' : '='.times(60),
		'INVALID_FORM_OBJ' : 'FATAL ERROR - Invalid form object',
		'DUPLICATED_ELEMENT' : 'WARNING - Ignored duplicated form element',
		'ELEMENT_WITHOUT_NAME' : 'WARNING - Ignored form element without " name " attribute',
		'ADD_ELEMENT' : 'INFO - Add form element',
		'ADD_GROUP' : 'INFO - Add form group',
		'ADD_GROUP_ELEMENT' : 'INFO - Add form group element',
		'ADD_ELEMENT_FUNCTION' : 'INFO - Add element function to',
		'ADD_GROUP_FUNCTION' : 'INFO - Add group function to',
		'VALIDATE_ELEMENT' : 'INFO - Validate element',
		'VALIDATE_GROUP' : 'INFO - Validate group',
		'ELEMENT_VALIDATION_SUCCEED' : 'INFO - Element validation SUCCEED',
		'ELEMENT_VALIDATION_FAILED' : 'INFO - Element validation FAILED',
		'GROUP_VALIDATION_SUCCEED' : 'INFO - Group validation SUCCEED',
		'GROUP_VALIDATION_FAILED' : 'INFO - Group validation FAILED',
		'FORM_VALIDATION_SUCCEED' : 'INFO - Form validation SUCCEED',
		'FORM_VALIDATION_FAILED' : 'INFO - Form validation FAILED'
	}
}, window.FormValidatorOptions || {});

/**
 * Load FormValidator options
 */
function loadFormValidatorOptions(obj) {
	if (typeof(FormValidatorOptions) == 'object' && typeof(obj) == 'object') {
		obj.classNames = FormValidatorOptions.classNames;
		obj.errorObjParams = FormValidatorOptions.errorObjParams;
		obj.debug = FormValidatorOptions.debug;
		obj.debugMessages = FormValidatorOptions.debugMessages;

		if (typeof(obj.elementObj) == 'object') {
			var formId = obj.elementObj.form.id;
			var objName = obj.elementObj.name;
			var errorMessageObj = FormValidatorOptions.errorMessages[formId][obj.elementObj.name];

			if (typeof(errorMessageObj) == 'object' && errorMessageObj.prefix) {
				obj.errorMessagePrefix = errorMessageObj.prefix;
			} else {
				obj.errorMessagePrefix = FormValidatorOptions.defaultErrorMessage.prefix;
			}

			if (typeof(errorMessageObj) == 'object' && errorMessageObj.postfix) {
				obj.errorMessagePostfix = errorMessageObj.postfix;
			} else {
				obj.errorMessagePostfix = FormValidatorOptions.defaultErrorMessage.postfix;
			}
		}
	}
}

/**
 * FormValidator class
 */
var FormValidator = Class.create();

FormValidator.prototype = {
	formId : undefined,				// Main form id
	formObj : undefined,				// Main form object
	formElements : [],				// Form element names array
	formGroups : [],					// Form element group names array
	e : {},								// Form element objects
	g : {},								// Form group objects
	debug : false,
	debugInfo : [],

	/**
	 * FormValidator constructor
	 */
	initialize : function(formId) {
		loadFormValidatorOptions(this);

		this.formId = formId;

		if ($(formId)) {
			this.formObj = $(formId);
			this.formElements = new Array();
			this.formGroups = new Array();
			this.e = new Object();
			this.g = new Object();

			this.debugInfo = new Array();

			this.addDebugInfo(this.debugMessages.HEAD);
			this.addDebugInfo(this.debugMessages.SEPARATOR);

			this.getFormElements();
		} else {
			if (this.debug) {
				this.addDebugInfo(this.debugMessages.INVALID_FORM_OBJ + ': " ' + formId + ' "');
				this.showDebugInfo();
			}
			return false;
		}
	},

	/**
	 * Get form elements
	 */
	getFormElements : function() {
		for (i=0; i<this.formObj.elements.length; i++) {
			var obj = this.formObj.elements[i];

			if (obj.name) {
				var groupCheck = obj.name.match(/(\w*)\[(\w*)\]/g);

				if (!groupCheck) {
					if (typeof(this.e[obj.name]) == 'undefined') {
						this.formElements[this.formElements.length] = {'name' : obj.name};
						this.e[obj.name] = new FormElement(obj);
						this.addDebugInfo(this.debugMessages.ADD_ELEMENT + ': " ' + obj.name + ' "');
					} else if (this.debug) {
						this.addDebugInfo(this.debugMessages.DUPLICATED_ELEMENT + ': " ' + obj.name + ' "');
					}
				} else {
					var groupName = groupCheck[0].substring(0, groupCheck[0].indexOf('['));

					if (typeof(this.g[groupName]) == 'undefined') {
						this.formGroups[this.formGroups.length] = {'name' : groupName};
						this.g[groupName] = {'name' : groupName, 'e' : [], 'required' : false, 'func' : []};
						this.g[groupName].e = new Array();
						this.g[groupName].func = new Array();
						this.addDebugInfo(this.debugMessages.ADD_GROUP + ': " ' + groupName + ' "');
					}

					var eIndex = this.g[groupName].e.length;

					this.g[groupName].e[eIndex] = new FormElement(obj);
					this.g[groupName].e[eIndex].group = true;

					if (this.g[groupName].e[eIndex].required && !this.g[groupName].required) {
						this.g[groupName].required = true;
					}
					this.addDebugInfo(this.debugMessages.ADD_GROUP_ELEMENT + ': " ' + groupName + ' / ' + eIndex + ' ( ' + obj.name + ' ) "');
				}
			} else if (this.debug) {
				this.addDebugInfo(this.debugMessages.ELEMENT_WITHOUT_NAME);
			}
		}
	},

	/**
	 * Validate form
	 */
	validateForm : function() {
		this.addDebugInfo(this.debugMessages.SEPARATOR);

		var errorNum = 0;

		for (e=0; e<this.formElements.length; e++) {
			elementObj = this.formElements[e];
			eObj = this.e[elementObj.name];

			if (eObj.required || eObj.numeric || eObj.notNull) {
				eErrorNum = errorNum;

				if (!this['validate' + eObj.tagName.capitalize()](eObj)) {
					eObj.showError();
					errorNum++;
				}
				if (eObj.func.length && errorNum == eErrorNum) {
					for (ef=0; ef<eObj.func.length; ef++) {
						if (!eObj.func[ef]()) {
							errorNum++;
						}
					}
				}
				this.addDebugInfo(this.debugMessages.VALIDATE_ELEMENT + ': " ' + eObj.name + ' "');
				if (errorNum == eErrorNum) {
					this.addDebugInfo(this.debugMessages.ELEMENT_VALIDATION_SUCCEED + ': " ' + eObj.name + ' "');
				} else {
					this.addDebugInfo(this.debugMessages.ELEMENT_VALIDATION_FAILED + ': " ' + eObj.name + ' "');
				}
			}
		}

		for (g=0; g<this.formGroups.length; g++) {
			groupObj = this.formGroups[g];
			gObj = this.g[groupObj.name];

			if (gObj.required) {
				gErrorNum = errorNum;

				if (!this.validateGroup(gObj)) {
					errorNum++;
				}
				if (gObj.func.length && errorNum == gErrorNum) {
					for (gf=0; gf<gObj.func.length; gf++) {
						if (!gObj.func[gf]()) {
							errorNum++;
						}
					}
				}
				this.addDebugInfo(this.debugMessages.VALIDATE_GROUP + ': " ' + gObj.name + ' "');

				if (errorNum == gErrorNum) {
					this.addDebugInfo(this.debugMessages.GROUP_VALIDATION_SUCCEED + ': " ' + gObj.name + ' "');
				} else {
					this.addDebugInfo(this.debugMessages.GROUP_VALIDATION_FAILED + ': " ' + gObj.name + ' "');
				}
			}
		}

		this.addDebugInfo(this.debugMessages.SEPARATOR);

		if (errorNum) {
			this.addDebugInfo(this.debugMessages.FORM_VALIDATION_FAILED);
			return false;
		} else {
			this.addDebugInfo(this.debugMessages.FORM_VALIDATION_SUCCEED);
			return true;
		}
	},

	/**
	 * Submit form
	 */
	submitForm : function() {
		this.formObj.submit();
	},

	/**
	 * Validate input
	 */
	validateInput : function(obj) {
		if (obj.empty) {
			return false;
		}
		if (obj.type == 'text' && obj.email && !this.validateEmail(obj.value)) {
			return false;
		}
		if (obj.type == 'text' && obj.numeric && !this.validateNumeric(obj.value)) {
			return false;
		}
		if (obj.type == 'text' && obj.notNull && !this.validateNotNull(obj.value)) {
			return false;
		}
		return true;
	},

	/**
	 * Validate textarea
	 */
	validateTextarea : function(obj) {
		if (obj.empty) {
			return false;
		}
		return true;
	},

	/**
	 * Validate select
	 */
	validateSelect : function(obj) {
		if (obj.empty) {
			return false;
		}
		if (obj.notNull && !this.validateNotNull(obj.selectedIndex)) {
			return false;
		}
		return true;
	},

	/**
	 * Validate group
	 */
	validateGroup : function(obj) {
		var groupNotEmpty = 0;

		for (i=0; i<obj.e.length; i++) {
			if (!obj.e[i].empty) {
				groupNotEmpty++;
			}
		}
		if (!groupNotEmpty) {
			for (i=0; i<obj.e.length; i++) {
				obj.e[i].showError();
			}
			return false;
		}
		return true;
	},

	/**
	 * Validate e-mail value
	 */
	validateEmail : function(value) {
		if (value.match(/^[_a-zA-Z0-9-]+(\.[_a-zA-Z0-9-]+)*@[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*(\.[a-z]{2,4})$/)) {
			return true;
		} else {
			return false;
		}
	},

	/**
	 * Validate numeric value
	 */
	validateNumeric : function(value) {
		if (value.match(/^[0-9]+$/)) {
			return true;
		} else {
			return false;
		}
	},

	/**
	 * Validate not null
	 */
	validateNotNull : function(value) {
		if (parseInt(value, 10)) {
			return true;
		} else {
			return false;
		}
	},

	/**
	 * Add element validation function
	 */
	addElementFunction : function (eObj, func) {
		if (eObj && typeof(func) == 'function') {
			eObj.func[eObj.func.length] = func;
			this.addDebugInfo(this.debugMessages.ADD_ELEMENT_FUNCTION + ': " ' + eObj.name + ' "');
		}
	},

	/**
	 * Add group validation function
	 */
	addGroupFunction : function (gObj, func) {
		if (gObj && typeof(func) == 'function') {
			gObj.func[gObj.func.length] = func;
			this.addDebugInfo(this.debugMessages.ADD_GROUP_FUNCTION + ': " ' + gObj.name + ' "');
		}
	},

	/**
	 * Add debug info
	 */
	addDebugInfo : function(info) {
		this.debugInfo[this.debugInfo.length] = info;
	},

	/**
	 * Show debug info
	 */
	showDebugInfo : function() {
		if (this.debug) {
			var debugInfo = '';

			this.addDebugInfo(this.debugMessages.SEPARATOR);

			this.debugInfo.each(function(info) {
				debugInfo = debugInfo + info + '\n';
			});

			alert(debugInfo);
		}
	}
}

/**
 * FormElement class
 */
var FormElement = Class.create();

FormElement.prototype = {
	elementObj : {},
	tagName : undefined,
	id : undefined,
	name : undefined,
	type : undefined,
	value : undefined,
	title : undefined,
	selectedIndex : undefined,
	group : false,

	empty : false,

	required : false,
	email : false,
	numeric : false,
	notNull : false,

	errorObjName : undefined,
	errorObj : false,
	errorMessagePrefix : undefined,
	errorMessagePostfix : undefined,
	errorMessage : undefined,

	func : [],

	/**
	 * FormElement constructor
	 */
	initialize : function(obj) {
		this.elementObj = obj;

		loadFormValidatorOptions(this);

		this.getParams();

		this['get' + this.tagName.capitalize() + 'Params']();

		this.errorObjName = this.errorObjParams.prefix + this.name.sub(/\[/, '_').sub(/\]/, '_') + this.errorObjParams.postfix;
		this.errorObj = $(this.errorObjName);

		if (typeof(this.errorMessagePrefix) != 'string') {
			this.errorMessagePrefix = '';
		}
		if (typeof(this.errorMessagePostfix) != 'string') {
			this.errorMessagePostfix = '';
		}
		this.errorMessage = this.errorMessagePrefix + this.title + this.errorMessagePostfix;

		this.func = new Array();

		this.clearError();
	},

	/**
	 * Get general parameters
	 */
	getParams : function () {
		this.tagName = this.elementObj.tagName.toLowerCase();
		this.id = this.elementObj.id;
		this.name = this.elementObj.name;
		this.type = this.elementObj.type;
		this.value = this.elementObj.value;
		this.title = this.elementObj.title;

		if (this.elementObj.className.include(this.classNames.required)) {
			this.required = true;
		}
		if (this.elementObj.className.include(this.classNames.email)) {
			this.email = true;
		}
		if (this.elementObj.className.include(this.classNames.numeric)) {
			this.numeric = true;
		}
		if (this.elementObj.className.include(this.classNames.notNull)) {
			this.notNull = true;
		}
	},

	/**
	 * Get input parameters
	 */
	getInputParams : function() {
		switch(this.type) {
			case 'text':
			case 'password':
			case 'file':
				if (this.value.strip().empty()) {
					this.empty = true;
				}
				break;
			case 'checkbox':
			case 'radio':
				if (!this.elementObj.checked) {
					this.empty = true;
				}
				break;
		}
	},

	/**
	 * Get textarea parameters
	 */
	getTextareaParams : function() {
		if (this.value.strip().empty()) {
			this.empty = true;
		}
	},

	/**
	 * Get select parameters
	 */
	getSelectParams : function() {
		this.selectedIndex = this.elementObj.selectedIndex;

		if (this.selectedIndex == -1) {
			this.empty = true;
		}
	},

	/**
	 * Show error message
	 */
	showError : function(customErrorMessage) {
		if (this.errorObj && this.errorObjParams.showMessage) {
			if (typeof(customErrorMessage) == 'undefined') {
				this.errorObj.innerHTML = this.errorMessage;
			} else {
				this.errorObj.innerHTML = customErrorMessage;
			}
		}
	},

	/**
	 * Clear error message
	 */
	clearError : function() {
		if (this.errorObj && this.errorObjParams.showMessage) {
			this.errorObj.innerHTML = '<!-- -->';
		}
	}
}