text: 'use strict';
  
  /*!
   * Module dependencies.
   */
  
  const CastError = require('./error/cast');
  const StrictModeError = require('./error/strict');
  const Types = require('./schema/index');
  const castTextSearch = require('./schema/operators/text');
  const get = require('./helpers/get');
  const getConstructorName = require('./helpers/getConstructorName');
  const getSchemaDiscriminatorByValue = require('./helpers/discriminator/getSchemaDiscriminatorByValue');
  const isOperator = require('./helpers/query/isOperator');
  const util = require('util');
  const isObject = require('./helpers/isObject');
  const isMongooseObject = require('./helpers/isMongooseObject');
  
  const ALLOWED_GEOWITHIN_GEOJSON_TYPES = ['Polygon', 'MultiPolygon'];
  
  /**
   * Handles internal casting for query filters.
   *
   * @param {Schema} schema
   * @param {Object} obj Object to cast
   * @param {Object} options the query options
   * @param {Query} context passed to setters
   * @api private
   */
  module.exports = function cast(schema, obj, options, context) {
    if (Array.isArray(obj)) {
      throw new Error('Query filter must be an object, got an array ', util.inspect(obj));
    }
  
    if (obj == null) {
      return obj;
    }
  
    // bson 1.x has the unfortunate tendency to remove filters that have a top-level
    // `_bsontype` property. But we should still allow ObjectIds because
    // `Collection#find()` has a special case to support `find(objectid)`.
    // Should remove this when we upgrade to bson 4.x. See gh-8222, gh-8268
    if (obj.hasOwnProperty('_bsontype') && obj._bsontype !== 'ObjectID') {
      delete obj._bsontype;
    }
  
    if (schema != null && schema.discriminators != null && obj[schema.options.discriminatorKey] != null) {
      schema = getSchemaDiscriminatorByValue(schema, obj[schema.options.discriminatorKey]) || schema;
    }
  
    const paths = Object.keys(obj);
    let i = paths.length;
    let _keys;
    let any$conditionals;
    let schematype;
    let nested;
    let path;
    let type;
    let val;
  
    options = options || {};
  
    while (i--) {
      path = paths[i];
      val = obj[path];
  
      if (path === '$or' || path === '$nor' || path === '$and') {
        if (!Array.isArray(val)) {
          throw new CastError('Array', val, path);
        }
        for (let k = 0; k < val.length; ++k) {
          if (val[k] == null || typeof val[k] !== 'object') {
            throw new CastError('Object', val[k], path + '.' + k);
          }
          val[k] = cast(schema, val[k], options, context);
        }
      } else if (path === '$where') {
        type = typeof val;
  
        if (type !== 'string' && type !== 'function') {
          throw new Error('Must have a string or function for $where');
        }
  
        if (type === 'function') {
          obj[path] = val.toString();
        }
  
        continue;
      } else if (path === '$expr') {
        if (typeof val !== 'object' || val == null) {
          throw new Error('`$expr` must be an object');
        }
        continue;
      } else if (path === '$elemMatch') {
        val = cast(schema, val, options, context);
      } else if (path === '$text') {
        val = castTextSearch(val, path);
      } else {
        if (!schema) {
          // no casting for Mixed types
          continue;
        }
  
        schematype = schema.path(path);
  
        // Check for embedded discriminator paths
        if (!schematype) {
          const split = path.split('.');
          let j = split.length;
          while (j--) {
            const pathFirstHalf = split.slice(0, j).join('.');
            const pathLastHalf = split.slice(j).join('.');
            const _schematype = schema.path(pathFirstHalf);
            const discriminatorKey = get(_schematype, 'schema.options.discriminatorKey');
  
            // gh-6027: if we haven't found the schematype but this path is
            // underneath an embedded discriminator and the embedded discriminator
            // key is in the query, use the embedded discriminator schema
            if (_schematype != null &&
                get(_schematype, 'schema.discriminators') != null &&
                discriminatorKey != null &&
                pathLastHalf !== discriminatorKey) {
              const discriminatorVal = get(obj, pathFirstHalf + '.' + discriminatorKey);
              if (discriminatorVal != null) {
                schematype = _schematype.schema.discriminators[discriminatorVal].
                  path(pathLastHalf);
              }
            }
          }
        }
  
        if (!schematype) {
          // Handle potential embedded array queries
          const split = path.split('.');
          let j = split.length;
          let pathFirstHalf;
          let pathLastHalf;
          let remainingConds;
  
          // Find the part of the var path that is a path of the Schema
          while (j--) {
            pathFirstHalf = split.slice(0, j).join('.');
            schematype = schema.path(pathFirstHalf);
            if (schematype) {
              break;
            }
          }
  
          // If a substring of the input path resolves to an actual real path...
          if (schematype) {
            // Apply the casting; similar code for $elemMatch in schema/array.js
            if (schematype.caster && schematype.caster.schema) {
              remainingConds = {};
              pathLastHalf = split.slice(j).join('.');
              remainingConds[pathLastHalf] = val;
              obj[path] = cast(schematype.caster.schema, remainingConds, options, context)[pathLastHalf];
            } else {
              obj[path] = val;
            }
            continue;
          }
  
          if (isObject(val)) {
            // handle geo schemas that use object notation
            // { loc: { long: Number, lat: Number }
  
            let geo = '';
            if (val.$near) {
              geo = '$near';
            } else if (val.$nearSphere) {
              geo = '$nearSphere';
            } else if (val.$within) {
              geo = '$within';
            } else if (val.$geoIntersects) {
              geo = '$geoIntersects';
            } else if (val.$geoWithin) {
              geo = '$geoWithin';
            }
  
            if (geo) {
              const numbertype = new Types.Number('__QueryCasting__');
              let value = val[geo];
  
              if (val.$maxDistance != null) {
                val.$maxDistance = numbertype.castForQueryWrapper({
                  val: val.$maxDistance,
                  context: context
                });
              }
              if (val.$minDistance != null) {
                val.$minDistance = numbertype.castForQueryWrapper({
                  val: val.$minDistance,
                  context: context
                });
              }
  
              if (geo === '$within') {
                const withinType = value.$center
                    || value.$centerSphere
                    || value.$box
                    || value.$polygon;
  
                if (!withinType) {
                  throw new Error('Bad $within parameter: ' + JSON.stringify(val));
                }
  
                value = withinType;
              } else if (geo === '$near' &&
                  typeof value.type === 'string' && Array.isArray(value.coordinates)) {
                // geojson; cast the coordinates
                value = value.coordinates;
              } else if ((geo === '$near' || geo === '$nearSphere' || geo === '$geoIntersects') &&
                  value.$geometry && typeof value.$geometry.type === 'string' &&
                  Array.isArray(value.$geometry.coordinates)) {
                if (value.$maxDistance != null) {
                  value.$maxDistance = numbertype.castForQueryWrapper({
                    val: value.$maxDistance,
                    context: context
                  });
                }
                if (value.$minDistance != null) {
                  value.$minDistance = numbertype.castForQueryWrapper({
                    val: value.$minDistance,
                    context: context
                  });
                }
                if (isMongooseObject(value.$geometry)) {
                  value.$geometry = value.$geometry.toObject({
                    transform: false,
                    virtuals: false
                  });
                }
                value = value.$geometry.coordinates;
              } else if (geo === '$geoWithin') {
                if (value.$geometry) {
                  if (isMongooseObject(value.$geometry)) {
                    value.$geometry = value.$geometry.toObject({ virtuals: false });
                  }
                  const geoWithinType = value.$geometry.type;
                  if (ALLOWED_GEOWITHIN_GEOJSON_TYPES.indexOf(geoWithinType) === -1) {
                    throw new Error('Invalid geoJSON type for $geoWithin "' +
                      geoWithinType + '", must be "Polygon" or "MultiPolygon"');
                  }
                  value = value.$geometry.coordinates;
                } else {
                  value = value.$box || value.$polygon || value.$center ||
                    value.$centerSphere;
                  if (isMongooseObject(value)) {
                    value = value.toObject({ virtuals: false });
                  }
                }
              }
  
              _cast(value, numbertype, context);
              continue;
            }
          }
  
          if (schema.nested[path]) {
            continue;
          }
          if (options.upsert && options.strict) {
            if (options.strict === 'throw') {
              throw new StrictModeError(path);
            }
            throw new StrictModeError(path, 'Path "' + path + '" is not in ' +
              'schema, strict mode is `true`, and upsert is `true`.');
          } else if (options.strict) {
            if (options.strict === 'throw') {
              throw new StrictModeError(path, 'Path "' + path + '" is not in ' +
                'schema and strict is \'throw\'.');
            }
            delete obj[path];
          }
        } else if (val == null) {
          continue;
        } else if (getConstructorName(val) === 'Object') {
          any$conditionals = Object.keys(val).some(isOperator);
  
          if (!any$conditionals) {
            obj[path] = schematype.castForQueryWrapper({
              val: val,
              context: context
            });
          } else {
            const ks = Object.keys(val);
            let $cond;
  
            let k = ks.length;
  
            while (k--) {
              $cond = ks[k];
              nested = val[$cond];
  
              if ($cond === '$not') {
                if (nested && schematype) {
                  _keys = Object.keys(nested);
                  if (_keys.length && isOperator(_keys[0])) {
                    for (const key in nested) {
                      nested[key] = schematype.castForQueryWrapper({
                        $conditional: key,
                        val: nested[key],
                        context: context
                      });
                    }
                  } else {
                    val[$cond] = schematype.castForQueryWrapper({
                      $conditional: $cond,
                      val: nested,
                      context: context
                    });
                  }
                  continue;
                }
              } else {
                val[$cond] = schematype.castForQueryWrapper({
                  $conditional: $cond,
                  val: nested,
                  context: context
                });
              }
            }
          }
        } else if (Array.isArray(val) && ['Buffer', 'Array'].indexOf(schematype.instance) === -1) {
          const casted = [];
          const valuesArray = val;
  
          for (const _val of valuesArray) {
            casted.push(schematype.castForQueryWrapper({
              val: _val,
              context: context
            }));
          }
  
          obj[path] = { $in: casted };
        } else {
          obj[path] = schematype.castForQueryWrapper({
            val: val,
            context: context
          });
        }
      }
    }
  
    return obj;
  };
  
  function _cast(val, numbertype, context) {
    if (Array.isArray(val)) {
      val.forEach(function(item, i) {
        if (Array.isArray(item) || isObject(item)) {
          return _cast(item, numbertype, context);
        }
        val[i] = numbertype.castForQueryWrapper({ val: item, context: context });
      });
    } else {
      const nearKeys = Object.keys(val);
      let nearLen = nearKeys.length;
      while (nearLen--) {
        const nkey = nearKeys[nearLen];
        const item = val[nkey];
        if (Array.isArray(item) || isObject(item)) {
          _cast(item, numbertype, context);
          val[nkey] = item;
        } else {
          val[nkey] = numbertype.castForQuery({ val: item, context: context });
        }
      }
    }
  }