/**
 * UNIVERSAL jQUERY-BASED SIMPLE FORM VALIDATOR
 */

function in_array(needle, haystack) {
  for(var i = 0, l = haystack.length; i < l; i++) if (haystack[i] == needle) return true;
  return false;
}

function is_string(v) {
  return (v === String(v));
}

function strlen(v) {
  return is_string(v) ? v.length : null;
}

function empty(v) {
  return (v === undefined || v === null || v === '' || v === [] || v === {} || $.trim(v) === '');
}

function chooseNotEmpty(a, b) {
  return (b === undefined || b === 0) ? a : b;
}

function isEmail(a) { // lightspeed test for string being a valid RFC e-mail address
  if (a === undefined) return false;
  ma = a.split('@');
  if (ma[1] === undefined) return false;
  var m1 = /^([\w!#\$%&\*\+-\/=\?\^_`\.\{\|\}~\d]+?(\.|-|_)?[\w\d])+?$/.test(ma[0]);
  var m2 = /^([\w\d-]+?(\.|-))+?[a-z]{2,6}$/.test(ma[1]);
  return m1 && m2;
}

var V = { // validator object

  cForm: null, // current form id
  cSubmit: null, // current submit name
  cName: null, // current element name
  cMatch: null, // current match
  types: {}, // types hash
  extras: {}, // extras hash
  marks: {}, // marks hash
  checks: {}, // checks hash
  skip: {}, // skip check hash (for hidden elements)
  fuzzyMatch: true, // if partial names would be matched
  validations: function() {}, // to be further defined
  filters: function() {}, // to be further defined
  lastCheck: null,
  keyCode: null,
  ctrl: false,
  alt: false,
  lastKey: null,
  setTrigger: null,
  triggers: {},

  /**
   * Matches form element, if element e given, it matches in elements parent form
   */
  fMatch: function(element_name, e) {
    f = this.fuzzyMatch ? '*' : '';
    if (e !== undefined) return e.parents('form').find('[name'+f+'="'+(this.cName=element_name)+'"]');
    return this.cMatch = $('#' + this.cForm + ' *[name'+f+'="'+(this.cName=element_name)+'"]');
  },

  /**
   * Performs check status update on given element if aplicable
   */
  check: function(e, status) {
    n = e.attr('name');
    h = e.is(':hidden') || e.parents().is(':hidden'); // element is hidden if itself or one of its descendant elements is hidden
    this.checks[n] = h ? true : status; // all hidden elements return true
    if ( (status != this.marks[n]) && !h) { // does the check only if status changed and element visible
      this.marks[n] = status;
      if (e.length==1) {
        if (status) e.removeClass('invalid'); else e.addClass('invalid');
      }
      x = true;
      $.each(this.checks, function(k, v) { if (!v) x = false; });
      if (x!=this.lastCheck) {
        if (this.setTrigger) this.triggers[this.setTrigger](x);
        V.fMatch(this.cSubmit, e).attr('disabled', (x ? '' : 'disabled'));
        this.lastCheck = x;
      }
    }
  },

  /**
   * Assigns actions function to a form name and submit element
   * @param string form_name
   * @param string submit_name
   * @param function actions (adding checks and filters)
   */
  addToForm: function(form_name, submit_name, actions) {
    this.cForm = form_name;
    this.cSubmit = submit_name;
    if (actions === undefined) actions = function() { window.alert('No actions defined!'); };
    actions();
  },

  /**
   * Assigns given check to element
   * @param string element_name
   * @param string check_type
   * @param bool check_extras
   */
  addCheck: function(element_name, check_type, check_extras) {
    e = this.fMatch(element_name); n = e.attr('name');
    if (n !== undefined) { // the only way to find out the element is actually matched
      this.types[n] = check_type;
      this.extras[n] = check_extras ? check_extras : [];
      e.bind((e.length==1) ? 'click keyup change focus blur' : 'click keypress', this.checkHandler);
      this.checkHandler(element_name);
      e.trigger('click');
    }
  },

  /**
   * Removes assigned check from element
   * @param string element_name
   */
  removeCheck: function(element_name, check_type, check_extras) {
    e = this.fMatch(element_name); n = e.attr('name');
    if (n !== undefined) { // the only way to find out the element is actually matched
      e.unbind((e.length==1) ? 'click keyup change focus blur' : 'click keypress', this.checkHandler);
      delete this.types[n];
      delete this.extras[n];
      delete this.checks[n];
    }
  },

  /**
   * Performs assigned check on named element
   * @param string n element's name
   */
  checkHandler: function(n) {
    e = is_string(n) ? V.fMatch(n) : $(this);
    n = e.attr('name');
    v = e.attr('value');
    c = e.attr('checked');
    if (c === undefined) c = false;
    if (V.extras[n] === undefined) empty_allowed = false;
    else {
      l = V.extras[n].length;
      empty_allowed = (V.extras[n][l-1] !== undefined) ? V.extras[n][l-1] : false; // last param has always 'empty value allowed' meaning!
    }
    if (empty_allowed && empty(v)) V.check(e, true); // trivial case
    else switch (V.types[n]) { // non trivial ones
      case 'isEmpty': V.check(e, empty(v)); break;
      case 'notEmpty': V.check(e, !empty(v)); break;
      case 'isChecked': ch = false; e.each(function() { if (this.checked) ch = true; }); V.check(e, ch); break;
      case 'isChosen':
        ch = false; e.each(function() { if (this.checked) ch = true; });
        V.check(e, ch);
        break;
      case 'lengthAtLeast': V.check(e, strlen(v)>=V.extras[n][0]); break;
      case 'wordsAtLeast':
        var words = v.split(/[\s\t.,;]+/);
        V.check(e, words.length >= V.extras[n][0] && words[1].length > 0 ); break;
      case 'matchRegex':
        V.check(e, V.extras[n][0].test(v));
        break;
      case 'isTaxNo':
        var ia = v.split(''); // input array
        var da = []; // digits array
        for (i in ia) if (ia[i].match(/\d/)) da.push(ia[i]);
        var dw = [ 6, 5, 7, 2, 3, 4, 5, 6, 7 ]; // weights
        var dm = 11; // modulo
        var cs = 0; // checksum
        for (i in dw) cs+= da[i] * dw[i];
        V.check(e, (cs % dm == da[9]) && da.length == 10);
        break;
      case 'isInteger':
        V.check(e, /^-?\d+$/.test(v));
        break;
      case 'isEmail':
        V.check(e, isEmail(v));
        break;
      case 'isPrice':
        V.check(e, /^\d{1,3}((\.|\,)\d\d)?$/.test(v));
        break;
      case 'isPhone':
        V.check(e, /^\d{9,9}$/.test(v));
        break;
      case 'isISODate':
        V.check(e, /^(19|20)\d\d-(0|1)\d-[0-3]\d$/.test(v));
        break;
      case 'isISOTime':
        V.check(e, /^[0-2]\d:[0-5]\d:[0-5]\d$/.test(v));
        break;
      case 'isISODateTime':
        V.check(e, /^(19|20)\d\d-(0|1)\d-[0-3]\d$/.test(v) || /^(19|20)\d\d-(0|1)\d-[0-3]\d [0-2]\d:[0-5]\d:[0-5]\d$/.test(v));
        break;
      case 'isPostalCode':
        V.check(e, /^\d\d-\d\d\d$/.test(v));
        break;
      case 'isEqual': // e1, e2, lengthAtLeast
        e1 = e; v1 = v; minLength = V.extras[n][1];
        e2 = $('*[name="'+V.extras[n][0]+'"]'); v2 = e2.attr('value');
        test = (v1 == v2) && (strlen(v)>=minLength);
        V.check(e1, test);
        V.check(e2, test);
        break;
      case 'custom':
        V.customCheck();
    }
  },

  /**
   * Performs custom check on element
   * @param object e
   * @param string n name
   * @param string v value
   * @param bool c checked
   */
  customCheck: function(e, n, v, c) {
    // to be defined elsewhere
    return true;
  },

  /**
   * Attaches filter to a form field, sets lenght limit if entered
   * @param string element_name
   * @param function filter_function
   * @param int limit
   */
  addFilter: function(element_name, filter_function, limit) {
    e = this.fMatch(element_name);
    if (e !== undefined ) {
      e.bind('keydown', this.keyDown);
      e.bind('keyup', this.keyUp)
      e.bind('keypress', filter_function);
      if (limit !== undefined) e.attr('maxlength', limit);
    }
  },

  /**
   * Removes filter from form field
   * @param string element_name
   * @param function filter_function
   * @param int limit
   */
  removeFilter: function(element_name, filter_function) {
    e = this.fMatch(element_name);
    if (e !== undefined) {
      e.unbind('keydown', this.keyDown);
      e.unbind('keyup', this.keyUp)
      e.unbind('keypress', filter_function);
    }
  },

  /**
   * keyDown internal handler
   * @param object e element
   */
  keyDown: function (e) {
    k = e.keyCode;
    if (k == 16) V.shift = true;
    if (k == 17) V.ctrl = true;
    if (k == 18) V.alt = true;
    V.keyCode = k;
  },

  /**
   * keyUp internal handler
   * @param object e elemet
   */
  keyUp: function(e) {
    k = e.keyCode;
    if (k == 16) V.shift = false;
    if (k == 17) V.ctrl = false;
    if (k == 18) V.alt = false;
  },

  /**
   * Returns true if special, unfiltered key pressed
   */
  specialKey: function() {
    if (in_array(this.keyCode, [8, 9, 37, 38, 39, 40, 46])) return true; // BS, ARROWS, DEL
    if (this.ctrl & in_array(this.keyCode, [89, 90, 88, 67, 86, 82])) return true; // CTRL + YXCVR
    return false;
  },

  /**
   * Returns true if the key is a digit
   */
  isDigitKey: function() {
    return !V.shift&&((V.keyCode>=48&&V.keyCode<=57)||(V.keyCode>=96&&V.keyCode<=105));
  },

  /**
   * Returns true if the key is a coma
   */
  isComaKey: function() {
    return V.keyCode == 188;
  },

  /**
   * Returns true if the key is a dot
   */
  isDotKey: function() {
    return V.keyCode == 190;
  },

  /**
   * Returns true if the key is a space
   */
  isSpaceKey: function() {
    return V.keyCode == 32;
  },

  /**
   * Returns true if the key is a dash
   */
  isDashKey: function() {
    return !V.shift && (V.keyCode == 109 || V.keyCode == 45 || V.keyCode == 189);
  },

  /**
   * Returns true if the key is a colon
   */
  isColonKey: function() {
    return V.shift && V.keyCode==59;
  },

  /**
   * Returns true if the key is a decimal point key
   */
  isDecimalPointKey: function() {
    return in_array(V.keyCode, [78, 110, 188, 190]);
  },

  /**
   * No spaces
   */
  fSpaceDisabled: function(e) {
    if (V.keyCode==32) return false;
  },

  /**
   * Digits only
   */
  fDigits: function(e) {
    return (V.specialKey()||V.isDigitKey());
  },

  /**
   * Coma, space, digits only
   */
  fCSDigits: function(e) {
    return (V.specialKey()||V.isComaKey()||V.isSpaceKey()||V.isDigitKey());
  },

  /**
   * Dash + digits only
   */
  fDDigits: function(e) {
    return (V.specialKey()||V.isDashKey()||(V.isDigitKey()));
  },

  /**
   * Dash + colon + digits only
   */
  fDCDigits: function(e) {
    return (V.specialKey()||(V.isDashKey())||V.isColonKey()||V.isDigitKey());
  },

  /**
   * Decimal numbers only
   */
  fDecimal: function(e) {
    return (V.specialKey()||V.isDigitKey()||V.isDecimalPointKey()||V.isDashKey());
  },

  /**
   * Decimal, non-negative numbers only
   */
  fNNDecimal: function(e) {
    return (V.specialKey()||V.isDigitKey()||V.isDecimalPointKey());
  }

}