'use strict';

const _ = require('lodash');
const Utils = require('./utils');
const Promise = require('./promise');
const debug = Utils.getLogger().debugContext('hooks');

const hookTypes = {
  beforeValidate: {params: 2},
  afterValidate: {params: 2},
  validationFailed: {params: 3},
  beforeCreate: {params: 2},
  afterCreate: {params: 2},
  beforeDestroy: {params: 2},
  afterDestroy: {params: 2},
  beforeRestore: {params: 2},
  afterRestore: {params: 2},
  beforeUpdate: {params: 2},
  afterUpdate: {params: 2},
  beforeSave: {params: 2, proxies: ['beforeUpdate', 'beforeCreate']},
  afterSave: {params: 2, proxies: ['afterUpdate', 'afterCreate']},
  beforeUpsert: {params: 2},
  afterUpsert: {params: 2},
  beforeBulkCreate: {params: 2},
  afterBulkCreate: {params: 2},
  beforeBulkDestroy: {params: 1},
  afterBulkDestroy: {params: 1},
  beforeBulkRestore: {params: 1},
  afterBulkRestore: {params: 1},
  beforeBulkUpdate: {params: 1},
  afterBulkUpdate: {params: 1},
  beforeFind: {params: 1},
  beforeFindAfterExpandIncludeAll: {params: 1},
  beforeFindAfterOptions: {params: 1},
  afterFind: {params: 2},
  beforeCount: {params: 1},
  beforeDefine: {params: 2, sync: true},
  afterDefine: {params: 1, sync: true},
  beforeInit: {params: 2, sync: true},
  afterInit: {params: 1, sync: true},
  beforeConnect: {params: 1},
  afterConnect: {params: 2},
  beforeSync: {params: 1},
  afterSync: {params: 1},
  beforeBulkSync: {params: 1},
  afterBulkSync: {params: 1}
};
exports.hooks = hookTypes;

const hookAliases = {
  beforeDelete: 'beforeDestroy',
  afterDelete: 'afterDestroy',
  beforeBulkDelete: 'beforeBulkDestroy',
  afterBulkDelete: 'afterBulkDestroy',
  beforeConnection: 'beforeConnect'
};
exports.hookAliases = hookAliases;

/**
 * get array of current hook and its proxied hooks combined
 * @private
 */
const getProxiedHooks = hookType =>
  hookTypes[hookType].proxies
    ? hookTypes[hookType].proxies.concat(hookType)
    : [hookType]
;

function getHooks(hookType) {
  return (this.options.hooks || {})[hookType] || [];
};

const Hooks = {
  /**
   * Process user supplied hooks definition
   *
   * @param {Object} hooks
   *
   * @private
   * @memberOf Sequelize
   * @memberOf Sequelize.Model
   */
  _setupHooks(hooks) {
    this.options.hooks = {};
    _.map(hooks || {}, (hooksArray, hookName) => {
      if (!_.isArray(hooksArray)) hooksArray = [hooksArray];
      hooksArray.forEach(hookFn => this.addHook(hookName, hookFn));
    });
  },

  runHooks(hooks) {
    if (!hooks) throw new Error('runHooks requires at least 1 argument');

    const hookArgs = Utils.sliceArgs(arguments, 1);
    let hookType;

    if (typeof hooks === 'string') {
      hookType = hooks;
      hooks = getHooks.call(this, hookType);

      if (this.sequelize) {
        hooks = hooks.concat(getHooks.call(this.sequelize, hookType));
      }
    }

    if (!Array.isArray(hooks)) {
      hooks = [hooks];
    }

    // synchronous hooks
    if (hookTypes[hookType] && hookTypes[hookType].sync) {
      for (let hook of hooks) {
        if (typeof hook === 'object') {
          hook = hook.fn;
        }

        debug(`running hook(sync) ${hookType}`);
        hook.apply(this, hookArgs);
      }
      return;
    }

    // asynchronous hooks (default)
    return Promise.each(hooks, hook => {
      if (typeof hook === 'object') {
        hook = hook.fn;
      }

      debug(`running hook ${hookType}`);
      return Promise.resolve(hook.apply(this, hookArgs));
    }).return();
  },

  hook() {
    Utils.deprecate('hook() method has been deprecated, please use addHook() method instead');
    return Hooks.addHook.apply(this, arguments);
  },

  /**
   * Add a hook to the model
   *
   * @param {String}    hookType
   * @param {String}    [name]    Provide a name for the hook function. It can be used to remove the hook later or to order hooks based on some sort of priority system in the future.
   * @param {Function}  fn        The hook function
   *
   * @memberOf Sequelize
   * @memberOf Sequelize.Model
   */
  addHook(hookType, name, fn) {
    if (typeof name === 'function') {
      fn = name;
      name = null;
    }

    debug(`adding hook ${hookType}`);
    const originalHookType = hookType;
    hookType = hookAliases[hookType] || hookType;

    if (hookAliases[originalHookType]) {
      Utils.deprecate(`${originalHookType} hook has been deprecated, please use ${hookType} hook instead`);
    }

    // check for proxies, add them too
    hookType = getProxiedHooks(hookType);

    _.each(hookType, type => {
      this.options.hooks[type] = getHooks.call(this, type);
      this.options.hooks[type].push(name ? {name, fn} : fn);
    });

    return this;
  },

  /**
   * Remove hook from the model
   *
   * @param {String} hookType
   * @param {String|Function} name
   *
   * @memberOf Sequelize
   * @memberOf Sequelize.Model
   */
  removeHook(hookType, name) {
    hookType = hookAliases[hookType] || hookType;
    const isReference = typeof name === 'function' ? true : false;

    if (!this.hasHook(hookType)) {
      return this;
    }

    Utils.debug(`removing hook ${hookType}`);

    // check for proxies, add them too
    hookType = getProxiedHooks(hookType);

    for (const type of hookType) {
      this.options.hooks[type] = this.options.hooks[type].filter(hook => {
        if (isReference && typeof hook === 'function') {
          return hook !== name; // check if same method
        } else if (!isReference && typeof hook === 'object') {
          return hook.name !== name;
        }
        return true;
      });
    }

    return this;
  },

  /**
   * Check whether the mode has any hooks of this type
   *
   * @param {String}  hookType
   *
   * @alias hasHooks
   * @memberOf Sequelize
   * @memberOf Sequelize.Model
   */
  hasHook(hookType) {
    return this.options.hooks[hookType] && !!this.options.hooks[hookType].length;
  }
};
Hooks.hasHooks = Hooks.hasHook;


