/*
	finalsiteValidator form validation
	Author: Charles Fahey
	Last Modified: 2009-11-09
	Version: 0.1
	
	requires: 
	1) jquery.json.js
	2) jquery.finalsiteValidator.cfm
	
*/

// configures our form elements for validation
jQuery.fn.fsValidator = function(options) {

	settings = jQuery.extend({ callback: null }, options);
	
	var regExp = new RegExp("fsValidate\[[a-zA-Z0-9,\(\)\-\=]+\]");
	
	// get our inputs to be validated
	var fieldsArray = jQuery(this).find(":input[class*=fsValidate],fieldset[class*=fsValidate]");
	// get collections of radios or checkboxes to be validated
	
	// for each of the validation elements
	jQuery.each( fieldsArray, function(){ 
	
		// extract the validation string
		var validateStr = regExp.exec( jQuery(this).attr("class") );
		
		// extract the validation list
		validateStr = validateStr[0].replace("fsValidate[", "");
		validateStr = validateStr.replace("]", "");
		// convert to array and attach to element
		jQuery(this).data( "validate", validateStr.split(",") );
		// also flag whether or not the field is required
		jQuery(this).data( "required", jQuery.inArray("required", jQuery(this).data( "validate" )) >= 0 );
		
	} );
	
};


// validates multiple elements by container id after batching them together (one call to server for all server-side validations)
jQuery.fn.fsValidateElements = function(options) {

	var options = jQuery.extend({ onFail: null, onPass: null }, options);
	
	var errorObj = new Object();
	var successObj = new Object();
	var coldFusionBatch = new Array();
	var coldFusionItem = new Object();
	var clientBatch = new Array();
	var clientItem = new Object();
	
	// get the elements
	var elems = jQuery(this).find("[class*=fsValidate]");
	
	// loop over the elements
	jQuery.each( elems, function(){
		
		var thisElem = jQuery(this);
		var fieldType = jQuery(thisElem).attr("type");
		
		// do we need to validate this?
		var validateThis = false;
		
		if ( 
			// if this is not required
			(!jQuery(thisElem).data( "required" )) 
			// but does have a value
			&& (jQuery(thisElem).val().length > 0) 
			// and is not among the specially validated elements
			&& (jQuery.inArray(fieldType, fsSpecialTypes) < 0) ) {
				
				// we need to validate
				validateThis = true;
		} else if (
			// if this is required
			jQuery(thisElem).data( "required" ) ) {
				validateThis = true;
		}
		
		// if this has been disabled
		if ( jQuery(thisElem).attr( "name" ) && jQuery(thisElem).attr( "name" ).indexOf( "_disabled" ) >= 0 ) {
			validateThis = false;
		}
		
		
		// if we need to validate this element, go to it
		if (validateThis) {
			
			// make sure there is a validation array for this element
			if ( jQuery.isArray( jQuery(thisElem).data( "validate" ) ) ) {
				
				// loop over our validation rules
				jQuery.each( jQuery(thisElem).data( "validate" ), function(){
					
					var fileTypeRegExp = /fileType\([a-zA-Z0-9\-]+\)/;
					var isFileType = fileTypeRegExp.test( this );
					
					var customRegExp = /custom\([a-zA-Z0-9\-\=]+\)/;
					var isCustom = customRegExp.test( this );

					// if this is not a fileType or custom validation
					// -- we'll save this validation to develop at a later date
					if ( !isFileType && !isCustom ) {
					
						// queue up our coldfusion validations
						if ( fsValidationRules[this].useColdFusion ) {
							
							coldFusionItem = new Object();
							coldFusionItem.elementID = jQuery(thisElem).attr("id");
							coldFusionItem.value = jQuery(thisElem).val();
							coldFusionItem.rule = this;
							
							coldFusionBatch.push( coldFusionItem );
							
						// and our client-side
						} else {
							
							clientItem = new Object();
							clientItem.elementID = jQuery(thisElem).attr("id");
							clientItem.value = jQuery(thisElem).val();
							clientItem.rule = this;
							clientItem.elementType = jQuery(thisElem).attr("type");
							
							clientBatch.push( clientItem );
							
						}
					
					}
					
					// custom validation is always client side, but it does not have a setting in the fsValidationRules because it is always custom
					if ( isCustom ) {
						clientItem = new Object();
						clientItem.elementID = jQuery(thisElem).attr("id");
						clientItem.value = jQuery(thisElem).val();
						clientItem.rule = this;
						clientItem.elementType = jQuery(thisElem).attr("type");
						clientBatch.push( clientItem );
					}
				
				} )
			}
			
		}
		
	} );
	
	// if there are server-side validations to perform
	if ( coldFusionBatch.length ) {
		
		// we pass our validation request along to ColdFusion to maintain integrity between clientside and serverside for complex validations
		// here we turn off async to force a wait for the server
		var coldFusionReturn = jQuery.ajax( { 
				url: jsPath + "jQuery/plugins/jquery.finalsiteValidator.cfm", 
				type: "POST", 
				data: ( { cmd: "fsValidate", items: jQuery.jSONToString( coldFusionBatch ) } ),
				async: false,
				dataType: "json" 
			} ).responseText;
			
		// expand our coldfusion return
		coldFusionReturn = jQuery.toJSON( coldFusionReturn );
		
		// loop over the returned structure (element ids)
		for (var elementID in coldFusionReturn) {
			
			// loop over its sub-structure (rules)
			for (var ruleName in coldFusionReturn[elementID]) {
				
				// if this elementID is not already being tracked
				if ( !jQuery.isArray( errorObj[elementID] ) ) {
					// add it to the error object
					errorObj[elementID] = new Array();
				}
				
				// set our error message
				errorObj[elementID].push( fsValidationRules[ruleName].alertMsg );
			}
			
		}
	}
	
	// now run our client-side stuff
	if ( clientBatch.length ) {
		
		// in this case we'll just loop through each element
		jQuery.each( clientBatch, function(){
			
			// if validation fails
			if ( !jQuery.fn.fsValidateValToRule( this.elementType, this.elementID, this.value, this.rule ) ) {
				
				// if this elementID is not already being tracked
				if ( !jQuery.isArray( errorObj[this.elementID] ) ) {
					// add it to the error object
					errorObj[this.elementID] = new Array();
				}
				
				// set our error message
				if ( this.rule.indexOf("custom") == 0 ) {
					errorObj[this.elementID].push( "Validation Error" );
				} else {
					errorObj[this.elementID].push( fsValidationRules[this.rule].alertMsg );
				}
				
			}
			
		})
		
	}
	
	// find the elements which did not produce errors
	jQuery.each( coldFusionBatch, function(){
		if ( !jQuery.isArray( errorObj[this.elementID] ) ) {
			successObj[this.elementID] = true;
		}
	})
	jQuery.each( clientBatch, function(){
		if ( !jQuery.isArray( errorObj[this.elementID] ) ) {
			successObj[this.elementID] = true;
		}
	})
	
	// if we have a callback function for passed validation
	if (typeof options.onPass == "function") { 
		// call it
		options.onPass( successObj ) 
	}
	
	// if we have a callback function for failed validation
	if (typeof options.onFail == "function") { 
		// call it
		options.onFail( errorObj ) 
	
	// otherwise we just return the error object
	} else {
		return errorObj;
	}
	
};

