"use strict";

var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");

exports.__esModule = true;
exports.postIndexingMetaSetup = postIndexingMetaSetup;
exports.ensureEmptyFilterCache = ensureEmptyFilterCache;
exports.intersectNodesByCounter = intersectNodesByCounter;
exports.unionNodesByCounter = unionNodesByCounter;
exports.getSortFieldIdentifierKeys = getSortFieldIdentifierKeys;
exports.getNodesFromCacheByValue = exports.ensureIndexByElemMatch = exports.ensureIndexByQuery = exports.getGatsbyNodePartial = void 0;

var _every2 = _interopRequireDefault(require("lodash/every"));

var _redux = require("../../redux");

var _query = require("../common/query");

var _2 = require("../");

var _getValueAt = require("../../utils/get-value-at");

const nodeIdToIdentifierMap = new Map();
/**
 * Grabs an instance of IGatsbyNodePartial for the given node.
 * This accepts an IGatsbyNode or IGatsbyNodePartial as input, which allows
 * us to conditionally store index fields on the partial if we encounter
 * one that hasn't been stored on the partial yet.
 */

const getGatsbyNodePartial = (node, indexFields, resolvedFields) => {
  // first, check if we have the partial in the cache
  const cacheKey = `${node.id}_____${node.internal.counter}`;
  let derefPartial = undefined;

  if (nodeIdToIdentifierMap.has(cacheKey)) {
    var _nodeIdToIdentifierMa;

    derefPartial = (_nodeIdToIdentifierMa = nodeIdToIdentifierMap.get(cacheKey)) === null || _nodeIdToIdentifierMa === void 0 ? void 0 : _nodeIdToIdentifierMa.deref(); // now check if we have it in memory and it has all the fields we need

    if (derefPartial && (0, _every2.default)(indexFields.map(field => derefPartial.gatsbyNodePartialInternalData.indexFields.has(field)))) {
      return derefPartial;
    }
  } // find all the keys of fields and store them and their values on the partial
  // if we've already passed this partial, merge both sets of index fields


  const dottedFields = {};
  const fieldsToStore = derefPartial ? new Set([...derefPartial.gatsbyNodePartialInternalData.indexFields, ...indexFields]) : new Set(indexFields);
  const sortFieldIds = getSortFieldIdentifierKeys([...fieldsToStore], resolvedFields);
  let fullNodeObject = node.gatsbyNodePartialInternalData ? undefined : node;

  for (const dottedField of sortFieldIds) {
    if (dottedField in node) {
      dottedFields[dottedField] = node[dottedField];
    } else {
      // if we haven't gotten the full node object, fetch it once
      if (!fullNodeObject) {
        fullNodeObject = (0, _2.getNode)(node.id);
      }

      if (dottedField.startsWith(`__gatsby_resolved.`) && !fullNodeObject.__gatsby_resolved) {
        const typeName = fullNodeObject.internal.type;

        const resolvedNodes = _redux.store.getState().resolvedNodesCache.get(typeName);

        const resolved = resolvedNodes === null || resolvedNodes === void 0 ? void 0 : resolvedNodes.get(fullNodeObject.id);

        if (resolved !== undefined) {
          fullNodeObject.__gatsby_resolved = resolved;
        }
      } // use the full node object to fetch the value


      dottedFields[dottedField] = (0, _getValueAt.getValueAt)(fullNodeObject, dottedField);
    }
  } // create the partial object


  const partial = Object.assign(dottedFields, {
    id: node.id,
    internal: {
      counter: node.internal.counter
    },
    gatsbyNodePartialInternalData: {
      indexFields: fieldsToStore
    }
  }); // set the object in the cache for later fetching

  nodeIdToIdentifierMap.set(cacheKey, new WeakRef(partial));
  return partial;
};

exports.getGatsbyNodePartial = getGatsbyNodePartial;

const sortByIds = (a, b) => a.internal.counter - b.internal.counter;

function postIndexingMetaSetup(filterCache, op) {
  // Loop through byValue and make sure the buckets are sorted by counter
  // Since we don't do insertion sort, we have to do it afterwards
  for (const bucket of filterCache.byValue) {
    bucket[1].sort(sortByIds);
  }

  if (op === `$ne` || op === `$nin`) {
    postIndexingMetaSetupNeNin(filterCache);
  } else if ([`$lt`, `$lte`, `$gt`, `$gte`].includes(op)) {
    postIndexingMetaSetupLtLteGtGte(filterCache, op);
  }
}

function postIndexingMetaSetupNeNin(filterCache) {
  // Note: edge cases regarding `null` and `undefined`. Here `undefined` signals
  // that the property did not exist as the filters do not support actual
  // `undefined` values.
  // For $ne, `null` only returns nodes that actually have the property
  // and in that case the property cannot be `null` either. For any other value,
  // $ne will return all nodes where the value is not actually the needle,
  // including nodes where the value is null.
  // A $nin does the same as an $ne except it filters multiple values instead
  // of just one.
  // For `$ne` we will take the list of all targeted nodes and eliminate the
  // bucket of nodes with a particular value, if it exists at all..
  const arr = [];
  filterCache.meta.nodesUnordered = arr;
  filterCache.byValue.forEach(v => {
    v.forEach(nodeId => {
      arr.push(nodeId);
    });
  });
}

