/* FORM VALIDATOR OBJECTS */

function Requirement(requirementName, requirementGroupName) {
	//public variables
	this.name = requirementName;
	this.requirementGroupName = requirementGroupName;
	this.disabled = false;
	this.numRequired = 0;
	this.isFulfilled = false;
	//array of form elements
	this.elements = new Array();
	
	this.toString = function() {
		var str = "Object: Requirement\n";
		str += "\nName: " + this.name;
		str += "\nRequirement Group: " + this.requirementGroupName;
		str += "\nDisabled: " + this.disabled;
		str += "\nNumber of Required Elements: " + this.numRequired;
		str += "\nFulfilled: " + this.isFulfilled;
		str += "\nElements:";
		for(var i = 0; i < this.elements.length; i++)
			str +=  "\n   " + this.elements[i].name + " " + this.elements[i].id + " <" + this.elements[i].type + ">";
			
		return str;
	}
}


function RequirementGroup(requirementGroupName) {
	//public variables
	this.name = requirementGroupName;
	this.disabled = false;
	this.numRequired = 0;
	this.numFulfilled = 0;
	this.missingRequirementsString = "";
	//array of Requirement objects
	this.requirements = new Array();
	
	this.toString = function() {
		var str = "Object: Requirement Group\n";
		str += "\nName: " + this.name;
		str += "\nDisabled: " + this.disabled;
		str += "\nNumber of Required Requirements: " + this.numRequired;
		str += "\nNumber Fulfilled: " + this.numFulfilled;
		str += "\nRequirements (" + this.requirements.length + "):";
		for(var x in this.requirements) {
			var thisRequirement = this.requirements[x];
			str +=  "\n   " + thisRequirement.name + " (" + thisRequirement.elements.length + ")";
		}
		str += "\nMissing Fields:" + this.missingRequirementsString;
		return str;
	}
}


