// class definition AdzFormValidator and AdzFormElement
// version 0.16 updated 21-01-2005
// Author: Adam Royle (AdzFormValidator at ifunk.net)
// class maintained at http://ifunk.net/AdzFormValidator/
// you must keep the above lines intact if you wish to use this class
// see other conditions at the website above
// you can remove any comments below

/**********************************
 THINGS TO DO:
 - add focus() to radio, checkbox, and select input types
 - Check compatibility on all browsers and input types
 - add ability to compare and restrict based on multiple form elements
 - turn restrict conditions into an arguments list so can have unlimited parameters
 - use prototyping for each validation type to allow easy extending
 - add addElementsByType() method

 
 THINGS TO PONDER:
 - have regex detection, and non-regex validation ??

 THINGS DONE
 - added param for type currency (v0.15)
 - add no param for type int (and check others) (v0.14)
 - find and use (or write) a really cool and correct email regex - DONE (20-7-04)
***********************************/

function AdzFormValidator(form){
	this.useForm(form);
	this.elements = new Array();
	this.isValid = false;
	this.select = true;
	this.focus = true;
	this.func = null;
	this.func_alert = null;
}

function AdzFormElement(name){
	this.name = name;
	this.desc = name;
	this.required = false;
	this.trim = false; // only used with text fields
	this.stripWhite = false; // only used with text fields
	this.isValid = false;
	this.alert = "The field '$desc' is required";
	this.select = true;
	this.focus = true;
	this.value = null;
}

AdzFormElement.prototype.validate = function(element){
	
	var i, objForm;
	
	this.type = element.type;
	this.value = element.value;

	if (!this.type) this.type = element[0].type;

	this.isValid = true;

	objForm = element.form;
	
	// -- check if field is empty

	switch (this.type){

		case "text":
		case "textarea":
		case "hidden":
		case "file":
		case "password":
			if (this.stripWhite) element.value = element.value.replace(/\s/g,'');
			if (this.trim) element.value = element.value.trim();
			this.data = !isBlank(element.value);
			this.empty = (element.value == '');
			break;

		case "checkbox":
		case "radio":
			this.empty = !(this.data = false);
			if (element.checked){
				this.empty = !(this.data = true);
				break;
			}
			for (i=0;i<element.length;i++){
				if (element[i].checked){
					this.empty = !(this.data = true);
					break;
				}
			}
			break;

		case "select-multiple":
			this.empty = !(this.data = false);
			for (i=0;i<element.length;i++) {
				if (element[i].selected){
					this.empty = !(this.data = true);
					break;
				}
			}
			break;

		default:
			// can't detect a valid / invalid response
			this.empty = !(this.data = true);
	}
	
	if (this.required && !this.data){
		//alert("required, no data");
		this.isValid = false;
	} else if (!this.required && this.empty){
		//alert("not required, is empty");
		this.isValid = true;
	} else if (this.data){
		//alert("has data, may or may not be required");
		if (!this.restrictType){
			//alert("has data, may or may not be required - no restrict");
			this.isValid = true;
		} else {
			//alert("has data, may or may not be required - has restrict");
			// -- validate the restriction
			switch (this.restrictType.toLowerCase()){

				case "numeric":
				case "number":
				case "num":
					this.isValid = isNumeric(element.value);
					if (this.isValid && isString(this.restrictCond)) this.isValid = eval(this.restrictCond.replace(/n/g,element.value));
					break;
				
				case "int":
				case "integer":
					this.isValid = isInteger(element.value);
					if (this.isValid && isString(this.restrictCond)) this.isValid = eval(this.restrictCond.replace(/n/g,element.value));
					break;
					
				case "length":
					this.isValid = eval(this.restrictCond.replace(/n/g,element.value.length));
					break;
	
				case "selected":
					this.isValid = eval(this.restrictCond.replace(/n/g,element.selectedIndex));
					break;
	
				case "email":
					this.isValid = isEmail(element.value);
					break;
	
				case "regex":
					this.restrictCond = isString(this.restrictCond) ? new RegExp(this.restrictCond,"i") : this.restrictCond;
					this.isValid = this.restrictCond.test(element.value);
					break;
					
				case "in_array":
					this.isValid = this.restrictCond.in_array(element.value);
					break;
					
				case "!in_array":
					this.isValid = !this.restrictCond.in_array(element.value);
					break;
					
				case "currency":
					this.isValid = isCurrency(element.value);
					if (this.isValid && isString(this.restrictCond)) this.isValid = eval(this.restrictCond.replace(/n/g,element.value.replace(/[\$,]/g,'')));
					break;
					
				case "date":
					this.isValid = validateDateString(element.value,this.restrictCond);
					break;
				
				case "value":
					this.isValid = (element.value == this.restrictCond);
					break;
					
				case "!value":
					this.isValid = (element.value != this.restrictCond);
					break;
				
				case "abn":
					this.isValid = isABN(element.value);
					break;
				
				case "acn":
					this.isValid = isACN(element.value);
					break;
					
			}
		}
	} else {
		alert("else");
		this.isValid = true;
	}
	
	//alert("field "+element.name+" is "+(this.isValid?"valid":"invalid"));
	
	return this.isValid;
}