function postIndexingMetaSetupLtLteGtGte(filterCache, op) {
  // Create an ordered array of individual nodes, ordered (grouped) by the
  // value to which the filter resolves. Nodes per value are ordered by
  // internal.counter, asc.
  // This way non-eq ops can simply slice the array to get a range.
  const entriesNullable = [...filterCache.byValue.entries()]; // These range checks never return `null` or `undefined` so filter those out
  // By filtering them out early, the sort should be faster. Could be ...

  const entries = entriesNullable.filter(([v]) => v != null); // Sort all arrays by its value, asc. Ignore/allow potential type casting.
  // Note: while `<` is the inverse of `>=`, the ordering might coerce values.
  // This coercion makes the op no longer idempotent (normally the result of
  // `a < b` is the opposite of `b >= a` for any a or b of the same type). The
  // exception is a number that is `NaN`, which we're ignoring here as it's most
  // likely a bug in the user code. However, when coercing the ops may end up
  // comparing against `NaN`, too. For example: `("abc" <= 12) !== (12 > "abc")`
  // which ends up doing `NaN <= 12` and `NaN > "abc"`, which will both yield
  // false.
  // So instead we potentially track two ordered lists; ascending and descending
  // and the only difference when comparing the inverse of one to the other
  // should be how these `NaN` cases end up getting ordered.
  // It's fine for `lt` and `lte` to use the same ordered list. Same for gt/gte.

  if (op === `$lt` || op === `$lte`) {
    // Order ascending; first value is lowest
    entries.sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
  } else if (op === `$gt` || op === `$gte`) {
    // Order descending; first value is highest
    entries.sort(([a], [b]) => a > b ? -1 : a < b ? 1 : 0);
  }

  const orderedNodes = [];
  const orderedValues = [];
  const offsets = new Map();
  entries.forEach(([v, bucket]) => {
    // Record the range containing all nodes with as filter value v
    // The last value of the range should be the offset of the next value
    // (So you should be able to do `nodes.slice(start, stop)` to get them)
    offsets.set(v, [orderedNodes.length, orderedNodes.length + bucket.length]); // We could do `arr.push(...bucket)` here but that's not safe with very
    // large sets, so we use a regular loop

    bucket.forEach(node => orderedNodes.push(node));
    orderedValues.push(v);
  });

  if (op === `$lt` || op === `$lte`) {
    filterCache.meta.valuesAsc = orderedValues;
    filterCache.meta.nodesByValueAsc = orderedNodes; // The nodesByValueAsc is ordered by value, but multiple nodes per value are
    // not ordered. To make lt as fast as lte, we must know the start and stop
    // index for each value. Similarly useful for for `ne`.

    filterCache.meta.valueRangesAsc = offsets;
  } else if (op === `$gt` || op === `$gte`) {
    filterCache.meta.valuesDesc = orderedValues;
    filterCache.meta.nodesByValueDesc = orderedNodes; // The nodesByValueDesc is ordered by value, but multiple nodes per value are
    // not ordered. To make gt as fast as gte, we must know the start and stop
    // index for each value. Similarly useful for for `ne`.

    filterCache.meta.valueRangesDesc = offsets;
  }
}
/**
 * Given a single non-elemMatch filter path, a list of node types, and a
 * cache, create a cache that for each resulting value of the filter contains
 * all the Nodes in a list.
 * This cache is used for applying the filter and is a massive improvement over
 * looping over all the nodes, when the number of pages (/nodes) scales up.
 */


const ensureIndexByQuery = (op, filterCacheKey, filterPath, nodeTypeNames, filtersCache, indexFields, resolvedFields) => {
  const state = _redux.store.getState();

  const resolvedNodesCache = state.resolvedNodesCache;
  const filterCache = {
    op,
    byValue: new Map(),
    meta: {}
  };
  filtersCache.set(filterCacheKey, filterCache); // We cache the subsets of nodes by type, but only one type. So if searching
  // through one node type we can prevent a search through all nodes, otherwise
  // it's probably faster to loop through all nodes. Perhaps. Maybe.

  if (nodeTypeNames.length === 1) {
    (0, _2.getDataStore)().iterateNodesByType(nodeTypeNames[0]).forEach(node => {
      addNodeToFilterCache({
        node,
        chain: filterPath,
        filterCache,
        resolvedNodesCache,
        indexFields,
        resolvedFields
      });
    });
  } else {
    // Here we must first filter for the node type
    // This loop is expensive at scale (!)
    (0, _2.getDataStore)().iterateNodes().forEach(node => {
      if (!nodeTypeNames.includes(node.internal.type)) {
        return;
      }

      addNodeToFilterCache({
        node,
        chain: filterPath,
        filterCache,
        resolvedNodesCache,
        indexFields,
        resolvedFields
      });
    });
  }

  postIndexingMetaSetup(filterCache, op);
};

