// Validator: a mostly declarative client-side form validator
// Copyright (c) 2005, Michael Schuerig, michael@schuerig.de
//
// License
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
// See http://www.gnu.org/copyleft/lesser.html
//
// REQUIREMENTS:
// * prototype.js - http://prototype.conio.net/ (included with Ruby on Rails)
// * protoplus.js
// * stdext.js

// TODO
// - substitute classname functions from Element with Element.Class functions
// - if complete form checking ever becomes too slow, change to incremental

Form.Validator = Class.create();

Form.Validator.Version = '0.1.2';

Form.Validator.debug = true;

Form.Validator.validatorsState = {};


Form.Validator.installForAllForms = function(options) {
  var forms = document.forms;
  for (var i = 0; i < forms.length; i++) {
    Form.Validator.install(forms[i], options);
  }
};

Form.Validator.install = function(form, options) {
  form.__validator = new Form.Validator(form, options);
};

Form.Validator.prototype = {
  // Options:
  // validators({name:function(validator),...}) - a hash of validation functions
  // formValidator(form, validator) - a validation function for the form as a whole
  // messageCallback(messages) - called when the error messages have changed
  // statusChangedCallback(element, valid) - called with a form element when its validation status has changed
  initialize: function(form, options) {
    this.form = $$(form, 'Form.Validator.new: ');
    if (!form) {
      return;
    }
    if (!options) {
      options = {}
    }
    this.validators = options.validators || {};
    this.formValidator = options.formValidator;
    this.messageCallback = options.messageCallback;
    this.statusChangedCallback = options.statusChangedCallback;
    this.submitButtons = Form.Element.findElements(this.form, 'submit', 'commit');
    this.errorMessages = [];
    this.oldErrorMessages = undefined;
    this._installValidators();
    this._installSubmitInterceptor();
    new PeriodicalExecuter(this.check.bind(this), 1.0);
  },

  addErrorMessage: function(msg) {
    this.errorMessages.pushUnlessNull(msg);
  },

  check: function() {
    this.formIsValid = true;
    this._clearErrorMessages();
    var elements = Form.getElements(this.form);
    for (var i = elements.length - 1; i >= 0; i--) {
      this._checkElement(elements[i]);
    }
    this._checkWholeForm();
    this._adjustSubmitButtons();
    this._callbackIfErrorsChanged();
  },

  _checkElement: function(element) {
    if (element && element.__checkValidity) {
//      try {
        var elementIsValid = element.__checkValidity(this);
if (!elementIsValid) {
//alert("INVALID: " + element.id);
}
        this.formIsValid = this.formIsValid && elementIsValid;
        Element.switchClassName(element, 'invalid', !elementIsValid);
        if (element.__wasValid !== elementIsValid) {
          if (this.statusChangedCallback) {
            this.statusChangedCallback(element, elementIsValid);
          }
          element.__wasValid = elementIsValid;
        }
//      } catch (e) {
//        if (Form.Validator.debug) {
//          throw e;
//        }
//      }
    }
  },

  _checkWholeForm: function() {
    if (this.formIsValid && this.formValidator) {
      try {
        this.formIsValid = this.formValidator(this.form, this);
      } catch (e) {
        if (Form.Validator.debug) {
          throw e;
        }
      }
    }
  },

  _installValidators: function() {
    var elements = Form.getElements(this.form);
    for (var i = 0; i < elements.length; i++) {
      var element = elements[i];
      if (element.tagName == 'SUBMIT' || element.type == 'submit') {
        continue;
      }
      var validatorFuncs = [];
      var classNames = element.className.split(' ');
      for (var j = 0; j < classNames.length; j++) {
        var className = classNames[j];
        if (className == 'mandatory') {
          validatorFuncs.pushUnlessNull(
            Form.Validator.getValueCheckerFor(element.type));
        } else {
          validatorFuncs.pushUnlessNull(
            this._getValidatorFor(element, className));
        }
      }
      if (validatorFuncs.length > 0) {
        element.__checkValidity = Function.andCombiner(validatorFuncs);
      }
    }
  },

  _installSubmitInterceptor: function() {
    Event.observe(this.form, 'submit', this._onSubmit.bindAsEventListener(this));
  },

  _getValidatorFor: function(element, className) {
    try {
      if (className.startsWith('validate_')) {
        var args = className.split('_');
        args.shift();
        var funcName = args.shift();
        var validatorGenerator = this.validators[funcName];
        if (validatorGenerator && validatorGenerator instanceof Function) {
          return validatorGenerator(element, args);
        }
      } else {
        return this.validators[className];
      }
    } catch (e) {
      return undefined;
    }
  },

  _clearErrorMessages: function() {
    this.oldErrorMessages = this.errorMessages;
    this.errorMessages = [];
  },

  _adjustSubmitButtons: function() {
    var disabled = !this.formIsValid;
    for (var i = this.submitButtons.length - 1; i >= 0; i--) {
      this.submitButtons[i].disabled = disabled;
    }
  },

  _callbackIfErrorsChanged: function() {
    if (this.messageCallback &&
        !this.errorMessages.equals(this.oldErrorMessages)) {
      this.messageCallback(this.errorMessages);
    }
  },

  _onSubmit: function(event) {
    if (Form.Validator.debug) {
      // allow a loophole to test submission of invalid forms
      return true;
    }
    if (!this.formIsValid) {
      return Event.stop(event);
    }
    return true;
  }
};