function FormValidator() {
	var fv_defaultFieldClass = "defaultFormField";
	var fv_requiredFieldClass = "requiredFormField";

	/*
		object shared among the forms which contains the necessary functions
		used for the RequirementGroups and its subclasses
	*/
	
	
	/* INSERTION FUNCTIONS */
	
	this.insertElement = function(thisElement) {
		/*
			This function inserts an element into either a form's requirementGroups or requirements array only if it is not disabled and has a requirementName.
		*/
		
		//get the requirementName
		var requirementName = getRequirementName(thisElement);
		
		//if the element is not disabled and has a requirementName, insert it
		if(!thisElement.disabled && requirementName) {
			//get the elements form
			var thisForm = thisElement.form;
			//get whether the element is required
			var elementIsRequired = (thisElement.getAttribute("required") == "true");
			//get the requirementGroupName
			var requirementGroupName = thisElement.getAttribute("requirementGroup");
			
			if(requirementGroupName) {
				//if the element is part of a requirementGroup, insert it into the form's requirementGroups array
				insertIntoRequirementsGroups(thisForm, requirementGroupName, thisElement, requirementName, elementIsRequired);
			} else {
				//if the element is not part of a requirementGroup, insert it into the form's requirements array
				insertIntoRequirements(thisForm.requirements, thisElement, requirementName, elementIsRequired);
			}
		}
	}
	
	function insertIntoRequirementsGroups(thisForm, requirementGroupName, thisElement, requirementName, elementIsRequired) {
		/*
			This function inserts an element into a form's requirementGroups array.
		*/
		
		//get the existing requirementGroup from the form's requirementGroups array by the requirementGroupName
		var thisRequirementGroup = thisForm.requirementGroups[requirementGroupName];
		
		//if there is no existing requirementGroup in the form's requirementGroups array by the requirementGroupName, create one
		if(!thisRequirementGroup) {
			thisRequirementGroup = new RequirementGroup(requirementGroupName);
			thisForm.requirementGroups[requirementGroupName] = thisRequirementGroup;
		}
		
		//insert the element into the requirementGroup's requirements array and get the number of required elements
		var numRequiredElements = insertIntoRequirements(thisRequirementGroup.requirements, thisElement, requirementName, elementIsRequired, requirementGroupName);
		
		//once the number of required elements in the requirementGroup reaches 1, increase the requirementGroup's number of required elements by 1
		if(numRequiredElements == 1)
			thisRequirementGroup.numRequired = thisRequirementGroup.numRequired + 1;
		
	}
	
	function insertIntoRequirements(requirements, thisElement, requirementName, elementIsRequired, requirementGroupName) {
		/*
			This function inserts an element into either requirementGroup's requirements array or a form's requirements array.
		*/
		
		//get the existing requirement from the requirements array by the requirementName
		var thisRequirement = requirements[requirementName];
		
		//if there is no existing requirement in the requirements array by the requirementName, create one
		if(!thisRequirement) {
			thisRequirement = new Requirement(requirementName, requirementGroupName);
			requirements[requirementName] = thisRequirement;
			//increase the length of the requirements array by 1
			requirements.length = requirements.length + 1;
		}
		
		//insert the element into the requirement's elemenents array
		thisRequirement.elements[thisRequirement.elements.length] = thisElement;
		
		//make a reference on the element to the requirement, in case the element is removed it will know which requirement to remove it from
		thisElement.requirement = thisRequirement;
		
		//if the element is required, increase the requirement's number of required elements by 1
		if(elementIsRequired)
			thisRequirement.numRequired = thisRequirement.numRequired + 1;
		
		//return the requirement's number of required elements
		return thisRequirement.numRequired;
					
	}
	
	
	/* REMOVAL FUNCTIONS */
	
	this.removeElement = function(thisElement) {
		/*
			This function removes an element from the requirement arrays
		*/
		
		var thisRequirement = thisElement.requirement;

		//if this element belongs to a requirement, remove it
		if(thisRequirement) {
			var elementIsRequired = (thisElement.getAttribute("required") == "true");

			var thisForm = thisElement.form;
			var requirementGroupName = thisRequirement.requirementGroupName;
			
			/*
				if thisRequirement belongs to a requirementGroup:
				1. set thisRequirementGroup to its parent requirementGroup
				2. set requirementsArray to the requirements array of thisRequirementGroup
				
				otherwise set requirementsArray to the form's requirements array
			*/
			if(requirementGroupName) {
				var thisRequirementGroup = thisForm.requirementGroups[requirementGroupName];
				var requirementsArray = thisRequirementGroup.requirements;
			} else {
				var requirementsArray = thisForm.requirements;
			}

			//find the element in thisRequirement's elements array and delete it
			for(var i = 0; i < thisRequirement.elements.length; i++) {
				if(thisElement == thisRequirement.elements[i]) {
					removeFromElements(thisRequirement.elements, i);
					break;
				}
			}
			
			//if thisElement is required decrease thisRequirement's numRequired elements by 1
			if(elementIsRequired) {
				thisRequirement.numRequired = thisRequirement.numRequired - 1;
				//if thisRequirement's numRequired is 0, decrease 1 from the thisRequirementGroup's numRequired
				if(thisRequirementGroup && (thisRequirement.numRequired == 0))
					thisRequirementGroup.numRequired = thisRequirementGroup.numRequired - 1;
			}

			//if there are no elements left in the requirementsArray, delete thisRequirement
			if(thisRequirement.elements.length == 0) {
				delete requirementsArray[thisRequirement.name];
				
				//decrease the length of the array by 1
				requirementsArray.length = requirementsArray.length - 1;
				
				//if there are no requirements left in the requirementsArray, delete thisRequirementGroup
				if(thisRequirementGroup && (requirementsArray.length == 0))
					delete thisForm.requirementGroups[thisRequirementGroup.name];
			}
		
			//set thisElement's requirement reference to null
			thisElement.requirement = null;
		}			
	}
	
	function removeFromElements(elements, elementNumber) {
		for(var i = elementNumber; i < (elements.length - 1); i++)
			elements[i] = elements[i+1];
		elements.length = elements.length - 1;
	}
	
	
	/* BUILDING AND MODIFICATION FUNCTIONS */
	
	this.buildRequirementArrays = function(thisForm) {
		thisForm.requirements = new Array();
		thisForm.requirementGroups = new Array();
		
		var defaultFormFieldClass = thisForm.getAttribute("defaultFieldClass");
		if(!defaultFormFieldClass) {
			thisForm.setAttribute("defaultFieldClass", fv_defaultFieldClass);
			defaultFormFieldClass = fv_defaultFieldClass;
		}
		
		var requiredFormFieldClass = thisForm.getAttribute("requiredFieldClass");
		if(!requiredFormFieldClass) {
			thisForm.setAttribute("requiredFieldClass", fv_requiredFieldClass);
			requiredFormFieldClass = fv_requiredFieldClass;
		}
		
		var strictDisabled = thisForm.getAttribute("strictDisabled");
		if(!strictDisabled) {
			thisForm.setAttribute("strictDisabled", "false");
		}
		
		for(var i=0; i < thisForm.elements.length; i++) {
			var thisElement = thisForm.elements[i];
			thisForm.validator.insertElement(thisElement);
			//alert(thisElement.name + "..." + thisElement.getAttribute("required"));
			if((thisElement.getAttribute("required") == "true") && isFieldEmpty(thisElement))
				changeElementClass(thisElement, requiredFormFieldClass);
			else
				changeElementClass(thisElement, defaultFormFieldClass);
			//thisElement.onpropertychange = thisForm.validator.changeElementProperty;
		}
	}
	
	function changeProperty(thisElement) {
		//remove the element and reinsert the element
		this.removeElement(thisElement);
		this.insertElement(thisElement);
	}
	
	this.changeElementProperty = function() {
		var property = event.propertyName;
		//disabled doesn't fire this function, changeProperty must be called explicitly
		switch(property) {
			case 'required':
			case 'fulfillsRequirement':
			case 'requirementGroup':
				changeProperty(event.srcElement);
			default:
				return;
		}
	}
	
	
	/* VALIDATION FUNCTIONS */
	
	this.validateForm = function(thisForm) {
		//this function checks and alerts all incomplete form fields in a form's requirements and requirementGroups
		
		if (thisForm.getAttribute("overrideValidation") == "true") {
			return true;
		}
		
		//var start = new Date();
		
		//check the requirements array on the form
		validateFormRequirements(thisForm);
		
		//check the requirementGroups array on the form
		validateFormRequirementGroups(thisForm);
		
		//var end = new Date();
		//alert(end-start + ' ms');
		
		//create an errorString of all missing form requirements
		var errorString = "";
		for(var req in thisForm.unfulfilledRequirements) {
			errorString += "\n   " + req;
		}
		
		//alert all missing form requirements
		if(errorString != "") {
			alert("The following fields are missing from this form:" + errorString);
		}
		
		//alert all incomplete form requirementGroups
		for(var reqGroup in thisForm.unfulfilledRequirementGroups) {
			var thisRequirementGroup = thisForm.requirementGroups[reqGroup];
			var requirementGroupErrorName = stringBeforeTilda(thisRequirementGroup.name);
			if(thisRequirementGroup.numRequired == 0) {
				var errorString = "You must either completely fill out " + requirementGroupErrorName + " or leave it empty.";
			} else {
				var errorString = "You must completely fill out " + requirementGroupErrorName + ".";
			}
			alert(errorString + "\n\nThe following fields are missing:" + thisRequirementGroup.missingRequirementsString);
		}
		
		//return true only when nothing in the form is missing
		return (thisForm.unfulfilledRequirements.length == 0) && (thisForm.unfulfilledRequirementGroups.length == 0);
		
	}
	
	function validateFormRequirementGroups(thisForm) {
		//create a new array of unfulfilledRequirementGroups and attach it to the form, then validate the form's requirementGroups array
		thisForm.unfulfilledRequirementGroups = new Array();
		validateRequirementGroups(thisForm);
	}
	
	function validateFormRequirements(thisForm) {
		//create a new array of unfulfilledRequirements and attach it to the form, then validate the form's requirements array
		thisForm.unfulfilledRequirements = new Array();
		validateRequirements(thisForm, thisForm.requirements);
	}
	
	function validateRequirementGroups(thisForm) {
		/*
			This function checks all of a form's requirementGroups.
			If a requirementGroup is incomplete, it will be placed in the form's unfulfilledRequirementGroups array.
			Afterwards, the requirementGroup's requirement's classes will be set.
		*/
		
		//loop through all of the requirementGroups
		for(var reqGroup in thisForm.requirementGroups) {
			var thisRequirementGroup = thisForm.requirementGroups[reqGroup];
			
			//set the number of fulfilled requirements to 0
			thisRequirementGroup.numFulfilled = 0;
			
			//set the missing requirements string to empty
			thisRequirementGroup.missingRequirementsString = "";
			
			//validate the requirementGroup's requirements array
			validateRequirements(thisForm, thisRequirementGroup.requirements, thisRequirementGroup);
			
			//check that the number of fulfilled requirements equals 0 and that the requirementGroup is not required
			var testIfEmptyButStillOK = (thisRequirementGroup.numFulfilled == 0) && (thisRequirementGroup.numRequired == 0);
			//check if the requirementGroup has all of it's requirements completely filled out
			var testIfComplete = (thisRequirementGroup.numFulfilled == thisRequirementGroup.requirements.length);
			
			//if either test fails, insert it into the form's unfulfilledRequirementGroups array and increase the length of that array by 1
			if(!(testIfEmptyButStillOK  || testIfComplete)) {
				thisForm.unfulfilledRequirementGroups[reqGroup] = thisRequirementGroup;
				thisForm.unfulfilledRequirementGroups.length = thisForm.unfulfilledRequirementGroups.length + 1;
			}
			
			//set the requirementGroup's requirement's classes
			setRequirementGroupClasses(reqGroup, thisForm);
			
		}
	}
	
	function validateRequirements(thisForm, requirementsArray, thisRequirementGroup) {
		/*
			This function checks all the requirements in the requirements array.
			If a requirement is required and is not fulfilled, then change the class to the requiredFieldClass. Otherwise, if it is fulfilled, then change the class to the defaultFieldClass.
			If the requirement is also part of a requirementGroup, it will concatenate the name of the requirement to the requirementGroup's missing requirements string.
		*/
		var defaultFormFieldClass = thisForm.getAttribute("defaultFieldClass");
		var requiredFormFieldClass = thisForm.getAttribute("requiredFieldClass");
		
		for(var req in requirementsArray) {
			var thisRequirement = requirementsArray[req];
			
			//set a requirements fulfilled status to false
			thisRequirement.isFulfilled = false;
			
			//validate all of the elements in the requirement
			validateElements(thisForm, thisRequirement);
			
			if(thisRequirementGroup) {
				if(thisRequirement.isFulfilled) {
					//if the requirement is part of a requirementGroup and is fulfilled, then increase the requirementGroup's number of fulfilled requirements by 1
					thisRequirementGroup.numFulfilled = thisRequirementGroup.numFulfilled + 1;
						
				} else {
					//if the requirement is part of a requirementGroup and is not fulfilled, then add the requirement's name to the requirementGroup's missing requirements string
					thisRequirementGroup.missingRequirementsString +=  "\n   " + thisRequirement.name;
				}
			} else if((thisRequirement.numRequired > 0) && (!thisRequirement.isFulfilled)) {
				//if the requirement is part of the form's requirements array and it is required but not fulfilled, then add the requirement to the form's unfulfilledRequirements array, increase the length of the array by 1, and change the requirement's class to the requiredFieldClass
				thisForm.unfulfilledRequirements[req] = thisRequirement;
				thisForm.unfulfilledRequirements.length = thisForm.unfulfilledRequirements.length + 1;
				changeRequirementClass(thisRequirement, requiredFormFieldClass);
			} else {
				//if the requirement is part of the form's requirements array and it is either not required, or required but fulfilled, then change the requirement's class to the defaultFieldClass
				changeRequirementClass(thisRequirement, defaultFormFieldClass);
			}
		}
	}
	
	function validateElements(thisForm, thisRequirement) {
		/*
			This function checks all of the elements in a requirement.
			If one of the elements is not empty, it will set the requirements fulfilled status to true.
		*/
		for(var i = 0; i < thisRequirement.elements.length; i++) {
			var thisElement = thisRequirement.elements[i];
			var elementIsEmpty = isFieldEmpty(thisElement);
			//alert(thisElement.name + ": " + elementIsEmpty); 
			if(!elementIsEmpty) {
				thisRequirement.isFulfilled = true;
				return;
			}
		}
	}
	
	
	/* OTHER FUNCTIONS */
	
	this.showRequirementGroupsAndRequirements = function(thisForm) {
		//this function alerts all of a forms requirements and requirementGroups
		for(var x in thisForm.requirementGroups) {
			var thisRequirementGroup = thisForm.requirementGroups[x];
			alert(thisRequirementGroup);
			for(var y in thisRequirementGroup.requirements) {
				var thisRequirement = thisRequirementGroup.requirements[y];
				alert(thisRequirement);
			}
		}
		for(var y in thisForm.requirements) {
			var thisRequirement = thisForm.requirements[y];
			alert(thisRequirement);
		}
	}
	
	function stringBeforeTilda(str) {
		/*
			This function is used for requirementGroups with numbers.
			It will return the name of the requirementGroup without the tilda and number.
			For example, if you have player1 and player2 as two separate requirementGroups, you should name them requirementGroup="player~1" and requirementGroup="player~2", so that this function will still return the error name as "player" but these requirementGroups will still be different requirementGroup objects identified by different names.
		*/
		var i = str.indexOf('~');
		if(i < 0)
			return str;
		else
			return str.substring(0, i);
	}
	
	function getRequirementName(thisElement) {
		/*
			A requirementName is the fulfillsRequirement defined in the tag.
			If no fulfillsRequirementName is defined, it is the name of the tag.
			If no name is defined, it is the id of the tag.
		*/
		var requirementName = thisElement.getAttribute("fulfillsRequirement");

		if(!requirementName)
			requirementName = getNameOrId(thisElement);
		
		return requirementName;
	}
	
	function changeRequirementClass(thisRequirement, newClass) {
		/*
			This function changes the class of all the elements in a requirement.
		*/
		for(var i = 0; i < thisRequirement.elements.length; i++)
			changeElementClass(thisRequirement.elements[i], newClass);
	}
	
	function setRequirementGroupClasses(reqGroup, thisForm) {
		/*
			This function changes the class of all the requirements in a requirementGroup.
			If a requirementGroup is unfulfilled and the requirement is unfulfilled it changes the requirement's class to the form's requiredFieldClass.
			Otherwise, it changes the requirement's class to the form's defaultFieldClass.
		*/
		var defaultFormFieldClass = thisForm.getAttribute("defaultFieldClass");
		var requiredFormFieldClass = thisForm.getAttribute("requiredFieldClass");
		
		var thisRequirementGroup = thisForm.requirementGroups[reqGroup];
		for(var req in thisRequirementGroup.requirements) {
			
			var thisRequirement = thisRequirementGroup.requirements[req];
			
			if(thisForm.unfulfilledRequirementGroups[reqGroup] && !thisRequirement.isFulfilled)
				changeRequirementClass(thisRequirement, requiredFormFieldClass);
			else
				changeRequirementClass(thisRequirement, defaultFormFieldClass);				
				
		}
	}
	
}

/* FORM VALIDATOR INITIALIZATION FUNCTIONS */

function createFormValidators() {
	/*
		This function is run after the page is loaded and does 3 things:
		1. A FormValidator is created to be shared among all the forms on the page.
		2. RequirementArrays are created for all the forms on the page.
		3. Each form has the validateForm function of the validator attached to its onsubmit event handler.
	*/
	var validator = new FormValidator();
	//var start = new Date();
	for(var i = 0; i < document.forms.length; i++) {
		var thisForm = document.forms[i];
		thisForm.validator = validator;
		validator.buildRequirementArrays(thisForm);
		var validateForm = function() {
			return validator.validateForm(thisForm);
		}
		modifyEventHandler(thisForm, "onsubmit", validateForm, "before");
		//validator.showRequirementGroupsAndRequirements(thisForm);
	}
	//var end = new Date();
	//alert(end-start + ' ms');
}

//The createFormValidators function is added to the window.onload event handler
modifyEventHandler(window, "onload", createFormValidators, "before");