AdzFormElement.prototype.restrict = function(type,cond){
	this.restrictType = type;
	this.restrictCond = cond;
}

AdzFormElement.prototype.showAlert = function(){
	var strAlert = this.alert;
	if (strAlert !== false){
		strAlert = strAlert.replace(/\$desc/g, this.desc);
		strAlert = strAlert.replace(/\$name/g, this.name);
		strAlert = strAlert.replace(/\$type/g, this.type);
		alert(strAlert);
	}
}

AdzFormValidator.prototype.useForm = function(form){
	if (typeof form != "undefined"){
		if (typeof form == "object"){
			this.form = form;
		} else if (typeof form == "string" || typeof form == "number"){
			this.form = document.forms[form];
		} else {
			alert("AdzFormValidator :: DEVELOPER ALERT :: useForm()\n\nInvalid form definition");
		}
	}
}

AdzFormValidator.prototype.add = function(){
	var i, args, name;
	
	args = AdzFormValidator.prototype.add.arguments;
	
	// add all the fields specified in the arguments of the function
	for (i=0;i<args.length;i++){
		name = args[i];
		if (!this[name] && !isBlank(name)){
			this[name] = new AdzFormElement(name);
			this.elements[this.elements.length] = this[name];
		}
	}
}

AdzFormValidator.prototype.addAll = function(){
	var i, field;

	if (!this.form){
		alert("AdzFormValidator :: DEVELOPER ALERT :: addAll()\n\nNo form selected!\n\nuse: obj.useForm('formName');");
	} else {
		// run through all of the form elements and add each one that has a name and is not a button
		for (i=0;i<this.form.elements.length;i++){
			field = this.form.elements[i];
			if (!isBlank(field.name)){
				if (field.type != "button" && field.type != "submit" && field.type != "reset"){
					this.add(field.name);
				}
			}
		}

	}

}

AdzFormValidator.prototype.remove = function(){
	var i, j, temp_array, args, name;
	
	args = AdzFormValidator.prototype.remove.arguments;
	
	temp_array = new Array();
	
	// remove all the fields specified in the arguments of the function
	for (j=0;j<args.length;j++){
		name = args[j];
	 	for (i=0;i<this.elements.length;i++){
	 		if (this.elements[i].name != name){
				temp_array[temp_array.length] = this.elements[i];
				delete this[name];
	     	}
	  	}
	}
	this.elements = temp_array;
}

AdzFormValidator.prototype.removeElementsByType = function(){
	var i, j, args;
	
	args = AdzFormValidator.prototype.removeElementsByType.arguments;
	
	// traverse through form and remove from AdzFormValidator object
	// if type of element is in the function arguments
	for (i=0;i<args.length;i++){
		for (j=0;j<this.form.elements.length;j++){
			if (this.form.elements[j].type == args[i]){
				this.remove(this.form.elements[j].name);
			}
		}
	}
}

AdzFormValidator.prototype.removeAll = function(){
	var i;

	// remove elements from validation object
	for (i in this.elements){
		delete this[this.elements[i].name];
	}

	// reset elements array
	this.elements = new Array();
}

AdzFormValidator.prototype.describe = function(){
	var element, i;
	var strMsg = "";

	if (this.elements.length == 0){
		strMsg += "No form elements!";
	} else {
		strMsg += "List of form elements:\n";
		for (i=0;i<this.elements.length;i++){
			element = this.elements[i];
			strMsg += " - "+element.name+" : "+element.restrictType+"\n";
		}
	}
	alert(strMsg);
}

AdzFormValidator.prototype.setGlobal = function(){
	
	var args, setType, setValue, element, i;
	
	args = AdzFormValidator.prototype.setGlobal.arguments;
	
	if (args.length <= 1){
		alert("AdzFormValidator :: DEVELOPER ALERT :: setGlobal()\n\nIncorrect number of arguments supplied.");
		return false;
	}
	
	setType = args[0];
	setValue = args[1];
	
	// run through all of the form elements and add each one that has a name and is not a button
	for (i=0;i<this.elements.length;i++){
		element = this.elements[i];
		
		switch (setType.toLowerCase()){
			
			// put custom ones in here
			
			// generic
			
			default:
				element[setType] = setValue;
				break;
			
		}
		
	}

}

AdzFormValidator.prototype.setUserFunction = function(func,func_alert){
	this.func = func;
	this.func_alert = func_alert;
}