Form.Validator.getValueCheckerFor = function(inputType) {
  switch (inputType) {
    case 'hidden':
    case 'password':
    case 'select-multiple':
    case 'select-one':
    case 'text':
    case 'textarea':
      return Form.Validator.Utils.checkHasValue;
    case 'radio':
      return Form.Validator.Utils.checkRadioSelected;
  }
  return undefined;
};

Form.Validator.Utils = {
  validatorState: function(stateName, ctor) {
    var state = Form.Validator.validatorsState[stateName];
    if (!state && ctor) {
      state = new ctor();
      Form.Validator.validatorsState[stateName] = state;
    }
    return state;
  },

  setValidatorState: function(stateName, value) {
    Form.Validator.validatorsState[stateName] = value;
  },

  checkHasValue: function() {
    return (!String.isBlank(this.value));
  },

  checkRadioSelected: function() {
    if (String.isBlank(this.name)) {
      this.__checkValidity = undefined;
      return true;
    }
    var isValid = this.checked;
    var radios = Form.Element.findElements(this.form, 'radio', this.name);
    if (!isValid) {
      for (var i = radios.length - 1; i >= 0; i--) {
        if (radios[i].checked) {
          isValid = true;
          break;
        }
      }
    }
    if (isValid) {
      for (var i = radios.length - 1; i >= 0; i--) {
        radios[i].__checkValidity = undefined;
        Element.Class.remove(radios[i], 'invalid');
      }
    } else {
      for (var i = radios.length - 1; i >= 0; i--) {
        Element.Class.add(radios[i], 'invalid');
      }
    }
    return isValid;
  },

  isValidDate: function(year, month, day) {
    if (month < 1 || month > 12) {
      return false;
    }
    if (day < 1 || day > 31) {
      return false;
    }
    if ((month == 4 || month == 6 || month == 9 || month == 11) && day == 31) {
      return false;
    }
    if (month == 2) {
      var leap = (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0));
      if (day > 29 || (day == 29 && !leap)) {
        return false;
      }
    }
    return true;
  },

  othersHaveDifferentValues: function(element, group) {
    var value = $F(element);
    if (!value) {
      return true;
    }
    for (var i = 0; i < group.length; i++) {
      var other = group[i];
      if (other != element) {
        var otherValue = $F(other);
        if (value == otherValue) {
          return false;
        }
      }
    }
    return true;
  }

};

