
/**
 * list of forms, with their validation rules.
 * 
 * values are added to this with inline javascript blocks by the {form} smarty tag.
 */
var formsToValidate = [];

var lastKeyDownFieldRec = false;

var formValidateController;

/**
 * validation controller. manages validation for all forms on the page.
 */
var FormValidateController = Class.create({
  
  /**
   * called when the page has finished loading. searches for forms, and adds validation
   * events to them.
   */
  initialize:function()
  {
    formsToValidate.each(function(formRec) {
      
      // setup submit event
      if (formRec.observe_submit) {
        var eventCallback = function(submitEvent) { this.handleFormSubmit(submitEvent, formRec); }.bind(this);
        $(formRec.id).observe('submit', eventCallback);
      }
      
      // setup key press events for each field
      formRec.fields.each(function(fieldRec) {
        if (!$(fieldRec.id))
          return;
        
        var eventCallback = function(keydownEvent) { this.handleFieldKeyDown(keydownEvent, fieldRec); }.bind(this);
        $(fieldRec.id).observe('keydown', eventCallback);
        var eventCallback = function(keyupEvent) { this.handleFieldKeyUp(keyupEvent, fieldRec); }.bind(this);
        $(fieldRec.id).observe('keyup', eventCallback);
        
        var eventCallback = function(focusEvent) { this.handleFieldFocus(focusEvent, fieldRec); }.bind(this);
        $(fieldRec.id).observe('focus', eventCallback);
        var eventCallback = function(blurEvent) { this.handleFieldBlur(blurEvent, fieldRec); }.bind(this);
        $(fieldRec.id).observe('blur', eventCallback);
      }.bind(this));
      
    }.bind(this));
  },
  
  /**
   * handle form submit. shows an alert and cancels event if there are invalid
   * fields.
   * 
   * also adds an "invalid-form-field" class to all invalid fields.
   */
  handleFormSubmit:function(submitEvent, formRec)
  {
    // if tinymce is in the form, we need to make it save now
    if (typeof(tinyMCE) != "undefined") {
      tinyMCE.triggerSave();
    }
    
    // validate fields
    var invalidFields = [];
    for (var i = 0; i < formRec.fields.length; i++) {
      var fieldRec = formRec.fields[i];
      
      if (!this.fieldIsValid(fieldRec)) {
        invalidFields.push(fieldRec);
        
        if ($(fieldRec.id)) {
          this.updateFieldIsValidCss(fieldRec, false);
          this.showValidationRulesForField(fieldRec);
        }
      } else {
        if ($(fieldRec.id)) {
          this.updateFieldIsValidCss(fieldRec, false);
          this.hideValidationRulesForField(fieldRec);
        }
      }
    }

    // all fields are valid
    if (invalidFields.length == 0) {
      return;
    }

    // generate alert message
    var errorMessage = 'The following errors occurred while trying to submit the form. Please correct them and try again:\n\n';
    
    for (var i = 0; i < invalidFields.length; i++) {
      var field = invalidFields[i];
      
      if (i != 0)
        errorMessage += ';\n';
      
      if (this.fieldHasLegalValue(field))
        errorMessage += '  • Please fill in the “'+field.displayName+'” field';
      else
        errorMessage += '  • The “'+field.displayName+'” field is invalid';
    }
    errorMessage += '.';
    
    alert(errorMessage);
    
    // cancel submit event
    submitEvent.stop();
  },
  
  handleFieldKeyDown:function(keyDownEvent, fieldRec)
  {
    if (keyDownEvent.keyCode == Event.KEY_TAB) {
      lastKeyDownFieldRec = false;
      return;
    }
    
    lastKeyDownFieldRec = fieldRec;
  },
  
  handleFieldKeyUp:function(keyUpEvent, fieldRec)
  {
    // we only care about the field that was key'd *down* on
    if (!lastKeyDownFieldRec)
      return;
    fieldRec = lastKeyDownFieldRec;
    lastKeyDownFieldRec = false;
    
    this.updateFieldIsValidCss(fieldRec, true);
  },
  
  handleFieldFocus:function(theEvent, fieldRec)
  {
    this.updateFieldIsValidCss(fieldRec, true);
  },
  
  handleFieldBlur:function(theEvent, fieldRec)
  {
    this.updateFieldIsValidCss(fieldRec, false);
  },
  
  updateFieldIsValidCss:function(fieldRec, hasFocus)
  {
    if (this.fieldIsValid(fieldRec)) { // if field is valid, add 'valid' and remove 'invalid'
      $(fieldRec.id).addClassName('valid-form-field');
      $(fieldRec.id).removeClassName('invalid-form-field');
    } else { // if field is invalid, remove 'valid' and add 'invalid' — unless the field has focus and is blank, then remove 'invalid'
      $(fieldRec.id).removeClassName('valid-form-field');
      
      if (hasFocus && this.fieldIsBlank(fieldRec)) {
        $(fieldRec.id).removeClassName('invalid-form-field');
      } else {
        $(fieldRec.id).addClassName('invalid-form-field');
      }
    }
  },
  
  /**
   * checks if a field contains valid data. checks all fields for an
   * "illegal" value or blank value if the field is required.
   * 
   * @return bool
   */
  fieldIsValid:function(fieldRec)
  {
    // does field even exist in the html? if not, it's invalid
    if (!$(fieldRec.id))
      return false;
    
    // check for invalid value
    if (!this.fieldHasLegalValue(fieldRec))
      return false;
    
    // check for required field
    if (fieldRec.required && this.fieldIsBlank(fieldRec))
      return false;
        
    return true;
  },
  
  fieldPassesValidationRules:function(fieldRec)
  {
    // does it have any rules?
    if (!fieldRec.validationRules)
      return true;
    
    // parse rules
    var rules = this.parseValidationRules(fieldRec.validationRules);
    
    // process rules
    var isValid = true;
    rules.each(function(rule) {
      if (!eval('this.validateFieldWith'+rule.name+'Rule(fieldRec, rule.params);'))
        isValid = false;
    }.bind(this));
    
    return isValid;
  },
  
  parseValidationRules:function(rulesStr)
  {
    matches = rulesStr.match(/^([a-z_]+)\(([^)]+?)\)$/);
    if (!matches) {
      return [];
    }
    
    return [
      {
        rule: rulesStr,
        name: matches[1],
        params: matches[2].split(',')
      }
    ];

  },
  
  validateFieldWithequaltofieldRule:function(fieldRec, params)
  {
    var value = $(fieldRec.id).getValue();
    var otherValue = $(params[0]+'_field').getValue();
    
    return (value == otherValue);
  },
    
  /**
   * checks if a field has no value.
   * 
   * @return bool
   */
  fieldIsBlank:function(fieldRec)
  {
    if ($(fieldRec.id).getAttribute('type') == 'checkbox') {
      return ($(fieldRec.id).checked == false);
    }
    
    return $(fieldRec.id).getValue() == '';
  },
  
  /**
   * checks if a field contains an "illegal" value, for example
   * a number field with a-z characters.
   * 
   * does not check for required fields with no value.
   * 
   * also checks if the field matches it's validation rules (note: 
   * validation rules in javascirpt are currently very limited.
   * 
   * @return bool
   */
  fieldHasLegalValue:function(fieldRec)
  {
    // does field even exist in the html? if not, it's invalid
    if (!$(fieldRec.id))
      return false;
    
    // check is valid integer
    if (fieldRec.type == 'int') {
      var fieldValue = $(fieldRec.id).value;
      
      if (fieldValue == '')
        return true; // check for blank value
      
      if (fieldValue.match(/^-?[0-9]+(\.[0-9]+)?$/) == null)
        return false;
    }
    
    // check validation rules
    if (!this.fieldPassesValidationRules(fieldRec))
      return false;
    
    return true;
  },
  
  hideValidationRulesForField:function(fieldRec)
  {
    var valRulesId = fieldRec.id + '_validation_rules';
    if ($(valRulesId))
      $(valRulesId).remove();
  },
  
  showValidationRulesForField:function(fieldRec)
  {
    var valRulesId = fieldRec.id + '_validation_rules';
    if ($(valRulesId))
      $(valRulesId).remove();
    
    var el = document.createElement('div');
    el.className = 'field-validation-rules';
    el.id = valRulesId;
    
    var errorMsg = '';
    if (this.fieldHasLegalValue(fieldRec)) {
      errorMsg = fieldRec.displayName + ' is a required field.';
    } else {
      errorMsg = fieldRec.displayName + ' must be a valid ' + fieldRec.displayType + '.';
    }
    el.innerHTML = errorMsg;
    
    $(fieldRec.id).parentNode.appendChild(el);
  },
  
  validateFeildsInElement:function(wrapperEl)
  {
    // find the form record
    var formEl = wrapperEl.up('form');
    var formRec = false;
    formsToValidate.each(function(formRecItem) {
      if (formRecItem.id == formEl.id)
        formRec = formRecItem;
    }.bind(this));
    
    if (!formRec)
      return false;
    
    // validate fields
    var invalidFields = [];
    for (var i = 0; i < formRec.fields.length; i++) {
      var fieldRec = formRec.fields[i];
      
      // is this field in wrapperEl?
      if ($(fieldRec.id) && !$(fieldRec.id).descendantOf(wrapperEl))
        continue;
      
      if (!this.fieldIsValid(fieldRec)) {
        invalidFields.push(fieldRec);
        
        if ($(fieldRec.id)) {
          this.updateFieldIsValidCss(fieldRec, false);
          this.showValidationRulesForField(fieldRec);
        }
      } else {
        if ($(fieldRec.id)) {
          this.updateFieldIsValidCss(fieldRec, false);
          this.hideValidationRulesForField(fieldRec);
        }
      }
    }

    // all fields are valid
    if (invalidFields.length == 0)
      return true;

    // generate alert message
    var errorMessage = 'The following errors occurred while trying to submit the form. Please correct them and try again:\n\n';
    
    for (var i = 0; i < invalidFields.length; i++) {
      var field = invalidFields[i];
      
      if (i != 0)
        errorMessage += ';\n';
      
      if (this.fieldHasLegalValue(field))
        errorMessage += '  • Please fill in the “'+field.displayName+'” field';
      else
        errorMessage += '  • The “'+field.displayName+'” field is invalid';
    }
    errorMessage += '.';
    
    alert(errorMessage);
    
    // cancel submit event
    return false;
  }
  
});

document.observe('dom:loaded', function() {
  formValidateController = new FormValidateController();
});