exports.ensureIndexByQuery = ensureIndexByQuery;

function ensureEmptyFilterCache(filterCacheKey, nodeTypeNames, filtersCache, indexFields, resolvedFields) {
  // This is called for queries without any filters
  // We want to cache the result since it's basically a list of nodes by type(s)
  // There are sites that have multiple queries which are empty
  const state = _redux.store.getState();

  const resolvedNodesCache = state.resolvedNodesCache;
  const orderedByCounter = [];
  filtersCache.set(filterCacheKey, {
    op: `$eq`,
    // Ignore.
    byValue: new Map(),
    meta: {
      orderedByCounter // This is what we want

    }
  });

  if (nodeTypeNames.length === 1) {
    (0, _2.getDataStore)().iterateNodesByType(nodeTypeNames[0]).forEach(node => {
      if (!node.__gatsby_resolved) {
        const typeName = node.internal.type;
        const resolvedNodes = resolvedNodesCache.get(typeName);
        const resolved = resolvedNodes === null || resolvedNodes === void 0 ? void 0 : resolvedNodes.get(node.id);

        if (resolved !== undefined) {
          node.__gatsby_resolved = resolved;
        }
      }

      orderedByCounter.push(getGatsbyNodePartial(node, indexFields, resolvedFields));
    });
  } else {
    // Here we must first filter for the node type
    // This loop is expensive at scale (!)
    (0, _2.getDataStore)().iterateNodes().forEach(node => {
      if (nodeTypeNames.includes(node.internal.type)) {
        if (!node.__gatsby_resolved) {
          const typeName = node.internal.type;
          const resolvedNodes = resolvedNodesCache.get(typeName);
          const resolved = resolvedNodes === null || resolvedNodes === void 0 ? void 0 : resolvedNodes.get(node.id);

          if (resolved !== undefined) {
            node.__gatsby_resolved = resolved;
          }
        }

        orderedByCounter.push(getGatsbyNodePartial(node, indexFields, resolvedFields));
      }
    });
  } // Since each node can only have one type, we shouldn't have to be concerned
  // about duplicates in this array. Just make sure they're sorted.


  orderedByCounter.sort(sortByIds);
}

function addNodeToFilterCache({
  node,
  chain,
  filterCache,
  resolvedNodesCache,
  indexFields,
  resolvedFields,
  valueOffset = node
}) {
  // There can be a filter that targets `__gatsby_resolved` so fix that first
  if (!node.__gatsby_resolved) {
    const typeName = node.internal.type;
    const resolvedNodes = resolvedNodesCache.get(typeName);
    node.__gatsby_resolved = resolvedNodes === null || resolvedNodes === void 0 ? void 0 : resolvedNodes.get(node.id);
  } // - for plain query, valueOffset === node
  // - for elemMatch, valueOffset is sub-tree of the node to continue matching


  let v = valueOffset;
  let i = 0;

  while (i < chain.length && v) {
    const nextProp = chain[i++];
    v = v[nextProp];
  }

  if (typeof v !== `string` && typeof v !== `number` && typeof v !== `boolean` && v !== null || i !== chain.length) {
    if (i === chain.length && Array.isArray(v)) {
      // The op resolved to an array
      // Add an entry for each element of the array. This would work for ops
      // like eq and ne, but not sure about range ops like lt,lte,gt,gte.
      v.forEach(v => markNodeForValue(filterCache, node, v, indexFields, resolvedFields));
      return;
    } // This means that either
    // - The filter resolved to `undefined`, or
    // - The filter resolved to something other than a primitive
    // Set the value to `undefined` to mark "path does not (fully) exist"


    v = undefined;
  }

  markNodeForValue(filterCache, node, v, indexFields, resolvedFields);
}

function markNodeForValue(filterCache, node, value, indexFields, resolvedFields) {
  let arr = filterCache.byValue.get(value);

  if (!arr) {
    arr = [];
    filterCache.byValue.set(value, arr);
  }

  const partial = getGatsbyNodePartial(node, indexFields, resolvedFields);

  if (!arr.includes(partial)) {
    arr.push(partial);
  }
}