AdzFormValidator.prototype.validate = function(){
	var element, elementName, formElement, i;
	
	if (!this.form){
		alert("AdzFormValidator :: DEVELOPER ALERT :: validate()\n\nNo form selected!\n\nuse: obj.useForm('formName');");
		this.isValid = false;
		return false;
	} else {
		// -- start the validation
		this.isValid = true;
		for (i=0;i<this.elements.length;i++){
			
			element = this.elements[i];
			elementName = element.name;
			formElement = this.form.elements[elementName];
			
			// -- return false if invalid element
			if (formElement.type == "button" || formElement.type == "submit" || formElement.type == "reset"){
				this.debug(this.DEBUG_DEV,"validate()","Form element '"+formElement.name+"' is of an unacceptable type.\n\nAdzFormValidator will not validate buttons.");
				this.isValid = false;
				return false;
			}

			// -- validate the form element
			if ( !element.validate(formElement) ) {

				this.isValid = false;
				element.showAlert();
				if (this.focus && element.focus && formElement.focus && !formElement.disabled && formElement.type != "hidden") formElement.focus();
				if (this.select && element.select && formElement.select && !formElement.disabled && formElement.type != "hidden") formElement.select();
				return false;
			}
		}
		
		// -- call the user defined function to validate more advanced stuff
		if (this.func){
			if (!this.func(this.form)){
				if (this.func_alert) alert(this.func_alert);
				this.isValid = false;
			}
		}
		
		return this.isValid;
	}
}

// these are all our aliases (for backwards compatibility)

AdzFormValidator.prototype.addAllElements = AdzFormValidator.prototype.addAll;
AdzFormValidator.prototype.removeAllElements = AdzFormValidator.prototype.removeAll;
AdzFormValidator.prototype.addFormElement = AdzFormValidator.prototype.add;
AdzFormValidator.prototype.removeFormElement = AdzFormValidator.prototype.remove;

AdzFormValidator.prototype.DEBUG_DEV = 1;

AdzFormValidator.prototype.debug = function(type,method,message){
	switch (type){
		case this.DEBUG_DEV:
			alert("!! DEVELOPER ALERT !!\n\n"+message);
			break;
	}
}

AdzFormElement.prototype.debug = AdzFormValidator.prototype.debug;

// validation checks


function isCurrency(s){
	var re = /^\$?\d{1,3}(,?\d{3})*(\.\d{1,2})?$/;
	return re.test(s);
}
/* old function 
function isEmail(s) {
	var re = /^[\w-\._]{1,}\@([\da-zA-Z-]{1,}\.){1,}[\da-zA-Z-]{2,3}$/i;
	return re.test(s);
}
*/
function isEmail(s){
	var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
	return re.test(s);
}

function isNumeric(s){
	var re = /^[\-]?[0-9]+[.]?[0-9]*$/;
	return re.test(s);
}

function isInteger(s){
	var re = /^[\-]?[0-9]+$/;
	return re.test(s);
}
/*
function isAlpha(s){
	var re = /^[\-]?[0-9]+[.]?[0-9]*$/;
	return re.test(s);
}

function isAlphaNumeric(s){
	var re = /^[\-]?[0-9]+[.]?[0-9]*$/;
	return re.test(s);
}*/

function isBlank(s){
	var re = /^\s*$/;
	return re.test(s);
}


/*******
 These functions found at http://www.crockford.com/javascript/remedial.html
*******/
function isAlien(a) {
   return isObject(a) && typeof a.constructor != 'function';
}
function isArray(a) {
    return isObject(a) && a.constructor == Array;
}
function isBoolean(a) {
    return typeof a == 'boolean';
}
function isEmpty(o) {
    var i, v;
    if (isObject(o)) {
        for (i in o) {
            v = o[i];
            if (isUndefined(v) && isFunction(v)) {
                return false;
            }
        }
    }
    return true;
}
function isFunction(a) {
    return typeof a == 'function';
}
function isNull(a) {
    return typeof a == 'object' && !a;
}
function isNumber(a) {
    return typeof a == 'number' && isFinite(a);
}
function isObject(a) {
    return (a && typeof a == 'object') || isFunction(a);
}
function isString(a) {
    return typeof a == 'string';
}
function isUndefined(a) {
    return typeof a == 'undefined';
}
/*******/

function isABN(s){
	var m, as, i, sum = 0;
	m = [10,1,3,5,7,9,11,13,15,17,19];
	s = String(s).replace(/\s/g,'');
	if (s.length != 11) return false;
	as = s.split('');
	as[0] = String(parseInt(as[0]) - 1);
	for (i=0;i<as.length;i++) sum += parseInt(as[i]) * m[i];
	return (sum % 89 == 0);
}

