/** 
* Copyright 2005 massimocorner.com
* @author      Massimo Foti (massimo@massimocorner.com)
* @version     0.5.0, 2005-04-28
 */

// Create all the validator objects required inside the document
function tmt_validatorInit(){
	var formNodes = document.getElementsByTagName("form");
	for(var i=0; i<formNodes.length; i++){
		if(formNodes[i].getAttribute("tmt:validate") == "true"){
			// Attach a validator object to each form that requires it
			formNodes[i].tmt_validator = new tmt_formValidator(formNodes[i]);
			// Set the form node's onsubmit event 
			formNodes[i].onsubmit = function(){
				return tmt_validateForm(this);
			}
		}
	}
}

// Perform the validation
function tmt_validateForm(formNode){
	var errorMsg = "";
	var formValidator = formNode.tmt_validator;
	// Be sure the form contains a validator object
	if(formValidator){
		// Validate all the fields
		for(var i=0; i<formValidator.validators.length; i++){
			if(formValidator.validators[i].validate()){
				errorMsg += formValidator.validators[i].getMessage() + "\n";
			}
		}
		if(errorMsg != ""){
			// We have errors, display them
			alert(errorMsg)
		}
		else{
			// Everything is fine, disable form submission to avoid multiple submits
			formValidator.blockSubmit();
		}
	}
	return errorMsg.length == 0; 
}

/* Object constructors */

// Form validator
function tmt_formValidator(formNode){
	// Store all the validator objects inside an array
	this.validators = new Array();
	var fieldsArray = tmt_getFieldNodes(formNode);
	for(var i=0; i<fieldsArray.length; i++){
		// Create a validator for each text field
		this.validators[this.validators.length] = new tmt_fieldValidator(fieldsArray[i]);
		// Set the onchange event for each image upload validation
		if((fieldsArray[i].getAttribute("type").toLowerCase() == "file")
			&&
			(fieldsArray[i].getAttribute("tmt:image") == "true"))
		{
			fieldsArray[i].onchange = function(){
				tmt_validateImg(this);
			}
		}
		if(fieldsArray[i].getAttribute("tmt:filters")){
			// Call the filter on the onkeyup event
			fieldsArray[i].onkeyup = function(){
				tmt_filterField(this);
			}
		}
	}
	var selectNodes = formNode.getElementsByTagName("select");
	for(var j=0; j<selectNodes.length; j++){
		// Create a validator for each select element
		this.validators[this.validators.length] =  new tmt_selectValidator(selectNodes[j]);
	}
	var boxTable = tmt_getBoxNodes(formNode);
	for(var boxName in boxTable){
		// Create a validator for each group of checkboxes
		this.validators[this.validators.length] = new tmt_boxValidator(boxTable[boxName]);
	}
	// Store all the submit buttons
	this.buttons = tmt_getSubmitNodes(formNode);
	// Define a method that can block multiple submits
	this.blockSubmit = function(){
		// Disable each submit button
		for(var i=0;i<this.buttons.length;i++){
			if(this.buttons[i].getAttribute("tmt:waitmessage")){
				this.buttons[i].value = this.buttons[i].getAttribute("tmt:waitmessage");
			}
			this.buttons[i].disabled = true;
		}
	}
}

// Select validator
function tmt_selectValidator(selectNode){
	var message = "";
	if(selectNode.getAttribute("tmt:message")){
		message = selectNode.getAttribute("tmt:message");
	}
	var errorClass = "";
	if(selectNode.getAttribute("tmt:errorclass")){
		errorClass = selectNode.getAttribute("tmt:errorclass");
	}
	var invalidIndex;
	if(selectNode.getAttribute("tmt:invalidindex")){
		invalidIndex = selectNode.getAttribute("tmt:invalidindex");
	}
	var invalidValue;
	if(selectNode.getAttribute("tmt:invalidvalue")){
		invalidValue = selectNode.getAttribute("tmt:invalidvalue");
	}	
	this.flagInvalid = function(){
		// Append the CSS class to the existing one
		if(errorClass){
			// Flag only if it's not already flagged
			if(selectNode.className.indexOf(errorClass) == -1){
				selectNode.className = selectNode.className + " " + errorClass;
			}
		}
	}
	this.flagValid = function(){
		// Remove the CSS class
		if(errorClass){
			selectNode.className = selectNode.className.replace(" " + errorClass, "");
		}
	}
	// Return the field's error message
	this.getMessage = function(){
		return message;
	}
	// Check if the select validate
	this.isValid = function(){
		// Check for value
		if(selectNode.value == invalidValue){
			return false;
		}
		// Check for index
		if(selectNode.selectedIndex == invalidIndex){
			return false;
		}		
		return true;
	}	
	this.validate = function(){
		// If the field contains error, flag it as invalid and return the error message
		if(!this.isValid()){
			this.flagInvalid();
			return true;
		}
		else{
			this.flagValid();
			return false;
		}
	}	
}