function applyTo(target) {
  _.mixin(target, Hooks);

  const allHooks = Object.keys(hookTypes).concat(Object.keys(hookAliases));
  for (const hook of allHooks) {
    target[hook] = function(name, callback) {
      return this.addHook(hook, name, callback);
    };
  }
}
exports.applyTo = applyTo;

/**
 * A hook that is run before validation
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with instance, options
 * @name beforeValidate
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run after validation
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with instance, options
 * @name afterValidate
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run when validation fails
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with instance, options, error. Error is the
 * SequelizeValidationError. If the callback throws an error, it will replace the original validation error.
 * @name validationFailed
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run before creating a single instance
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with attributes, options
 * @name beforeCreate
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run after creating a single instance
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with attributes, options
 * @name afterCreate
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run before creating or updating a single instance, It proxies `beforeCreate` and `beforeUpdate`
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with attributes, options
 * @name beforeSave
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run before upserting
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with attributes, options
 * @name beforeUpsert
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run after upserting
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with attributes, options
 * @name afterUpsert
 * @memberOf Sequelize.Model
 */

/**
  * A hook that is run after creating or updating a single instance, It proxies `afterCreate` and `afterUpdate`
  * @param {String}   name
  * @param {Function} fn   A callback function that is called with attributes, options
  * @name afterSave
  * @memberOf Sequelize.Model
  */

/**
 * A hook that is run before destroying a single instance
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with instance, options
 *
 * @name beforeDestroy
 * @alias beforeDelete
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run after destroying a single instance
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with instance, options
 *
 * @name afterDestroy
 * @alias afterDelete
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run before restoring a single instance
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with instance, options
 *
 * @name beforeRestore
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run after restoring a single instance
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with instance, options
 *
 * @name afterRestore
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run before updating a single instance
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with instance, options
 * @name beforeUpdate
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run after updating a single instance
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with instance, options
 * @name afterUpdate
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run before creating instances in bulk
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with instances, options
 * @name beforeBulkCreate
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run after creating instances in bulk
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with instances, options
 * @name afterBulkCreate
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run before destroying instances in bulk
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options
 *
 * @name beforeBulkDestroy
 * @alias beforeBulkDelete
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run after destroying instances in bulk
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options
 *
 * @name afterBulkDestroy
 * @alias afterBulkDelete
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run before restoring instances in bulk
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options
 *
 * @name beforeBulkRestore
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run after restoring instances in bulk
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options
 *
 * @name afterBulkRestore
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run before updating instances in bulk
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options
 * @name beforeBulkUpdate
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run after updating instances in bulk
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options
 * @name afterBulkUpdate
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run before a find (select) query
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options
 * @name beforeFind
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run before a find (select) query, after any { include: {all: ...} } options are expanded
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options
 * @name beforeFindAfterExpandIncludeAll
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run before a find (select) query, after all option parsing is complete
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options
 * @name beforeFindAfterOptions
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run after a find (select) query
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with instance(s), options
 * @name afterFind
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run before a count query
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options
 * @name beforeCount
 * @memberOf Sequelize.Model
 */

/**
 * A hook that is run before a define call
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with attributes, options
 * @name beforeDefine
 * @memberOf Sequelize
 */

/**
 * A hook that is run after a define call
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with factory
 * @name afterDefine
 * @memberOf Sequelize
 */

/**
 * A hook that is run before Sequelize() call
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with config, options
 * @name beforeInit
 * @memberOf Sequelize
 */

/**
 * A hook that is run after Sequelize() call
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with sequelize
 * @name afterInit
 * @memberOf Sequelize
 */

/**
 * A hook that is run before a connection is created
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with config passed to connection
 * @name beforeConnect
 * @memberOf Sequelize
 */

/**
 * A hook that is run after a connection is created
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with the connection object and thye config passed to connection
 * @name afterConnect
 * @memberOf Sequelize
 */

/**
 * A hook that is run before Model.sync call
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options passed to Model.sync
 * @name beforeSync
 * @memberOf Sequelize
 */

/**
 * A hook that is run after Model.sync call
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options passed to Model.sync
 * @name afterSync
 * @memberOf Sequelize
 */

/**
  * A hook that is run before sequelize.sync call
  * @param {String}   name
  * @param {Function} fn   A callback function that is called with options passed to sequelize.sync
  * @name beforeBulkSync
  * @memberOf Sequelize
  */

/**
  * A hook that is run after sequelize.sync call
  * @param {String}   name
  * @param {Function} fn   A callback function that is called with options passed to sequelize.sync
  * @name afterBulkSync
  * @memberOf Sequelize
  */