const ensureIndexByElemMatch = (op, filterCacheKey, filter, nodeTypeNames, filtersCache, indexFields, resolvedFields) => {
  // Given an elemMatch filter, generate the cache that contains all nodes that
  // matches a given value for that sub-query
  const state = _redux.store.getState();

  const {
    resolvedNodesCache
  } = state;
  const filterCache = {
    op,
    byValue: new Map(),
    meta: {}
  };
  filtersCache.set(filterCacheKey, filterCache);

  if (nodeTypeNames.length === 1) {
    (0, _2.getDataStore)().iterateNodesByType(nodeTypeNames[0]).forEach(node => {
      addNodeToBucketWithElemMatch({
        node,
        valueAtCurrentStep: node,
        filter,
        filterCache,
        resolvedNodesCache,
        indexFields,
        resolvedFields
      });
    });
  } else {
    // Expensive at scale
    (0, _2.getDataStore)().iterateNodes().forEach(node => {
      if (!nodeTypeNames.includes(node.internal.type)) {
        return;
      }

      addNodeToBucketWithElemMatch({
        node,
        valueAtCurrentStep: node,
        filter,
        filterCache,
        resolvedNodesCache,
        indexFields,
        resolvedFields
      });
    });
  }

  postIndexingMetaSetup(filterCache, op);
};

exports.ensureIndexByElemMatch = ensureIndexByElemMatch;

function addNodeToBucketWithElemMatch({
  node,
  valueAtCurrentStep,
  // Arbitrary step on the path inside the node
  filter,
  filterCache,
  resolvedNodesCache,
  indexFields,
  resolvedFields
}) {
  // There can be a filter that targets `__gatsby_resolved` so fix that first
  if (!node.__gatsby_resolved) {
    const typeName = node.internal.type;
    const resolvedNodes = resolvedNodesCache.get(typeName);
    node.__gatsby_resolved = resolvedNodes === null || resolvedNodes === void 0 ? void 0 : resolvedNodes.get(node.id);
  }

  const {
    path,
    nestedQuery
  } = filter; // Find the value to apply elemMatch to

  let i = 0;

  while (i < path.length && valueAtCurrentStep) {
    const nextProp = path[i++];
    valueAtCurrentStep = valueAtCurrentStep[nextProp];
  }

  if (path.length !== i) {
    // Found undefined before the end of the path
    return;
  } // `v` should now be an elemMatch target, probably an array (but maybe not)


  if (!Array.isArray(valueAtCurrentStep)) {
    // It's possible to `elemMatch` on a non-array so let's support that too
    valueAtCurrentStep = [valueAtCurrentStep];
  } // Note: We need to check all elements because the node may need to be added
  // to multiple buckets (`{a:[{b:3},{b:4}]}`, for `a.elemMatch.b/eq` that
  // node ends up in buckets for value 3 and 4. This may lead to duplicate
  // work when elements resolve to the same value, but that can't be helped.


  valueAtCurrentStep.forEach(elem => {
    if (nestedQuery.type === `elemMatch`) {
      addNodeToBucketWithElemMatch({
        node,
        valueAtCurrentStep: elem,
        filter: nestedQuery,
        filterCache,
        resolvedNodesCache,
        indexFields,
        resolvedFields
      });
    } else {
      // Now take same route as non-elemMatch filters would take
      addNodeToFilterCache({
        node,
        chain: nestedQuery.path,
        filterCache,
        resolvedNodesCache,
        indexFields,
        resolvedFields,
        valueOffset: elem
      });
    }
  });
}

const binarySearchAsc = (values, needle) => {
  let min = 0;
  let max = values.length - 1;
  let pivot = Math.floor(values.length / 2);

  while (min <= max) {
    const value = values[pivot];

    if (needle < value) {
      // Move pivot to middle of nodes left of current pivot
      // assert pivot < max
      max = pivot;
    } else if (needle > value) {
      // Move pivot to middle of nodes right of current pivot
      // assert pivot > min
      min = pivot;
    } else {
      // This means needle === value
      // TODO: except for NaN ... and potentially certain type casting cases
      return [pivot, pivot];
    }

    if (max - min <= 1) {
      // End of search. Needle not found (as expected). Use pivot as index.
      // If the needle was not found, max-min==1 and max is returned.
      return [min, max];
    }

    pivot = min + Math.floor((max - min) / 2);
  } // Shouldn't be reachable


  return undefined;
};

const binarySearchDesc = (values, needle) => {
  let min = 0;
  let max = values.length - 1;
  let pivot = Math.floor(values.length / 2);

  while (min <= max) {
    const value = values[pivot];

    if (needle < value) {
      // Move pivot to middle of nodes right of current pivot
      // assert pivot < min
      min = pivot;
    } else if (needle > value) {
      // Move pivot to middle of nodes left of current pivot
      // assert pivot > max
      max = pivot;
    } else {
      // This means needle === value
      // TODO: except for NaN ... and potentially certain type casting cases
      return [pivot, pivot];
    }

    if (max - min <= 1) {
      // End of search. Needle not found (as expected). Use pivot as index.
      // If the needle was not found, max-min==1 and max is returned.
      return [min, max];
    }

    pivot = min + Math.floor((max - min) / 2);
  } // Shouldn't be reachable


  return undefined;
};
/**
 * Given the cache key for a filter and a target value return the list of nodes
 * that resolve to this value. The returned array should be ordered by id.
 * This returns `undefined` if there is no such node
 *
 * Basically if the filter was {a: {b: {slug: {eq: "foo/bar"}}}} then it will
 * return all the nodes that have `node.slug === "foo/bar"`. That usually (but
 * not always) at most one node for slug, but this filter can apply to anything.
 *
 * Arrays returned by this function must be ordered by internal.counter and
 * not contain duplicate nodes (!)
 */