// Checkbox validator (one for each group of boxes sharing the same name)
function tmt_boxValidator(boxGroup){
	var minchecked = 0;
	var maxchecked = boxGroup.boxes.length;
	var message = "";
	var errorClass = "";
	// Since checkboxes from the same group can have conflicting attribute values, the last one win
	for(var i=0;i<boxGroup.boxes.length;i++){
		if(boxGroup.boxes[i].getAttribute("tmt:minchecked")){
			minchecked = boxGroup.boxes[i].getAttribute("tmt:minchecked");
		}
		if(boxGroup.boxes[i].getAttribute("tmt:maxchecked")){
			maxchecked = boxGroup.boxes[i].getAttribute("tmt:maxchecked");
		}
		if(boxGroup.boxes[i].getAttribute("tmt:message")){
			message = boxGroup.boxes[i].getAttribute("tmt:message");
		}
		if(boxGroup.boxes[i].getAttribute("tmt:errorclass")){
			errorClass = boxGroup.boxes[i].getAttribute("tmt:errorclass");
		}
	}
	this.flagInvalid = function(){
		// Append the CSS class to the existing one
		if(errorClass){
			for(var i=0;i<boxGroup.boxes.length;i++){
				// Flag only if it's not already flagged
				if(boxGroup.boxes[i].className.indexOf(errorClass) == -1){
					boxGroup.boxes[i].className = boxGroup.boxes[i].className + " " + errorClass;
				}
			}
		}
	}
	this.flagValid = function(){
		// Remove the CSS class
		if(errorClass){
			for(var i=0;i<boxGroup.boxes.length;i++){
				boxGroup.boxes[i].className = boxGroup.boxes[i].className.replace(" " + errorClass, "");
			}
		}
	}
	// Return the field's error message
	this.getMessage = function(){
		return message;
	}
	// Check if the boxes validate
	this.isValid = function(){
		var checkCounter = 0;
		for(var i=0;i<boxGroup.boxes.length;i++){
		    // For each checked box, increase the counter
			if(boxGroup.boxes[i].checked){
				checkCounter++;
			}
		}
		return (checkCounter >=  minchecked) && (checkCounter <= maxchecked);
	}
	this.validate = function(){
		var errorMsg = "";
		// If the checkboxes group contains error, flag it as invalid and return the error message
		if(!this.isValid()){
			errorMsg += this.getMessage();
			this.flagInvalid();
		}
		else{
			this.flagValid();
		}	
		return errorMsg;
	}
}

// Field validator
function tmt_fieldValidator(fieldNode){
	var required = false;
	if(fieldNode.getAttribute("tmt:required") == "true"){
		required = true;
	}
	var message = "";
	if(fieldNode.getAttribute("tmt:message")){
		message = fieldNode.getAttribute("tmt:message");
	}
	var errorClass = "";
	if(fieldNode.getAttribute("tmt:errorclass")){
		errorClass = fieldNode.getAttribute("tmt:errorclass");
	}
	this.flagInvalid = function(){
		// Append the CSS class to the existing one
		if(errorClass){
			// Flag only if it's not already flagged
			if(fieldNode.className.indexOf(errorClass) == -1){
				fieldNode.className = fieldNode.className + " " + errorClass;
			}
		}
		// Put focus and cursor inside the first field
		fieldNode.focus();
		fieldNode.select();
	}
	this.flagValid = function(){
		// Remove the CSS class
		if(errorClass){
			fieldNode.className = fieldNode.className.replace(" " + errorClass, "");
		}
	}
	// Return the field's error message
	this.getMessage = function(){
		return message;
	}
	// Check if the field is empty
	this.isEmpty = function(){
		return fieldNode.value == "";
	}
	// Check if the field is required
	this.isRequired = function(){
		return required;
	}
	// Check if the field satisfy the rules associated with it
	this.isValid = function(){
		// Be careful, this method contains multiple exit points!!!
		if(this.isEmpty()){
			if(this.isRequired()){
				return false;
			}
			else{
				return true;
			}
		}
		else{
			// Loop over all the available rules
			for(var rule in tmt_globalRules){
				// Check if the current rule is required for the field
				if(fieldNode.getAttribute("tmt:" + rule)){
					// Invoke the rule
					if(!eval("tmt_globalRules." + rule + "(fieldNode)")){
						return false;
					}
				}
			}
		}
		return true;
	}
	this.validate = function(){
		// If the field contains error, flag it as invalid and return the error message
		if(!this.isValid()){
			this.flagInvalid();
			return true;
		}
		else{
			this.flagValid();
			return false;
		}
	}
}

