/*
Foul - Form Validation Language Version 1.6
Copyright (C) 2005  Bryan English

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*/

function Foul(){

   //----------------------------------------------------------------------------//	
   // Properties

   this.version = '1.6';
   this.form = null;                //shortcut to form in question//
   this.breakpoints = false;        //auto checks on blur//
   this.interactive = false;        //auto formats on blur//
   this.tests = new Array();        //array of test strings//
   this.tests_index = new Array();  //easy lookup for field tests : tests_index['field'][n] = tests index//
	this.formats = new Array();      //array of format strings//
   this.formats_index = new Array();//easy lookup for field formats//
	this.local = new Array();        //used as a alternative to a form field for add'l tests//
	this.defmsg = {                  //used to automatically add error messages//
		"^\~([^~]+)\~$": "[[field]] is a required field.",
		" is (null|empty|blank)": "[[field]] is a required field.",
		" not email": "Please enter a valid email address.",
		"numeric": " is a number."
		};	

	//----------------------------------------------------------------------------//	
   // Methods

	//--------------------------------------------//
	// ADD adds a test
	//--------------------------------------------//
	this.add = function(v,m,i){
      var checksum = v.split("~"); //check for typos//
      if ((checksum.length+1) % 2 == 1){this.error("Syntax error in " + v);}
		if (!m){
			for (var reg in this.defmsg){
				if(v.search(new RegExp(reg)) != -1){
               var field = v.match(/\~([^~]+)\~/)[1];
					m = this.defmsg[reg].replace(/\[\[field\]\]/g,field);
					break;
				}
         }
      }

      //add fields to index array//
      var matches,re = new RegExp('\~([^~]+)\~','g');
      while(matches = re.exec(v)){
         if (!this.tests_index[RegExp.$1]){
            this.tests_index[RegExp.$1] = new Array();
         }

         this.tests_index[RegExp.$1][this.tests_index[RegExp.$1].length] = this.tests.length;         
      }

      //add to test array//
		this.tests[this.tests.length] = new Array(v,m,i);

   };

	this.when = this.add;

	//--------------------------------------------//
	// FORMAT assigns formatting to a field
	//--------------------------------------------//

   this.format = function(v){
      var checksum = v.split("~"); //check for typos//
      if((checksum.length+1) % 2 == 1){
         this.error("Syntax error in " + v);
      }

      //add fields to index array//
      var matches,re = new RegExp('\~([^~]+)\~','g');
      while(matches = re.exec(v)){
         if (!this.formats_index[RegExp.$1]){
            this.formats_index[RegExp.$1] = new Array();
         }

         this.formats_index[RegExp.$1][this.formats_index[RegExp.$1].length] = this.formats.length;         
      }

      //add to formats array//
      var n = v.match(/\~([^~]+)\~/)[1];
		this.formats[this.formats.length] = new Array(v,n);
   };


      
   //--------------------------------------------//
	// ERROR reports a script error
	//--------------------------------------------//
	this.error = function(e){
      var buffer = 'Foul ' + this.version + ' Error!';
      buffer += '\n_______________________________________\n';
      buffer += e + '\n\n';
      alert(buffer);
   }
   
   //--------------------------------------------//
	// GET VALUE get a value from any form control
	//--------------------------------------------//
	this.get_value = function(e){
		if(e.type!=null)
			switch(e.type){
				case "text": case "hidden": case "password": case "textarea":return(e.value);break;
				case "checkbox":return(((e.checked)?e.value:''));break;
				case "select-one":var o = e.options[e.selectedIndex];
					return(((o.value==null)?o.text:o.value));break;
				}
		else
			for(var cnt=0;cnt<e.length;cnt++)
				if(e[cnt].checked)return(e[cnt].value);

		return(false);
		};


	//--------------------------------------------//
	// STRING MERGE - merges template with data
	//--------------------------------------------//

   this.string_merge = function(template,data,reverse){
      var dindex = (reverse)?data.length-1:0;
      var dend = (reverse)?-1:data.length;
      var cnt = (reverse)?template.length-1:0;
      var end = (reverse)?-1:template.length;
      var inc = (reverse)?-1:1;
      var value = newdata = '';
   
      while (cnt != end && dindex != dend){

         switch (template.charAt(cnt)){

            case 'x': //replace with next data char
               value = data.charAt(dindex);
               dindex+=inc;
            break;

            case 'X': //replace with next data char and add next template char
               cnt+=inc;

               if(reverse){
                  value = template.charAt(cnt) + data.charAt(dindex);
               } else {
                  value = data.charAt(dindex) + template.charAt(cnt);
               }

               dindex+=inc;
            break;

            default: //add template char
               value = template.charAt(cnt); 
            break;
         }

         newdata = ((reverse==null)?newdata:'') + value + ((reverse)?newdata:''); 
         cnt+=inc;
      }

		return(newdata);
	};


	//--------------------------------------------//
	// VALIDATE optional function to do the dirty work
	//--------------------------------------------//
	this.validate = function(form){
		var errors = '';
		this.formatter(form);
		errors = this.test(form);		 
		if(errors!=''){
			alert("There is a problem with your submission:\n"+errors);
			return false;
			}
		return true;
		}	

	//--------------------------------------------//
	// INSPECT - used to auto-format/check on blur
	//--------------------------------------------//
	this.inspect = function(field){

      //attach form to foul object//
      this.form = field.form;

      //format field//
      if (this.interactive && this.formats_index[field.name]){
         for(var cnt=0;cnt<this.formats_index[field.name].length;cnt++){
      		this.tokenize(this.formats[this.formats_index[field.name][cnt]][0]);
         }
      }

      if (this.breakpoints && this.tests_index[field.name]){
         var errors = '';
         //run validation on this field//
         for(var cnt=0;cnt<this.tests_index[field.name].length;cnt++){
            if(this.tokenize(this.tests[this.tests_index[field.name][cnt]][0])){
	   		   alert(this.tests[this.tests_index[field.name][cnt]][1]);
               return;
            }            
         }
      }
	}	

   
   //--------------------------------------------//
	// CHOMP perl ripoff
	//--------------------------------------------//
   this.chomp = function(str){
   	  if(!str)return '';
      str = str.match(/\s*(.*\S)\s*/);
      return str[1];
      }

	//--------------------------------------------//
	// ONION peel layers strings via paranthesis
	//--------------------------------------------//
   this.onion = function(str,start,end){
      
      var cnt,tally = 1;

      for(cnt=1;cnt<str.length && tally!=0;cnt++){
         if(str.charAt(cnt) == start)tally++;
         if(str.charAt(cnt) == end)tally--;
         }
      return(str.substring(1,cnt-1));
      }

	//--------------------------------------------//
	// PARSE foul parser 
	//--------------------------------------------//
   this.tokenize = function(str){

      var left,right,bool = null;
      var result = false;
      str = this.chomp(str);
	  
      //check for paranthesis//
      if(str.charAt(0) == '('){
         left = this.onion(str,"(",")");
         right = str.substring(str.indexOf(left)+left.length,str.length);
         left = str.substring(1,str.length-1);
         result = this.tokenize(left);
         }
      //else just split and eval the left part//
      else{         
         left = str.match(/\~[^\~]+\~.*?(?= and | or |$)/)[0];
         right = str.substring(str.indexOf(left)+left.length,str.length);
         result = this.evaluate(left);
         //-1 result means test was canceled//
			result = (result==-1?false:result)
         }

      bool = right.match(/ or | and |\s*$/)[0];
      right = right.substring(right.indexOf(bool)+bool.length,right.length);

      //get recursive!//
      switch(this.chomp(bool)){
         case "":
            return result;
         case "and":
            return(result && this.tokenize(right));
         case "or":
            return(result || this.tokenize(right));
         }
      }

	//--------------------------------------------//
	// FORMATTER formats the form data            //
	//--------------------------------------------//
   this.formatter = function(form){
      this.form = form;

      for(var cnt=0;cnt<this.formats.length;cnt++)
         this.tokenize(this.formats[cnt][0]);			

      };	
		
	//--------------------------------------------//
	// TEST parseing the tests and create error  //
	//--------------------------------------------//
   this.test = function(form){
		var errors = '';
      this.form = form;

      for(var cnt=0;cnt<this.tests.length;cnt++)
         if(this.tokenize(this.tests[cnt][0]))
			   errors += '\n- ' + this.tests[cnt][1];

		return errors;
      };		 

   //--------------------------------------------//
	// EVALUATE where the field testing is done
	//--------------------------------------------//
   this.evaluate = function(str){
      var list = str.match(/\~([^~]+)\~(?:\s(\S+)(?:\s(.*))?)?/);
      var field = list[1];		
      var value = (this.form[field])?this.get_value(this.form[field]):this.local[field];
      var test = (list[2]==null||list[2]=="")?"null":list[2];
      var param = list[3];      
	  
      switch(test){

			//FORMATTER//         
         case 'as': 
            if(value == null || value == '')return(true);				
				param = param.split(' ');
				switch(param[0]){
					case 'number':
					case 'integer': // remove all non-number chars//
						this.form[field].value = (isNaN(parseInt(value.replace(/[^0-9\.]/g,''))))?'':parseInt(value.replace(/[^0-9\.]/g,''));
						break;
						
					case 'float':
					case 'decimal': // remove all non-number chars//
						this.form[field].value = (isNaN(parseFloat(value.replace(/[^0-9\.]/g,''))))?'':parseFloat(value.replace(/[^0-9\.]/g,''));
						break;

					case 'padded':
						var s = new String(value);
						while(s.length < param[1])
							s = param[2] + s;
						this.form[feld].value = s;
						break;

               case 'phone-us':
                  var s = value.replace(/\D/g,'');
                  this.form[field].value = this.string_merge('(Xxx) xxx-xxxx',s,true);
                  break;

               case 'credit-card':
                  var s = value.replace(/\D/g,'');
                  this.form[field].value = this.string_merge('xxxx xxxx xxxx xxxx',s);
                  break;

               case 'ssn':
               case 'social-security-number':
                  var s = value.replace(/\D/g,'');
                  this.form[field].value = this.string_merge('xxx-xx-xxxx',s);
                  break;
               }

				return(true);
				break;
			//END FORMATTER//         

			case 'is':
			case 'has':
         case '=':
            return(this.evaluate('~'+field+'~ '+ param));
            break;

         case '!':
         case 'not':
				var result = this.evaluate('~'+field+'~ '+ param);
				if(result!=-1)
	               return(!this.evaluate('~'+field+'~ '+ param));
				else
				   return(-1);
            break;

         case 'empty':
         case 'blank':
         case 'null':
            if(value == null || value == '')return(true);
            break;

         case 'range':
         case 'between':
            if(value == null || value == '')return(-1);
            param = param.split(/\s/);
				if(value > param[0] && value < param[1])return(true);
            break;

         case 'greater-than':
         case '>':
            if(value == null || value == '' || isNaN(value))return(-1);
				if(value > param)return(true);
            break;

         case 'less-than':
         case '<':
            if(value == null || value == '' || isNaN(value))return(-1);
				if(value < param)return(true);
            break;

         case 'email':
            if(value == null || value == '')return(-1);
            if(/^.+\@..+\..+/.test(value))return(true);
            break;

         case 'length':
            if(value == null || value == '')return(-1);
            param = param.split(/\s/);
				if(param.length > 1){
					this.local["_LOCAL_" + field] = parseInt(value.length);
					return(this.evaluate('~_LOCAL_'+field+'~ '+ param.join(' ')));
					}
				else{
					return(value.length == parseInt(param[0]));
					}
            break;

         case 'number':
         case 'float':
         case 'decimal':
         case 'numeric':
            if(value == null || value == '')return(-1);
            if(!isNaN(value))return(true);
            break;

			case 'valid_credit_card':
			case 'valid_cc':
			case 'vcc':
				if(value == null || value == '')return(false);
            value = value.replace(/\D/g,'');
				if (value.length > 19)
					return (false);

				var sum = 0; mul = 1; l = value.length;
				for (i = 0; i < l; i++) {
					var digit = value.substring(l-i-1,l-i);
					var tproduct = parseInt(digit ,10)*mul;
					if (tproduct >= 10)
						sum += (tproduct % 10) + 1;
					else
						sum += tproduct;
					if (mul == 1)
						mul++;
					else
						mul--;
					}
				if ((sum % 10) == 0)return (true);
				break;

         case 'date-us':
            if(value == null || value == '')return(-1);
            if(/\d\d?\/\d\d?\/\d{2,4}/.test(value))return(true);
            break;

         case 'date-us-y2k':
            if(value == null || value == '')return(-1);
            if(/\d\d?\/\d\d?\/\d{4}/.test(value))return(true);
            break;

         case 'password':
            if(value == null || value == '')return(-1);         
            if((!/\s|\t|\n|\r/.test(value)) && this.evaluate('~'+ field +'~ length > 4'))return(true);
            break;

         case '==':
         case 'same-as':
         case 'equal-to':
            if(value == null || value == '')return(-1);
            if(value == this.get_value(this.form[this.chomp(param)]))return(true);
            break;

         case 'password-verified':
         case 'password-confirmed':
            if(value == null || value == '')return(-1);
            if(value == this.get_value(this.form['confirm_' + field]))return(true);
            break;

         case 'blanks':
         case 'spaces':
            if(value == null || value == '')return(-1);                   
            if(/\s/.test(value))return(true);
            break;

         default:
            this.error('"' + test + "\" test doesn't exist.");
            break;

		}
      
      return false;
      };

   }


var foul = new Foul();
   