/**
 * Form Validation Utility
 * @class FormValidator
 * @author Syed Ghouseuddin Irfan
 */
FormValidator = function() {
    this.initialize.apply(this, arguments);
};

FormValidator.prototype = {
  form: null,
  /* Array of all the Form fields for which rules have been set */
  formFields: [],
  /* Array of all the Form fields names for which rules have been set */
  fieldNames: [],
  fieldConstraints: {},
  /* Hash of all the allowed reportModes, default is 'alert'. Other one is 'changeClass' */
  reportModes : ['alert'],
  /* Regular expression tests that have been pre-written. */
  tests : {
    'EMAIL'           : /\w{1,}[@][\w\-]{1,}([.]([\w\-]{1,})){1,3}$/,
    'NAME'            : /^[a-zA-Z0-9\-\s,.]+$/,
    'ZIPCODE'         : /\d{5}(-\d{4})?/,
    'DATE'            : /^\d{1,2}(\-|\/|\.)\d{1,2}\1\d{4}$/,
    'CITY'            : /^[A-Z,a-z,\s]+[-,.,#,'\s]?[A-Z,a-z,\s]+$/,
    'SSN'             : /^\d{3}\-\d{2}\-\d{4}$/,
    'NUMERIC'         : /^[0-9]+$/,
    'ALPHA'           : /^[a-zA-Z\-]+$/,
    'ALPHANUMERIC'    : /^[a-zA-Z0-9\-_]+$/,
    'CREDITCARD'      : /^((4\d{3})|(5[1-5]\d{2})|(6011))\d{4}\d{4}\d{4}|3[4,7]\d{13}$/,
    'BEGINWITHLETTER' : /^[a-zA-Z].*$/,
    'BEGINWITHNUMBER' : /^[0-9].*$/,
    'SECRETANSWER'    : /^[a-zA-Z0-9\_\.]+$/
  },
  /**
   * Function that gets called as a constructor
   * @param {Object} formName name of the form to be validated
   */
  initialize: function(formName) {
    this.form = document.forms[formName];
    /* Used to bind onSubmit handler of the form with the validate method */
    //this.form.onsubmit = this.validate;
    /* Cache the reference to the object - FormValidator */
    this.form.xFv = this;
  },
  /**
   * Applies a constraint to a form field and through options defines the errorMessage, errorClass to be applied to the erroEle.
   * @param {Object} field name of the form field for which the rule has been set
   * @param {Object} constraint constraint applied to the form field, eg NUMERIC
   * @param {Object} option hash of errorMessage, errorClass, errorEle.
   */
  set: function(fieldName, constraint, option){
    var cField = this.form.elements[fieldName];
    if(!this.fieldConstraints[fieldName]) {
      this.fieldNames.push(fieldName);
      this.fieldConstraints[fieldName] = new Array();
    }
    this.fieldConstraints[fieldName].push({cons: constraint, opt: option});
  },
  /**
   * Returns the constraints for the passed 'field' value.
   * @param {Object} field name of the form field for which the rule has been set.
   */
  get: function(fieldName) {
    return this.fieldConstraints[fieldName];
  },
  /**
   * unsets the rules defined using 'set' for variable passed as arguments.
   * @param {Object} comma separated fields for which rules need to be unset.
   */
  unset: function() {
    for(var i=0;i<arguments.length; i++) {
      this.fieldConstraints[arguments[i]] = new Array();
    }
  },
  trim: function(formField) {
    formField.value = formField.value.replace(/^\s+/, "").replace(/\s+$/, "");
  },
  /**
   * Function called onSubmit of the form.
   * Can also be called explicitly using -
   * var fv = new FormValidator('testForm');
   * fv.validate();
   */
  validate: function() {
    var valResult;
    if(!this.xFv) { this.xFv = this; }
    /* Iterate over all the form fields */
    for(var i=0; i < this.xFv.fieldNames.length; i++) {
      var fieldFound = false;
      /** If no argument is passed, code will validate entire form - all fields */
      if(arguments.length!=0) {
        for(var d=0;d<arguments.length; d++) {
          if(this.xFv.fieldNames[i].toLowerCase()==arguments[d].toLowerCase()) {
            //we need to trim leading and trailing spaces for all input fields and reset their values.
            //when fields to be validated are passed as arguments, validate only those fields and not entire form
            if (this.xFv.form.elements[this.xFv.fieldNames[i]].type == "text") {
              this.xFv.trim(this.xFv.form.elements[this.xFv.fieldNames[i]]);
            }
            fieldFound = true;
          }
        }
       } else {  //no arguments passed, so validating all form fields now
        if (this.xFv.form.elements[this.xFv.fieldNames[i]].type == "text") {
          this.xFv.trim(this.xFv.form.elements[this.xFv.fieldNames[i]]);
        }
        fieldFound = true;
      }
      if(fieldFound==true) {
        var optionalFound = false;
        var fieldConstraints = this.xFv.fieldConstraints[this.xFv.fieldNames[i]];
        var consLength = fieldConstraints.length;
        var currFieldName = this.xFv.fieldNames[i];
        var currField = this.xFv.form.elements[currFieldName];
        /* Iterate over all the form field Constraints */
        for(var k=0; k<consLength; k++) {
          /* Check if a field has been marked as OPTIONAL and set optionalFound accordingly */
          if(fieldConstraints[k]['cons'].toUpperCase()=="OPTIONAL") {
            optionalFound = true;
          }
        }
        /* For optional 'text' fields don't do anything and just continue, no validations to happen if the FORM FIELD IS BLANK */
        /* But, if an optional field has been filled by the user, then validate it according to the constraints */
        if(optionalFound == true && (currField.type && (currField.type=="text"))) {
          if(currField.value=="" || currField.value==null) {
            //@ToDo: fix this - related to bugz: 254659
            //ErrorReporter.setOriginalClass(fieldConstraints[j]['opt']);
            continue;
          }
        }
        /* Iterate over all the form field Constraints */
        for(var j=0; j < consLength; j++) {
          /* DoNOT validate if the constraint is OPTIONAL */
          if(fieldConstraints[j]['cons'].toUpperCase()=="OPTIONAL") {
            continue;
          }
          /* This will store the validationResult as returned by checkField */
          valResult = this.xFv.checkField(currFieldName, fieldConstraints[j]['cons']);
          /* In case validation test failed */
          if(valResult==false) {
            ErrorReporter.reportError((currField[0] && (currField[0].name))?currField[0]:currField, fieldConstraints[j]['opt'], this.xFv.reportModes);
            return false;
          } else {
            ErrorReporter.setOriginalClass(fieldConstraints[j]['opt']);
            ((currField[0] && (currField[0].name))?currField[0]:currField).focus();

          }
        }
      }
    }
    return true;
  },
  /**
   * Meat of the validation logic is performed here
   * @param {Object} fieldName name of the field to be validated as defined using fv.set
   * @param {Object} constraint constraint applied to the form field.
   * @return {Boolean} true or false
   */
  checkField: function(fieldName, constraint) {
    if(this.tests[constraint]) {
      return this.tests[constraint].test(this.form.elements[fieldName].value);
    } else {
      if (typeof(constraint) == 'string') {
        var cons = (constraint.indexOf("_")!=-1)?constraint.split("_"):[constraint];
        var formEle = (this.form.elements[fieldName][0] && (this.form.elements[fieldName][0].name))?this.form.elements[fieldName][0]:this.form.elements[fieldName];
        switch(cons[0].toUpperCase()) {
          /* Constraint that checks if the value of a textfield is not empty */
          /* Applies to input types : text */
          case 'NOTBLANK' :
            return this.notBlank(fieldName);
          break;
          case 'POBOXCHK' :
              var streetaddr=this.form.elements[fieldName].value;
              streetaddr=streetaddr.toLowerCase();
              streetaddr=streetaddr.replace(/^\s+/, "").replace(/\s+$/, "");
              PoBoxRegex=/^p([ |\.\,\#\%\-\:\;\$\_\=\(\)\'\&\"]*(ost)+[ |\.\,\#\%\-\:\;\$\_\=\(\)\'\&\"]*(office)+[ |\.\,\#\%\-\:\;\$\_\=\(\)\'\&\"]*|[ |\.\,\#\%\-\:\;\$\_\=\(\)\'\&\"]*(o)+[ |\.\,\#\%\-\:\;\$\_\=\(\)\'\&\"]*(box)+[ |\.\,\#\%\-\:\;\$\_\=\(\)\'\&\"]*|[ |\.\,\#\%\-\:\;\$\_\=\(\)\'\&\"]*(ost)+[ |\.\,\#\%\-\:\;\$\_\=\(\)\'\&\"]*(box)+[ |\.\,\#\%\-\:\;\$\_\=\(\)\'\&\"]*)+[ |a-z0-9\s\.\,\#\%\-\:\;\$\_\=\(\)\'\&\"]*$/
              if(eval(streetaddr.search(PoBoxRegex))!=-1) { return false; }
          break;
          /* Constraint that checks if the value of a textfield matches with a specified field */
          /* Applies to input types : text */
          /* Usage : MATCH_password where password is the name of the field that the current field's value should match with */
          case "MATCH":
            if(formEle.value!=this.form.elements[cons[1]].value && formEle.value!="" && formEle.value!=null) {
              return false;
            }
          break;
          case "TESTRBUTTON":
            var testvalue="";
            for (i=0; i<this.form.elements[fieldName].length; i++)
            {
                if(this.form.elements[fieldName][i].checked == true)
                {   testvalue = this.form.elements[fieldName][i].value; } // if loop
            } // for loop

            if(testvalue!=this.form.elements[cons[1]].value && testvalue!="" && testvalue!=null)
            {   return false; } // last if loop
          break;
          case "DONOTMATCH":
            if(formEle.value==this.form.elements[cons[1]].value && formEle.value!="" && formEle.value!=null) {
              return false;
            }
          break;
          /* Constraint that checks if the length of a textfield is less than the minimum allowed */
          /* Applies to input types : text */
          /* Usage : MINLENGTH_4 where 4 is the minimum length of the field allowed */
          case "MINLENGTH":
            if(formEle.value.length < parseInt(cons[1])) {
              return false;
            }
          break;
          /* Constraint that checks if the length of a textfield exceeds the maximum allowed */
          /* Applies to input types : text */
          /* Usage : MAXLENGTH_10 where 10 is the maximum length of the field allowed */
          case "MAXLENGTH":
            if(formEle.value.length > parseInt(cons[1])) {
              return false;
            }
          break;
          /* Constraint that checks if an option has been selected from a dropdown */
          /* Applies to input types : select (drop-downs) */
          /* Usage : MININDEX_1 where 1 is the minimum index of the option to be selected */
          case "MININDEX":
            if(formEle.selectedIndex < parseInt(cons[1]) ) {
              return false;
            }
          break;
          /* Validates if the given year is less than current year i.e. throws an error if it is a past year */
          case "VALIDATEYEAR":
            var tDate=new Date();
            var tyear=tDate.getFullYear()
            var year=this.form.elements[fieldName].value;
            if(year < tyear){
              return false;
            }
          break;
          /* Validates if the given month is a past month */
          case "VALIDATEMONTH" :
            var tDate=new Date();
            var tmonth=tDate.getMonth()+1;
            var tyear=tDate.getFullYear();
            var month=this.form.elements[fieldName].value;
            if(tyear == this.form.elements[cons[1]].value){
              if(month < tmonth){
                   return false;
                }
            }
          break;
          /* Constraint that checks if atleast one radio / checkbox from a group has been checked */
          /* Applies to input types : radio / checkbox */
          case "CHECKED":
            if(cons[1]) {
              if(this.form.elements[formEle.name][parseInt(cons[1])].checked==true) {
                return true;
              } else {
                return false;
              }
            } else {
              var eleChecked = false;
              for(var i=0; i<this.form.elements.length; i++) {
                if(this.form.elements[i].name == formEle.name && this.form.elements[i].checked == true) {
                  eleChecked = true;
                }
              }
              if(eleChecked == false) {
                return false;
              }
            }
          break;

        }
      }
    }
  },
  notBlank: function(fieldName) {
    var blankEx = new RegExp(/^\s*$/);
    var f = this.form[fieldName];
    if (f.value == '' || blankEx.test(f.value)) {
      return false;
    } else {
      return true;
    }
  },
  /**
   * Function to add more tests
   * @param {String} ruleName name of the new test
   * @param {String} ruleExpression regular expression for the test.
   */
  addTest: function(ruleName, ruleExpression) {
    this.tests[ruleName] = ruleExpression;
  }
}

ErrorReporter = {
  /**
   * Function that calls the appropriate reporting method. reportAlert or reportChangeClass
   * @param {Object} field reference to the form field object
   * @param {Object} option map of errorMessage, errorClass, errorEle.
   * @param {Object} reportModes hash of alert & changeClass.
   */
  reportError: function(field, option, reportModes) {
    if (typeof(option) == 'string') {
      this.reportAlert(option, field);
    } else {
      if(option.errorMessage) {
        if(reportModes) {
          for(var i=0; i<reportModes.length; i++) {
            switch(reportModes[i].toUpperCase()) {
              case 'ALERT' :
                this.reportAlert(option.errorMessage, field, option);
              break;
              case 'CHANGECLASS' :
                this.reportChangeClass(field, option);
              break;
            }
          }
        } else {
          this.reportAlert(option.errorMessage, field);
        }
      } else {
        field.focus();
      }
    }
  },
  reportChangeClass: function(field, option) {
    var reportEle = document.getElementById(option.errorEle);
    if(reportEle) {
      if(option.errorClass){
        reportEle.setAttribute("oldClass", (reportEle.className)?reportEle.className:"");
        reportEle.className = option.errorClass;
      }
    }
    field.focus();
  },
  reportAlert: function(errMsg, field) {
    alert(errMsg);
    field.focus();
  },
  setOriginalClass: function(option) {
    var reportEle = document.getElementById(option.errorEle);
    if(reportEle) {
      if(option.errorClass){
        reportEle.className = (reportEle.getAttribute("oldClass"))?reportEle.getAttribute("oldClass"):"";
      }
    }
  }
}


/*****************************************

  Usage :

  Creating a FormValidator object
  var fv = new FormValidator('formName');

  Setting the reporting mode. Default is alert
  fv.reportModes = ['alert','changeclass'];
  or
  fv.reportModes = ['alert'];
  or
  fv.reportModes = ['changeclass'];


  Creating rules/constraints for a field
  fv.set('fieldName', 'CONSTRAINT', 'errorMessage');
  or
  fv.set('fieldName', 'CONSTRAINT', {options});

  Constraints can be : EMAIL, ZIPCODE, DATE, CITY, SSN, NUMERIC, ALPHANUMERIC, ALPHA, CREDITCARD, BEGINWITHLETTER, BEGINWITHNUMBER, NOTBLANK, OPTIONAL, MATCH_fieldName, MININDEX_val, MINLENGTH_val, MAXLENGTH_val, CHECKED


  if the third parameter of "set" is a string (errorMessage) the reporting method for that field will be reportAlert

  errorMessage can also be defined in the options map. Ex : fv.set('fieldName', 'CONSTRAINT', { errorMessage: 'CANNOT BE BLANK' });

  if the class of a particular element needs to be changed on error, then two additional fields ( errorClass, errorEle) also need to be passed
  Ex: fv.set('fieldName', 'CONSTRAINT', { errorMessage: 'CANNOT BE BLANK', errorClass:'classRed', errorEle: 'myDiv' });

******************************************/