/**
* Simple javascript validation.
*
* Requires Prototype.js
*
* If a field has the 'validate' classname, or the custom classname, it will automatically be checked for 'presence
* of'.  Select boxes and check boxes will be checked for their appopriate properties.  Other tags available below. 
* The custom class name can be overridden, in case you have conflicting CSS classes(ie. already using the term validate for something)
*  
*
* Initial selector must come first in the chain of css classes, ie. validate || validate-html || validate-url-html || customClassName-url
*
* Initilization:
* var example = new Validation();    ||    var example = new Validation({ 'customSelectorClass' : 'magicClassName', 'customRegex' : '/myRegex/' }); 
* var example = new Validation({ 'customSelectorClass' : '.magicClassName', 'customErrors' : { 'presence' : 'Fill in the field!' } })
* 
* customSelectorClass  => Class used to grab all elements.  Appended to the front of the css classname, as shown in the examples.
* customErrorClass     => Used for assigning custom error css classname. 
* customRegex          => Used in conjunction with the custom-regex class suffix for custom validation.
* customErrors : {presence: 'No blanks!'} => Will accept - presence, email, url, checked, select, html, customRegex, numeric, phone, alpha, alphnum, equal
*                lengthLess, lengthGreater (The lengths append the limits to the end, so keep that in mind.)
* 
* Usage:
*
* 	if(example.validate()){
*		$('myForm').submit();
*	}
*
*   -- OR --
*
*	<input type="submit" id="formSubmitter" value="Submit" onclick="return example.validate();" />
*
* Class Names:
* validate || custom class    =>  Validates presence of 
* -email                      =>  Validates email format of
* -url                        =>  Validates URL format of
* -html                       =>  Validates that input is not html.
* -custom-regex               =>  Validates custom regex
* -alpha                      =>  Validates that input is a letter
* -numeric                    =>  Validates that input is numeric
* -alphnum                    =>  Validates that input is alphanumeric
* -equal                      =>  Validates if all elements with this tag are equal. [Requires a name!!]
* -override-presence          =>  Overrides original presence of (in case we want to only validate format and field is not required)
* -length-greater-[ 1-5 digits ] => Validates that the filed is NOT greater than the supplied number
* -length-less-[ 1-5 digits ]    => Validates that the filed is NOT less than the supplied number
* -length-exact-[ 1-5 digits ]   => Validates that the filed is exactly equal to number supplied
* -radio                      => Validates at least one radio is selected [Requires a name!!!!]
*
* Example:
*   <input type="text" id="websiteURL" class="validate-url-override-presence" />   =>  validates url format but not presence
*   <input type="text" id="name" class="validate-html-email-length-less-6" />      =>  validates email format of, length of at least 6 and checks for html tags(error if they exist)
*   <input type="text" id="name" class="validate" />                               =>  simple presence of check
*/