// This global objects store all the validation routines as methods
// Every rule is stored as a method that accepts the field node as only argument and return a boolean
var tmt_globalRules = new Object;
tmt_globalRules.maxlength = function(fieldNode){
	if(fieldNode.value.length > fieldNode.getAttribute("tmt:maxlength")){
		return false;
	}
	return true;
}
tmt_globalRules.minlength = function(fieldNode){
	if(fieldNode.value.length < fieldNode.getAttribute("tmt:minlength")){
		return false;
	}
	return true;
}
tmt_globalRules.minnumber = function(fieldNode){
	if(parseFloat(fieldNode.value) < fieldNode.getAttribute("tmt:minnumber")){
		return false;
	}
	return true;
}
tmt_globalRules.maxnumber = function(fieldNode){
	if(parseFloat(fieldNode.value) > fieldNode.getAttribute("tmt:maxnumber")){
		return false;
	}
	return true;
}
tmt_globalRules.pattern = function(fieldNode){
	var reg = tmt_globalPatterns[fieldNode.getAttribute("tmt:pattern")];
	return reg.test(fieldNode.value);
}
tmt_globalRules.datepattern = function(fieldNode){
	var globalObj = tmt_globalDatePatterns[fieldNode.getAttribute("tmt:datepattern")];
	var dateBits = fieldNode.value.split(/[\/\-\.]/);
	// First try to create a new date out of the bits
	var testDate = new Date(dateBits[globalObj.y], (dateBits[globalObj.m]-1), dateBits[globalObj.d]);
	// Make sure values match after conversion
	var isDate = (testDate.getFullYear() == dateBits[globalObj.y])
			 && (testDate.getMonth() == dateBits[globalObj.m]-1)
			 && (testDate.getDate() == dateBits[globalObj.d]);
	// If it's a date and it matches the RegExp, it's a go
	return isDate && globalObj.rex.test(fieldNode.value);
}
tmt_globalRules.equalsto = function(fieldNode){
	var twinNode = document.getElementById(fieldNode.getAttribute("tmt:equalsto"));
	return twinNode.value == fieldNode.value;
}

// This global objects store all the RegExp patterns for strings
var tmt_globalPatterns = new Object;
tmt_globalPatterns.email = new RegExp("^[\\w\\.=-]+@[\\w\.-]+\\.[\\w\\.-]{2,4}$");
tmt_globalPatterns.lettersonly = new RegExp("^[a-zA-Z]*$");
tmt_globalPatterns.alphanumeric = new RegExp("^\\w*$");
tmt_globalPatterns.integer = new RegExp("^-?\\d\\d*$");
tmt_globalPatterns.positiveinteger = new RegExp("^\\d\\d*$");
tmt_globalPatterns.number = new RegExp("^-?(\\d\\d*\\.\\d*$)|(^-?\\d\\d*$)|(^-?\\.\\d\\d*$)");
tmt_globalPatterns.filepath_pdf = new RegExp("\\\\[\\w_]*\\.(pdf)$");
tmt_globalPatterns.filepath_jpg_gif = new RegExp("\\\\[\\w_]*\\.(gif|jpg)$");
tmt_globalPatterns.filepath_jpg = new RegExp("\\\\[\\w_]*\\.(jpg)$");
tmt_globalPatterns.filepath_zip = new RegExp("\\\\[\\w_]*\\.(jpg)$");
tmt_globalPatterns.filepath = new RegExp("\\\\[\\w_]*\\.\\w{3}$");

