/**
 * Rules: an interactive form validator
 *
 * @author Nick Nettleton
 * @copyright 2007 / Loft Digital / www.loftdigital.com / info@loftdigital.com
 */

	/**
	 * Rules engine
	 */
	
	// todo: enable check-as-you-type, but
	// only display error if/when (1) the field has ever been correct/error, or it can no longer be correct unless you delete some characters
	// 

	var rules_last_error_message ;
	
	function rules_init(form)
	{
		// older browsers degrade
		if(!document.getElementById) return false ;

		// 1. set up the events for the form
		// todo: add support for onsubmit action already on form
		form.onsubmit = function()
		{
			if(rules_validate_form(this)){
				return true ;
			}
			alert('Please check and correct the errors on your form.') ;
			return false ;
		}

		// 2. update the ui
		// todo
		// ?
		
		// todo: add support for error message(s) to be displayed at the outset, on return from server

		// 3. init form fields
		var fields = rules_get_form_fields(form) ;
		for(var i=0, l=fields.length; i<l; i++){
			rules_init_field(fields[i]) ;
		}

		// 4. return
		return true ;
	}

	function rules_get_form_fields(form)
	{
		// 1. compile a list of fields
		var fields = [] ;
		for(var i=0, l=form.length; i<l; i++){
			var field = form[i] ;
			if(!field || !field.tagName) continue ;
			switch(field.tagName.toLowerCase()){
				/* multiple forms with the same name in IE
				case 'form' : 
					var form_fields = rules_get_form_fields(field) ;
					for(var j=0, k=form_fields.length; j<k; j++){
						fields[fields.length] = form_fields[j] ;
					}
					break ;
				*/
				case 'select'	: 
				case 'textarea'	:
					fields[fields.length] = field ;
					break ;
				case 'input'	:
					switch(field.type){
						case 'button' :
						case 'submit' :
						case 'reset' :
							// we don't support these
							break ;
						default :
							fields[fields.length] = field ;
					}
			}
		}
		
		// 2. return
		return fields ;
	}
	
	function rules_validate_form(form)
	{
		// 1. validate the fields
		var ok = true ;
		var fields = rules_get_form_fields(form) ;
		for(var i=0, l=fields.length; i<l; i++){
			var ok2 = rules_validate_field(fields[i]) ;
			if(!ok2) ok = false ;
		}
		
		// 2. update the ui
		// *** TODO
		// enable/disable submits when whole form is valid
		// but to do this we need to update rules_validate_field() so this can be done without updating the ui by the field
		//rules_toggle_form_submits(form, ok) ;
		
		// 3. return
		return ok ;
	}

	function rules_toggle_form_submits(form, state)
	{
		for(var i=0, l=form.length; i<l; i++){
			var el = form[i] ;
			if(el.tagName.toLowerCase() == 'input' && el.type == 'submit'){
				if(state == el.disabled){
					el.disabled = !state ;
				}
			}
		}
	}

	function rules_init_field(field)
	{
		// 1. set up the events for the field
		var old_onfocus = field.onfocus;
		field.onfocus		= function () {
			if (old_onfocus && typeof old_onfocus === 'function') {
				old_onfocus.apply(field);
			}
			rules_enter_field(field) ;
		}; // possible probs? todo: check this
		var old_onblur = field.onblur;
		field.onblur		= function () {
			if (old_onblur && typeof old_onblur === 'function') {
				old_onblur.apply(field);
			}
			rules_validate_field(field) ;
		} // possible probs? todo: check this
		var old_onchange = field.onchange;
		field.onchange		= function () {
			if (old_onchange && typeof old_onchange === 'function') {
				old_onchange.apply(field);
			}
			rules_validate_field(field) ;
		}
		//field.onkeyup		= function(){ rules_validate_field(field) ; }
		
		// 2. check the field has some rules
		var form_rules	= field.form.rules[field.name] ;
		if(!form_rules){
			form_rules = field.form.rules[field.name] = {} ;
		}
		
		// 3. update the ui
		// todo: enable an error message to appear at start-up, for postback
		var span = rules_get_message_container(field) ;
		if(span){
			if(form_rules.required){
				span.className = 'rules_required' ;
				span.innerHTML = 'Required' ;
			} else {
				span.className = 'rules_start' ;
				span.innerHTML = '&nbsp;' ;
			}
		}

		// 4. if the field has a value, validate it right away
		if(rules_get_field_value(field)){
			rules_validate_field(field) ;
		}
		
		// 5. return
		return true
	}
	
	function rules_enter_field(field)
	{
		// todo: causes flicker on checkboxes when you click on them (onfocus->onchange events in quick succession)
		// 1. update ui - display the help message, if there is one
		var form_rules	= field.form.rules[field.name] ;
		if(form_rules.help_message){
			var span = rules_get_message_container(field) ;
			if(span){
				span.className = 'rules_help' ;
				span.innerHTML = form_rules.help_message ;
			}
		}
		
		// 2. return
		return true ;
	}

	function rules_validate_field(field)
	{
		// 1. validate the field
		var is_error		= '' ;
		var value		= rules_get_field_value(field) ;
		var form_rules		= field.form.rules[field.name] ;
		rules_last_error_message = '' ;
		if(form_rules){
			for(var i in form_rules){
				var validator_name = i ;
				var validator = rules[i] ;
				if(!validator) continue ; //
				var result = validator(form_rules[i], value, field) ;
				//alert(i + '=' + result) ;
				if(!result){
					is_error = true ;
					break ;
				}
			}
		}
		
		// 2. update the ui
		var span = rules_get_message_container(field) ;
		if(span && validator_name != 'ajax'){ // ajax is async, so we have to let it run and sort out the display update itself...
			if(is_error){
				if(rules_last_error_message){ // error message set by checking function
					var error_message = rules_last_error_message ;
				} else if(form_rules.error_message){ // manual error message
					var error_message = form_rules.error_message ;
				} else if(form_rules.help_message){ // or use help message as a backup
					var error_message = form_rules.help_message ;
				} else { // or just tell em it's an error
					var error_message = 'Error' ;
				}
				span.className	= 'rules_error' ;
				span.innerHTML	= error_message ;
				
			} else {
				span.className	= 'rules_ok' ;
				span.innerHTML	= 'OK' ;
			}
		}
		
		// 3. return
		return is_error ? false : true ;
	}

	function rules_get_field_value(field)
	{
		// 1. get the field value
		switch(field.tagName.toLowerCase()){
			
			case 'textarea' :
				return field.value ;
				
			case 'select' :
				return field[field.selectedIndex].value ;
				// multi-select - todo
				
			case 'input' :
			
				switch(field.type.toLowerCase()){
				
					case '' :
					case 'text' :
					case 'file' :
					case 'password' :
						// rtrim
						// todo: feature to disable/control this: { trim: 0|1|2|3}
						var v = field.value ;
						v2 = v.replace(/^\s+|\s+$/g, '') ;
						if(v != v2) field.value = v2 ;
						return v2 ;
						break ;
						
					// return the value of the checked radio in the group
					case 'radio' :
						var v= '' ;
						var group = field.form[field.name] ;
						for(var i=0, l=group.length; i<l; i++){
							if(group[i].checked){
								v = group[i].value ;
								break ;
							}
						}
						return v ;
						
						break ;
						
					// return value only if checked
					case 'checkbox' :
						return field.checked ? (field.value ? field.value : true) : '' ;
						break ;
				
				}
		}
		
		// todo
		// add support for custom types
		// only do testing based on given rules names, not _all_ fields, to support custom types?
		
		// 2. trimming?
		// todo
		
		// 3. return
		return false ;
	}

	function rules_get_message_container(field)
	{
		// use id of first in group for radios
		if(field.tagName.toLowerCase() == 'input' && field.type == 'radio'){
			var id = field.form[field.name][0].id + '_rules' ;
		} else {
			var id = field.id + '_rules' ;
		}
		var c = document.getElementById(id) ;
		return c ;
	}

	

	/**
	 * Bundled-in rules
	 */

	var rules = {} ;	
	rules.required = function(required, value)
	{
		if(!required) return true ;
		if(value){
			return true ;
		}
		rules_last_error_message = 'Required' ;
		return false ;
	}

	rules.min = function(min, value)
	{
		//alert(field_value.length) ;
		if(value.length >= min){
			return true ;
		}
		rules_last_error_message = 'Must be at least ' + min + ' character' ;
		if(min != 1) rules_last_error_message += 's' ;
		return false ;
	}
	
	rules.max = function(max, value)
	{
		//alert(field_value.length) ;
		if(value.length <= max){
			return true ;
		}
		rules_last_error_message = 'Maximum ' + max + ' character' ;
		if(max != 1) rules_last_error_message += 's' ;
		return false ;
	}

	rules.minnum = function(min, value, field)
	{
		if(isNaN(new Number(value))){
			rules_last_error_message = 'Please enter a valid number' ;
			return false ;
		}
		if(value >= min){
			return true ;
		}
		rules_last_error_message = 'Must not be less than ' + min ;
		return false ;
	}
	
	rules.maxnum = function(max, value)
	{
		if(isNaN(new Number(value))){
			rules_last_error_message = 'Please enter a valid number' ;
			return false ;
		}
		if(value <= max){
			return true ;
		}
		rules_last_error_message = 'Must not be greater than ' + max ;
		return false ;
	}
	
	rules.nowhite = function(bool, value)
	{
		if(!bool) return true ;
		if(!value) return true ;
		var re = /^\S+$/ ;
		var result = re.test(value) ;
		//alert(field_value.length) ;
		if(result){
			return true ;
		}
		rules_last_error_message = 'Should not include space characters' ;
		return false ;
	}

	rules.numeric = function(bool, value)
	{
		if(!bool) return true ;
		if(!value) return true ;
		var re = /^[\d]*$/ ;
		var result = re.test(value) ;
		if(result){
			return true ;
		}
		rules_last_error_message = 'Characters 0-9 only' ;
		return false ;
	}
	
	rules.alpha = function(bool, value)
	{
		if(!bool) return true ;
		if(!value) return true ;
		var re = /^[a-z]*$/i ;
		var result = re.test(value) ;
		if(result){
			return true ;
		}
		rules_last_error_message = 'Characters A-Z only' ;
		return false ;
	}

	rules.alphanumeric = function(bool, value)
	{
		if(!bool) return true ;
		if(!value) return true ;
		var re = /^[a-z\d]*$/i ;
		var result = re.test(value) ;
		if(result){
			return true ;
		}
		rules_last_error_message = 'Characters A-Z and 0-9 only' ;
		return false ;
	}

	rules.number = function(bool, value, field)
	{
		if(!bool) return true ;
		if(!value) return true ;
		value = value.replace(/,/g, '') ;
		var n = new Number(value) ;
		//alert(n) ;
		if(!isNaN(n)){
			if(field.value != value) field.value = value ; // todo: rules_get_field_value, rules_set_field_vale. also, we can't set the value on all field types: to fix
			return true ;
		}
		rules_last_error_message = 'Please enter a valid number' ;
		return false ;
	}
	
	rules.email = function(bool, value)
	{
		if(!bool) return true ;
		if(!value) return true ;
		var re = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/ ;
		var result = re.test(value) ;
		if(value.length > 80) {
			rules_last_error_message = 'Please enter a valid email address' ;
			return false ;
		}
		if(result){
			return true ;
		}
		rules_last_error_message = 'Please enter a valid email address' ;
		return false ;
	}

	rules.phone = function(bool, value)
	{
		if(!bool) return true ;
		if(!value) return true ;
		var re = /^[\d\s\+\(\)\.\-]+$/ ;
		var result = re.test(value) ;
		if(result){
			return true ;
		}
		rules_last_error_message = 'Please enter a valid phone number' ;
		return false ;
	}

	rules.date = function(param, value, field)
	{
		if(!param) return true ;
		if(!value) return true ;
		
		// extract format:
		var parts = param.split(/[^a-z]/i) ;
		var d_pos, m_pos, y_pos ;
		for(var i=0, l=parts.length; i<l; i++){
			switch(parts[i]){
				case 'dd' : d_pos = i ; break ;
				case 'mm' : m_pos = i ; break ;
				case 'yy' : y_pos = i ; break ;
			}
		}

		if(typeof(param) == 'string'){

			// hand written?
			if(/[a-z]/i.test(value)){
				var date = new Date(value) ;
				
			// format written?
			} else {
				var parts2 = value.split(/[^\d]/i) ;
				var date = new Date(parts2[y_pos], parts2[m_pos]-1, parts2[d_pos]) ;
			}
			
			var d = date.getDate() ;
			var m = date.getMonth() + 1;
			var y = date.getFullYear() ;
			if(d && m && y){ // format
				if(d < 10) d = '0' + d.toString() ;
				if(m < 10) m = '0' + m.toString() ;
				var p = [] ;
				p[y_pos] = y ;
				p[m_pos] = m ;
				p[d_pos] = d ;
				var v = p.join('/') ;
				if(field.value != v) field.value = v ; // todo: rules_get_field_value, also, we can't set the value on all field types: to fix
				return true ;
			}
			rules_last_error_message = 'Please enter a date in the format ' + param ;
			return false ;
			
		} else {
			var date = new Date(value) ;
			var d = date.getDate() ;
			var m = date.getMonth() ;
			var y = date.getFullYear() ;
			//alert(date) ; alert(m) ;
			// TODO: us date format issues in here. ugh.
			if(d && y){ // format
				var m2 = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][m] ;
				var v = d + ' ' + m2 + ' ' + y ;
				if(field.value != v) field.value = v ; // todo: rules_get_field_value, also, we can't set the value on all field types: to fix
				return true ;
			}
			rules_last_error_message = 'Please enter a valid date' ;
			return false ;
		}
	}
	
	rules.mustmatch = function(fieldname, value, field)
	{
		var field2 = field.form[fieldname] ;
		if(!field2) return true ;
		if(value == rules_get_field_value(field2)){
			return true ;
		}
		rules_last_error_message = '' ; // requires manual
		return false ;
	}

	rules.custom = function(func, value)
	{
		rules_last_error_message = '' ; // requires manual
		return func(value) ;
	}
	
	rules.pattern = function(pattern, value)
	{
		var result = pattern.test(value) ;
		if(result){
			return true ;
		}
		rules_last_error_message = '' ; // requires manual
		return false ;
	}
	
	rules.creditcard = function(bool, value)
	{
		if(!value) return true ;
		var number = value.replace(/\s+/g, '') ;
		
		if(number.match(/\D/)){
			rules_last_error_message = 'Please enter a valid card number' ;
			return false ;
		}
		number = number.split('').reverse().join('') ;
		var digits = '';
		for(var i = 0; i < number.length; i++){
			digits += '' + ((i%2) ? number.charAt(i) * 2 : number.charAt(i)) ;
		}
		var sum = 0 ;
		for (var i = 0; i < digits.length; i++){
			sum += (digits.charAt(i) * 1) ;
		}
		// valid card numbers will be transformed into a multiple of 10
		var ok = (sum % 10) ? false : true ;
		if(ok) return true ;
		rules_last_error_message = 'Please enter a valid card number' ;
		return false ;
	}

	rules.ajax = function (url, value, field)
	{
		if(!value) return true ;

		var span = rules_get_message_container(field) ;
		if(span){
			span.className = 'rules_loading' ;
			span.innerHTML = 'Checking...' ;
		}
				
		var url = url + '?' + escape(field.name) + '=' + escape(value) + '&_nocache=' + escape(new Date().getTime()) ;
				
		var http ;
		try { http = new ActiveXObject('Msxml2.XMLHTTP') ; } catch(e){
		try { http = new ActiveXObject('Microsoft.XMLHTTP') ; } catch(e){
		try { http = new XMLHttpRequest() ; http.overrideMimeType('text/xml'); } catch(e){
			//alert('Your browser does not support this feature.') ;
			//return false ;
			return true ; 
		}}}
		http.async = true;
		http.open('GET', url, true) ;
		http.onreadystatechange = function(){
			
			//alert(http.readyState) ;
			//alert(http.responseText) ;
			
			if(http.readyState < 4){
				return true ;
			}

			if(http.status != 200 || !http.responseText){
				if(span){
					span.className	= 'rules_ok' ;
					span.innerHTML	= 'OK' ;
					return true ;
				}
			}

			// error: we need to update display
			span.className	= 'rules_error' ;
			span.innerHTML	= http.responseText ;
			return true ;
		}
		http.send(null) ;
		
		return true ;
	}
