'use strict';

var Validator = require('validator')
  , Utils = require('./utils')
  , sequelizeError = require('./errors')
  , Promise = require('./promise')
  , DataTypes = require('./data-types')
  , _ = require('lodash');

function noop() {}

// Deprecate this.
Validator.notNull = function() {
  throw new Error('Warning "notNull" validation has been deprecated in favor of Schema based "allowNull"');
};

// https://github.com/chriso/validator.js/blob/1.5.0/lib/validators.js

Validator.extend('notEmpty', function(str) {
  return !str.match(/^[\s\t\r\n]*$/);
});

Validator.extend('len', function(str, min, max) {
  return this.isLength(str, min, max);
});

Validator.extend('isUrl', function(str) {
  return this.isURL(str);
});

Validator.extend('isIPv6', function(str) {
  return this.isIP(str, 6);
});

Validator.extend('isIPv4', function(str) {
  return this.isIP(str, 4);
});

Validator.extend('notIn', function(str, values) {
  return !this.isIn(str, values);
});

Validator.extend('regex', function(str, pattern, modifiers) {
  str += '';
  if (Object.prototype.toString.call(pattern).slice(8, -1) !== 'RegExp') {
    pattern = new RegExp(pattern, modifiers);
  }
  return str.match(pattern);
});

Validator.extend('notRegex', function(str, pattern, modifiers) {
  return !this.regex(str, pattern, modifiers);
});

Validator.extend('isDecimal', function(str) {
  return str !== '' && str.match(/^(?:-?(?:[0-9]+))?(?:\.[0-9]*)?(?:[eE][\+\-]?(?:[0-9]+))?$/);
});

Validator.extend('min', function(str, val) {
  var number = parseFloat(str);
  return isNaN(number) || number >= val;
});

Validator.extend('max', function(str, val) {
  var number = parseFloat(str);
  return isNaN(number) || number <= val;
});

Validator.extend('not', function(str, pattern, modifiers) {
  return this.notRegex(str, pattern, modifiers);
});

Validator.extend('contains', function(str, elem) {
  return str.indexOf(elem) >= 0 && !!elem;
});

Validator.extend('notContains', function(str, elem) {
  return !this.contains(str, elem);
});

Validator.extend('is', function(str, pattern, modifiers) {
  return this.regex(str, pattern, modifiers);
});

function extendModelValidations(modelInstance) {
  Validator.extend('isImmutable', function(str, param, field) {
    return (modelInstance.isNewRecord || modelInstance.dataValues[field] === modelInstance._previousDataValues[field]);
  });
}


/**
 * 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 = 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.settle([
    self._builtinValidators(),
    self._customValidators()
  ]).then(function() {
    if (self.errors.length) {
      return new sequelizeError.ValidationError(null, self.errors);
    }

    return new Promise(function(resolve) {
      resolve();
    });
  });
};

/**
 * Invoke the Validation sequence:
 *   - Before Validation Model Hooks
 *   - Validation
 *   - After Validation 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) {
        throw 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 .settle().
 * @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));
    }
  });

  return Promise.settle(validators);
};

/**
 * Will run all the custom validators.
 *
 * @return {Promise(Array.<Promise.PromiseInspection>)} A promise from .settle().
 * @private
 */
InstanceValidator.prototype._customValidators = function() {


  var validators = [];
  var self = this;
  Utils._.each(this.modelInstance.__options.validate, function(validator,
    validatorType) {

    var valprom = self._invokeCustomValidator(validator, validatorType)
      // errors are handled in settling, stub this
      .catch(noop);

    validators.push(valprom);
  });

  return Promise.settle(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));
    }

    var validatorPromise = self._invokeBuiltinValidator(value, test, validatorType, field);
    // errors are handled in settling, stub this
    validatorPromise.catch(noop);
    validators.push(validatorPromise);
  });

  return Promise.settle(validators).then(this._handleSettledResult.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) {
  // check if Validator knows that kind of validation test
  if (typeof Validator[validatorType] !== 'function') {
    throw new Error('Invalid validator function: ' + validatorType);
  }

  // extract extra arguments for the validator
  var validatorArgs = test.args || test;

  if (!Array.isArray(validatorArgs)) {
    if (validatorType === 'isImmutable') {
      validatorArgs = [validatorArgs, field];
    } else if (validatorType === 'isIP') {
      validatorArgs = [];
    } else {
      validatorArgs = [validatorArgs];
    }
  } else {
    validatorArgs = validatorArgs.slice(0);
  }

  if (!Validator[validatorType].apply(Validator, [value].concat(validatorArgs))) {
  // extract the error msg
    throw test.msg || 'Validation ' + validatorType + ' failed';
  }
});

/**
 * 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.settle.
 *
 * If errors are found it populates this.error.
 *
 * @param {string} field The attribute name.
 * @param {Array.<Promise.PromiseInspection>} Promise inspection objects.
 * @private
 */
InstanceValidator.prototype._handleSettledResult = 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);
};