function isACN(s){
	var m, as, i, sum = 0, r;
	m = [8,7,6,5,4,3,2,1];
	s = String(s).replace(/\s/g,'');
	if (s.length != 9) return false;
	as = s.split('');
	for (i=0;i<8;i++) sum += parseInt(as[i]) * m[i];
	r = 10 - (sum % 10);
	if (r == 10) r = 0;
	return (r == parseInt(as[8]));
}

String.prototype.trim = function(){
	return this.replace(/^\s+/,'').replace(/\s+$/,'');
}

Array.prototype.in_array = function(value){
	for (var i=0;i<this.length;++i)
		if (this[i].toString().toLowerCase() == value.toString().toLowerCase()) return true;
	return false;
}

if (!Array.prototype.push){
	Array.prototype.push = function(value){
		this[this.length] = value;
	}
}

function defangRegexChars(strInput){
	var find = [ "\\", ".", "^", "$", "+", "*", "?", "(", ")", "[", "]", "{", "}" ];
	for (i=0;i<find.length;i++)
		strInput = strInput.replace( new RegExp("\\"+find[i],"g") , "\\"+find[i] );
	return strInput;
}

function getFullYear(y){
	var iy = parseInt(y,10);
	if ( isNaN(iy) || iy < 0 || iy > 99 ) return y; // no hard feelings
	return iy + (iy < 50 ? 2000 : 1900);
}

function isValidDate(y,m,d){
	if (!y || !m) return false;
	if (typeof(d) == 'undefined') d = 1;
	m--; // month is zero-indexed
	var oDate = new Date(y,m,d);
	if (oDate.getDate() == d &&
		oDate.getMonth() == m &&
		oDate.getFullYear() == y) return true;
	return false;
}

function validateDateString(strDate,strPattern){
	
	var d, m, y, yy, yyyy;
	var re, h, i, j;
	var aDate, aPositions, aMatches;
	var aMatchList, aMatchListValues, strPatternPos, tmpArr;
	
	// quick check
	
	if (!strPattern || !strDate) return false;
	
	// set our replacement regex
	
	d = "([0-3]?[0-9])";
	m = "([0-1]?[0-9])";
	y = "([0-9]{2}|[0-9]{4})";
	yy = "([0-9]{2})";
	yyyy = "([0-9]{4})";
	
	// preliminary replacements (defang regex chars);
		
	strPattern = defangRegexChars(strPattern);
	
	// we may need this later (to work out positioning)
	
	strPatternPos = strPattern;
	
	// create our pattern
	
	strPattern = strPattern.replace(/d+/gi,'d');
	strPattern = strPattern.replace(/d/gi,d);
	strPattern = strPattern.replace(/m+/gi,'m');
	strPattern = strPattern.replace(/m/gi,m);
	strPattern = strPattern.replace(/y{4,}/gi,'yyyy');
	strPattern = strPattern.replace(/y{4}/gi,yyyy);
	strPattern = strPattern.replace(/y{2,}/gi,'yy');
	strPattern = strPattern.replace(/y{2}/gi,yy);
	strPattern = strPattern.replace(/y/gi,y);
	
	// test our pattern against the string
	
	re = new RegExp('^'+strPattern+'$');
	if (!re.test(strDate)) return false;
	
	// extract date portions from our string
	
	aMatches = re.exec(strDate);

	// structure replacements
	
	strPatternPos = "#"+strPatternPos+"#";
	strPatternPos = strPatternPos.replace(/d+/gi,'#d#');
	strPatternPos = strPatternPos.replace(/m+/gi,'#m#');
	strPatternPos = strPatternPos.replace(/([^y])y([^y])/gi,'$1#y#$2');
	strPatternPos = strPatternPos.replace(/([^y])y{2,3}([^y])/gi,'$1#yy#$2');
	strPatternPos = strPatternPos.replace(/y{4,}/gi,'#yyyy#');
	
	aMatchList = ['#d#','#m#','#y#','#yy#','#yyyy#'];
	aMatchListValues = ['d','m','y','y','y'];

	tmpArr = [];
	for (h=0;h<aMatchList.length;h++){
		for (i=0;i<strPatternPos.length;i++){
			j = strPatternPos.indexOf(aMatchList[h],i);
			if (j == -1) break; // none of this character - go to next character
			// record this info - a match has been found
			tmpArr[j] = aMatchListValues[h];
			i = j;
		}
	}

	aPositions = [null]; // need this value so the index reference against aMatch is right
	for (i=0;i<tmpArr.length;i++) if (tmpArr[i]) aPositions.push(tmpArr[i]);

	aDate = [];
	for (i=0;i<aPositions.length;i++) aDate[aPositions[i]] = aMatches[i];

	return isValidDate(getFullYear(aDate.y), aDate.m, aDate.d);
	
}