/**
 * MDBO Javascript Form Validator
 *
 * Author: Mike Sollanych
 * Version: 2.0
 */

function FormValidator(form_id) {

	function toString() {
		return "MDBO Validator Object - version 2.0 - form: " + this.form_id;
	}
	
	// Add a declaration to the list.
	function addDeclaration(elementId, validtype, required, maxlen, minlen) {
		
		var current_element = document.getElementById(elementId);
		
		if (!current_element) {
			return false;
		}
		
		// Get label for element 
		var current_element_label = getLabelForElement(elementId);
		
		// Set up the Onchange Handler.
		onchangefunction = Function('validateOnChange(this, "'+validtype+'", '+required+', '+maxlen+', '+minlen+')');
		
		current_element.addEventListener("change", onchangefunction, false);
		current_element.addEventListener("blur", onchangefunction, false);
			
		// current_element.onblur = this.onchangeHandler;
		
		this.declarations.push([elementId, current_element_label, validtype, required, maxlen, minlen]);
		
		this.form.declarations = this.declarations;
	}

	// Add a comparison to the list - in other words, define two things as having
	// to be equal. Primarily for passwords; checked only at submit.
	function addComparison(firstElementId, secondElementId) {
		
		var first_element_label = getLabelForElement(firstElementId);
		var second_element_label = getLabelForElement(secondElementId);
		
		this.comparisons.push([firstElementId, secondElementId, first_element_label, second_element_label]);
		
		this.form.comparisons = this.comparisons;
	}
	
	// Add a conditional requirement. If the first provided one passes the requirement,
	// the second one must as well.
	function addConditionalRequirement(origElementId, origvalue, condElementId, condvalidtype, maxlen, minlen) {
		var origElementLabel = getLabelForElement(origElementId);
		var condElementLabel = getLabelForElement(condElementId);
		
		this.conditionals.push([origElementId, origElementLabel, origvalue, condElementId, condElementLabel, condvalidtype, maxlen, minlen]);
		
		this.form.conditionals = this.conditionals;
	}
	
	
	// Get back a declaration for an elementId.
	function getDeclaration(elementId) {
		
		for (i = 0; i <= this.declarations.length; i++) {
			
			if (this.declarations[i][0] == elementId) {
				return this.declarations[i];
			}
		}
		
		// If we get here, the declaration wasn't found,
		// and this should never have been called.
		alert ("getDeclaration could not find a declaration for element ID: " + elementId);
	}
	
	/**
	 * Construction code
	 */


	// Get form object	
	var form = document.getElementById(form_id);
	
	if (!form) {
		alert ("Form Validator: error retrieving form object.");
		return false;
	}
	else {
		this.form_id = form_id;
		this.form = form;
	}
	
	// Assign methods above into this FormValidator
	this.toString = toString;
	this.addDeclaration = addDeclaration;
	this.addComparison = addComparison;
	this.addConditionalRequirement = addConditionalRequirement;
	this.getDeclaration = getDeclaration;
	this.checkData = checkData;
		
	// Create array to hold all validation and comparisondeclarations
	this.declarations = Array();
	this.comparisons = Array();
	this.conditionals = Array();
	
	// Assign event handlers to the form
	form.onsubmit = validateOnSubmit;
	form.declarations = this.declarations;
	form.comparisons = this.comparisons;
	form.conditionals = this.conditionals;
}



