/**
 *	This class manages javascript validation mechanisms for an HTML form.
 *
 *	Requirements: Prototype / Scriptaculous
 */

function Validation(formId) {
	var formId = formId;

	/**
	 * An array of validation rules to apply. Each rules is a struct with the following
	 * properties:
	 *	fieldID : The ID of the form field to be validated.
	 *	isRequired : True if the field is required, false otherwise.
	 *	checkType : The validation strategy for the field (ex: none, email, phone, custom...).
	 *	errorMessage : The error message to be displayed when the rule is broken.
	 *	isValidFunction : Used when checkType is "custom". A custom function to execute
	 *		that receives the fieldID and returns true if the field is valid, false
	 *		otherwise.
	 */
	var rules = new Array();

	/**
	 * Validates all rules for this validation object and calls the appropriate
	 * callback function depending on success or failure.
	 * Returns: True if validation succeeds, false otherwise.
	 */
	this.validate = function validate() {
		var errors = new Array();
		var success = new Array();
		var currRule = null;
		var currValue = null;
		var fieldElement = null;

		for (var i = 0; i < rules.length; i++) {
			currRule = rules[i];

			// Check required fields and validator function
			if (currRule.checkType != "custom") {
				currValue = $F(currRule.fieldID);

				if ((currRule.isRequired && this.isEmpty(currValue))
					|| (! this.isEmpty(currValue) && ! currRule.isValidFunction(currValue))) {
					errors.push({fieldID: currRule.fieldID, errorMessage: currRule.errorMessage});
				}
				else {
					success.push({fieldID: currRule.fieldID});
				}
			} else {
				if (currRule.fieldID != null)
					currValue = $F(currRule.fieldID);
				else
					currValue = null;

				if (! currRule.isValidFunction(currValue, currRule.isRequired)) {
					errors.push({fieldID: currRule.fieldID, errorMessage: currRule.errorMessage});
				} else {
					success.push({fieldID: currRule.fieldID});
				}
			}
		}

		if (errors.length > 0) {
			this.onValidateFailure(errors);
			this.doFailureDisplay(errors);
			this.doSuccessDisplay(success);

			return false;
		}
		else {
			this.onValidateSuccess();
			this.doFailureDisplay(errors);
			this.doSuccessDisplay(success);
			return true;
		}
	}

	this.onValidateSuccess = function onValidateSuccess() {
		var formValElement = $("FormValidation-" + formId);
		//formValElement.innerHTML = "";  //This was taken out because it erases the outter error innerHTML, which needs to just hide in this case
		Element.hide(formValElement);
	}

	this.doSuccessDisplay = function doSuccessDisplay(fieldArray) {
		fieldArray.each(function(currField) {
			Element.removeClassName(currField.fieldID, "exceptionField");
		});
	}

	this.onValidateFailure = function (errors) {
		var formValElement = $("FormValidation-" + formId);
		var formErrorElement = $("FormValidation-" + formId + "-Errors");
		var valHtml = "";
		valHtml += '<ul>';
		for (var i = 0; i < errors.length; i++) {
			valHtml = valHtml + '<li>' + errors[i].errorMessage + '</li>';
		}
		valHtml += '</ul>';
		
		formErrorElement.innerHTML = valHtml;	

		Element.show(formValElement);
		new Effect.ScrollTo(formValElement);
	}

	this.doFailureDisplay = function doFailureDisplay(fieldArray) {
		fieldArray.each(function(currField) {
			var field = $(currField.fieldID);
			if (field) {
				Element.addClassName(field, "exceptionField");
				field.title = currField.errorMessage;
			}
		});
	}

	/**
	 * Adds a rule to the list of rules to enforce for validation.
	 */
	this.addRule = function addRule(fieldID, isRequired, checkType, errorMessage, customValidatorFunction) {
		var validatorFunction = null;

		// Apply the correct callback based on checkType
		switch (checkType) {
		case "email":
			validatorFunction = this.isValidEmail;
			break;
		case "telephone":
			validatorFunction = this.isValidPhone;
			break;
		case "integer":
			validatorFunction = this.isValidNumeric;
			break;
		case "creditcard":
			validatorFunction = this.isCreditCard;
			break;	
		case "expirationDate":
			validatorFunction = this.isExpiryDate;
			break;		
		case "custom":
			validatorFunction = customValidatorFunction;
			break;
		default:
			validatorFunction = function (value) { return true; };
			break;
		}

		rules.push({
			fieldID : fieldID,
			isRequired : isRequired,
			checkType : checkType,
			errorMessage : errorMessage,
			isValidFunction : validatorFunction
		});
	}

	/**
	 * Clears all validation rules so no validation is performed.
	 */
	this.clearRules = function clearRules() {
		rules = new Array();
	}

	/**
	 * Returns true if a value has been entered into the given field,
	 * false otherwise.
	 */
	this.isEmpty = function (value) {
		if(typeof(value).toLowerCase() == 'string')
			return value.replace(/^\s+/,'').replace(/\s+$/,'').length == 0 ? true : false;
		else
			return value.length == 0 ? true : false;
	}

	/**
	 * Returns true if the given value is a valid number.
	 */
	this.isValidInteger = function (value) {
		var numberRegex = /^\d+$/;
		return numberRegex.test(value);
	}

	/**
	 * Returns true if the given value is a valid phone number.
	 * Algorithm:
	 * A simple check that there are 10 - 15 numeric digits in the value.
	 */
	this.isValidPhone = function (value) {
		// Remove all non-digits
		value = value.replace(/\D/g, '');
		// Check for a length between 10 and 15
		if (value.length >= 10 && value.length <= 15) {
			return true;
		}
		else {
			return false;
		}
	}

	/**
	 * Returns true if the given value is a valid email.
	 * Algorithm:
	 * Uses a regular expression taken from the ColdFusion MX 7.02 cfform.js code
	 * since it is trustworthy.
	 */
	this.isValidEmail = function (value) {
		var emailRegex = /^[a-zA-Z_0-9-'\+~]+(\.[a-zA-Z_0-9-'\+~]+)*@([a-zA-Z_0-9-]+\.)+[a-zA-Z]{2,7}$/;
		// Trim whitespace
		value = value.replace(/^\s+/,'').replace(/\s+$/,'');
		// Test email regex
		return emailRegex.test(value);
	}

	/**
	 * Returns true if the given year, month, and day represent a valid date.
	 */
	this.isDate = function isDate(year, month, day) {
		// month argument must be in the range 1 - 12
		month = month - 1; // javascript month range : 0- 11
		var tempDate = new Date(year,month,day);
		if ((year == tempDate.getFullYear()) && (month == tempDate.getMonth()) && (day == tempDate.getDate()))
			return true;
		else
			return false;
	}
	
	/*
	
	This routine checks the credit card number. The following checks are made:
	
	1. A number has been provided
	2. The number is a right length for the card
	3. The number has an appropriate prefix for the card
	4. The number has a valid modulus 10 number check digit if required
	
	If the validation fails an error is reported.
	
	The structure of credit card formats was gleaned from a variety of sources on 
	the web, although the best is probably on Wikepedia ("Credit card number"):
	
	  http://en.wikipedia.org/wiki/Credit_card_number
	
	Parameters:
	            cardnumber           number on the card
	            cardname             name of card as defined in the card list below
	
	Author:     John Gardner
	Date:       1st November 2003
	Updated:    26th Feb. 2005      Additional cards added by request
	Updated:    27th Nov. 2006      Additional cards added from Wikipedia
	
	*/
	this.isCreditCard = function isCreditCard (cardnumber) {   
		  // Array to hold the permitted card characteristics
		  var cards = new Array();
		  var cardname = $F('billing_CreditCardType');	
		
		  // Define the cards we support. You may add addtional card types.
		  
		  //  Name:      As in the selection box of the form - must be same as user's
		  //  Length:    List of possible valid lengths of the card number for the card
		  //  prefixes:  List of possible prefixes for the card
		  //  checkdigit Boolean to say whether there is a check digit
		  
		  cards [0] = {name: "VISA", 
		               length: "13,16", 
		               prefixes: "4",
		               checkdigit: true};
		  cards [1] = {name: "Master Card", 
		               length: "16", 
		               prefixes: "51,52,53,54,55",
		               checkdigit: true};
		  cards [2] = {name: "Diners Club", 
		               length: "14,16", 
		               prefixes: "300,301,302,303,304,305,36,38,55",
		               checkdigit: true};
		  cards [3] = {name: "CarteBlanche", 
		               length: "14", 
		               prefixes: "300,301,302,303,304,305,36,38",
		               checkdigit: true};
		  cards [4] = {name: "AMEX", 
		               length: "15", 
		               prefixes: "34,37",
		               checkdigit: true};
		  cards [5] = {name: "Discover", 
		               length: "16", 
		               prefixes: "6011,650",
		               checkdigit: true};
		  cards [6] = {name: "JCB", 
		               length: "15,16", 
		               prefixes: "3,1800,2131",
		               checkdigit: true};
		  cards [7] = {name: "enRoute", 
		               length: "15", 
		               prefixes: "2014,2149",
		               checkdigit: true};
		  cards [8] = {name: "Solo", 
		               length: "16,18,19", 
		               prefixes: "6334, 6767",
		               checkdigit: true};
		  cards [9] = {name: "Switch", 
		               length: "16,18,19", 
		               prefixes: "4903,4905,4911,4936,564182,633110,6333,6759",
		               checkdigit: true};
		  cards [10] = {name: "Maestro", 
		               length: "16", 
		               prefixes: "5020,6",
		               checkdigit: true};
		  cards [11] = {name: "VisaElectron", 
		               length: "16", 
		               prefixes: "417500,4917,4913",
		               checkdigit: true};
		               
		  // Establish card type
		  var cardType = -1;
		  for (var i=0; i<cards.length; i++) {
		
		    // See if it is this card (ignoring the case of the string)
		    if (cardname.toLowerCase () == cards[i].name.toLowerCase()) {
		      cardType = i;
		      break;
		    }
		  }
		  
		  // If card type not found, report an error
		  if (cardType == -1) {
		     ccErrorNo = 0;
		     return false; 
		  }
		   
		  // Ensure that the user has provided a credit card number
		  if (cardnumber.length == 0)  {
		     ccErrorNo = 1;
		     return false; 
		  }
		    
		  // Now remove any spaces from the credit card number
		  cardnumber = cardnumber.replace (/\s/g, "");
		  
		  // Check that the number is numeric
		  var cardNo = cardnumber
		  var cardexp = /^[0-9]{13,19}$/;
		  if (!cardexp.exec(cardNo))  {
		     ccErrorNo = 2;
		     return false; 
		  }
		       
		  // Now check the modulus 10 check digit - if required
		  if (cards[cardType].checkdigit) {
		    var checksum = 0;                                  // running checksum total
		    var mychar = "";                                   // next char to process
		    var j = 1;                                         // takes value of 1 or 2
		  
		    // Process each digit one by one starting at the right
		    var calc;
		    for (i = cardNo.length - 1; i >= 0; i--) {
		    
		      // Extract the next digit and multiply by 1 or 2 on alternative digits.
		      calc = Number(cardNo.charAt(i)) * j;
		    
		      // If the result is in two digits add 1 to the checksum total
		      if (calc > 9) {
		        checksum = checksum + 1;
		        calc = calc - 10;
		      }
		    
		      // Add the units element to the checksum total
		      checksum = checksum + calc;
		    
		      // Switch the value of j
		      if (j ==1) {j = 2} else {j = 1};
		    } 
		  
		    // All done - if checksum is divisible by 10, it is a valid modulus 10.
		    // If not, report an error.
		    if (checksum % 10 != 0)  {
		     ccErrorNo = 3;
		     return false; 
		    }
		  }  
		
		  // The following are the card-specific checks we undertake.
		  var LengthValid = false;
		  var PrefixValid = false; 
		  var undefined; 
		
		  // We use these for holding the valid lengths and prefixes of a card type
		  var prefix = new Array ();
		  var lengths = new Array ();
		    
		  // Load an array with the valid prefixes for this card
		  prefix = cards[cardType].prefixes.split(",");
		      
		  // Now see if any of them match what we have in the card number
		  for (i=0; i<prefix.length; i++) {
		    var exp = new RegExp ("^" + prefix[i]);
		    if (exp.test (cardNo)) PrefixValid = true;
		  }
		      
		  // If it isn't a valid prefix there's no point at looking at the length
		  if (!PrefixValid) {
		     ccErrorNo = 3;
		     return false; 
		  }
		    
		  // See if the length is valid for this card
		  lengths = cards[cardType].length.split(",");
		  for (j=0; j<lengths.length; j++) {
		    if (cardNo.length == lengths[j]) LengthValid = true;
		  }
		  
		  // See if all is OK by seeing if the length was valid. We only check the 
		  // length if all else was hunky dory.
		  if (!LengthValid) {
		     ccErrorNo = 4;
		     return false; 
		  };   
		  
		  // The credit card is in the required format.
		  return true;
		}
		
		
		
	/**
	 * Returns true if the given Expiration date is valid
	 */	
	this.isExpiryDate = function isExpiryDate () {   
		function isNum(argvalue) {
			argvalue = argvalue.toString();
			
			if (argvalue.length == 0)
			return false;
			
			for (var n = 0; n < argvalue.length; n++)
			if (argvalue.substring(n, n+1) < "0" || argvalue.substring(n, n+1) > "9")
			return false;
			
			return true;
		}
		
		var year = $F('year');	
		var month = $F('month');	
		
		if (!isNum(year+""))
		return false;
		if (!isNum(month+""))
		return false;
		today = new Date();
		expiry = new Date(year, month);
		if (today.getTime() > expiry.getTime())
		return false;
		else
		return true;
		
	}
}


