text: '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);
      }
    }
  }
  