// Check data and return true or false based on the declaration set.
function checkData(testvalue, validtype, required, maxlen, minlen) {
	
	// Ensure testvalue is a string
	if ((typeof testvalue) != "string") {
		alert ("not a string! " + testvalue);
	}
	
	
	// First, was input required?
	if (required == true) {
		if (testvalue.length < 1) { 
			return false;
		}
	}
	else {
		// If input was not required, and element is empty, return true
		if (testvalue.length == 0) {
			return true;
		}
	}
	
	// Second, was max length specified?
	if ((maxlen > 0) && (testvalue.length > maxlen)) {
		return false;
	}
	
	// Third, was minimum length specified?
	if ((minlen > 0) && (testvalue.length < minlen)) {
		return false;
	}
	
	// Fourth, switch based on validation type.
	switch (validtype) {
		
		case 'integer':
			var charpos = testvalue.search("[^0-9]"); 
			if(testvalue.length > 0 &&  charpos >= 0) {
				return false;
			}
		break;
		
		case 'float':
			var charpos = testvalue.search("[^0-9.]");
			if(testvalue.length > 0 &&  charpos >= 0) {
				return false;
			}
		break;
		
		case 'alpha':
			var charpos = testvalue.search("[^A-Za-z ]");
			if(testvalue.length > 0 &&  charpos >= 0) {
				return false;
			}
		break;
		
		case 'alphanumeric':
			var charpos = testvalue.search("[^A-Za-z 0-9]");
			if(testvalue.length > 0 &&  charpos >= 0) {
				return false;
			}
		break;
		
		case 'alphadash':
			var charpos = testvalue.search("[^A-Za-z \-]");
			if(testvalue.length > 0 &&  charpos >= 0) {
				return false;
			}
		break;
		
		// MD5 Hash
		case 'md5':
			// Hex chars only
			var charpos = testvalue.search("[^0-9a-f]");
			if(testvalue.length > 0 &&  charpos >= 0) {
				return false;
			}
			// 32 chars
			if(testvalue.length != 32) {
				return false;
			}
		break;
		
		// Date. ISO Sortable
		case 'date':
			if (testvalue.match(/^\d{4}[\-\/\s]?((((0[13578])|(1[02]))[\-\/\s]?(([0-2][0-9])|(3[01])))|(((0[469])|(11))[\-\/\s]?(([0-2][0-9])|(30)))|(02[\-\/\s]?[0-2][0-9]))$/)) {
				return true;
			}
			else {
				return false;
			}
		break;
		
		// Date with Time. ISO Sortable
		case 'datetime':
		
			if (testvalue.match(/^[0-9]{4}-(((0[13578]|(10|12))-(0[1-9]|[1-2][0-9]|3[0-1]))|(02-(0[1-9]|[1-2][0-9]))|((0[469]|11)-(0[1-9]|[1-2][0-9]|30))) (([01][0-9])|([2][0-3])):([0-5][0-9]):([0-5][0-9])$/)) {
				return true;
			}
			else {
				return false;
			}
		break;
		
		// Date, greater than today
		case 'future_date':
		
			// Check it's a date at all
			if (!testvalue.match(/^\d{4}[\-\/\s]?((((0[13578])|(1[02]))[\-\/\s]?(([0-2][0-9])|(3[01])))|(((0[469])|(11))[\-\/\s]?(([0-2][0-9])|(30)))|(02[\-\/\s]?[0-2][0-9]))$/)) {
				return false;
			}
		
			var earliest_valid = new Date();
			earliest_valid.setTime(earliest_valid.getTime() + (24*60*60*1000));
			
			var provided = new Date();
			provided.setFullYear(testvalue.substr(0,4));
			provided.setMonth(testvalue.substr(5,2) - 1);
			provided.setDate(testvalue.substr(8,2));
			
			if (provided.getTime() >= earliest_valid.getTime()) {
				return true;
			}
			
			return false;
		break;
		
		// Anything case is when you just want to make sure there's something!
		case 'anything':
			return true;
		break;
	}
	return true;
}

// Onchange Handler. 
function validateOnChange(element, validtype, required, maxlen, minlen) {

	var testvalue = "";
	
	if (element.options) { testvalue = element.options[element.selectedIndex].value; }
	else { testvalue = element.value; }
	
	var validation = checkData(testvalue, validtype, required, maxlen, minlen);
	
	toggleValidType(element, validation);
}

// Toggle the Valid stat (style, really) of an element
function toggleValidType(element, valid) {
	
	if (!valid) {
		if (element.className.indexOf("invalid") >= 0) {
			// Already invalid
			return true;
		}
		else {
			// Check if it has a classname
			if (element.className.length > 0) {
				// Append _invalid to it
				element.className = element.className + "_invalid";
			}
			else {
				element.className = "invalid";
			}
		}
	}
	else {
		if (element.className.indexOf("invalid") >= 0) {
			// It's invalid. 
			
			// See if it's just "invalid"
			if (element.className == "invalid") {
				// Remove
				element.className = "";
			}
			else {
				// We need to remove _invalid from the end
				var invalidpos = element.className.indexOf("_invalid");
				element.className = element.className.substr(0, invalidpos);
			}
		}
		else {
			// Already valid
			return true;
		}
	}
}
			