const getNodesFromCacheByValue = (filterCacheKey, filterValue, filtersCache, wasElemMatch) => {
  const filterCache = filtersCache.get(filterCacheKey);

  if (!filterCache) {
    return undefined;
  }

  const op = filterCache.op;

  if (op === `$eq`) {
    // Arrays in byValue are assumed to be ordered by counter
    if (filterValue == null) {
      var _filterCache$byValue$, _filterCache$byValue$2;

      // Edge case for null; fetch all nodes for `null` and `undefined` because
      // `$eq` also returns nodes without the path when searching for `null`.
      // Not all ops do so, so we map non-existing paths to `undefined`.
      const arrNull = (_filterCache$byValue$ = filterCache.byValue.get(null)) !== null && _filterCache$byValue$ !== void 0 ? _filterCache$byValue$ : [];
      const arrUndef = (_filterCache$byValue$2 = filterCache.byValue.get(undefined)) !== null && _filterCache$byValue$2 !== void 0 ? _filterCache$byValue$2 : []; // Merge the two (ordered) arrays and return an ordered deduped array
      // TODO: is there a reason left why we cant just cache this merged list?

      return unionNodesByCounter(arrNull, arrUndef);
    }

    return filterCache.byValue.get(filterValue);
  }

  if (op === `$in`) {
    if (!Array.isArray(filterValue)) {
      throw new Error("The argument to the `in` comparator should be an array");
    }

    const filterValueArr = filterValue;
    const set = new Set(); // TODO: we can also mergeSort for every step. this may perform worse because of how memory in js works.
    // For every value in the needle array, find the bucket of nodes for
    // that value, add this bucket of nodes to one list, return the list.

    filterValueArr.forEach(v => {
      var _filterCache$byValue$3;

      return (_filterCache$byValue$3 = filterCache.byValue.get(v)) === null || _filterCache$byValue$3 === void 0 ? void 0 : _filterCache$byValue$3.forEach(v => set.add(v));
    });
    const arr = [...set]; // this is bad for perf but will guarantee us a unique set :(

    arr.sort(sortByIds); // Note: it's very unlikely that the list of filter values is big so .includes should be fine here

    if (filterValueArr.includes(null)) {
      // Like all other ops, `in: [null]` behaves weirdly, allowing all nodes
      // that do not actually have a (complete) path (v=undefined)
      const nodes = filterCache.byValue.get(undefined);

      if (nodes) {
        // This will also dedupe so don't do that immediately
        return unionNodesByCounter(nodes, arr);
      }
    } // elemMatch can cause a node to appear in multiple buckets so we must dedupe first


    if (wasElemMatch) {
      expensiveDedupeInline(arr);
    }

    return arr;
  }

  if (op === `$nin`) {
    // This is essentially the same as the $ne operator, just with multiple
    // values to exclude.
    if (!Array.isArray(filterValue)) {
      throw new Error(`The $nin operator expects an array as value`);
    }

    const values = new Set(filterValue);
    const set = new Set(filterCache.meta.nodesUnordered); // Do the action for "$ne" for each element in the set of values

    values.forEach(filterValue => {
      removeBucketFromSet(filterValue, filterCache, set);
    }); // TODO: there's probably a more efficient algorithm to do set
    //       subtraction in such a way that we don't have to re-sort

    return [...set].sort(sortByIds);
  }

  if (op === `$ne`) {
    const set = new Set(filterCache.meta.nodesUnordered);
    removeBucketFromSet(filterValue, filterCache, set); // TODO: there's probably a more efficient algorithm to do set
    //       subtraction in such a way that we don't have to resort here

    return [...set].sort(sortByIds);
  }

  if (op === `$regex`) {
    // Note: $glob is converted to $regex so $glob filters go through here, too
    // Aside from the input pattern format, further behavior is exactly the same.
    // The input to the filter must be a string (including leading/trailing slash and regex flags)
    // By the time the filter reaches this point, the filterValue has to be a regex.
    if (!(filterValue instanceof RegExp)) {
      throw new Error(`The value for the $regex comparator must be an instance of RegExp`);
    }

    const regex = filterValue;
    const arr = [];
    filterCache.byValue.forEach((nodes, value) => {
      // TODO: does the value have to be a string for $regex? Can we auto-ignore any non-strings? Or does it coerce.
      // Note: for legacy reasons partial paths should also be included for regex
      if (value !== undefined && regex.test(String(value))) {
        nodes.forEach(node => arr.push(node));
      }
    }); // TODO: we _can_ cache this list as well. Might make sense if it turns out that $regex is mostly used with literals
    // TODO: it may make sense to first collect all buckets and then to .concat them, or merge sort them

    arr.sort(sortByIds); // elemMatch can cause a node to appear in multiple buckets so we must dedupe

    if (wasElemMatch) {
      expensiveDedupeInline(arr);
    }

    return arr;
  }

  if (filterValue == null) {
    if (op === `$lt` || op === `$gt`) {
      // Nothing is lt/gt null
      return undefined;
    } // This is an edge case and this value should be directly indexed
    // For `lte`/`gte` this should only return nodes for `null`, not a "range"


    return filterCache.byValue.get(filterValue);
  }

  if (Array.isArray(filterValue)) {
    throw new Error("Array is an invalid filter value for the `" + op + "` comparator");
  }

  if (filterValue instanceof RegExp) {
    // This is most likely an internal error, although it is possible for
    // users to talk to this API more directly.
    throw new Error(`A RegExp instance is only valid for $regex and $glob comparators`);
  }

  if (op === `$lt`) {
    // First try a direct approach. If a value is queried that also exists then
    // we can prevent a binary search through the whole list, O(1) vs O(log n)
    const ranges = filterCache.meta.valueRangesAsc;
    const nodes = filterCache.meta.nodesByValueAsc;
    const range = ranges.get(filterValue);

    if (range) {
      const arr = nodes.slice(0, range[0]);
      arr.sort(sortByIds); // elemMatch can cause a node to appear in multiple buckets so we must dedupe

      if (wasElemMatch) {
        expensiveDedupeInline(arr);
      }

      return arr;
    } // Query may ask for a value that doesn't appear in the list, like if the
    // list is [1, 2, 5, 6] and the query is <= 3. In that case we have to
    // apply a search (we'll do binary) to determine the offset to slice from.
    // Note: for lte, the valueAsc array must be list at this point


    const values = filterCache.meta.valuesAsc; // It shouldn't find the targetValue (but it might) and return the index of
    // the two value between which targetValue sits, or first/last element.

    const point = binarySearchAsc(values, filterValue);

    if (!point) {
      return undefined;
    }

    const [pivotMin, pivotMax] = point; // Each pivot index must have a value and a range
    // The returned min/max index may include the lower/upper bound, so we still
    // have to do lte checks for both values.

    let pivotValue = values[pivotMax];

    if (pivotValue > filterValue) {
      pivotValue = values[pivotMin];
    } // Note: the pivot value _shouldnt_ match the filter value because that
    // means the value was actually found, but those should have been indexed
    // so should have yielded a result in the .get() above.


    const [exclPivot, inclPivot] = ranges.get(pivotValue); // Note: technically, `5 <= "5" === true` but `5` would not be cached.
    // So we have to consider weak comparison and may have to include the pivot

    const until = pivotValue < filterValue ? inclPivot : exclPivot;
    const arr = nodes.slice(0, until);
    arr.sort(sortByIds); // elemMatch can cause a node to appear in multiple buckets so we must dedupe

    if (wasElemMatch) {
      expensiveDedupeInline(arr);
    }

    return arr;
  }

  if (op === `$lte`) {
    // First try a direct approach. If a value is queried that also exists then
    // we can prevent a binary search through the whole list, O(1) vs O(log n)
    const ranges = filterCache.meta.valueRangesAsc;
    const nodes = filterCache.meta.nodesByValueAsc;
    const range = ranges.get(filterValue);

    if (range) {
      const arr = nodes.slice(0, range[1]);
      arr.sort(sortByIds); // elemMatch can cause a node to appear in multiple buckets so we must dedupe

      if (wasElemMatch) {
        expensiveDedupeInline(arr);
      }

      return arr;
    } // Query may ask for a value that doesn't appear in the list, like if the
    // list is [1, 2, 5, 6] and the query is <= 3. In that case we have to
    // apply a search (we'll do binary) to determine the offset to slice from.
    // Note: for lte, the valueAsc array must be list at this point


    const values = filterCache.meta.valuesAsc; // It shouldn't find the targetValue (but it might) and return the index of
    // the two value between which targetValue sits, or first/last element.

    const point = binarySearchAsc(values, filterValue);

    if (!point) {
      return undefined;
    }

    const [pivotMin, pivotMax] = point; // Each pivot index must have a value and a range
    // The returned min/max index may include the lower/upper bound, so we still
    // have to do lte checks for both values.

    let pivotValue = values[pivotMax];

    if (pivotValue > filterValue) {
      pivotValue = values[pivotMin];
    } // Note: the pivot value _shouldnt_ match the filter value because that
    // means the value was actually found, but those should have been indexed
    // so should have yielded a result in the .get() above.


    const [exclPivot, inclPivot] = ranges.get(pivotValue); // Note: technically, `5 <= "5" === true` but `5` would not be cached.
    // So we have to consider weak comparison and may have to include the pivot

    const until = pivotValue <= filterValue ? inclPivot : exclPivot;
    const arr = nodes.slice(0, until);
    arr.sort(sortByIds); // elemMatch can cause a node to appear in multiple buckets so we must dedupe

    if (wasElemMatch) {
      expensiveDedupeInline(arr);
    }

    return arr;
  }

  if (op === `$gt`) {
    // First try a direct approach. If a value is queried that also exists then
    // we can prevent a binary search through the whole list, O(1) vs O(log n)
    const ranges = filterCache.meta.valueRangesDesc;
    const nodes = filterCache.meta.nodesByValueDesc;
    const range = ranges.get(filterValue);

    if (range) {
      const arr = nodes.slice(0, range[0]).reverse();
      arr.sort(sortByIds); // elemMatch can cause a node to appear in multiple buckets so we must dedupe

      if (wasElemMatch) {
        expensiveDedupeInline(arr);
      }

      return arr;
    } // Query may ask for a value that doesn't appear in the list, like if the
    // list is [1, 2, 5, 6] and the query is <= 3. In that case we have to
    // apply a search (we'll do binary) to determine the offset to slice from.
    // Note: for gte, the valueDesc array must be list at this point


    const values = filterCache.meta.valuesDesc; // It shouldn't find the targetValue (but it might) and return the index of
    // the two value between which targetValue sits, or first/last element.

    const point = binarySearchDesc(values, filterValue);

    if (!point) {
      return undefined;
    }

    const [pivotMin, pivotMax] = point; // Each pivot index must have a value and a range
    // The returned min/max index may include the lower/upper bound, so we still
    // have to do gte checks for both values.

    let pivotValue = values[pivotMax];

    if (pivotValue < filterValue) {
      pivotValue = values[pivotMin];
    } // Note: the pivot value _shouldnt_ match the filter value because that
    // means the value was actually found, but those should have been indexed
    // so should have yielded a result in the .get() above.


    const [exclPivot, inclPivot] = ranges.get(pivotValue); // Note: technically, `5 >= "5" === true` but `5` would not be cached.
    // So we have to consider weak comparison and may have to include the pivot

    const until = pivotValue > filterValue ? inclPivot : exclPivot;
    const arr = nodes.slice(0, until).reverse();
    arr.sort(sortByIds); // elemMatch can cause a node to appear in multiple buckets so we must dedupe

    if (wasElemMatch) {
      expensiveDedupeInline(arr);
    }

    return arr;
  }

  if (op === `$gte`) {
    // First try a direct approach. If a value is queried that also exists then
    // we can prevent a binary search through the whole list, O(1) vs O(log n)
    const ranges = filterCache.meta.valueRangesDesc;
    const nodes = filterCache.meta.nodesByValueDesc;
    const range = ranges.get(filterValue);

    if (range) {
      const arr = nodes.slice(0, range[1]).reverse();
      arr.sort(sortByIds); // elemMatch can cause a node to appear in multiple buckets so we must dedupe

      if (wasElemMatch) {
        expensiveDedupeInline(arr);
      }

      return arr;
    } // Query may ask for a value that doesn't appear in the list, like if the
    // list is [1, 2, 5, 6] and the query is <= 3. In that case we have to
    // apply a search (we'll do binary) to determine the offset to slice from.
    // Note: for gte, the valueDesc array must be list at this point


    const values = filterCache.meta.valuesDesc; // It shouldn't find the targetValue (but it might) and return the index of
    // the two value between which targetValue sits, or first/last element.

    const point = binarySearchDesc(values, filterValue);

    if (!point) {
      return undefined;
    }

    const [pivotMin, pivotMax] = point; // Each pivot index must have a value and a range
    // The returned min/max index may include the lower/upper bound, so we still
    // have to do gte checks for both values.

    let pivotValue = values[pivotMax];

    if (pivotValue < filterValue) {
      pivotValue = values[pivotMin];
    } // Note: the pivot value _shouldnt_ match the filter value because that
    // means the value was actually found, but those should have been indexed
    // so should have yielded a result in the .get() above.


    const [exclPivot, inclPivot] = ranges.get(pivotValue); // Note: technically, `5 >= "5" === true` but `5` would not be cached.
    // So we have to consider weak comparison and may have to include the pivot

    const until = pivotValue >= filterValue ? inclPivot : exclPivot;
    const arr = nodes.slice(0, until).reverse();
    arr.sort(sortByIds); // elemMatch can cause a node to appear in multiple buckets so we must dedupe

    if (wasElemMatch) {
      expensiveDedupeInline(arr);
    }

    return arr;
  } // Unreachable because we checked all values of FilterOp (which op is)


  return undefined;
};

