/** 
* Copyright 2005-2008 massimocorner.com
* @author      Massimo Foti (massimo@massimocorner.com)
* @version     2.0.1, 2008-07-29
* @require     tmt_core.js
* @require     tmt_form.js
*/

if(typeof(tmt) == "undefined"){
	alert("Error: tmt.core JavaScript library missing");
}

if(typeof(tmt.form) == "undefined"){
	alert("Error: tmt.form JavaScript library missing");
}

tmt.validator = {};
tmt.validator.DEFAULT_DATE_PATTERN = "YYYY-MM-DD";
tmt.validator.DEFAULT_CALLBACK = "tmt.validator.defaultCallback";
tmt.validator.DEFAULT_CALLBACK_MULTISECTION = "tmt.validator.multiSectionDefaultCallback";

/**
* Initialize the library
*/
tmt.validator.init = function(){
	var formNodes = tmt.filterNodesByAttributeValue("tmt:validate", "true", document.getElementsByTagName("form"));
	for(var i=0; i<formNodes.length; i++){
		formNodes[i].tmt_validator = true;
		var fields = formNodes[i].elements;
		for(var x = 0; x < fields.length; x++){
			if(fields[x].getAttribute("tmt:filters")){
				// Call the filters on the onkeyup and onblur events
				tmt.addEvent(fields[x], "keyup", function(){tmt.validator.filterField(this);});
				tmt.addEvent(fields[x], "blur", function(){tmt.validator.filterField(this);});
			}
		}
		// Set the form node's onsubmit event 
		// We use a gigantic hack to preserve exiting calls attached to the onsubmit event (most likely validation routines)
		if(typeof formNodes[i].onsubmit != "function"){
			formNodes[i].onsubmit = function(){
				return tmt.validator.validateForm(this);
			}
		}
		else{
			// Store a reference to the old function
			formNodes[i].tmt_oldSubmit = formNodes[i].onsubmit;
			formNodes[i].onsubmit = function(){
				// If the existing function return true, validate the form
				if(this.tmt_oldSubmit()){
					return tmt.validator.validateForm(this);
				}
				return false;
			}
		}
	}
}

/**
* Validate a form 
* Accepts either an id (string) or a DOM node reference
*/
tmt.validator.validateForm = function(form){
	var formNode = tmt.get(form);
	formNode.tmt_validator = true;
	var formValidator = tmt.validator.formValidatorFactory(formNode);
	var activeValidators = tmt.validator.executeValidators(formValidator.validators);
	// Forward errors to the callback
	eval(formValidator.callback + "(formNode, activeValidators)");	
	if(activeValidators.length == 0){
		// Everything is fine, disable form submission to avoid multiple submits
		formValidator.blockSubmit();
	}
	return activeValidators.length == 0; 
}

/**
* Validate an array of form fields
* Array's elements can contain either an id (string) or a DOM node reference
* Second argument is an optional callback
* Returns true if no field contains errors, false otherwise
*/
tmt.validator.validateFields = function(fieldsArray, callback){
	if(fieldsArray.length == 0){
		return true;
	}
	// If no callback, use form's callback
	if(!callback){
		callback = tmt.validator.getCallback(tmt.get(fieldsArray[0]).form);
	}
	// Get the form node out of the first field
	var formNode = tmt.get(fieldsArray[0]).form;
	var validators = [];
	for(var i = 0; i < fieldsArray.length; i++){
		var fieldNode = tmt.get(fieldsArray[i]);
		if(tmt.form.isFormField(fieldNode)){
			validators.push(tmt.validator.fieldValidatorFactory(fieldNode));
		}
	}
	// Store all the field validators that contains errors
	var activeValidators = tmt.validator.executeValidators(validators);
	// Forward errors to the callback
	eval(callback + "(formNode, activeValidators)");
	return activeValidators.length == 0;
}

/**
* Validate all the fields contained inside a given start node
* Start node can be either an id (string) or a DOM node reference
* Second argument is an optional callback
* Returns true if no field contains errors, false otherwise
*/
tmt.validator.validateChildFields = function(startNode, callback){
	var fieldsArray = tmt.form.getChildFields(startNode);
	return tmt.validator.validateFields(fieldsArray, callback);
}

/**
* Validate a form field
* Accepts either an id (string) or a DOM node reference and an optional callback
* Returns true if the field contains no errors, false otherwise
*/
tmt.validator.validateField = function(field, callback){
	var fieldNode = tmt.get(field);
	if(!tmt.form.isFormField(fieldNode)){
		return false;
	}
	if(!callback){
		callback = "tmt.validator.defaultFieldCallback";
	}
	var fieldType = fieldNode.type.toLowerCase();
	// Skip fieldsets
	if(fieldNode.tagName.toLowerCase() == "fieldset"){
		return;
	}
	var validator = tmt.validator.fieldValidatorFactory(fieldNode);
	var haveError = validator.validate();
	
	if(haveError){
		eval(callback + "(fieldNode, validator)");
	}
	else{
		eval(callback + "(fieldNode, null)");
	}
	return haveError;
}