// This global objects store all the info required for date validation
var tmt_globalDatePatterns = new Object;
tmt_globalDatePatterns["YYYY-MM-DD"] = tmt_dateInfo("^\([0-9]{4}\)\\-\([0-2][0-9]\)\\-\([0-3][0-9]\)$", 0, 1, 2);
tmt_globalDatePatterns["YYYY-M-D"] = tmt_dateInfo("^\([0-9]{4}\)\\-\([0-2]?[0-9]\)\\-\([0-3]?[0-9]\)$", 0, 1, 2);
tmt_globalDatePatterns["MM.DD.YYYY"] = tmt_dateInfo("^\([0-2][0-9]\)\\.\([0-3][0-9]\)\\.\([0-9]{4}\)$", 2, 0, 1);
tmt_globalDatePatterns["M.D.YYYY"] = tmt_dateInfo("^\([0-2]?[0-9]\)\\.\([0-3]?[0-9]\)\\.\([0-9]{4}\)$", 2, 0, 1);
tmt_globalDatePatterns["MM/DD/YYYY"] = tmt_dateInfo("^\([0-2][0-9]\)\/\([0-3][0-9]\)\/\([0-9]{4}\)$", 2, 0, 1);
tmt_globalDatePatterns["M/D/YYYY"] = tmt_dateInfo("^\([0-2]?[0-9]\)\/\([0-3]?[0-9]\)\/\([0-9]{4}\)$", 2, 0, 1);
tmt_globalDatePatterns["MM-DD-YYYY"] = tmt_dateInfo("^\([0-2][0-9]\)\\-\([0-3][0-9]\)\\-\([0-9]{4}\)$", 2, 0, 1);
tmt_globalDatePatterns["M-D-YYYY"] = tmt_dateInfo("^\([0-2]?[0-9]\)\\-\([0-3]?[0-9]\)\\-\([0-9]{4}\)$", 2, 0, 1);
tmt_globalDatePatterns["DD.MM.YYYY"] = tmt_dateInfo("^\([0-3][0-9]\)\\.\([0-2][0-9]\)\\.\([0-9]{4}\)$", 2, 1, 0);
tmt_globalDatePatterns["D.M.YYYY"] = tmt_dateInfo("^\([0-3]?[0-9]\)\\.\([0-2]?[0-9]\)\\.\([0-9]{4}\)$", 2, 1, 0);
tmt_globalDatePatterns["DD/MM/YYYY"] = tmt_dateInfo("^\([0-3][0-9]\)\/\([0-2][0-9]\)\/\([0-9]{4}\)$", 2, 1, 0);
tmt_globalDatePatterns["D/M/YYYY"] = tmt_dateInfo("^\([0-3]?[0-9]\)\/\([0-2]?[0-9]\)\/\([0-9]{4}\)$", 2, 1, 0);
tmt_globalDatePatterns["DD-MM-YYYY"] = tmt_dateInfo("^\([0-3][0-9]\)\\-\([0-2][0-9]\)\\-\([0-9]{4}\)$", 2, 1, 0);
tmt_globalDatePatterns["D-M-YYYY"] = tmt_dateInfo("^\([0-3]?[0-9]\)\\-\([0-2]?[0-9]\)\\-\([0-9]{4}\)$", 2, 1, 0);

/* Helper functions */

// Create an object that stores info required for date validation
function tmt_dateInfo(rex, y, m, d){
	var infoObj = new Object;
	infoObj.rex = new RegExp(rex);
	infoObj.y = y;
	infoObj.m = m;
	infoObj.d = d;
	return infoObj;
}

// Get an array of submit button nodes contained inside a given node
function tmt_getSubmitNodes(startNode){
	var submitArray = new Array();
	var inputNodes = startNode.getElementsByTagName("input");
	// Get an array of submit nodes
	for(var i=0; i<inputNodes.length; i++){
		if(inputNodes[i].getAttribute("type").toLowerCase() == "submit"){
			submitArray[submitArray.length] = inputNodes[i];
		}
	}
	return submitArray;
}

// Get an array of input and textarea nodes contained inside a given node
function tmt_getFieldNodes(startNode){
	var inputsArray = new Array();
	var inputNodes = startNode.getElementsByTagName("input");
	var areaNodes = startNode.getElementsByTagName("textarea");
	// Get an array of text, password and file nodes
	for(var i=0; i<inputNodes.length; i++){
		var fieldType = inputNodes[i].getAttribute("type").toLowerCase();
		if((fieldType == "text") || (fieldType == "password") || (fieldType == "file") || (fieldType == "hidden")){
			inputsArray[inputsArray.length] = inputNodes[i];
		}
	}
	// Append textarea nodes too
	for(var j=0; j<areaNodes.length; j++){
	    inputsArray[inputsArray.length] = areaNodes[j];
	}
	return inputsArray;
}