exports.getNodesFromCacheByValue = getNodesFromCacheByValue;

function removeBucketFromSet(filterValue, filterCache, set) {
  if (filterValue === null) {
    // Edge case: $ne with `null` returns only the nodes that contain the full
    // path and that don't resolve to null, so drop `undefined` as well.
    let cache = filterCache.byValue.get(undefined);
    if (cache) cache.forEach(node => set.delete(node));
    cache = filterCache.byValue.get(null);
    if (cache) cache.forEach(node => set.delete(node));
  } else {
    // Not excluding null so it should include undefined leafs or leafs where
    // only the partial path exists for whatever reason.
    const cache = filterCache.byValue.get(filterValue);
    if (cache) cache.forEach(node => set.delete(node));
  }
}
/**
 * Finds the intersection of two arrays in O(n) with n = min(a.length, b.length)
 * The returned set should not contain duplicate nodes.
 *
 * The input should be ordered by node.internal.counter and it will return a
 * list that is also ordered by node.internal.counter
 */


function intersectNodesByCounter(a, b) {
  let pointerA = 0;
  let pointerB = 0; // TODO: perf check: is it helpful to init the array to min(maxA,maxB) items?

  const result = [];
  const maxA = a.length;
  const maxB = b.length;
  let lastAdded = undefined; // Used to dedupe the list

  while (pointerA < maxA && pointerB < maxB) {
    const counterA = a[pointerA].internal.counter;
    const counterB = b[pointerB].internal.counter;

    if (counterA < counterB) {
      pointerA++;
    } else if (counterA > counterB) {
      pointerB++;
    } else {
      if (a[pointerA].id !== b[pointerB].id) {
        throw new Error(`Invariant violation: inconsistent node counters detected`);
      } // nodeA===nodeB. Make sure we didn't just add this node already.
      // Since input arrays are sorted, the same node should be grouped
      // back to back, so even if both input arrays contained the same node
      // twice, this check would prevent the result from getting duplicate nodes


      if (lastAdded !== a[pointerA]) {
        result.push(a[pointerA]);
        lastAdded = a[pointerA];
      }

      pointerA++;
      pointerB++;
    }
  }

  return result;
}
/**
 * Merge two lists of nodes.
 * The returned set should not contain duplicate nodes.
 *
 * The input should be ordered by node.internal.counter and it will return a
 * list that is also ordered by node.internal.counter
 */


