text: 'use strict';
  
  const MongooseError = require('../../error/index');
  const SkipPopulateValue = require('./SkipPopulateValue');
  const get = require('../get');
  const getDiscriminatorByValue = require('../discriminator/getDiscriminatorByValue');
  const isPathExcluded = require('../projection/isPathExcluded');
  const getConstructorName = require('../getConstructorName');
  const getSchemaTypes = require('./getSchemaTypes');
  const getVirtual = require('./getVirtual');
  const lookupLocalFields = require('./lookupLocalFields');
  const mpath = require('mpath');
  const normalizeRefPath = require('./normalizeRefPath');
  const util = require('util');
  const utils = require('../../utils');
  
  const modelSymbol = require('../symbols').modelSymbol;
  const populateModelSymbol = require('../symbols').populateModelSymbol;
  const schemaMixedSymbol = require('../../schema/symbols').schemaMixedSymbol;
  
  module.exports = function getModelsMapForPopulate(model, docs, options) {
    let i;
    let doc;
    const len = docs.length;
    const available = {};
    const map = [];
    const modelNameFromQuery = options.model && options.model.modelName || options.model;
    let schema;
    let refPath;
    let Model;
    let currentOptions;
    let modelNames;
    let modelName;
  
    const originalModel = options.model;
    let isVirtual = false;
    const modelSchema = model.schema;
  
    let allSchemaTypes = getSchemaTypes(modelSchema, null, options.path);
    allSchemaTypes = Array.isArray(allSchemaTypes) ? allSchemaTypes : [allSchemaTypes].filter(v => v != null);
    const _firstWithRefPath = allSchemaTypes.find(schematype => get(schematype, 'options.refPath', null) != null);
  
    for (i = 0; i < len; i++) {
      doc = docs[i];
      let justOne = null;
      schema = getSchemaTypes(modelSchema, doc, options.path);
      // Special case: populating a path that's a DocumentArray unless
      // there's an explicit `ref` or `refPath` re: gh-8946
      if (schema != null &&
          schema.$isMongooseDocumentArray &&
          schema.options.ref == null &&
          schema.options.refPath == null) {
        continue;
      }
      // Populating a nested path should always be a no-op re: #9073.
      // People shouldn't do this, but apparently they do.
      if (options._localModel != null && options._localModel.schema.nested[options.path]) {
        continue;
      }
      const isUnderneathDocArray = schema && schema.$isUnderneathDocArray;
      if (isUnderneathDocArray && get(options, 'options.sort') != null) {
        return new MongooseError('Cannot populate with `sort` on path ' + options.path +
          ' because it is a subproperty of a document array');
      }
  
      modelNames = null;
      let isRefPath = !!_firstWithRefPath;
      let normalizedRefPath = _firstWithRefPath ? get(_firstWithRefPath, 'options.refPath', null) : null;
      let schemaOptions = null;
  
      if (Array.isArray(schema)) {
        const schemasArray = schema;
        for (const _schema of schemasArray) {
          let _modelNames;
          let res;
          try {
            res = _getModelNames(doc, _schema);
            _modelNames = res.modelNames;
            isRefPath = isRefPath || res.isRefPath;
            normalizedRefPath = normalizeRefPath(normalizedRefPath, doc, options.path) ||
              res.refPath;
            justOne = res.justOne;
          } catch (error) {
            return error;
          }
  
          if (isRefPath && !res.isRefPath) {
            continue;
          }
          if (!_modelNames) {
            continue;
          }
          modelNames = modelNames || [];
          for (const modelName of _modelNames) {
            if (modelNames.indexOf(modelName) === -1) {
              modelNames.push(modelName);
            }
          }
        }
      } else {
        try {
          const res = _getModelNames(doc, schema);
          modelNames = res.modelNames;
          isRefPath = res.isRefPath;
          normalizedRefPath = res.refPath;
          justOne = res.justOne;
          schemaOptions = get(schema, 'options.populate', null);
        } catch (error) {
          return error;
        }
  
        if (!modelNames) {
          continue;
        }
      }
  
      const _virtualRes = getVirtual(model.schema, options.path);
      const virtual = _virtualRes == null ? null : _virtualRes.virtual;
  
      let localField;
      let count = false;
      if (virtual && virtual.options) {
        const virtualPrefix = _virtualRes.nestedSchemaPath ?
          _virtualRes.nestedSchemaPath + '.' : '';
        if (typeof virtual.options.localField === 'function') {
          localField = virtualPrefix + virtual.options.localField.call(doc, doc);
        } else if (Array.isArray(virtual.options.localField)) {
          localField = virtual.options.localField.map(field => virtualPrefix + field);
        } else {
          localField = virtualPrefix + virtual.options.localField;
        }
        count = virtual.options.count;
  
        if (virtual.options.skip != null && !options.hasOwnProperty('skip')) {
          options.skip = virtual.options.skip;
        }
        if (virtual.options.limit != null && !options.hasOwnProperty('limit')) {
          options.limit = virtual.options.limit;
        }
        if (virtual.options.perDocumentLimit != null && !options.hasOwnProperty('perDocumentLimit')) {
          options.perDocumentLimit = virtual.options.perDocumentLimit;
        }
      } else {
        localField = options.path;
      }
      let foreignField = virtual && virtual.options ?
        virtual.options.foreignField :
        '_id';
  
      // `justOne = null` means we don't know from the schema whether the end
      // result should be an array or a single doc. This can result from
      // populating a POJO using `Model.populate()`
      if ('justOne' in options && options.justOne !== void 0) {
        justOne = options.justOne;
      } else if (virtual && virtual.options && virtual.options.refPath) {
        const normalizedRefPath =
          normalizeRefPath(virtual.options.refPath, doc, options.path);
        justOne = !!virtual.options.justOne;
        isVirtual = true;
        const refValue = utils.getValue(normalizedRefPath, doc);
        modelNames = Array.isArray(refValue) ? refValue : [refValue];
      } else if (virtual && virtual.options && virtual.options.ref) {
        let normalizedRef;
        if (typeof virtual.options.ref === 'function') {
          normalizedRef = virtual.options.ref.call(doc, doc);
        } else {
          normalizedRef = virtual.options.ref;
        }
        justOne = !!virtual.options.justOne;
        isVirtual = true;
        if (!modelNames) {
          modelNames = [].concat(normalizedRef);
        }
      } else if (schema && !schema[schemaMixedSymbol]) {
        // Skip Mixed types because we explicitly don't do casting on those.
        if (options.path.endsWith('.' + schema.path)) {
          justOne = Array.isArray(schema) ?
            schema.every(schema => !schema.$isMongooseArray) :
            !schema.$isMongooseArray;
        }
      }
  
      if (!modelNames) {
        continue;
      }
  
      if (virtual && (!localField || !foreignField)) {
        return new MongooseError('If you are populating a virtual, you must set the ' +
          'localField and foreignField options');
      }
  
      options.isVirtual = isVirtual;
      options.virtual = virtual;
      if (typeof localField === 'function') {
        localField = localField.call(doc, doc);
      }
      if (typeof foreignField === 'function') {
        foreignField = foreignField.call(doc);
      }
  
      let match = get(options, 'match', null) ||
        get(currentOptions, 'match', null) ||
        get(options, 'virtual.options.match', null) ||
        get(options, 'virtual.options.options.match', null);
  
      let hasMatchFunction = typeof match === 'function';
      if (hasMatchFunction) {
        match = match.call(doc, doc);
      }
  
      if (Array.isArray(localField) && Array.isArray(foreignField) && localField.length === foreignField.length) {
        match = Object.assign({}, match);
        for (let i = 1; i < localField.length; ++i) {
          match[foreignField[i]] = convertTo_id(mpath.get(localField[i], doc, lookupLocalFields), schema);
          hasMatchFunction = true;
        }
  
        localField = localField[0];
        foreignField = foreignField[0];
      }
  
      const localFieldPathType = modelSchema._getPathType(localField);
      const localFieldPath = localFieldPathType === 'real' ? modelSchema.path(localField) : localFieldPathType.schema;
      const localFieldGetters = localFieldPath && localFieldPath.getters ? localFieldPath.getters : [];
      let ret;
  
      const _populateOptions = get(options, 'options', {});
  
      const getters = 'getters' in _populateOptions ?
        _populateOptions.getters :
        options.isVirtual && get(virtual, 'options.getters', false);
      if (localFieldGetters.length > 0 && getters) {
        const hydratedDoc = (doc.$__ != null) ? doc : model.hydrate(doc);
        const localFieldValue = mpath.get(localField, doc, lookupLocalFields);
        if (Array.isArray(localFieldValue)) {
          const localFieldHydratedValue = mpath.get(localField.split('.').slice(0, -1), hydratedDoc, lookupLocalFields);
          ret = localFieldValue.map((localFieldArrVal, localFieldArrIndex) =>
            localFieldPath.applyGetters(localFieldArrVal, localFieldHydratedValue[localFieldArrIndex]));
        } else {
          ret = localFieldPath.applyGetters(localFieldValue, hydratedDoc);
        }
      } else {
        ret = convertTo_id(mpath.get(localField, doc, lookupLocalFields), schema);
      }
  
      const id = String(utils.getValue(foreignField, doc));
      options._docs[id] = Array.isArray(ret) ? ret.slice() : ret;
  
      // Re: gh-8452. Embedded discriminators may not have `refPath`, so clear
      // out embedded discriminator docs that don't have a `refPath` on the
      // populated path.
      if (isRefPath && normalizedRefPath != null) {
        const pieces = normalizedRefPath.split('.');
        let cur = '';
        for (let j = 0; j < pieces.length; ++j) {
          const piece = pieces[j];
          cur = cur + (cur.length === 0 ? '' : '.') + piece;
          const schematype = modelSchema.path(cur);
          if (schematype != null &&
              schematype.$isMongooseArray &&
              schematype.caster.discriminators != null &&
              Object.keys(schematype.caster.discriminators).length > 0) {
            const subdocs = utils.getValue(cur, doc);
            const remnant = options.path.substr(cur.length + 1);
            const discriminatorKey = schematype.caster.schema.options.discriminatorKey;
            modelNames = [];
            for (const subdoc of subdocs) {
              const discriminatorName = utils.getValue(discriminatorKey, subdoc);
              const discriminator = schematype.caster.discriminators[discriminatorName];
              const discriminatorSchema = discriminator && discriminator.schema;
              if (discriminatorSchema == null) {
                continue;
              }
              const _path = discriminatorSchema.path(remnant);
              if (_path == null || _path.options.refPath == null) {
                const docValue = utils.getValue(localField.substr(cur.length + 1), subdoc);
                ret = ret.map(v => v === docValue ? SkipPopulateValue(v) : v);
                continue;
              }
              const modelName = utils.getValue(pieces.slice(j + 1).join('.'), subdoc);
              modelNames.push(modelName);
            }
          }
        }
      }
  
      let k = modelNames.length;
      while (k--) {
        modelName = modelNames[k];
        if (modelName == null) {
          continue;
        }
  
        // `PopulateOptions#connection`: if the model is passed as a string, the
        // connection matters because different connections have different models.
        const connection = options.connection != null ? options.connection : model.db;
  
        try {
          Model = originalModel && originalModel[modelSymbol] ?
            originalModel :
            modelName[modelSymbol] ? modelName : connection.model(modelName);
        } catch (error) {
          // If `ret` is undefined, we'll add an empty entry to modelsMap. We shouldn't
          // execute a query, but it is necessary to make sure `justOne` gets handled
          // correctly for setting an empty array (see gh-8455)
          if (ret !== undefined) {
            return error;
          }
        }
  
        let ids = ret;
        const flat = Array.isArray(ret) ? utils.array.flatten(ret) : [];
  
        if (isRefPath && Array.isArray(ret) && flat.length === modelNames.length) {
          ids = flat.filter((val, i) => modelNames[i] === modelName);
        }
  
        if (!available[modelName] || currentOptions.perDocumentLimit != null || get(currentOptions, 'options.perDocumentLimit') != null) {
          currentOptions = {
            model: Model
          };
  
          if (isVirtual && get(virtual, 'options.options')) {
            currentOptions.options = utils.clone(virtual.options.options);
          } else if (schemaOptions != null) {
            currentOptions.options = Object.assign({}, schemaOptions);
          }
          utils.merge(currentOptions, options);
  
          // Used internally for checking what model was used to populate this
          // path.
          options[populateModelSymbol] = Model;
  
          available[modelName] = {
            model: Model,
            options: currentOptions,
            match: hasMatchFunction ? [match] : match,
            docs: [doc],
            ids: [ids],
            allIds: [ret],
            localField: new Set([localField]),
            foreignField: new Set([foreignField]),
            justOne: justOne,
            isVirtual: isVirtual,
            virtual: virtual,
            count: count,
            [populateModelSymbol]: Model
          };
          map.push(available[modelName]);
        } else {
          available[modelName].localField.add(localField);
          available[modelName].foreignField.add(foreignField);
          available[modelName].docs.push(doc);
          available[modelName].ids.push(ids);
          available[modelName].allIds.push(ret);
          if (hasMatchFunction) {
            available[modelName].match.push(match);
          }
        }
      }
    }
    return map;
  
    function _getModelNames(doc, schema) {
      let modelNames;
      let discriminatorKey;
      let isRefPath = false;
      let justOne = null;
  
      if (schema && schema.caster) {
        schema = schema.caster;
      }
      if (schema && schema.$isSchemaMap) {
        schema = schema.$__schemaType;
      }
  
      if (!schema && model.discriminators) {
        discriminatorKey = model.schema.discriminatorMapping.key;
      }
  
      refPath = schema && schema.options && schema.options.refPath;
  
      const normalizedRefPath = normalizeRefPath(refPath, doc, options.path);
  
      if (modelNameFromQuery) {
        modelNames = [modelNameFromQuery]; // query options
      } else if (normalizedRefPath) {
        if (options._queryProjection != null && isPathExcluded(options._queryProjection, normalizedRefPath)) {
          throw new MongooseError('refPath `' + normalizedRefPath +
            '` must not be excluded in projection, got ' +
            util.inspect(options._queryProjection));
        }
  
        if (modelSchema.virtuals.hasOwnProperty(normalizedRefPath) && doc.$__ == null) {
          modelNames = [modelSchema.virtuals[normalizedRefPath].applyGetters(void 0, doc)];
        } else {
          modelNames = utils.getValue(normalizedRefPath, doc);
        }
  
        if (Array.isArray(modelNames)) {
          modelNames = utils.array.flatten(modelNames);
        }
  
        isRefPath = true;
      } else {
        let modelForCurrentDoc = model;
        let schemaForCurrentDoc;
        let discriminatorValue;
  
        if (!schema && discriminatorKey && (discriminatorValue = utils.getValue(discriminatorKey, doc))) {
          // `modelNameForFind` is the discriminator value, so we might need
          // find the discriminated model name
          const discriminatorModel = getDiscriminatorByValue(model.discriminators, discriminatorValue) || model;
          if (discriminatorModel != null) {
            modelForCurrentDoc = discriminatorModel;
          } else {
            try {
              modelForCurrentDoc = model.db.model(discriminatorValue);
            } catch (error) {
              return error;
            }
          }
  
          schemaForCurrentDoc = modelForCurrentDoc.schema._getSchema(options.path);
  
          if (schemaForCurrentDoc && schemaForCurrentDoc.caster) {
            schemaForCurrentDoc = schemaForCurrentDoc.caster;
          }
        } else {
          schemaForCurrentDoc = schema;
        }
        const _virtualRes = getVirtual(modelForCurrentDoc.schema, options.path);
        const virtual = _virtualRes == null ? null : _virtualRes.virtual;
  
        if (schemaForCurrentDoc != null) {
          justOne = !schemaForCurrentDoc.$isMongooseArray && !schemaForCurrentDoc._arrayPath;
        }
  
        let ref;
        let refPath;
  
        if ((ref = get(schemaForCurrentDoc, 'options.ref')) != null) {
          ref = handleRefFunction(ref, doc);
          modelNames = [ref];
        } else if ((ref = get(virtual, 'options.ref')) != null) {
          ref = handleRefFunction(ref, doc);
  
          // When referencing nested arrays, the ref should be an Array
          // of modelNames.
          if (Array.isArray(ref)) {
            modelNames = ref;
          } else {
            modelNames = [ref];
          }
  
          isVirtual = true;
        } else if ((refPath = get(schemaForCurrentDoc, 'options.refPath')) != null) {
          isRefPath = true;
          refPath = normalizeRefPath(refPath, doc, options.path);
          modelNames = utils.getValue(refPath, doc);
          if (Array.isArray(modelNames)) {
            modelNames = utils.array.flatten(modelNames);
          }
        } else {
          // We may have a discriminator, in which case we don't want to
          // populate using the base model by default
          modelNames = discriminatorKey ? null : [model.modelName];
        }
      }
  
      if (!modelNames) {
        return { modelNames: modelNames, isRefPath: isRefPath, refPath: normalizedRefPath, justOne: justOne };
      }
  
      if (!Array.isArray(modelNames)) {
        modelNames = [modelNames];
      }
  
      return { modelNames: modelNames, isRefPath: isRefPath, refPath: normalizedRefPath, justOne: justOne };
    }
  };
  
  /*!
   * ignore
   */
  
  function handleRefFunction(ref, doc) {
    if (typeof ref === 'function' && !ref[modelSymbol]) {
      return ref.call(doc, doc);
    }
    return ref;
  }
  
  /*!
   * Retrieve the _id of `val` if a Document or Array of Documents.
   *
   * @param {Array|Document|Any} val
   * @return {Array|Document|Any}
   */
  
  function convertTo_id(val, schema) {
    if (val != null && val.$__ != null) {
      return val._id;
    }
    if (val != null && val._id != null && (schema == null || !schema.$isSchemaMap)) {
      return val._id;
    }
  
    if (Array.isArray(val)) {
      for (let i = 0; i < val.length; ++i) {
        if (val[i] != null && val[i].$__ != null) {
          val[i] = val[i]._id;
        }
      }
      if (val.isMongooseArray && val.$schema()) {
        return val.$schema()._castForPopulate(val, val.$parent());
      }
  
      return [].concat(val);
    }
  
    // `populate('map')` may be an object if populating on a doc that hasn't
    // been hydrated yet
    if (getConstructorName(val) === 'Object' &&
        // The intent here is we should only flatten the object if we expect
        // to get a Map in the end. Avoid doing this for mixed types.
        (schema == null || schema[schemaMixedSymbol] == null)) {
      const ret = [];
      for (const key of Object.keys(val)) {
        ret.push(val[key]);
      }
      return ret;
    }
    // If doc has already been hydrated, e.g. `doc.populate('map').execPopulate()`
    // then `val` will already be a map
    if (val instanceof Map) {
      return Array.from(val.values());
    }
  
    return val;
  }