'use strict';

var PromiseProvider = require('../../promise_provider');
var VersionError = require('../../error').VersionError;

module.exports = applyHooks;

/*!
 * Register hooks for this model
 *
 * @param {Model} model
 * @param {Schema} schema
 */

function applyHooks(model, schema) {
  var q = schema && schema.callQueue;
  var toWrapEl;
  var len;
  var i;
  var j;
  var pointCut;
  var keys;
  var newName;

  model.$appliedHooks = true;
  for (i = 0; i < schema.childSchemas.length; ++i) {
    var childModel = schema.childSchemas[i].model;
    if (childModel.$appliedHooks) {
      continue;
    }
    applyHooks(childModel, schema.childSchemas[i].schema);
    if (childModel.discriminators != null) {
      keys = Object.keys(childModel.discriminators);
      for (j = 0; j < keys.length; ++j) {
        applyHooks(childModel.discriminators[keys[j]],
          childModel.discriminators[keys[j]].schema);
      }
    }
  }

  if (!q.length) {
    return;
  }

  // we are only interested in 'pre' hooks, and group by point-cut
  var toWrap = { post: [] };
  var pair;

  for (i = 0; i < q.length; ++i) {
    pair = q[i];
    if (pair[0] !== 'pre' && pair[0] !== 'post' && pair[0] !== 'on') {
      continue;
    }
    var args = [].slice.call(pair[1]);
    pointCut = pair[0] === 'on' ? 'post' : args[0];
    if (!(pointCut in toWrap)) {
      toWrap[pointCut] = {post: [], pre: []};
    }
    if (pair[0] === 'post') {
      toWrap[pointCut].post.push(args);
    } else if (pair[0] === 'on') {
      toWrap[pointCut].push(args);
    } else {
      toWrap[pointCut].pre.push(args);
    }
  }

  // 'post' hooks are simpler
  len = toWrap.post.length;
  toWrap.post.forEach(function(args) {
    model.on.apply(model, args);
  });
  delete toWrap.post;

  // 'init' should be synchronous on subdocuments
  if (toWrap.init && (model.$isSingleNested || model.$isArraySubdocument)) {
    if (toWrap.init.pre) {
      toWrap.init.pre.forEach(function(args) {
        model.prototype.$pre.apply(model.prototype, args);
      });
    }
    if (toWrap.init.post) {
      toWrap.init.post.forEach(function(args) {
        model.prototype.$post.apply(model.prototype, args);
      });
    }
    delete toWrap.init;
  }
  if (toWrap.set) {
    // Set hooks also need to be sync re: gh-3479
    newName = '$__original_set';
    model.prototype[newName] = model.prototype.set;
    if (toWrap.set.pre) {
      toWrap.set.pre.forEach(function(args) {
        model.prototype.$pre.apply(model.prototype, args);
      });
    }
    if (toWrap.set.post) {
      toWrap.set.post.forEach(function(args) {
        model.prototype.$post.apply(model.prototype, args);
      });
    }
    delete toWrap.set;
  }

  toWrap.validate = toWrap.validate || { pre: [], post: [] };

  keys = Object.keys(toWrap);
  len = keys.length;
  for (i = 0; i < len; ++i) {
    pointCut = keys[i];
    // this is so we can wrap everything into a promise;
    newName = ('$__original_' + pointCut);
    if (!model.prototype[pointCut]) {
      continue;
    }
    if (model.prototype[pointCut].$isWrapped) {
      continue;
    }
    if (!model.prototype[pointCut].$originalFunction) {
      model.prototype[newName] = model.prototype[pointCut];
    }
    model.prototype[pointCut] = (function(_newName) {
      return function wrappedPointCut() {
        var Promise = PromiseProvider.get();

        var _this = this;
        var args = [].slice.call(arguments);
        var lastArg = args.pop();
        var fn;
        var originalError = new Error();
        var $results;
        if (lastArg && typeof lastArg !== 'function') {
          args.push(lastArg);
        } else {
          fn = lastArg;
        }

        var promise = new Promise.ES6(function(resolve, reject) {
          args.push(function(error) {
            if (error) {
              // gh-2633: since VersionError is very generic, take the
              // stack trace of the original save() function call rather
              // than the async trace
              if (error instanceof VersionError) {
                error.stack = originalError.stack;
              }
              if (!fn) {
                _this.$__handleReject(error);
              }
              reject(error);
              return;
            }

            // There may be multiple results and promise libs other than
            // mpromise don't support passing multiple values to `resolve()`
            $results = Array.prototype.slice.call(arguments, 1);
            resolve.apply(promise, $results);
          });

          _this[_newName].apply(_this, args);
        });
        if (fn) {
          if (this.constructor.$wrapCallback) {
            fn = this.constructor.$wrapCallback(fn);
          }
          promise.then(
            function() {
              process.nextTick(function() {
                fn.apply(null, [null].concat($results));
              });
            },
            function(error) {
              process.nextTick(function() {
                fn(error);
              });
            });
        }
        return promise;
      };
    })(newName);
    model.prototype[pointCut].$originalFunction = newName;
    model.prototype[pointCut].$isWrapped = true;

    toWrapEl = toWrap[pointCut];
    var _len = toWrapEl.pre.length;
    for (j = 0; j < _len; ++j) {
      args = toWrapEl.pre[j];
      args[0] = newName;
      model.prototype.$pre.apply(model.prototype, args);
    }

    _len = toWrapEl.post.length;
    for (j = 0; j < _len; ++j) {
      args = toWrapEl.post[j];
      args[0] = newName;
      model.prototype.$post.apply(model.prototype, args);
    }
  }
}