// validates a value against a rule
jQuery.fn.fsValidateValToRule = function( elementType, elementID, val, rule ) {
	
	var returnBool = true;
	var validateElem = jQuery("#" + elementID);
	
	// if this is a fieldset
	if ( jQuery(validateElem).tagName() == "fieldset" ) {

		// grab our radios or checkboxes (whose name attributes will match the element id attribute passed to this function)
		var validateArray = jQuery(validateElem).find(":radio[name=" + elementID + "],:checkbox[name=" + elementID + "]");
		
		// required is the only rule for now
		if ( rule == "required" ) {
			var checkCount = jQuery(validateElem).find(":radio[name=" + elementID + "]:checked,:checkbox[name=" + elementID + "]:checked");
			
			// if nothing was checked, return false unless this is hidden
			if ( checkCount.length == 0 ) { returnBool = ( jQuery(validateElem).find(":radio, :checkbox").eq(0).attr( "name" ).indexOf( "_disabled" ) >= 0 ) }
		}
	
	// if our element does not need special handling
	} else if ( (jQuery.inArray(elementType, fsSpecialTypes) < 0) ) {
		
		// we handle required specifically
		if ( rule == "required" ) {
			
			if ( !jQuery.trim( val ).length ) { returnBool = false; }
			
		// we handle multi-point specifically
		} else if ( rule.indexOf("custom") == 0 ) {
		
			// in this case our custom rule has a base64 encoded rule string inside the parenthesis
			var ruleStr = rule.replace( "custom(", "" ).replace( ")", "" );
			var ruleJSON = $j.base64Decode( ruleStr );
			var validationSettings = $j.toJSON( ruleJSON );
			
			var vdtRegEx = "";
			var vdtCount = "*";
			var vdtSpace = "";
			
			// type of validation equates with a regex
			switch( validationSettings.type ) {
				
				case "alpha" :
					// here's the basic regex we're using
					vdtRegEx = "a-zA-Z";
					break;
				
				case "alphanumeric" :
					vdtRegEx = "a-zA-Z\\d";
					break;
				
				case "numeric" :
					vdtRegEx = "\\d";
					break;
				
				case "advanced" :
					vdtRegEx = validationSettings.chars;
					break;
					
			}

			// we may also be testing for a range of length
			if ( validationSettings.len.on ) {
				vdtCount = "{" + validationSettings.len.min + "," + validationSettings.len.max + "}";
			}
			
			// we may be allowing spaces (not for numerics)
			if ( validationSettings.spaces && validationSettings.type != "numeric" ) {
				vdtSpace = "\\s";
			}
			
			// build our regex pattern
			var validateRegEx = new RegExp( "^[" + vdtRegEx + vdtSpace + "]" + vdtCount + "$" );
			
			// test it
			var passValidation = validateRegEx.test( jQuery.trim( val ) );
			
			// if it didn't pass
			if ( !passValidation ) { returnBool = false; }
			
			// if we are also testing for a numeric range (and haven't already failed)
			if ( returnBool && validationSettings.type == "numeric" && validationSettings.num.on ) {
				
				// if we are outside our value range
				if ( isNaN( val*1 ) || val*1 < validationSettings.num.min || val*1 > validationSettings.num.max ) {
					returnBool = false;
				}
				
			}
			
		// all others will be regex-based for now
		} else {
			
			// test our regex
			returnBool = fsValidationRules[rule].regex.test(jQuery.trim( val ))
			
		}
		
	// special handling for certain types
	} else {
		
		// the only expected rule for these types is "required" -- for now anyway
		if ( rule == "required" ) {
			
			switch( elementType ) {
				
				case "radio" :
					// we *should* be able to count on the the parent form to contain all matching elements
					var parentContainer = jQuery(validateElem).parents("form");
					// we need to find associated radio buttons
					var radioName = jQuery(validateElem).attr("name");
					var radioArray = jQuery(parentContainer).find(":radio[name=" + radioName + "]:checked");
					
					// there should be at least one checked
					returnBool = ( radioArray.length > 0 );
					break;
					
				case "checkbox" :
					var parentContainer = jQuery(validateElem).parents("form");
					var checkName = jQuery(validateElem).attr("name");
					var checkArray = jQuery(parentContainer).find(":checkbox[name=" + checkName + "]:checked");
					returnBool = ( checkArray.length > 0 );
					break;
					
				case "select-one" :
					// if a select is required, then there must be such a thing as an invalid selection, probably the first one
					returnBool = ( jQuery("#" + elementID + " option").index(jQuery("#" + elementID + " option:selected")) > 0 );
					break;
					
				case "select-multiple" :
					// if a select is required, then there must be such a thing as an invalid selection, probably the first one
					alert( jQuery("#" + elementID + " option").index(jQuery("#" + elementID + " option:selected")) );
					break;
			
			};

		}
	}
	
	return returnBool;
	
};