// Execute multiple validators. Returns an array of validators containing errors
// Returns and empty array if no errors
tmt.validator.executeValidators = function(validators){
	var validatedFields = {};
	// Store all the field validators that contains errors
	var activeValidators = [];
	// Validate all the fields
	for(var i=0; i<validators.length; i++){
		if(validatedFields[validators[i].name]){
			// Already validated checkbox or radio, skip it
			continue;
		}
		if(validators[i].validate()){
			activeValidators[activeValidators.length] = validators[i];
		}
		// Keep track of validated fields
		validatedFields[validators[i].name] = true;
	}
	return activeValidators;
}

/* Object factories */

// Create a form validator
tmt.validator.formValidatorFactory = function(formNode){
	var obj = {};
	// Store all the field validators inside an array
	obj.validators = [];
	obj.callback = tmt.validator.getCallback(formNode);
	for(var i = 0; i < formNode.elements.length; i++){
		if(tmt.form.isFormField(formNode.elements[i])){
			obj.validators.push(tmt.validator.fieldValidatorFactory(formNode.elements[i]));
		}
	}
	// Retrieve all the submit buttons
	obj.buttons = tmt.form.getSubmitNodes(formNode);
	// Define a method that can block multiple submits
	obj.blockSubmit = function(){
		// Check to see if have to disable submit buttons
		if(!formNode.getAttribute("tmt:blocksubmit") && !(formNode.getAttribute("tmt:blocksubmit") == "false")){
			// Disable each submit button
			for(var i=0; i<obj.buttons.length; i++){
				if(obj.buttons[i].getAttribute("tmt:waitmessage")){
					obj.buttons[i].value = obj.buttons[i].getAttribute("tmt:waitmessage");
				}
				obj.buttons[i].disabled = true;
			}
		}
	}
	
	return obj;
}

// Generate a field validator
tmt.validator.fieldValidatorFactory = function(fieldNode){
	var fieldType = fieldNode.type.toLowerCase();
	var validator = {};
	// Skip fieldsets
	if(fieldNode.tagName.toLowerCase() == "fieldset"){
		return validator;
	}
	// Handle different kind of fields
	switch(fieldType){
		case "select-multiple":
			validator = tmt.validator.selectValidatorFactory(fieldNode);
			break;
		case "select-one":
			validator = tmt.validator.selectValidatorFactory(fieldNode);
			break;
		case "radio":
			validator = tmt.validator.radioValidatorFactory(tmt.form.getFieldGroup(fieldNode));
			break;
		case "checkbox":
			validator = tmt.validator.boxValidatorFactory(tmt.form.getFieldGroup(fieldNode));
			break;
		// Skip reset
		case "reset":
			return validator;
			break;
		// Skip buttons
		case "button":
			return validator;
			break;
		// default handles all the text fields
		default:
			validator = tmt.validator.textValidatorFactory(fieldNode);
			break;
	}
	return validator;
}

// Create an abstract field validator, to be extended/decorated for specific needs
tmt.validator.abstractValidatorFactory = function(fieldNode){
	var obj = {};
	obj.message = "";
	obj.name = "";
	if(fieldNode.name){
		obj.name = fieldNode.name;
	}
	else if(fieldNode.id){
		obj.name = fieldNode.id;
	}
	obj.errorClass = "";
	if(fieldNode.getAttribute("tmt:message")){
		obj.message = fieldNode.getAttribute("tmt:message");
	}
	if(fieldNode.getAttribute("tmt:errorclass")){
		obj.errorClass = fieldNode.getAttribute("tmt:errorclass");
	}
	obj.flagInvalid = function(){
		// Append the CSS class to the existing one
		if(obj.errorClass){
			tmt.addClass(fieldNode, obj.errorClass);
		}
		// Set the title attribute in order to show a tootip
		fieldNode.setAttribute("title", obj.message);
	}
	obj.flagValid = function(){
		// Remove the CSS class
		if(obj.errorClass){
			tmt.removeClass(fieldNode, obj.errorClass);
		}
		fieldNode.removeAttribute("title");
	}
	obj.validate = function(){
		// If the field contains error, flag it as invalid and return false
		// Be careful, this method contains multiple exit points!!!
		if(fieldNode.disabled){
			// Disabled fields are always valid
			obj.flagValid();
			return false;
		}
		if(!obj.isValid()){
			obj.flagInvalid();
			return true;
		}
		else{
			obj.flagValid();
			return false;
		}
	}
	
	return obj;
}

