/*******************************************************************************
 * Checker
 * 
 * Imports:
 * 
 *   Class
 *   Fx
 *   Options
 * 
 * Methods:
 * 
 *   check(identifier, rules)
 * 
 */



/*******************************************************************************
 * Classes
 */
var Checker = new Class({
  Implements: Options,
  options: {
    effect: "color",
    effects: {
      color: "#EBF2FF",
      opacity: {
        count: 4,
        delay: 250,
        duration: 200
      },
      shaking: {
        count: 3,
        delay: 100,
        duration: 150
      }
    },
    rules: {
      email: {
        message: "Please specify a valid email address for this field",
        regexp: /^[a-z0-9.\-_]+@[a-z0-9\-_]+\.[a-z]{2,}(?:\.[a-z]{2,})?$/i
      },
      length: {
        fixed: {
          message: "Please enter exactly %0 characters for this field",
          value: 10
        },
        minimum: {
          message: "Please enter at least %0 characters for this field",
          value: 3
        },
        maximum: {
          message: "Please enter at most %0 characters for this field",
          value: 255
        }
      },
      number: {
        float: {
          message: "Please specify a float for this field",
          regexp: /^[-+]?\d+(\.\d+)+$/
        },
        integer: {
          message: "Please specify an integer for this field",
          regexp: /^[-+]?\d+$/
        },
        minimum: {
          message: "Please enter a value greater than %0",
          value: 0
        },
        maximum: {
          message: "Please enter a value lower than %0",
          value: 1024
        }
      },
      required: {
        allowDefaultValue: false,
        message: "Please specify a value for this field",
        regexp: /[^.*]/
      }
    }
  },
  initialize: function(services, options) {
    // Saves a reference to the bouquet of services
    this.services = services;
    
    // Merges the default options with the ones given as parameters
    this.setOptions(options);
  },
  check: function(identifier, rules) {
    var result = false;
    
    // Retrieves field from identifier
    var field = $(identifier);
    
    if (field !== null) {
      // Merges the default values with the ones specified in the rules object
      rules = this.merge(rules, this.options.rules);
      
      // Applies each validation rule to the field
      for (var key in rules) {
        result = this.validate(field, key, rules[key]);
        
        // Exits loop whenever validation has failed
        if (!result) {
          break;
        }
      }
    }
    
    return result;
  },
  displayError: function(field, message, values) {
    // Replaces variables by values in the message to display
    if ($defined(values)) {
      values.each(function(value, index) {
        message = message.replace("%" + index, value);
      });
    }
    
    // Makes sure the field is visible (displayed in the viewport)
    if (field.isInside(window, 50)) {
      this.highlight(field, message);
    } else {
      // Initializes smooth scrolling effect
      var scrollEffect = new Fx.Scroll(window, {offset: {'y': -100}, link: "cancel"});
      
      // Highlights field only when scrolling is done
      scrollEffect.addEvent("onComplete", function() {
        this.highlight(field, message);
      }.bind(this));
      
      // Scrolls the window to the field
      scrollEffect.toElement(field);
    }
  },
  highlight: function(field, message) {
    // Places cursor onto the field
    field.focus();
    
    // Displays an error message
    this.services.getAlert().error(message);
    
    // Checks whether the field should be highlighted
    if (this.options.effect !== false) {
      // Retrieves the highlight effect associated to this field
      var effect = field.retrieve("effect");
      
      // Initializes the highlight effect if necessary
      if (effect === null) {
        switch(this.options.effect) {
          case "color":
            effect = new Fx.Highlight(field, {link: "cancel"});
            
            break;
          
          case "opacity":
            effect = new Fx.Tween(field, {property: "opacity", duration: this.options.effects.opacity.duration, link: "cancel"});
            
            break;
          
          case "shaking":
            effect = new Fx.Tween(field, {property: "margin-left", duration: this.options.effects.shaking.duration, unit: "px", transition: Fx.Transitions.linear, link: "cancel"});
            
            break;
          
          default:
            return;
        }
        
        // Associates the highlight effect to this field to be able to reuse it
        field.store("effect", effect);
      }
      
      if (this.options.effect === "color") {
        effect.start(this.options.effects.color);
      } else {
        if (this.options.effect === "opacity") {
          var count = this.options.effects.opacity.count;
          var delay = this.options.effects.opacity.delay;
          var from = 0;
          var to = 1;
        } else if (this.options.effect === "shaking") {
          var count = this.options.effects.shaking.count;
          var delay = this.options.effects.shaking.delay;
          var from = 20;
          var to = -20;
        }
        
        // Declares chain of events
        var chain = new Chain();
        
        // Fills in the chain with the visual effects
        for (var i = 0; i < count; i++) {
          chain.chain(function() {
            effect.start(from);
          });
          
          chain.chain(function() {
            effect.start(to);
          });
        }
        
        if (this.options.effect === "shaking") {
          chain.chain(function() {
            effect.start(0);
          });
        }
        
        // Retrieves the total number of effects
        count = chain.count();
        
        // Runs every effects in the chain sequentially
        for (i = 0; i < count; i++) {
          chain.callChain.delay(i * delay, chain);
        }
      }
    }
  },
  isRule: function(element) {
    // Determines if the specified element have objects as children which means 
    // in that case it is not a rule object (a rule object is only made of 
    // properties) 
    if ($type(element) === "object") {
      for (var key in element) {
        if ($type(element[key]) === "object") {
          return false;
        } else {
          return true;
        }
      }
    }
    
    return false;
  },
  merge: function(original, defaults) {
    if (this.isRule(original)) {
      for (var key in defaults) {
        if (!$defined(original[key])) {
          original[key] = $unlink(defaults[key]);
        }
      }
    } else {
      var i = 0;
      
      for (var key in original) {
        i++;
        
        if ($defined(defaults[key])) {
          original[key] = this.merge(original[key], defaults[key]);
        } else {
          delete original[key];
        }
      }
      
      // Handles the case where the original object does not hold any properties
      if (i === 0) {
        for (var key in defaults) {
          original[key] = defaults[key];
        }
      }
    }
    
    return original;
  },
  validate: function(field, method, rules) {
    // Retrieves the value entered by the user
    var value = field.getProperty("value");
    
    // Makes sure the list of rules is not a sole object (which is always the 
    // case for simple rules) 
    if (this.isRule(rules)) {
      rules = {method: rules};
    }
    
    // Parses each rule and validates value in regard
    for (var key in rules) {
      var rule = rules[key];
      
      if (method === "email") {
        var regexp = rule.regexp;
        
        if (!regexp.test(value)) {
          this.displayError(field, rule.message);
          
          return false;
        }
      } else if (method === "length") {
        if (key === "minimum") {
          if (value.length < rule.value) {
            this.displayError(field, rule.message, [rule.value]);
            
            return false;
          }
        } else if (key === "maximum") {
          if (value.length > rule.value) {
            this.displayError(field, rule.message, [rule.value]);
            
            return false;
          }
        } else if (key === "fixed") {
          if (value.length != rule.value) {
            this.displayError(field, rule.message, [rule.value]);
            
            return false;
          }
        } 
      } else if (method === "number") {
        if (key === "minimum") {
          if (value < rule.value) {
            this.displayError(field, rule.message, [rule.value]);
            
            return false;
          }
        } else if (key === "maximum") {
          if (value > rule.value) {
            this.displayError(field, rule.message, [rule.value]);
            
            return false;
          }
        }
      } else if (method === "required") {
        if (!rule.allowDefaultValue) {
          if (field.value === field.defaultValue) {
            this.displayError(field, rule.message);
            
            return false;
          }
        }
        
        var regexp = rule.regexp;
        
        if (!regexp.test(value)) {
          this.displayError(field, rule.message);
          
          return false;
        }
      } 
    }
    
    return true;
  }
});