text: 'use strict';
  
  var validator = require('./utils/validator-extras').validator
    , extendModelValidations = require('./utils/validator-extras').extendModelValidations
    , Utils = require('./utils')
    , sequelizeError = require('./errors')
    , Promise = require('./promise')
    , DataTypes = require('./data-types')
    , _ = require('lodash');
  
  /**
   * The Main Instance Validator.
   *
   * @param {Instance} modelInstance The model instance.
   * @param {Object} options A dict with options.
   * @constructor
   */
  var InstanceValidator = module.exports = function(modelInstance, options) {
    options = _.clone(options) || {};
  
    if (options.fields && !options.skip) {
      options.skip = Utils._.difference(Object.keys(modelInstance.Model.attributes), options.fields);
    }
  
    // assign defined and default options
    this.options = Utils._.defaults(options, {
      skip: []
    });
  
    this.modelInstance = modelInstance;
  
    /**
     * Exposes a reference to validator.js. This allows you to add custom validations using `validator.extend`
     * @name validator
     */
    this.validator = validator;
  
    /**
     *  All errors will be stored here from the validations.
     *
     * @type {Array} Will contain keys that correspond to attributes which will
     *   be Arrays of Errors.
     */
    this.errors = [];
  
    /** @type {boolean} Indicates if validations are in progress */
    this.inProgress = false;
  
    extendModelValidations(modelInstance);
  };
  
  /** @define {string} The error key for arguments as passed by custom validators */
  InstanceValidator.RAW_KEY_NAME = '__raw';
  
  /**
   * The main entry point for the Validation module, invoke to start the dance.
   *
   * @return {Promise}
   */
  InstanceValidator.prototype.validate = function() {
    if (this.inProgress) {
      throw new Error('Validations already in progress.');
    }
    this.inProgress = true;
  
    var self = this;
    return Promise.all([
      self._builtinValidators(),
      self._customValidators()
    ].map(function(promise) {
      return promise.reflect();
    })).then(function() {
      if (self.errors.length) {
        return new sequelizeError.ValidationError(null, self.errors);
      } else {
        return null;
      }
    });
  };
  
  /**
   * Invoke the Validation sequence:
   *   - Before Validation Model Hooks
   *   - Validation
   *   - On validation success: After Validation Model Hooks
   *   - On validation failure: Validation Failed Model Hooks
   *
   * @return {Promise}
   */
  InstanceValidator.prototype.hookValidate = function() {
    var self = this;
    return self.modelInstance.Model.runHooks('beforeValidate', self.modelInstance, self.options).then(function() {
      return self.validate().then(function(error) {
        if (error) {
          return self.modelInstance.Model.runHooks('validationFailed', self.modelInstance, self.options, error).then(function(newError) {
            throw newError || error;
          });
        }
      });
    }).then(function() {
      return self.modelInstance.Model.runHooks('afterValidate', self.modelInstance, self.options);
    }).return(self.modelInstance);
  };
  
  /**
   * Will run all the built-in validators.
   *
   * @return {Promise(Array.<Promise.PromiseInspection>)} A promise from .reflect().
   * @private
   */
  InstanceValidator.prototype._builtinValidators = function() {
    var self = this;
  
    // promisify all attribute invocations
    var validators = [];
    Utils._.forIn(this.modelInstance.rawAttributes, function(rawAttribute, field) {
      if (self.options.skip.indexOf(field) >= 0) {
        return;
      }
  
      var value = self.modelInstance.dataValues[field];
  
      if (!rawAttribute._autoGenerated && !rawAttribute.autoIncrement) {
        // perform validations based on schema
        self._validateSchema(rawAttribute, field, value);
      }
  
      if (self.modelInstance.validators.hasOwnProperty(field)) {
        validators.push(self._builtinAttrValidate.call(self, value, field).reflect());
      }
    });
  
    return Promise.all(validators);
  };
  
  /**
   * Will run all the custom validators.
   *
   * @return {Promise(Array.<Promise.PromiseInspection>)} A promise from .reflect().
   * @private
   */
  InstanceValidator.prototype._customValidators = function() {
    var validators = [];
    var self = this;
    Utils._.each(this.modelInstance.$modelOptions.validate, function(validator, validatorType) {
      if (self.options.skip.indexOf(validatorType) >= 0) {
        return;
      }
  
      var valprom = self._invokeCustomValidator(validator, validatorType)
        // errors are handled in settling, stub this
        .catch(function() {})
        .reflect();
  
      validators.push(valprom);
    });
  
    return Promise.all(validators);
  };
  
  /**
   * Validate a single attribute with all the defined built-in validators.
   *
   * @param {*} value Anything.
   * @param {string} field The field name.
   * @return {Promise} A promise, will always resolve,
   *   auto populates error on this.error local object.
   * @private
   */
  InstanceValidator.prototype._builtinAttrValidate = function(value, field) {
    var self = this;
    // check if value is null (if null not allowed the Schema pass will capture it)
    if (value === null || typeof value === 'undefined') {
      return Promise.resolve();
    }
  
    // Promisify each validator
    var validators = [];
    Utils._.forIn(this.modelInstance.validators[field], function(test,
      validatorType) {
  
      if (['isUrl', 'isURL', 'isEmail'].indexOf(validatorType) !== -1) {
        // Preserve backwards compat. Validator.js now expects the second param to isURL and isEmail to be an object
        if (typeof test === 'object' && test !== null && test.msg) {
          test = {
            msg: test.msg
          };
        } else if (test === true) {
          test = {};
        }
      }
  
      // Check for custom validator.
      if (typeof test === 'function') {
        return validators.push(self._invokeCustomValidator(test, validatorType, true, value, field).reflect());
      }
  
      var validatorPromise = self._invokeBuiltinValidator(value, test, validatorType, field);
      // errors are handled in settling, stub this
      validatorPromise.catch(function() {});
      validators.push(validatorPromise.reflect());
    });
  
    return Promise.all(validators).then(this._handleReflectedResult.bind(this, field));
  };
  
  /**
   * Prepare and invoke a custom validator.
   *
   * @param {Function} validator The custom validator.
   * @param {string} validatorType the custom validator type (name).
   * @param {boolean=} optAttrDefined Set to true if custom validator was defined
   *   from the Attribute
   * @return {Promise} A promise.
   * @private
   */
  InstanceValidator.prototype._invokeCustomValidator = Promise.method(function(validator, validatorType, optAttrDefined, optValue, optField) {
    var validatorFunction = null;  // the validation function to call
    var isAsync = false;
  
    var validatorArity = validator.length;
    // check if validator is async and requires a callback
    var asyncArity = 1;
    var errorKey = validatorType;
    var invokeArgs;
    if (optAttrDefined) {
      asyncArity = 2;
      invokeArgs = optValue;
      errorKey = optField;
    }
    if (validatorArity === asyncArity) {
      isAsync = true;
    }
  
    if (isAsync) {
      if (optAttrDefined) {
        validatorFunction = Promise.promisify(validator.bind(this.modelInstance, invokeArgs));
      } else {
        validatorFunction = Promise.promisify(validator.bind(this.modelInstance));
      }
      return validatorFunction().catch(this._pushError.bind(this, false, errorKey));
    } else {
      return Promise.try(validator.bind(this.modelInstance, invokeArgs)).catch(this._pushError.bind(this, false, errorKey));
    }
  });
  
  /**
   * Prepare and invoke a build-in validator.
   *
   * @param {*} value Anything.
   * @param {*} test The test case.
   * @param {string} validatorType One of known to Sequelize validators.
   * @param {string} field The field that is being validated
   * @return {Object} An object with specific keys to invoke the validator.
   * @private
   */
  InstanceValidator.prototype._invokeBuiltinValidator = Promise.method(function(value, test, validatorType, field) {
    var self = this;
    // Cast value as string to pass new Validator.js string requirement
    var valueString = String(value);
    // check if Validator knows that kind of validation test
    if (typeof validator[validatorType] !== 'function') {
      throw new Error('Invalid validator function: ' + validatorType);
    }
    var validatorArgs = self._extractValidatorArgs(test, validatorType, field);
    if (!validator[validatorType].apply(validator, [valueString].concat(validatorArgs))) {
    // extract the error msg
      throw new Error(test.msg || 'Validation ' + validatorType + ' failed');
    }
  });
  
  /**
   * Will extract arguments for the validator.
   *
   * @param {*} test The test case.
   * @param {string} validatorType One of known to Sequelize validators.
   * @param {string} field The field that is being validated.
   * @private
   */
  InstanceValidator.prototype._extractValidatorArgs = function(test, validatorType, field) {
    var validatorArgs = test.args || test;
    var isLocalizedValidator = typeof(validatorArgs) !== 'string' && (validatorType === 'isAlpha' || validatorType === 'isAlphanumeric' || validatorType === 'isMobilePhone');
  
    if (!Array.isArray(validatorArgs)) {
      if (validatorType === 'isImmutable') {
        validatorArgs = [validatorArgs, field];
      } else if (isLocalizedValidator || validatorType === 'isIP') {
        validatorArgs = [];
      } else {
        validatorArgs = [validatorArgs];
      }
    } else {
      validatorArgs = validatorArgs.slice(0);
    }
    return validatorArgs;
  };
  
  /**
   * Will validate a single field against its schema definition (isnull).
   *
   * @param {Object} rawAttribute As defined in the Schema.
   * @param {string} field The field name.
   * @param {*} value anything.
   * @private
   */
  InstanceValidator.prototype._validateSchema = function(rawAttribute, field, value) {
    var error;
  
    if (rawAttribute.allowNull === false && ((value === null) || (value === undefined))) {
      error = new sequelizeError.ValidationErrorItem(field + ' cannot be null', 'notNull Violation', field, value);
      this.errors.push(error);
    }
  
    if (rawAttribute.type === DataTypes.STRING || rawAttribute.type instanceof DataTypes.STRING || rawAttribute.type === DataTypes.TEXT || rawAttribute.type instanceof DataTypes.TEXT) {
      if (Array.isArray(value) || (_.isObject(value) && !value._isSequelizeMethod) && !Buffer.isBuffer(value)) {
        error = new sequelizeError.ValidationErrorItem(field + ' cannot be an array or an object', 'string violation', field, value);
        this.errors.push(error);
      }
    }
  };
  
  
  /**
   * Handles the returned result of a Promise.reflect.
   *
   * If errors are found it populates this.error.
   *
   * @param {string} field The attribute name.
   * @param {Array.<Promise.PromiseInspection>} Promise inspection objects.
   * @private
   */
  InstanceValidator.prototype._handleReflectedResult = function(field, promiseInspections) {
    var self = this;
    promiseInspections.forEach(function(promiseInspection) {
      if (promiseInspection.isRejected()) {
        var rejection = promiseInspection.error();
        self._pushError(true, field, rejection);
      }
    });
  };
  
  /**
   * Signs all errors retaining the original.
   *
   * @param {boolean} isBuiltin Determines if error is from builtin validator.
   * @param {string} errorKey The error key to assign on this.errors object.
   * @param {Error|string} rawError The original error.
   * @private
   */
  InstanceValidator.prototype._pushError = function(isBuiltin, errorKey, rawError) {
    var message = rawError.message || rawError || 'Validation error';
    var error = new sequelizeError.ValidationErrorItem(message, 'Validation error', errorKey, rawError);
    error[InstanceValidator.RAW_KEY_NAME] = rawError;
  
    this.errors.push(error);
  };
  