var Validation = Class.create({
	initialize : function(params) {
		//Check to see if we've been passed params in JSON. If not, create empty object.
		var params            = params || {};
		params.customErrors   = params.customErrors || {};
		//Determine whether we're using the default or a custom selector
		this.selector					=	(null != params.customSelectorClass && typeof(params.customSelectorClass) != 'undefined' && params.customSelectorClass !='') 	? 
										'*[class*='+params.customSelectorClass+']' : 
										'*[class*=validate]';
						
		//Determine whether we're using the default or a custom error class name.
		this.errorClass					=	params.customErrorClass || 'error-validation';
		//Use custom errors, if provided, or fall back to default.
		this.errorMessage				= {
			'presence'        :  params.customErrors.presence       || 'Cannot be blank.',
			'email'           :  params.customErrors.email          || 'Must be a valid email address.',
			'url'             :  params.customErrors.url            || 'Must be a valid URL. (ie. http://www.your-url.com)',
			'checked'         :  params.customErrors.checked        || 'Required',
			'radio'           :  params.customErrors.radio          || 'Must select one option.',
			'select'          :  params.customErrors.select         || 'Must select an option.',
			'html'            :  params.customErrors.html           || 'Must not contain HTML.',
			'customRegex'     :  params.customErrors.customRegex    || 'Custom parameters not met.',
			'numeric'         :  params.customErrors.numeric        || 'Must contain numbers only.',
			'alpha'           :  params.customErrors.alpha          || 'Must contain letters only.',
			'alphnum'         :  params.customErrors.alphanum       || 'Must contain letters and numbers only.',
			'equal'           :  params.customErrors.equal          || 'Fields do not match.',
			'lengthLess'      :  params.customErrors.lengthLess     || 'Minimum character limit: ',
			'lengthGreater'   :  params.customErrors.lengthGreater  || 'Maximum character limit: ',
			'lengthExact'     :  params.customErrors.lengthGreater  || 'Characters must equal: '
		};
		//Regex 
		this.regex							= {
			'html'	        : /<([A-Z][A-Z0-9]*)[^>]*>(.*?)<\/\1>/i,  
			'url'           : /^(http|https|ftp):\/\/(([A-Z0-9][A-Z0-9_-]*)(\.[A-Z0-9][A-Z0-9_-]*)+)(:(\d+))?\/?/i, 
			'email'         : /^[-_a-zA-Z0-9.]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,6}$/, 
			'numeric'       : /^[0-9]*$/, 
			'alpha'         : /^[a-zA-Z]*$/, 
			'alphnum'       : /^[a-zA-Z0-9]*$/, 
			'custom'        : params.customRegex  || null
		}

	},
	
	validate : function(){
		//Grab all the objects to be validated and store them for iteration.  This is done
		//here, as it needs to be reloaded each time.
		this.validationObjects   = $$(this.selector);	
					
		//This will store an array of objects, each having the object itself and the errors it has
		this.errors          = [];
		var equalElements    = [];
		var radioBoxes       = [];
		//This removes all the errors before we re-evaluate.
		this.cleanUp();		
		//Cycle through objects to validate
		this.validationObjects.each(function(el){
			var objClass	=	el.className;	
				
			if(el.type == 'text' || el.type == 'textarea' || el.type == 'textbox' || el.type == 'password'){
				if(!(/-override-presence/.test(objClass)))	this.validatePresenceOf(el);
				
				if((/-email/.test(objClass))) this.validateEmailFormatOf(el);
				
				if((/-url/.test(objClass))) this.validateURLFormatOf(el);
				
				if((/-html/.test(objClass))) this.validateNotHTML(el);
				
				if((/-alpha/.test(objClass))) this.validateAlphaFormatOf(el);
				
				if((/-numeric/.test(objClass))) this.validateNumericFormatOf(el);
				
				if((/-alphnum/.test(objClass))) this.validateAlphaNumericFormatOf(el);
				
				if((/-equal/.test(objClass))) equalElements.push(el);
				
				if((/-custom-regex/.test(objClass)) && null != this.regex.custom) this.validateCustomRegex(el);	
				
				if((/-length-greater-[0-9]{1,5}/.test(objClass)) || (/-length-less-[0-9]{1,5}/.test(objClass))) this.validateLengthOf(el);

			}
			
			if(el.type == 'select-one')	this.validateSelectedIndex(el);
			
			if(el.type == 'checkbox') this.validateChecked(el);
			
			if(el.type == 'radio')	radioBoxes.push(el);
							
		}.bind(this)); // Must bind this object to the 'this' keyword, otherwise prototypes global functions will return the window
	  	
		//With the following two checks, we need them all before we can compare
		//So, we push them above and check to see if we're using them here.
		//Check for equals
		if(equalElements.length > 0){
			this.validateEquals(equalElements);
		}
				
		//Check for radio boxes
		if(radioBoxes.length > 0){
			this.validateRadio(radioBoxes);
		}
	
		if(this.errors.length > 0){
			//Label all Errored
			this.labelInvalid();
			//Going to use this instead of prototypes normal, so I can add some pixels
			this.jumpToFirstError();
			return false;
		} else {
			return true;	
		}		
	},
			
	labelInvalid : function(){
		//Why is he doing this in reverse, you may ask.  It helps with the z-index!
		//DO NOT remove the false parameter from reverse, otherwise it affects the original.
		this.errors.each(function(error, index){
			//Add error classname to the elements with the incorrect input.
			error.el.addClassName(this.errorClass);
			//Man do I hate browser sniffing, but I am at a loss as to how I can solve this issue
			//I get different width/height when I have border(even on parent elements!!) when I use getWidth() [Prototype]
			//This is the only way I can get accurate widths 
			//Grab widths and heights for positioning of new error div
			var tempWidth         = (!/MSIE/.test(navigator.userAgent)) ? error.el.clientWidth : error.el.offsetWidth;
			//var tempWidth         = error.el.clientWidth;
			var tempHeight        = error.el.offsetHeight;

			var tempTop           = error.el.cumulativeOffset()[1]+tempHeight;
			var tempLeft          = error.el.cumulativeOffset()[0];
			
			var errorMessageWidth = (tempWidth < 100) ? 100 : tempWidth;
			//Create a container to hold the error messages. 
			var errorMessageContainer       = document.createElement('div');
			//IE freaks if we don't run it through prototype first.
			errorMessageContainer           = $(errorMessageContainer);
			errorMessageContainer.className = 'validator-error-message-container';
			//Do not change the Zindex
			errorMessageContainer.setStyle({ 
				border: '1px solid #FF0000',
				backgroundColor: '#a00',
  				position: 'absolute', 
  				color: '#FFFFFF',
  				fontFamily: 'Arial, Helvetica, sans-serif', 
  				zIndex: '9999',
  				fontSize: '12px',
  				paddingTop: '5px',
  				paddingBottom: '5px',
  				textAlign: 'center',
  				display: 'none',
  				width: errorMessageWidth+'px',
  				top: tempTop+'px',
  				left: tempLeft+'px'
  			});
  		  	
  		var errorMessageNotifier        = document.createElement('div');
  		//IE freaks if we don't run it through prototype first.
  		errorMessageNotifier            = $(errorMessageNotifier);
		errorMessageNotifier.className	= 'validator-error-notify';
		//Place the exclamation outside the the element for these two inputs
		var errorMessageNotifierLeft = (error.el.type == 'select-one' || error.el.type == 'checkbox' || error.el.type == 'radio')	? 
																	(tempLeft-12) : ((tempLeft+tempWidth)-10);
			//Do not change the Zindex
			errorMessageNotifier.setStyle({ 
				border: '1px solid #FF0000',
				backgroundColor: '#a00',
				position: 'absolute', 
				color: '#FFFFFF',
				fontFamily: 'Arial, Helvetica, sans-serif', 
				zIndex: '999',
				fontSize: '10px',
				paddingRight: '1px',
				textAlign: 'center',
				width: '9px',
				top: error.el.cumulativeOffset()[1]+'px',
				left: errorMessageNotifierLeft+'px'
			});
			
			errorMessageNotifier.insert('!');
			error.el.parentNode.appendChild(errorMessageNotifier);
			
			//Cycle through the errors and create the innerHTML from them.
			error.errorCode.each(function(message){
				errorMessageContainer.insert( message+'<br />', {position: 'bottom'});
			});
			  			
			error.el.parentNode.appendChild(errorMessageContainer);
				
			error.el.observe('mouseover', function(event){
				errorMessageContainer.style.display	= '';
				errorMessageNotifier.style.display	= 'none';
			});
			error.el.observe('mouseout', function(event){
				errorMessageContainer.style.display	= 'none';
				errorMessageNotifier.style.display	= '';
			});
			error.el.observe('focus', function(event){
				errorMessageContainer.style.display	= '';
				errorMessageNotifier.style.display	= 'none';
			});
			error.el.observe('blur', function(event){
				errorMessageContainer.style.display	= 'none';
				errorMessageNotifier.style.display	= '';
			});
			
			
		}.bind(this));

	},
		
	//You should never touch anything in this function, unless you know EXACTLY what you're doing with it.
	markForError : function(el, errorCode){
		//Loop through the errors array and determine if the error is on the same object.  
		//If so, add to the existing array that exists within that object. If not, create the new object in the original array
		if(previousElement = this.errors.find(function(obj){ return obj.el == el}) ){	
			//If we find the object in the array, return it an use it for the assigning of the code.
			previousElement.errorCode.push(errorCode);
		} else {
			//Otherwise, we have a new entry.
			this.errors.push({'el' : el, 'errorCode' : [errorCode] });
		}
		
	},
		
	cleanUp: function(){
		//Remove old error messages, if they exist.
		$$('.validator-error-message-container').each(function(el){
			el.remove();
		});
		$$('.validator-error-notify').each(function(el){
			el.remove();
		});
		
		//Remove error class from elements
		$$('.'+this.errorClass).each(function(el){
			el.removeClassName(this.errorClass);
			el.stopObserving('focus');
			el.stopObserving('mouseover');
			el.stopObserving('blur');
			el.stopObserving('mouseout');
		}.bind(this));
	},
	
	jumpToFirstError: function(){
		var pos = this.errors[0].el.cumulativeOffset();
   		window.scrollTo(pos[0], pos[1]-150);
	},
	
	// Validations
	validatePresenceOf : function(el){
		if(null == el.value || el.value.length < 1){
			this.markForError(el, this.errorMessage.presence);
		}
	},
	
	validateURLFormatOf : function(el){
		if(!(this.regex.url.test(el.value))){
			this.markForError(el, this.errorMessage.url);
		}
	},
	
	validateEmailFormatOf : function(el){
		if(!(this.regex.email.test(el.value))){
			this.markForError(el, this.errorMessage.email);
		}
	},
	
	validateAlphaFormatOf : function(el){
		if(!(this.regex.alpha.test(el.value))){
			this.markForError(el, this.errorMessage.alpha);
		}
	},
	
	validateNumericFormatOf : function(el){
		if(!(this.regex.numeric.test(el.value))){
			this.markForError(el, this.errorMessage.numeric);
		}
	},
	
	validateAlphaNumericFormatOf : function(el){
		if(!(this.regex.alphnum.test(el.value))){
			this.markForError(el, this.errorMessage.alphnum);
		}
	},
	
	validateNotHTML : function(el){
		if((this.regex.html.test(el.value))){
			this.markForError(el, this.errorMessage.html);
		}
	},
	
	validateCustomRegex : function(el){
		if(!(this.regex.custom.test(el.value))){
			this.markForError(el, this.errorMessage.email);
		}
	}, 
	
	validateSelectedIndex : function(el){
		if(el.selectedIndex == 0){
			this.markForError(el, this.errorMessage.select);
		}
	}, 
	
	validateChecked : function(el){
		if(!el.checked){
			this.markForError(el, this.errorMessage.checked);
		}
	},
	
	validateLengthOf: function(el){
		var tempLengthHolder = /length-[a-zA-Z]{4,7}-[0-9]{1,5}/.exec(el.className);
		
		actuallength	     = tempLengthHolder.toString().split("-")[2];
		if(/less/.test(tempLengthHolder)){
			if(el.value.length < actuallength){
				this.markForError(el, this.errorMessage.lengthLess+actuallength);
			}
		} else if(/greater/.test(tempLengthHolder)){
			if(el.value.length > actuallength){
				this.markForError(el, this.errorMessage.lengthGreater+actuallength);
			}
		} else if(/exact/.test(tempLengthHolder)){
			if(el.value.length != actuallength){
				this.markForError(el, this.errorMessage.lengthExact+actuallength);
			}
		}
	},

	validateRadio: function(radioBoxes){
		var tempArray = [];
		//Take every radio to be validated and get it's name
		radioBoxes.each(function(el){
			tempArray.push(el.name);
		});		
		//Turn it into unique names
		tempArray	=	tempArray.uniq();
		tempArray.each(function(name){
			//Grab an array of elements with that name and check to see if they are all not checked
			//If so, we're going to go through them and mark them for error.	
			var elementGroup	=	$$('*[name='+name+']');	
			if(elementGroup.length > 1){
				if(elementGroup.all(function(n){ return n.checked == false; })){	
					elementGroup.each(function(el){
						this.markForError(el, this.errorMessage.radio);
					}.bind(this));
				}			
			}
		}.bind(this));		
	},
	
	validateEquals: function(equalElements){	
		var tempArray = [];
		//Take every radio to be validated and get it's name
		equalElements.each(function(el){
			tempArray.push(el.name);
		});		
		//Turn it into unique names
		tempArray	=	tempArray.uniq();	
		tempArray.each(function(name){
			//Grab an array of elements with that name and check to see if they are all not checked
			//If so, we're going to go through them and mark them for error.	
			var elementGroup	=	$$('*[name='+name+']');	
			if(elementGroup.length > 1){
				//Now, compare every element in group to first element
				if(!elementGroup.all(function(el) { return el.value == elementGroup[0].value; })){
					elementGroup.each(function(el){
						this.markForError(el, this.errorMessage.equal);
					}.bind(this));
				}			
			}
		}.bind(this));		
			
	}
});




