/*!
 * Module dependencies.
 */

var NodeJSDocument = require('./document');
var EventEmitter = require('events').EventEmitter;
var MongooseError = require('./error');
var Schema = require('./schema');
var ObjectId = require('./types/objectid');
var utils = require('./utils');
var ValidationError = MongooseError.ValidationError;
var InternalCache = require('./internal');
var PromiseProvider = require('./promise_provider');
var VersionError = require('./error').VersionError;

var Embedded;

/**
 * Document constructor.
 *
 * @param {Object} obj the values to set
 * @param {Object} [fields] optional object containing the fields which were selected in the query returning this document and any populated paths data
 * @param {Boolean} [skipId] bool, should we auto create an ObjectId _id
 * @inherits NodeJS EventEmitter http://nodejs.org/api/events.html#events_class_events_eventemitter
 * @event `init`: Emitted on a document after it has was retrieved from the db and fully hydrated by Mongoose.
 * @event `save`: Emitted when the document is successfully saved
 * @api private
 */

function Document(obj, schema, fields, skipId, skipInit) {
  if (!(this instanceof Document)) {
    return new Document(obj, schema, fields, skipId, skipInit);
  }


  if (utils.isObject(schema) && !schema.instanceOfSchema) {
    schema = new Schema(schema);
  }

  // When creating EmbeddedDocument, it already has the schema and he doesn't need the _id
  schema = this.schema || schema;

  // Generate ObjectId if it is missing, but it requires a scheme
  if (!this.schema && schema.options._id) {
    obj = obj || {};

    if (obj._id === undefined) {
      obj._id = new ObjectId();
    }
  }

  if (!schema) {
    throw new MongooseError.MissingSchemaError();
  }

  this.$__setSchema(schema);

  this.$__ = new InternalCache;
  this.$__.emitter = new EventEmitter();
  this.isNew = true;
  this.errors = undefined;

  if (typeof fields === 'boolean') {
    this.$__.strictMode = fields;
    fields = undefined;
  } else {
    this.$__.strictMode = this.schema.options && this.schema.options.strict;
    this.$__.selected = fields;
  }

  var required = this.schema.requiredPaths();
  for (var i = 0; i < required.length; ++i) {
    this.$__.activePaths.require(required[i]);
  }

  this.$__.emitter.setMaxListeners(0);
  this._doc = this.$__buildDoc(obj, fields, skipId);

  if (!skipInit && obj) {
    this.init(obj);
  }

  this.$__registerHooksFromSchema();

  // apply methods
  for (var m in schema.methods) {
    this[m] = schema.methods[m];
  }
  // apply statics
  for (var s in schema.statics) {
    this[s] = schema.statics[s];
  }
}

/*!
 * Inherit from the NodeJS document
 */

Document.prototype = Object.create(NodeJSDocument.prototype);
Document.prototype.constructor = Document;

/*!
 * Browser doc exposes the event emitter API
 */

Document.$emitter = new EventEmitter();

utils.each(
    ['on', 'once', 'emit', 'listeners', 'removeListener', 'setMaxListeners',
      'removeAllListeners', 'addListener'],
    function(emitterFn) {
      Document[emitterFn] = function() {
        return Document.$emitter[emitterFn].apply(Document.$emitter, arguments);
      };
    });

/*!
 * Executes methods queued from the Schema definition
 *
 * @api private
 * @method $__registerHooksFromSchema
 * @deprecated
 * @memberOf Document
 */

Document.prototype.$__registerHooksFromSchema = function() {
  Embedded = Embedded || require('./types/embedded');
  var Promise = PromiseProvider.get();

  var _this = this;
  var q = _this.schema && _this.schema.callQueue;
  var toWrapEl;
  var len;
  var i;
  var j;
  var pointCut;
  var keys;
  if (!q.length) {
    return _this;
  }

  // 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') {
      _this[pair[0]].apply(_this, pair[1]);
      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) {
    _this.on.apply(_this, args);
  });
  delete toWrap.post;

  // 'init' should be synchronous on subdocuments
  if (toWrap.init && _this instanceof Embedded) {
    if (toWrap.init.pre) {
      toWrap.init.pre.forEach(function(args) {
        _this.$pre.apply(_this, args);
      });
    }
    if (toWrap.init.post) {
      toWrap.init.post.forEach(function(args) {
        _this.$post.apply(_this, args);
      });
    }
    delete toWrap.init;
  } else if (toWrap.set) {
    // Set hooks also need to be sync re: gh-3479
    if (toWrap.set.pre) {
      toWrap.set.pre.forEach(function(args) {
        _this.$pre.apply(_this, args);
      });
    }
    if (toWrap.set.post) {
      toWrap.set.post.forEach(function(args) {
        _this.$post.apply(_this, args);
      });
    }
    delete toWrap.set;
  }

  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;
    var newName = ('$__original_' + pointCut);
    if (!_this[pointCut]) {
      continue;
    }
    if (_this[pointCut].$isWrapped) {
      continue;
    }
    _this[newName] = _this[pointCut];
    _this[pointCut] = (function(_newName) {
      return function wrappedPointCut() {
        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;
              }
              _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);
          }
          return promise.then(
            function() {
              process.nextTick(function() {
                fn.apply(null, [null].concat($results));
              });
            },
            function(error) {
              process.nextTick(function() {
                fn(error);
              });
            });
        }
        return promise;
      };
    })(newName);
    _this[pointCut].$isWrapped = true;

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

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

/*!
 * Module exports.
 */

Document.ValidationError = ValidationError;
module.exports = exports = Document;
