import { ComputeOptions } from "../../core";
import { concat, Lazy } from "../../lazy";
import { TIME_UNITS } from "../../types";
import {
  assert,
  isArray,
  isDate,
  isNil,
  isNumber,
  isObject,
  isString,
  resolve,
  ValueMap
} from "../../util";
import { $dateAdd } from "../expression/date/dateAdd";
import { $sort } from "./sort";
const EMPTY_OBJECT = Object.freeze({});
const $densify = (collection, expr, options) => {
  const { step, bounds, unit } = expr.range;
  if (unit) {
    assert(TIME_UNITS.includes(unit), "");
    assert(
      Number.isInteger(step) && step > 0,
      "The step parameter in a range statement must be a whole number when densifying a date range."
    );
  } else {
    assert(
      isNumber(step) && step > 0,
      "The step parameter in a range statement must be a strictly positive numeric value."
    );
  }
  if (isArray(bounds)) {
    assert(
      !!bounds && bounds.length === 2,
      "A bounding array in a range statement must have exactly two elements."
    );
    assert(
      (bounds.every(isNumber) || bounds.every(isDate)) && bounds[0] < bounds[1],
      "A bounding array must be an ascending array of either two dates or two numbers."
    );
    assert(
      unit && !bounds.some(isNumber),
      "Numeric bounds may not have unit parameter."
    );
  }
  if (expr.partitionByFields) {
    assert(
      isArray(expr.partitionByFields),
      "$densify: `partitionByFields` must be an array of strings"
    );
  }
  collection = $sort(collection, { [expr.field]: 1 }, options);
  const nilOptions = ComputeOptions.init(options, null);
  const computeNextValue = (value) => {
    return isNumber(value) ? value + step : $dateAdd(
      EMPTY_OBJECT,
      { startDate: value, unit, amount: step },
      nilOptions
    );
  };
  const isValidUnit = !!unit && TIME_UNITS.includes(unit);
  const getFieldValue = (o) => {
    const v = resolve(o, expr.field);
    if (isNil(v)) return v;
    if (isNumber(v)) {
      assert(
        !isValidUnit,
        "$densify: Encountered non-date value in collection when step has a date unit."
      );
    } else if (isDate(v)) {
      assert(
        isValidUnit,
        "$densify: Encountered date value in collection when step does not have a date unit."
      );
    } else {
      assert(false, "$densify: Densify field type must be numeric or a date");
    }
    return v;
  };
  const peekItem = new Array();
  const nilFieldsIterator = Lazy(() => {
    const item = collection.next();
    const fieldValue = getFieldValue(item.value);
    if (isNil(fieldValue)) return item;
    peekItem.push(item);
    return { done: true };
  });
  const nextDensifyValueMap = ValueMap.init(
    options.hashFunction
  );
  const [lower, upper] = isArray(bounds) ? bounds : [bounds, bounds];
  let maxFieldValue = void 0;
  const updateMaxFieldValue = (value) => {
    maxFieldValue = maxFieldValue === void 0 || maxFieldValue < value ? value : maxFieldValue;
  };
  const rootKey = [];
  const densifyIterator = Lazy(() => {
    const item = peekItem.length > 0 ? peekItem.pop() : collection.next();
    if (item.done) return item;
    let partitionKey = rootKey;
    if (isArray(expr.partitionByFields)) {
      partitionKey = expr.partitionByFields.map(
        (k) => resolve(item.value, k)
      );
      assert(
        partitionKey.every(isString),
        "$densify: Partition fields must evaluate to string values."
      );
    }
    assert(isObject(item.value), "$densify: collection must contain documents");
    const itemValue = getFieldValue(item.value);
    if (!nextDensifyValueMap.has(partitionKey)) {
      if (lower == "full") {
        if (!nextDensifyValueMap.has(rootKey)) {
          nextDensifyValueMap.set(rootKey, itemValue);
        }
        nextDensifyValueMap.set(partitionKey, nextDensifyValueMap.get(rootKey));
      } else if (lower == "partition") {
        nextDensifyValueMap.set(partitionKey, itemValue);
      } else {
        nextDensifyValueMap.set(partitionKey, lower);
      }
    }
    const densifyValue = nextDensifyValueMap.get(partitionKey);
    if (
      // current item field value is lower than current densify value.
      itemValue <= densifyValue || // range value equals or exceeds upper bound
      upper != "full" && upper != "partition" && densifyValue >= upper
    ) {
      if (densifyValue <= itemValue) {
        nextDensifyValueMap.set(partitionKey, computeNextValue(densifyValue));
      }
      updateMaxFieldValue(itemValue);
      return item;
    }
    nextDensifyValueMap.set(partitionKey, computeNextValue(densifyValue));
    updateMaxFieldValue(densifyValue);
    const denseObj = { [expr.field]: densifyValue };
    if (partitionKey) {
      partitionKey.forEach((v, i) => {
        denseObj[expr.partitionByFields[i]] = v;
      });
    }
    peekItem.push(item);
    return { done: false, value: denseObj };
  });
  if (lower !== "full") return concat(nilFieldsIterator, densifyIterator);
  let paritionIndex = -1;
  let partitionKeysSet = void 0;
  const fullBoundsIterator = Lazy(() => {
    if (paritionIndex === -1) {
      const fullDensifyValue = nextDensifyValueMap.get(rootKey);
      nextDensifyValueMap.delete(rootKey);
      partitionKeysSet = Array.from(nextDensifyValueMap.keys());
      if (partitionKeysSet.length === 0) {
        partitionKeysSet.push(rootKey);
        nextDensifyValueMap.set(rootKey, fullDensifyValue);
      }
      paritionIndex++;
    }
    do {
      const partitionKey = partitionKeysSet[paritionIndex];
      const partitionMaxValue = nextDensifyValueMap.get(partitionKey);
      if (partitionMaxValue < maxFieldValue) {
        nextDensifyValueMap.set(
          partitionKey,
          computeNextValue(partitionMaxValue)
        );
        const denseObj = { [expr.field]: partitionMaxValue };
        partitionKey.forEach((v, i) => {
          denseObj[expr.partitionByFields[i]] = v;
        });
        return { done: false, value: denseObj };
      }
      paritionIndex++;
    } while (paritionIndex < partitionKeysSet.length);
    return { done: true };
  });
  return concat(nilFieldsIterator, densifyIterator, fullBoundsIterator);
};
export {
  $densify
};
