'use strict';

var Utils = require('./utils')
  , Promise = require('./promise');

/**
 * Hooks are function that are called before and after  (bulk-) creation/updating/deletion and validation. Hooks can be added to you models in three ways:
 *
 * 1. By specifying them as options in `sequelize.define`
 * 2. By calling `hook()` with a string and your hook handler function
 * 3. By calling the function with the same name as the hook you want

 * ```js
 * // Method 1
 * sequelize.define(name, { attributes }, {
 *   hooks: {
 *     beforeBulkCreate: function () {
 *       // can be a single function
 *     },
 *     beforeValidate: [
 *       function () {},
 *       function() {} // Or an array of several
 *     ]
 *   }
 * })
 *
 * // Method 2
 * Model.hook('afterDestroy', function () {})
 *
 * // Method 3
 * Model.afterBulkUpdate(function () {})
 * ```
 *
 * @see {Sequelize#define}
 * @mixin Hooks
 * @name Hooks
 */

var hookTypes = {
  beforeValidate: {params: 2},
  afterValidate: {params: 2},
  beforeCreate: {params: 2},
  afterCreate: {params: 2},
  beforeDestroy: {params: 2},
  afterDestroy: {params: 2},
  beforeUpdate: {params: 2},
  afterUpdate: {params: 2},
  beforeBulkCreate: {params: 2},
  afterBulkCreate: {params: 2},
  beforeBulkDestroy: {params: 1},
  afterBulkDestroy: {params: 1},
  beforeBulkUpdate: {params: 1},
  afterBulkUpdate: {params: 1},
  beforeFind: {params: 1},
  beforeFindAfterExpandIncludeAll: {params: 1},
  beforeFindAfterOptions: {params: 1},
  afterFind: {params: 2},
  beforeDefine: {params: 2, sync: true},
  afterDefine: {params: 1, sync: true},
  beforeInit: {params: 2, sync: true},
  afterInit: {params: 1, sync: true}
};

var hookAliases = {
  beforeDelete: 'beforeDestroy',
  afterDelete: 'afterDestroy',
  beforeBulkDelete: 'beforeBulkDestroy',
  afterBulkDelete: 'afterBulkDestroy'
};

var Hooks = {
  replaceHookAliases: function(hooks) {
    var realHookName;

    Utils._.each(hooks, function(hooksArray, name) {
      // Does an alias for this hook name exist?
      if (realHookName = hookAliases[name]) {
        // Add the hooks to the actual hook
        hooks[realHookName] = (hooks[realHookName] || []).concat(hooksArray);

        // Delete the alias
        delete hooks[name];
      }
    });

    return hooks;
  },

  runHooks: function(hooks) {
    var self = this
      , fn
      , fnArgs = Utils.sliceArgs(arguments, 1)
      , hookType;

    if (typeof fnArgs[fnArgs.length - 1] === 'function') {
      fn = fnArgs.pop();
    }

    if (typeof hooks === 'string') {
      hookType = hooks;
      hooks = this.options.hooks[hookType] || [];
      if (!Array.isArray(hooks)) hooks = hooks === undefined ? [] : [hooks];
      if (this.sequelize) hooks = hooks.concat(this.sequelize.options.hooks[hookType] || []);
    }

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

    // run hooks as sync functions if flagged as sync
    if (hookTypes[hookType] && hookTypes[hookType].sync) {
      hooks.forEach(function(hook) {
        if (typeof hook === 'object') hook = hook.fn;
        return hook.apply(self, fnArgs);
      });
      return;
    }

    // run hooks async
    var promise = Promise.each(hooks, function (hook) {
      if (typeof hook === 'object') {
        hook = hook.fn;
      }

      if (hookType && hook.length > hookTypes[hookType].params) {
        hook = Promise.promisify(hook, self);
      }

      return hook.apply(self, fnArgs);
    }).return();

    if (fn) {
      return promise.nodeify(fn);
    }

    return promise;
  },

  hook: function() {
    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
   *
   * @alias hook
   */
  addHook: function(hookType, name, fn) {
    if (typeof name === 'function') {
      fn = name;
      name = null;
    }

    hookType = hookAliases[hookType] || hookType;

    this.options.hooks[hookType] = this.options.hooks[hookType] || [];
    this.options.hooks[hookType].push(!!name ? {name: name, fn: fn} : fn);
    return this;
  },

  /**
   * Remove hook from the model
   *
   * @param {String} hookType
   * @param {String} name
   */
  removeHook: function(hookType, name) {
    hookType = hookAliases[hookType] || hookType;

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

    this.options.hooks[hookType] = this.options.hooks[hookType].filter(function (hook) {
      // don't remove unnamed hooks
      if (typeof hook === 'function') {
        return true;
      }

      return typeof hook === 'object' && hook.name !== name;
    });

    return this;
  },

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

Hooks.hasHooks = Hooks.hasHook;

/**
 * A hook that is run before validation
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with instance, options, callback(err)
 * @name beforeValidate
 */

/**
 * A hook that is run after validation
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with instance, options, callback(err)
 * @name afterValidate
 */

/**
 * 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, callback(err)
 * @name beforeCreate
 */

/**
 * 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, callback(err)
 * @name afterCreate
 */

/**
 * 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, callback(err)
 *
 * @name beforeDestroy
 * @alias beforeDelete
 */

/**
 * 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, callback(err)
 *
 * @name afterDestroy
 * @alias afterDelete
 */

/**
 * 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, callback(err)
 * @name beforeUpdate
 */

/**
 * 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, callback(err)
 * @name afterUpdate
 */

/**
 * 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, callback(err)
 * @name beforeBulkCreate
 */

/**
 * 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, callback(err)
 * @name afterBulkCreate
 */

/**
 * A hook that is run before destroying instances in bulk
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options, callback(err)
 *
 * @name beforeBulkDestroy
 * @alias beforeBulkDelete
 */

/**
 * A hook that is run after destroying instances in bulk
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options, callback(err)
 *
 * @name afterBulkDestroy
 * @alias afterBulkDelete
 */

/**
 * A hook that is run after updating instances in bulk
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options, callback(err)
 * @name beforeBulkUpdate
 */

/**
 * A hook that is run after updating instances in bulk
 * @param {String}   name
 * @param {Function} fn   A callback function that is called with options, callback(err)
 * @name afterBulkUpdate
 */

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

/**
 * 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, callback(err)
 * @name beforeFindAfterExpandIncludeAll
 */

/**
 * 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, callback(err)
 * @name beforeFindAfterOptions
 */

/**
 * 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, callback(err)
 * @name afterFind
 */

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

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

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

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

module.exports = {
  hooks: hookTypes,
  hookAliases: hookAliases,

  applyTo: function(Model) {
    Utils._.mixin(Model, Hooks);
    Utils._.mixin(Model.prototype, Hooks);

    var allHooks = Object.keys(hookTypes).concat(Object.keys(hookAliases));
    allHooks.forEach(function(hook) {
      Model.prototype[hook] = function(callback) {
        return this.addHook(hook, callback);
      };
    });
  }
};
