// Copyright (C) 2011 Neal Stewart
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
// of the Software, and to permit persons to whom the Software is furnished to do
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.


(function(Backbone) {

// Require Underscore and Backbone if there's a `require` function.
// This makes `backbone.validations` work on the server or when using
// `browserify`.
if (typeof require !== 'undefined') {
  _ = require('underscore');
  Backbone = require('backbone');
}

// Premade Validators
Backbone.Validations = {};

var validators = {
  "custom" : function(methodName, attributeName, model, valueToSet) {
    return model[methodName](attributeName, valueToSet);
  },

  "required" : function(isRequired, attributeName, model, valueToSet) {
    if (isRequired && (_.isNull(valueToSet) || _.isUndefined(valueToSet) || valueToSet === "")) {
      return "required";
    } else {
      return false;
    }
  },

  "in" : function(whitelist, attributeName, model, valueToSet) {
    return _.include(whitelist, valueToSet) ? undefined : "in";
  },

  "email" : function(type, attributeName, model, valueToSet) {
    var emailRegex = new RegExp("[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?", "i");

    if (_.isString(valueToSet) && !valueToSet.match(emailRegex)) {
      return "email";
    }
  },

  "url" : function(type, attributeName, model, valueToSet) {
    // taken from jQuery UI validation
    var urlRegex = /^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(\#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i;
    if (_.isString(valueToSet) && !valueToSet.match(urlRegex)) {
      return "url";
    }
  },

  "number" : function(type, attributeName, model, valueToSet) {
    return isNaN(valueToSet) ? 'number' : undefined;
  },

  "Array": function(type, attributeName, model, valueToSet) {
    return _.isArray(valueToSet) ? undefined : 'Array';
  },

  "Boolean": function(type, attributeName, model, valueToSet) {
    return _.isBoolean(valueToSet) ? undefined : 'Boolean';
  },

  "String": function(type, attributeName, model, valueToSet) {
    return _.isString(valueToSet) ? undefined : 'String';
  },

  "digits": function (type, attributeName, model, valueToSet) {
    var isBeingSet = !_.isUndefined(valueToSet);
    return (!/^\d+$/.test(valueToSet) && isBeingSet) ? 'digits' : undefined;
  },

  "pattern" : function(pattern, attributeName, model, valueToSet) {
    if (_.isString(valueToSet)) {
      if (pattern.test(valueToSet)) {
        return false;
      } else {
        return "pattern";
      }
    }
  },

  "min" : function(minimumValue, attributeName, model, valueToSet) {
    if (valueToSet < minimumValue) {
      return "min";
    }
  },

  "max" : function(maximumValue, attributeName, model, valueToSet) {
    if (valueToSet > maximumValue) {
      return "max";
    }
  },

  "minlength" : function(minlength, attributeName, model, valueToSet) {
    if (_.isString(valueToSet)) {
      if (valueToSet.length < minlength) { return "minlength"; }
    }
  },

  "maxlength" : function(maxlength, attributeName, model, valueToSet) {
    if (_.isString(valueToSet)) {
      if (valueToSet.length > maxlength) { return "maxlength"; }
    }
  },

  "arrayElem": function(validator, attributeName, model, valueToSet) {
    if (_.isArray(valueToSet) && !_.all(valueToSet, validator)) {
      return "validElem";
    } else return false;
  }
};

var customValidators = {};
var getCustomValidator = function(name) {
  var cv = customValidators[name];
  if (!cv) { throw "custom validator '"+name+"' could not be found."; }
  return cv;
};

Backbone.Validations.addValidator = function(name, validator) {
  if (validators.hasOwnProperty(name) || customValidators.hasOwnProperty(name)) {
    throw "existing validator";
  }
  customValidators[name] = validator;
};


/*
  The newValidate method overrides validate in Backbone.Model.
  It has the same interface as the validate function which you
  would provide.

  The returned object looks like this:

    {
      attributeName : ["required", "of", "errors"],
      otherAttributeName: ["and", "so", "on"]
    }

  */
function newValidate(attributes) {
  var errorsForAttribute,
      errorHasOccured,
      errors = {};

  for (var attrName in this._attributeValidators) {
    var valueToSet = attributes[attrName];
    var validateAttribute = this._attributeValidators[attrName];
    if (validateAttribute)  {
      errorsForAttribute = validateAttribute(this, valueToSet);
    }
    if (errorsForAttribute) {
      errorHasOccured = true;
      errors[attrName] = errorsForAttribute;
    }
  }

  return errorHasOccured ? errors : false;
}

function createMinValidator(attributeName, minimumValue) {
  return _.bind(validators.min, null, minimumValue);
}

function createMaxValidator(attributeName, maximumValue) {
  return _.bind(validators.max, null, maximumValue);
}


/* createValidator takes in:
    - the model
    - the name of the attribute
    - the type of validation
    - the description of the validation

   returns a function that takes in:
     - the value being set for the attribute

     and either returns nothing (undefined),
     or the error name (string).
  */
function createValidator(attributeName, type, description) {
  var validator,
      validatorMethod,
      customValidator;

  if (type === "type") {
    type = description;
  }
  validator = validators[type];

  if (!validator) { validator = getCustomValidator(type); }

  if (!validator) { throw "Improper validation type '"+type+"'" ; }

  return _.bind(validator, null, description, attributeName);
}

function createAttributeValidator(attributeName, attributeDescription) {
  var validatorsForAttribute = [],
      type,
      desc;

  for (type in attributeDescription) {
    desc = attributeDescription[type];
    validatorsForAttribute.push(createValidator(attributeName, type, desc));
  }

  return function(model, valueToSet, hasOverridenError, options) {
    var validator,
        result,
        errors = [];

    for (var i = 0, length = validatorsForAttribute.length; i < length; i++) {
      validator = validatorsForAttribute[i];
      result = validator(model, valueToSet);
      if (result) {
        if (_.isArray(result)) {
          errors = errors.concat(result);
        } else {
          errors.push(result);
        }
      }
    }

    if (errors.length) {
      return errors;
    } else {
      return false;
    }
  };
}

function createValidators(modelValidations) {
  var attributeValidations,
      attributeValidators = {};

  for (var attrName in modelValidations) {
    attributeValidations = modelValidations[attrName];
    attributeValidators[attrName] = createAttributeValidator(attrName, attributeValidations);
  }

  return attributeValidators;
}

var oldValidate = Backbone.Model.prototype._validate;
function newPerformValidation(attrs, options) {
  var result = oldValidate.apply(this, arguments);

  if(!result){
    _.each(this.validationError, function(error, name) {
      this.trigger('invalid:' + name, this, this.validationError, options);
    }, this);
  }
  return result;
}

// save the old backbone
var oldModel = Backbone.Model;

// Constructor for our new Validations Model
Backbone.Validations.Model = Backbone.Model.extend({
  constructor : function() {
    // if they pass an object, construct the new validations
    if (typeof this.validate === "object" && this.validate !== null) {
      if (!this.constructor.prototype._attributeValidators) {
        this.constructor.prototype._attributeValidators = createValidators(this.validate);
        this.constructor.prototype.validate = newValidate;
        this.constructor.prototype._validate = newPerformValidation;
      }
    }

    oldModel.apply(this, arguments);
  }
});

// Override Backbone.Model with our new Model
Backbone.Model = Backbone.Validations.Model;


// Requisite noConflict
Backbone.Validations.Model.noConflict =  function() {
  Backbone.Model = oldModel;
};

}(typeof Backbone === 'undefined' ? null : Backbone));