jQuery.fn.tagName = function() {
    return this.get(0).tagName.toLowerCase();
}

// these types of elements are handled specially
fsSpecialTypes = [ "radio", "checkbox", "select-one", "select-multiple" ];

// all our validation rules
fsValidationRules = {

	"creditcard": {
		"useColdFusion": true,
		"regex":"",
		"alertMsg":"* Invalid credit card number" },
	"dutchZip": {
		"useColdFusion": false,
		"regex":/^[1-9]\d{3}\s?[a-zA-Z]{2}$/,
		"alertMsg":"* Must be valid Dutch postal code" },
	"email": {
		"useColdFusion": false,
		"regex":/^[a-zA-Z0-9_\.\-]+\@([a-zA-Z0-9\-]+\.)+[a-zA-Z0-9]{2,4}$/,
		"alertMsg":"* Invalid email address" },	
	"eurodate": {
		"useColdFusion": true,
		"regex":"",
		"alertMsg":"* Invalid date" },
	"USdate": {
		"useColdFusion": true,
		"regex":"",
		"alertMsg":"* Invalid date" },
	"integer": {
		"useColdFusion": true,
		"regex":"",
		"alertMsg":"* Must be an integer" },
	"monetary": {
		"useColdFusion": false,
		"regex":/^[-]?\d+(\.(\d{2}))?$/,
		"alertMsg":"* Must be a monetary value" },
	"numeric": {
		"useColdFusion": true,
		"regex":"",
		"alertMsg":"* Must be a numeric value" },
	"onlyAlphaNum": {
		"useColdFusion": false,
		"regex":/^[a-zA-Z0-9]+$/,
		"alertMsg":"* Letters &amp; Numbers only" },
	"onlyNumber": {
		"useColdFusion": false,
		"regex":/^[0-9\ ]+$/,
		"alertMsg":"* Numbers only" },	
	"onlyLetter": {
		"useColdFusion": false,
		"regex":/^[a-zA-Z\ \']+$/,
		"alertMsg":"* Letters only" },
	"required": {
		"useColdFusion": false,
		"regex":"",
		"alertMsg":"* This field is required",
		"alertMsgRadio":"* Please select an option",
		"alertMsgCheckbox":"* This checkbox is required",
		"alertMsgCheckboxMulti":"* You must select at least one checkbox",
		"alertMsgSelect":"* This checkbox is required" },
	"ssn": {
		"useColdFusion": true,
		"regex":"",
		"alertMsg":"* Invalid Social Security Number" },
	"telephone": {
		"useColdFusion": true,
		"regex":"",
		"alertMsg":"* Must be valid U.S. Telephone Number" },	
	"url": {
		"useColdFusion": true,
		"regex":"",
		"alertMsg":"* Invalid URL" },	
	"urlClientSide": {
		"useColdFusion": false,
		"regex":/^((?:http|https):\/\/[a-z0-9\/\?=_#&%~-]+(\.[a-z0-9\/\?=_#&%~-]+)+)|(www(\.[a-z0-9\/\?=_#&%~-]+){2,})$/,
		"alertMsg":"* Invalid URL" },	
	"zipcode": {
		"useColdFusion": true,
		"regex":"",
		"alertMsg":"* Must be a valid 5 or 9 digit U.S. zipcode" }
	};
			