// Onsubmit Handler.
function validateOnSubmit(event) {

	var decs = this.declarations;
	var coms = this.comparisons;
	var conds = this.conditionals;
	
	// check everything in the form!
	for (i = 0; i < decs.length; i++) {
		
		// Get current element
		var current_element = document.getElementById(decs[i][0]);
		var testvalue = "";
		
		if (current_element.options) { 
			
			if (current_element.selectedIndex < 0) {
				alert ("No value is selected in the field labelled " + decs[i][1] + ". Please select a value before continuing.");
				
				event.returnValue = false;
				return false;
			}
			
			testvalue = current_element.options[current_element.selectedIndex].value; 
		}
		else { 
			testvalue = current_element.value; 
		}
		
		// Validate element
		var current_validation = checkData(testvalue, decs[i][2], decs[i][3], decs[i][4], decs[i][5]);
		
		if (!current_validation) {
			alert ("The value in the field labelled " + decs[i][1] + " is incorrect. \n It may be of the wrong length, contain invalid characters, or may not have been filled in at all. \n Please check it and try submitting the form again.");
			current_element.focus();
			
			event.returnValue = false;
			return false;
		}
		
		toggleValidType(current_element, current_validation);
	}
	
	// Check comparisons if there are any
	
	for (i = 0; i < coms.length; i++) {
		
		// Get elements
		var first_element = document.getElementById(coms[i][0]);
		var second_element = document.getElementById(coms[i][1]);
		
		if (first_element.value != second_element.value) {
			alert ("The value in the field labelled " + coms[i][2] + " does not match the value in the field labelled " + coms[i][3] + ". Please make sure both are the same, and try submitting the form again.");
			
			event.returnValue = false;
			return false;
		}
	}
	
	
	// Check conditionals if there are any
	for (i = 0; i < conds.length; i++) {
		
		// Get first element and determine if we need to czech it
		var origElement = document.getElementById(conds[i][0]);
		
		if (!origElement) {
			alert ("MDBO Validator: could not retrieve object for conditional validation.");
			return false;
		}
		
		var origConditionalValue = conds[i][2];
		
		var performTest = false;
		
		// Determine value in element
		if (origElement.type == 'checkbox' || origElement.type == 'radio') {
			
			// Checkboxes are validated true or false based on the checked property
			// and the origsetvalue (true or false).
			if (origElement.checked && origConditionalValue == true) performTest = true;
			if (!origElement.checked && origConditionalValue == false) performTest = true;
		}
		else {
			if (origElement.options) {
				var origSetValue = origElement.options[origElement.selectedIndex].value;
			}
			else {
				var origSetValue = origElement.value; 
			}
			
			if (origSetValue == origConditionalValue) performTest = true;
		}
		
		if (performTest) {
			// We do need to run the check.
			var condElement = document.getElementById(conds[i][3]);
			
			// Get the value in the dependant element:
			if (condElement.options) {
				if (condElement.selectedIndex < 0) {
					alert ("No value is selected in the field labelled " + conds[i][4] + ". Please select a value before continuing.");
					
					event.returnValue = false;
					return false;
				}
				
				condSetValue = condElement.options[condElement.selectedIndex].value; 
			}
			else { condSetValue = condElement.value; }
			
			var validates = checkData(condSetValue, conds[i][5], true, conds[i][6], conds[i][7]);
			
			if (!validates) {
				alert ("If the field marked " + conds[i][1] + " is set to '" + conds[i][2] + "', the field marked " + conds[i][4] + " must be filled in. \n Please check this field and ensure it is filled in right.");
				// alert (print_r(conds[i]) + "for " + condSetValue);
				
				condElement.focus();
				
				event.returnValue = false;
				return false;
			}
			
			toggleValidType(condElement, validates);
		}
	}
			
	// Nothing returned false, so return true!
	return true;
	
}