/**
 * MDBO Javascript Form Validator
 *
 * Requires: Prototype, MDBO Common Javascript
 *
 * Author: Mike Sollanych
 * Version: 2.5 
 */

function FormValidator(form_id) {

	function toString() {
		return "MDBO Validator Object - version 2.5 - form: " + this.form_id;
	}
	
	// Add a declaration to the list.
	function addDeclaration(elementId, validtype, required, maxlen, minlen) {
		
		var current_element = $(elementId);
		if (!current_element) return false;
		
		// Get label for element 
		try {
			var current_element_label = getLabelForElement(elementId);
		}
		catch (e) {
			var current_element_label = false;
		}
		
		// Set up the Onchange Handler.
		var 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;
		
		return true;
	}

	// 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;
		
		return true;
	}
	
	// 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;
		
		return true;
	}
	
	// Add a callback requirement. Provide a callback function to be run on form submit.
	function addCallbackRequirement(callback) {
		this.callbacks.push(callback);
		this.form.callbacks = this.callbacks;
		
		return true;
	}
	
	
	// 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);
		return false;
	}
	
	// Set the validator to submit unchecked checkboxes with the value of "true", as "false" and checked.
	function setSubmitUncheckedMode(mode) {
		this.form.submituncheckedmode = mode;
		return true;
	}
	
	/**
	 * Construction code
	 */
	// Get form object	
	var form = $(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.addCallbackRequirement = addCallbackRequirement;
	this.getDeclaration = getDeclaration;
	this.checkData = checkData;
	this.setSubmitUncheckedMode = setSubmitUncheckedMode;
		
	// Create array to hold all validation and comparisondeclarations
	this.declarations = Array();
	this.comparisons = Array();
	this.conditionals = Array();
	this.callbacks = Array();
	
	// Assign event handlers to the form
	form.onsubmit = validateOnSubmit;
	form.declarations = this.declarations;
	form.comparisons = this.comparisons;
	form.conditionals = this.conditionals;
	form.submituncheckedmode = false; // default
	
	return this;
}

// 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 ((validtype != "checked_checkbox") && ((typeof testvalue) != "string")) {
		alert ("MDBO Validator Error: not a string! " + testvalue);
	}
	// First, If there is an empty string, was input required? 
	if (testvalue.length < 1) return !required;
	
	// 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':
			if (!testvalue.match(/^[-+]?\d*\.?\d*$/)) 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;
		
		case 'alphadashdot':
			var charpos = testvalue.search("[^A-Za-z \-.]");
			if(testvalue.length > 0 &&  charpos >= 0) return false;
		break;
		
		case 'alphanumdashdot':
			var charpos = testvalue.search("[^A-Za-z0-9 \-.]");
			if(testvalue.length > 0 &&  charpos >= 0) return false;
		break;
				
		case 'alphanumdashdotamp':
			var charpos = testvalue.search("[^A-Za-z0-9 \-.&]");
			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 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 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 false;
		break;
		
		case 'email':
			if (!testvalue.match(/^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,9})$/))
				return false;
		break;
		
		case 'checked_checkbox':
			return testvalue;
		break;
		
		// The "anything" case is when you just want to make sure there is something in the field, no validation.
		case 'anything':
			return true;
		break;
	}
	return true;
}

// Onchange Handler. 
function validateOnChange(element, validtype, required, maxlen, minlen) {

	// Get testing value
	var testvalue = "";
	if (element.options) testvalue = element.options[element.selectedIndex].value;
	else testvalue = element.value;
	
	// Check and set invalid classname if needed
	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) $(element).addClassName("invalid");
	else $(element).removeClassName("invalid");
}
			
// Onsubmit Handler.
function validateOnSubmit(event) {

	var decs = this.declarations;
	var coms = this.comparisons;
	var conds = this.conditionals;
	var callbacks = this.callbacks;
	var submituncheckedmode = this.submituncheckedmode;
	
	// 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) continue;
		
		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 if (current_element.type == "checkbox" || current_element.type == "radio") {
			testvalue = current_element.checked ? true : false;
		}
		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 check it
		var origElement = document.getElementById(conds[i][0]);
		
		if (!origElement) {
			// alert ("MDBO Validator: could not retrieve object for conditional validation.");
			// return false;
			continue;
		}
		
		var origConditionalValue = conds[i][2];
		var performTest = false;
		
		// Determine value in original (source) element
		if (origElement.type == 'checkbox' || origElement.type == 'radio') 
			var origSetValue = origElement.checked;
		else if (origElement.options) 
			var origSetValue = origElement.options[origElement.selectedIndex].value;
		else 
			var origSetValue = origElement.value; 
			
		if (origSetValue == origConditionalValue) performTest = true;
		
		// Run the check.
		if (performTest) {
			var condElement = document.getElementById(conds[i][3]);
			
			// Get the value in the element to be checked:
			if (condElement.options) {
				
				if (condElement.selectedIndex < 0) {
					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);
		}
	}
	
	
	// Run all provided callbacks, if any.
	if (callbacks) for (i = 0; i < callbacks.length; i++) {
		if (!callbacks[i]()) {
			// Callback failed.
			event.returnValue = false;
			return false;
		}
	}
	
	
	// While we are here: if we're going to submit unchecked-true checkboxes as checked-false,
	// do it here. Go over all the checkboxes that are unchecked, change the value to false and check them.
	if (submituncheckedmode) {
		var inputs = $(this).getElements();
		for (var i in inputs) {
			var current = inputs[i];
			if (current.type == "checkbox" && current.value == "true" && !current.checked) {
				current.value = "false";
				current.checked = true;
			}
		}
	}
			
	// Nothing returned false, so return true!
	return true;
}