function unionNodesByCounter(a, b) {
  // TODO: perf check: is it helpful to init the array to max(maxA,maxB) items?
  const arr = [];
  let lastAdded = undefined; // Used to dedupe the list

  let pointerA = 0;
  let pointerB = 0;
  const maxA = a.length;
  const maxB = b.length;

  while (pointerA < maxA && pointerB < maxB) {
    const counterA = a[pointerA].internal.counter;
    const counterB = b[pointerB].internal.counter;

    if (counterA < counterB) {
      if (lastAdded !== a[pointerA]) {
        arr.push(a[pointerA]);
        lastAdded = a[pointerA];
      }

      pointerA++;
    } else if (counterA > counterB) {
      if (lastAdded !== b[pointerB]) {
        arr.push(b[pointerB]);
        lastAdded = b[pointerB];
      }

      pointerB++;
    } else {
      if (lastAdded !== a[pointerA]) {
        arr.push(a[pointerA]);
        lastAdded = a[pointerA];
      }

      pointerA++;
      pointerB++;
    }
  }

  while (pointerA < maxA) {
    if (lastAdded !== a[pointerA]) {
      arr.push(a[pointerA]);
      lastAdded = a[pointerA];
    }

    pointerA++;
  }

  while (pointerB < maxB) {
    if (lastAdded !== b[pointerB]) {
      arr.push(b[pointerB]);
      lastAdded = b[pointerB];
    }

    pointerB++;
  }

  return arr;
}

function expensiveDedupeInline(arr) {
  // An elemMatch filter may cause duplicates to appear in a bucket.
  // Since the bucket is sorted those should now be back to back
  // Worst case this is a fast O(n) loop that does nothing.
  let prev = undefined; // We copy-on-find because a splice is expensive and we can't use Sets

  let j = 0;

  for (let i = 0; i < arr.length; ++i) {
    const node = arr[i];

    if (node !== prev) {
      // Only start copying the remainder of the list once a dupe is found
      if (i !== j) {
        arr[j] = node;
      }

      ++j;
      prev = node;
    }
  }

  arr.length = j;
}

function getSortFieldIdentifierKeys(indexFields, resolvedFields) {
  const dottedFields = (0, _query.objectToDottedField)(resolvedFields);
  const dottedFieldKeys = Object.keys(dottedFields);
  const fieldKeys = indexFields.map(field => {
    if (dottedFields[field] || dottedFieldKeys.some(key => field.startsWith(key))) {
      return `__gatsby_resolved.${field}`;
    } else {
      return field;
    }
  });
  return fieldKeys;
}
//# sourceMappingURL=indexing.js.map