// Return an object (sort of an hashtable) containing checkboxes data
function tmt_getBoxNodes(formNode){
	// This object will store data about checkboxes, just as an hash table
	var boxHolder = new Object;
	var boxNodes = formNode.getElementsByTagName("input");
	for(var i=0; i<boxNodes.length; i++){
		if(boxNodes[i].getAttribute("type").toLowerCase() == "checkbox"){
			// Store the reference to make it easier to read the code
			var boxName = boxNodes[i].name;
			if(boxHolder[boxName]){
				// We already have an entry with the same name
				// Append the DOM node to the relevant entry in the hash table
				boxHolder[boxName].boxes[boxHolder[boxName].boxes.length] = boxNodes[i];
			}
			else{
				// Create an entry inside the hash table
				boxHolder[boxName] = new Object;
				boxHolder[boxName].name = boxName;
				// Initialize the array that will store all the DOM nodes that share the same name
				boxHolder[boxName].boxes = new Array;
				boxHolder[boxName].boxes[0] = boxNodes[i];
			}
		}
	}
	return boxHolder;
}

/* Image upload validation */

tmt_globalRules.image = function(fieldNode){
	// If the flag isn't defined we assume things are fine
	if(!fieldNode.isValidImg){
		fieldNode.isValidImg = "true";
	}
	return fieldNode.isValidImg == "true";
}

// Check the currently selected image and set a validity flag
function tmt_validateImg(fieldNode){
	var imgURL = "file:///" + fieldNode.value;
	var img = new Image();
	img.maxSize =  fieldNode.getAttribute("tmt:imagemaxsize");
	img.maxWidth = fieldNode.getAttribute("tmt:imagemaxwidth");
	img.minWidth = fieldNode.getAttribute("tmt:imageminwidth");
	img.maxHeight = fieldNode.getAttribute("tmt:imagemaxheight");
	img.minHeight = fieldNode.getAttribute("tmt:imageminheight");
	// Store a reference to the input field
	img.fieldNode = fieldNode;
	// The image's data can be read only after loading. That's why we need a callback
	img.onload = tmt_validateImgCallback;
	img.src = imgURL;
}

function tmt_validateImgCallback(){
	var errorsCount = 0;
	// Check every constrain and increment the error counter accordingly
	if(this.fileSize && this.maxSize && (this.fileSize/1024) > this.maxSize){
		errorsCount ++;
	}
	if(this.maxWidth && (this.width > this.maxWidth)){
		errorsCount ++;
	}
	if(this.minWidth && (this.width < this.minWidth)){
		errorsCount ++;
	}
	if(this.maxHeight && (this.height > this.maxHeight)){
		errorsCount ++;
	}
	if(this.minHeight && (this.height < this.minHeight)){
		errorsCount ++;
	}
	// Store the valid flag inside the DOM node itself
	this.fieldNode.isValidImg = (errorsCount != 0) ? "false" : "true";
}

/* Filters */

// This global objects store all the info required for filters
var tmt_globalFilters = new Object;

tmt_globalFilters.ltrim = tmt_filterInfo("^(\\s*)(\\b[\\w\\W]*)$", "$2");
tmt_globalFilters.rtrim = tmt_filterInfo("^([\\w\\W]*)(\\b\\s*)$", "$1");

tmt_globalFilters.nospaces = tmt_filterInfo("\\s*", "");
tmt_globalFilters.nocommas = tmt_filterInfo(",", "");
tmt_globalFilters.nodots = tmt_filterInfo("\\.", "");
tmt_globalFilters.noquotes = tmt_filterInfo("'", "");
tmt_globalFilters.nodoublequotes = tmt_filterInfo('"', "");
tmt_globalFilters.nohtml = tmt_filterInfo("<[^>]*>", "");

tmt_globalFilters.alphanumericonly = tmt_filterInfo("[^\\w]", "");
tmt_globalFilters.numbersonly = tmt_filterInfo("[^\\d]", "");
tmt_globalFilters.lettersonly = tmt_filterInfo("[^a-zA-Z]", "");

tmt_globalFilters.commastodots = tmt_filterInfo(",", ".");
tmt_globalFilters.dotstocommas = tmt_filterInfo("\\.", ",");


// Create an object that stores info required for filters
function tmt_filterInfo(rex, replaceStr){
	var infoObj = new Object;
	infoObj.rex = new RegExp(rex, "g");
	infoObj.str = replaceStr;
	return infoObj;
}

// Clean up the field based on filter's info
function tmt_filterField(fieldNode){
	var filtersArray = fieldNode.getAttribute("tmt:filters").split(",");
	for(var i=0; i<filtersArray.length; i++){
		var filtObj = tmt_globalFilters[filtersArray[i]];
		// Be sure we have the filter's data, then clean up
		if(filtObj){
			fieldNode.value = fieldNode.value.replace(filtObj.rex, filtObj.str);
		}
	}
}

window.onload = tmt_validatorInit;