/* author: richard lidstone 2006 */
if(!window.$)											// check for necessary lib(s)
{															// if not found - bung 'em in
	document.write('<script type="text/javascript" src="/common/script/jquery-latest.js"></script>');
}
function validationHook()
{
	// there's a race condition - even after $(document).ready() says everything is processed $("input[@validate]").size() is sometimes zero - check it. 
	if(window.$ && $(document).size && $("input[@validate], select[@validate], textarea[@validate]").size() > 0)
	{
		$(document).ready(function()
		{
			vtor = new Validator();
			vtor.init();
		});
	}else{
		setTimeout('validationHook()', 50);
	}
}
var vtor;
validationHook();			// hook into page load




 
/* 
	 *** the validator object ***
*/
function Validator()
{
	/*
		this method defines the lowest-level yes/no validation operation
		add new validators into this method 							                     <---------------- look here!
	*/
	this.validateValue = function(value, validator, data)
	{
		var isValid = false;
		
		switch(validator)
		{
			case 'req':
			case 'required':
				isValid = value.length > 0;
				break;
				
			case 'radio-req':			// checks entire group - valid if one radio element is selected
				if(!document[data])
				{
					document[data] = $("input[@name='" + data + "']");
				}
				
				document[data].each(function()
				{
					if((!isValid && this.checked) || this.disabled)
					{
						isValid = true;
					}
				});
				break;
				
			case 'reqOneOf':						// this should be used as "req:reqOneOf(id1,id3,...)"
				var fields = data.split(',');
				isValid = vtor.validateValue(value, 'req');
				$(fields).each(function()
				{
					var field = $('#'+this).get(0).value;
					isValid = isValid || vtor.validateValue(field, 'req');
				});
				break;
			
		/*** numbers *******************************************/
				
			case 'int':
			case 'long':
			case 'integer':
				isValid = /^\d+$/.test(value);
				break;
				
			case 'double':
			case 'float':
			case 'number':
				isValid = /^\d+(\.\d+)?$/.test(value);
				break;
			
			case 'money':
				isValid = /^\d+(\.\d{2})?$/.test(value);
				break;
				
			case 'lowerBound':
			    isValid = (parseFloat(value) >= parseFloat(data));
			    break;
			case 'upperBound':
			    isValid = (parseFloat(value) <= parseFloat(data));
			    break;
			
		/*** dates *********************************************/
				
			case 'date':
				//isValid = /^(\d{1,2}\/){2}(\d{2}|\d{4})$/.test(value);			// basic approximation of date mask - needs improvement
				isValid = /^(3[0-1]|[0-2]?\d)([\\\/])(1[0-2]|0?\d)[\\\/](\d{2}|\d{4})$/.test(value);		// better
				break;
				
			case 'dd/mm/yyyy':
				isValid = /^(3[0-1]|[0-2]\d)\/(1[0-2]|0\d)\/\d{4}$/.test(value);		// more rigid date style
				break;
				
		/*** email *********************************************/
				
			case 'email':
				isValid = /^[^ @]+@[^\. @]+(\.[^\. @]+)+$/.test(value);				// basic approximation of email mask - needs improvement
				break;
		
			case 'email-strict':
				isValid = /^(\w|[-])+(\.(\w|[-])+)*@((\[([0-1]?\d?\d|2[0-4]\d|25[0-5])\.([0-1]?\d?\d|2[0-4]\d|25[0-5])\.([0-1]?\d?\d|2[0-4]\d|25[0-5])\.([0-1]?\d?\d|2[0-4]\d|25[0-5])\])|((([a-zA-Z0-9])+(([-])+([a-zA-Z0-9])+)*\.)+([a-zA-Z])+(([-])+([a-zA-Z0-9])+)*))$$/.test(value);
				break;
				
		/*** contact *******************************************/
				
			case 'internationalPhone':
				isValid = /^[0-9 ()-]{7,20}$/.test(value);
				break;
			
			
			case 'address':
				isValid = /^[a-zA-Z0-9-\'\.,\/ ]+$/.test(value);
				break;
				
			case 'suburb':
				isValid = /^[a-zA-Z0-9-\' ]+$/.test(value);
				break;
				
			case 'state-short-au':
				isValid = /^(act|nsw|nt|qld|sa|tas|vic|wa)$/.test(value);
				break;
				
			case 'postcode-au':
				isValid = /^(?!\d{5})0?[1-9]\d{2,3}$/.test(value);
				break;
				
			case 'postcode-au-vic':
				isValid = /^[38]\d{3}$/.test(value);
				break;
				
		/*** misc **********************************************/
				
			case 'name':
				isValid = /^[a-z\'\- ]+$/i.test(value);
				break;
				
			case 'name-long':
			case 'name-full':
			case 'fullName':
				isValid = /^[a-z\'\-\s]+$/i.test(value);
				break;

			case 'password':
				isValid = /^.{6,30}$/.test(value);
				break;
				
			// verifies one element's value is equal to another element's value - passes validation if the current element's value == the value of the element who's id=<data>
			case 'matches':	
				isValid = (value == $('#' + data).get(0).value);
				break;
				
			case 'isNot':
				isValid = (value != data);
				break;
				
			case 'isNotRegex':
				isValid = !(new RegExp(data).test(value));
				break;
			
			case 'creditCard':
				isValid = checkCreditCard(value, $('#' + data).get(0).value);
				this.setErrorMsg('creditCard', ccErrors[ccErrorNo]);				
				break;

			case 'cardSecurityCode':
				isValid = checkCardSecurityCode(value, $('#' + data).get(0).value);
				break;
				
			case 'nonZero':									// use req:nonZero(id1,id2,...)
				var fields = data.split(',');
				var val = (vtor.validateValue(value, 'int') ? parseInt(value, 10) : 0);
				$(fields).each(function()
				{
					var field = $('#'+this).get(0).value;
					if(vtor.validateValue(field, 'int'))
					{
						val += parseInt(field, 10);
					}
				});
				isValid = (val > 0);
				break;
				
			case 'othersNonZero':
				isValid = vtor.validateValue(0, 'nonZero', data);
				break;
				                                                            
			case 'words':			// words(-25) = 25 words or less |  words(+25) = 25 words or more |  words(25) = 25 words exactly
				var words = value.replace(/^\s+|\s+$/g,"").split(' ');
				if(data.substring(0,1) == "-")
				{
					var num = parseInt(data.substring(1), 10)
					isValid = words.length <= num;
				}
				else if(data.substring(0,1) == "+")
				{
					var num = parseInt(data.substring(1), 10)
					isValid = words.length >= num;
				}
				else
				{
					var num = parseInt(data, 10)
					isValid = words.length == num;
				}
				break;		
			
			case 'regex':								// because of the split above this won't work on regexs that match an explicit space - the regex will be split above and called in 2 or more invaild pieces...
				isValid = eval(data).test(value);					// use the passed data string as a regex
				break;
				
			default:
				alert('invalid validator: ' + validator);				
				break;
		}
		return isValid;
	},
	
	this.init = function()
	{
		
		$("form").bind("submit", function() { return vtor.allValid(); })	// prevent forms from submitting when invalid
		
		// wrapping save() in a form validation test
		if(window.save)
		{
			window.oldSavePreValidator = window.save;			// store the old save()
			window.save = function()							// ... and define a new one
			{
				if((arguments.length > 0 && arguments[0].toLowerCase() == '?dontvalidate') || vtor.allValid())								// ... that checks the form is valid
				{
					var aaaargs = '';
					for(var i=0; i<arguments.length; i++)
					{
						if(i > 0 || arguments[0].toLowerCase() != '?dontvalidate')
						{
							aaaargs += ",'" + arguments[i] + "'";
						}
					}
					//oldSavePreValidator(arguments);						// ... and calls the old save() when it is
					eval('oldSavePreValidator(' + aaaargs.substring(1) + ');');
				}
			}
		}
		
		// add events / methods to all participating <input>s 
		$("input[@validate], select[@validate], textarea[@validate]").each(function()
		{
			if(!this.validate)		// firefox needs the validate attribute to be set in code (just a quirk i think - ie automatically works)
			{
				this.validate = this.getAttribute('validate');
			}
			
			if(this.type && this.type == 'radio')
			{
				//this.onclick = function() { vtor.validate(this); }							// add validate() to the element's onselect
				$(this).click(function() { vtor.validate(this); });							// add validate() to the element's onselect
			}else{
				this.onchange = function() { vtor.validate(this); }							// add validate() to the element's onchange
			}
			this.validateMe = function() { return vtor.validate(this); }				// add validateMe() to the element itself = ie myElement.validateMe()
			this.isValid  = function() { return vtor.isValid(this); }					// add isValid() to the element itself = ie myElement.isValid()
			this.validationCode = function() { return vtor.validationCode(this); }		// add validationCode() to the element itself = ie myElement.validationCode()
		});
	}
	
	
	/*
		adds the class ".validationFailed" to the parent of the passed <input> element if it is invalid
		adds a class ".validationFailed#" where # is a number 1-8 corresponding to the validator(s) that failed
		*and* returns true if the passed element is valid (for convenience)
	//*/
	this.validate = function(element)
	{
		//var parent = $("..", element);
		var parent = vtor.getValidationFailedTarget(element);
		var code = this.validationCode(element);
		
		
		/*
			if(code == 0)		// if this element passed all validation tests
			{
				parent.removeClass('validationFailed');		// remove main failure class
				for(var i=1; i<=8; i++)						// remove all individual failure classes
				{
					parent.removeClass('validationFailed' + i);
				}
			}
			else
			{
				parent.addClass('validationFailed');	// add main validation failure class
				for(var i=0; i<8; i++)					// add individual failure classes for each failed validator
				{
					var mask = 1 << i;
					if((code & mask) == mask)
					{
						parent.addClass('validationFailed' + (i+1));
					}else{
						parent.removeClass('validationFailed' + (i+1));			// we need to remove any failure classes that may have been set on a previous test
					}
				}
			}
		/*/
		if(code == 0)							// if this element passed all validation tests...
		{
			parent.removeClass('validationFailed');		// ensure there is no main failure class
		}else{
			parent.addClass('validationFailed');		// add main validation failure class
		}
		
		for(var i=0; i<8; i++)					// process code for each failed validator
		{
			var mask = 1 << i;
			if((code & mask) == mask)
			{
				parent.addClass('validationFailed' + (i+1));			// if the validator failed add an individual failure class
			}else{
				parent.removeClass('validationFailed' + (i+1));			// if the validator passed ensure there is no individual failure class (we need to remove any failure classes that may have been set on a previous test)
			}
		}
		
		if(!window.noRecheckAllFailures)
		{
			vtor.recheckAllFailures();
		}
		//*/
		return (code == 0);
	}
	
	//* --- quick-fix hack - re-checks all validation and turns off any newly invalid failure messages (does not turn on any failure messages) ---
	this.recheckAllFailures = function()
	{
		elements = $("input[@validate], select[@validate], textarea[@validate]");			// default to all inputs with 'validate' attributes		
		var areValid = true;
		
		elements.each(function()
		{
			var parent = vtor.getValidationFailedTarget(this);
			var code = this.validationCode(this);
			
			for(var i=0; i<8; i++)					// process code for each failed validator
			{
				var mask = 1 << i;
				if((code & mask) != mask)
				{
					parent.removeClass('validationFailed' + (i+1));			// if the validator passed ensure there is no individual failure class (we need to remove any failure classes that may have been set on a previous test)
				}
			}
			
			if(code == 0)							// if this element passed all validation tests...
			{
				parent.removeClass('validationFailed');		// ensure there is no main failure class
			}
			
			areValid = (code == 0) && areValid;		// vtor.validate(this) must go first for highlighting to work (shortcut-and)
		});
		
		if(areValid)
		{
			$('body, form').removeClass('formValidationFailed');
		}
	}
	//*/
	
	
	/*
		returns true if the passed element is valid
	//*/
	this.isValid = function(element)
	{
		return (this.validationCode(element) == 0);
	}
	
	
	/*
		returns a packed integer indication of which validators passed and which failed
		code == 0 means the element passed validation
		code > 0 means the element failed validation
	//*/
	this.validationCode = function(element)
	{
		var mask = 1;
		var code = 0;
		var validators = (''+element.validate).split(' '); 					// this will break user-defined regexs that match an explicit space - beware
		
		if(!element.trim)
		{
			element.trim = element.getAttribute('trim');
		}
		if(element.trim && element.trim == 'true')
		{
			element.value = (''+element.value).replace(/^\s+|\s+$/g, '');		// trim
		}
		
		
		$(validators).each(function()
		{
			var validator = ''+this;
			var data = null;
			if(/^([^\(]+)\((.+)\)$/.test(validator))
			{
				validator = RegExp.$1;
				data = RegExp.$2;
			}
			
			if(!element.disabled)	// assume true if disabled
			{
				if(element.value.length > 0 || validator == 'req' || validator == 'required' || validator.substring(0,4) == "req:" || validator == 'nonZero')		// assume true if nothing is entered - unless a value is required
				{
					if(validator.substring(0,4) == "req:")
					{
						validator = validator.substring(4);
					}
					if(!vtor.validateValue(element.value, validator, data))
					{
						code |= mask
					}
				}
			}
			mask <<= 1;
		});
		
		if(code > 0 && element.onValidationFailed)
		{
			element.onValidationFailed(code)
		}
		return code;
	}
	
	
	/*
		returns true if all of the passed elements are valid
		*and* it adds the class '.formValidationFailed' to <body> if the form as a whole fails
		if no elements are passed it will assume 'input[@validate], select[@validate], textarea[@validate]'
	//*/
	this.allValid = function(elements)
	{
		if(!elements) { elements = $("input[@validate], select[@validate], textarea[@validate]"); }			// default to all inputs with 'validate' attributes		
		//vtor.areValid = true;
		var areValid = true;
		
		elements.each(function()
		{
			//vtor.areValid = vtor.validate(this) && vtor.areValid;		// vtor.validate(this) must go first for highlighting to work (shortcut-and)
			areValid = vtor.validate(this) && areValid;		// vtor.validate(this) must go first for highlighting to work (shortcut-and)
		});
		
		//var areValid = vtor.areValid;		// not sure if this is necessary
		//vtor.areValid = null;				// ... but it might highlight a concurrency problem?
		
		if(areValid)
		{
			$('body').removeClass('formValidationFailed');
		}else{
			$('body').addClass('formValidationFailed');
		}
		
		return areValid;
	}
	
	// a temporary working variable used by this.allValid();
	this.areValid = null;
	
	
	
	this.getValidationFailedTarget = function(element)
	{
		if(!element.vfTargetElement)
		{
			if(!element.validationFailedTarget)
			{
				element.validationFailedTarget = element.getAttribute('validationFailedTarget');
			}
			
			if(element.validationFailedTarget)
			{
				element.vfTargetElement = $(element.validationFailedTarget, element);
			}else{
				element.vfTargetElement = $("..", element);
			}
		}
		
		return element.vfTargetElement;
	}
	
	
	this.setErrorMsg = function(key, msg)
	{
		this.errorMsgs[key] = msg;
		//eval('vtor.errorMsgs.'+key+' = "' + msg +'";');
		//alert(this.errorMsgs[key]);
		//eval('alert(this.errorMsgs.' + key +')');
	}
	
	this.getErrorMsg = function(key)
	{
		return this.errorMsgs[key];
	}
	
	this.errorMsgs = {};
}


function out(msg)
{
	$('#out').append(msg + '<br/>')
}


function checkCardSecurityCode(cscnumber, cardname)
{
var ret = true;
//var cscErrorNo = 0;
//var cscErrors = new Array ()
 
//cscErrors [0] = "CSC is incorrect.";

//cscErrorNo = 0;

	switch(cardname.toLowerCase())
	{
		case "amex":
			if (cscnumber.length != 4)
				ret = false;	
			break;
		case "visa":
		case "mastercard":
		case "diner":
		case "dinersclub":
			if (cscnumber.length != 3)
				ret = false;	
			break;
		default:
			ret = true;
			break;
	}

//	alert(cardname);
     return ret; 
}
















/*============================================================================*/
 
/*
 
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
 
*/
 
/*
   If a credit card number is invalid, an error reason is loaded into the 
   global ccErrorNo variable. This can be be used to index into the global error  
   string array to report the reason to the user if required:
   
   e.g. if (!checkCreditCard (number, name) alert (ccErrors(ccErrorNo);
*/
 
var ccErrorNo = 0;
var ccErrors = new Array ()
 
ccErrors [0] = "Unknown card type";
ccErrors [1] = "No card number provided";
ccErrors [2] = "Credit card number is in invalid format";
ccErrors [3] = "Credit card number is invalid";
ccErrors [4] = "Credit card number has an inappropriate number of digits";
 
function checkCreditCard (cardnumber, cardname) {
 
     
  // Array to hold the permitted card characteristics
  var cards = new Array();
 
  // 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: "MasterCard", 
               length: "16", 
               prefixes: "51,52,53,54,55",
               checkdigit: true};
  cards [2] = {name: "DinersClub", 
               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};

//Additional card types from Money DB CardType table
  cards [12] = {name: "AmexPurchaseCard", 
               length: "15", 
               prefixes: "34,37",
               checkdigit: true};

  cards [13] = {name: "VisaDebit", 
               length: "13,16", 
               prefixes: "4",
               checkdigit: true};

  cards [14] = {name: "VisaPurchaseCard", 
               length: "13,16", 
               prefixes: "4",
               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; 
  }
  
  // Check that the number is numeric, although we do permit a space to occur  
  // every four digits. 
  var cardNo = cardnumber
  var cardexp = /^([0-9]{4})\s?([0-9]{4})\s?([0-9]{4})\s?([0-9]{1,4})$/;
  if (!cardexp.exec(cardNo))  {
     ccErrorNo = 2;
     return false; 
  }
    
  // Now remove any spaces from the credit card number
  cardexp.exec(cardNo);
  cardNo = RegExp.$1 + RegExp.$2 + RegExp.$3 + RegExp.$4;
       
  // 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;
}
 
/*============================================================================*/