// Create a validator for text and texarea fields
tmt.validator.textValidatorFactory = function(fieldNode){
	var obj = tmt.validator.abstractValidatorFactory(fieldNode);
	obj.type = "text";
	// Put focus and cursor inside the field
	obj.getFocus = function(){
		// This try block is required to solve an obscure issue with IE and hidden fields
		try{
			fieldNode.focus();
			fieldNode.select();
		}
		catch(exception){
		}
	}
	// Check if the field is empty
	obj.isEmpty = function(){
		return fieldNode.value == "";
	}
	// Check if the field is required
	obj.isRequired = function(){
		var requiredAtt = fieldNode.getAttribute("tmt:required");
		if(requiredAtt){
			// Plain vanilla validation, true/false
			if((requiredAtt == "true") || (requiredAtt == "false")){
				return eval(requiredAtt);
			}
			// It's a conditional validation. Invoke the relevant function
			return(eval(requiredAtt + "(fieldNode)"));
		}
		return false;
	}
	// Check if the field satisfy the rules associated with it
	// Be careful, this method contains multiple exit points!!!
	obj.isValid = function(){
		if(obj.isEmpty()){
			if(obj.isRequired()){
				return false;
			}
			else{
				return true;
			}
		}
		else{
			// It's empty. Loop over all the available rules
			for(var rule in tmt.validator.rules){
				// Check if the current rule is required for the field
				if(fieldNode.getAttribute("tmt:" + rule)){
					// Invoke the rule
					if(!eval("tmt.validator.rules." + rule + "(fieldNode)")){
						return false;
					}
				}
			}
		}
		return true;
	}
	
	return obj;
}

// Create a validator for <select> fields
tmt.validator.selectValidatorFactory = function(selectNode){
	var obj = tmt.validator.abstractValidatorFactory(selectNode);
	obj.type = "select";
	var invalidIndex;
	if(selectNode.getAttribute("tmt:invalidindex")){
		invalidIndex = selectNode.getAttribute("tmt:invalidindex");
	}
	var invalidValue;
	if(selectNode.getAttribute("tmt:invalidvalue") != null){
		invalidValue = selectNode.getAttribute("tmt:invalidvalue");
	}
	// Check if the field satisfy the rules associated with it
	// Be careful, this method contains multiple exit points!!!
	obj.isValid = function(){
		// Whenever a "size" attribute is available, the browser reports -1 as selectedIndex
		// Fix this weirdness
		if(selectNode.selectedIndex == -1){
			selectNode.selectedIndex = 0;
		}
		// Check for index
		if(selectNode.selectedIndex == invalidIndex){
			return false;
		}
		// Check for value
		if(selectNode.value == invalidValue){
			return false;
		}
		// Loop over all the available rules
		for(var rule in tmt.validator.rules){
			// Check if the current rule is required for the field
			if(selectNode.getAttribute("tmt:" + rule)){
				// Invoke the rule
				if(!eval("tmt.validator.rules." + rule + "(selectNode)")){
					return false;
				}
			}
		}
		return true;
	}
	
	return obj;
}

// Create generic validator for grouped fields (radio and checkboxes)
tmt.validator.groupValidatorFactory = function(buttonGroup){
	var obj = {};
	obj.name = buttonGroup[0].name;
	obj.message = "";
	obj.errorClass = "";
	// Since fields from the same group can have conflicting attribute values, the last one win
	for(var i=0; i<buttonGroup.length; i++){
		if(buttonGroup[i].getAttribute("tmt:message")){
			obj.message = buttonGroup[i].getAttribute("tmt:message");
		}
		if(buttonGroup[i].getAttribute("tmt:errorclass")){
			obj.errorClass = buttonGroup[i].getAttribute("tmt:errorclass");
		}
	}
	obj.flagInvalid = function(){
		// Append the CSS class to the existing one
		if(obj.errorClass){
			for(var i=0; i<buttonGroup.length; i++){
				tmt.addClass(buttonGroup[i], obj.errorClass);
				buttonGroup[i].setAttribute("title", obj.message);
			}
		}
	}
	obj.flagValid = function(){
		// Remove the CSS class
		if(obj.errorClass){
			for(var i=0; i<buttonGroup.length; i++){
				tmt.removeClass(buttonGroup[i], obj.errorClass);
				buttonGroup[i].removeAttribute("title");
			}
		}
	}
	obj.validate = function(){
		//var errorMsg = "";
		// If the field group contains er