import {
  ProcessingMode
} from "./core";
import { concat, Lazy } from "./lazy";
import { $limit } from "./operators/pipeline/limit";
import { $project } from "./operators/pipeline/project";
import { $skip } from "./operators/pipeline/skip";
import { $sort } from "./operators/pipeline/sort";
import { cloneDeep, has } from "./util";
const OPERATORS = { $sort, $skip, $limit };
class Cursor {
  #source;
  #predicate;
  #projection;
  #options;
  #operators = {};
  #result = null;
  #buffer = [];
  constructor(source, predicate, projection, options) {
    this.#source = source;
    this.#predicate = predicate;
    this.#projection = projection;
    this.#options = options;
  }
  /** Returns the iterator from running the query */
  fetch() {
    if (this.#result) return this.#result;
    this.#result = Lazy(this.#source).filter(this.#predicate);
    const mode = this.#options.processingMode;
    if (mode & ProcessingMode.CLONE_INPUT) this.#result.map(cloneDeep);
    for (const op of ["$sort", "$skip", "$limit"]) {
      if (has(this.#operators, op)) {
        this.#result = OPERATORS[op](
          this.#result,
          this.#operators[op],
          this.#options
        );
      }
    }
    if (Object.keys(this.#projection).length) {
      this.#result = $project(this.#result, this.#projection, this.#options);
    }
    if (mode & ProcessingMode.CLONE_OUTPUT) this.#result.map(cloneDeep);
    return this.#result;
  }
  /** Returns an iterator with the buffered data included */
  fetchAll() {
    const buffered = Lazy([...this.#buffer]);
    this.#buffer = [];
    return concat(buffered, this.fetch());
  }
  /**
   * Return remaining objects in the cursor as an array. This method exhausts the cursor
   * @returns {Array}
   */
  all() {
    return this.fetchAll().value();
  }
  /**
   * Returns the number of objects return in the cursor. This method exhausts the cursor
   * @returns {Number}
   */
  count() {
    return this.all().length;
  }
  /**
   * Returns a cursor that begins returning results only after passing or skipping a number of documents.
   * @param {Number} n the number of results to skip.
   * @return {Cursor} Returns the cursor, so you can chain this call.
   */
  skip(n) {
    this.#operators["$skip"] = n;
    return this;
  }
  /**
   * Constrains the size of a cursor's result set.
   * @param {Number} n the number of results to limit to.
   * @return {Cursor} Returns the cursor, so you can chain this call.
   */
  limit(n) {
    this.#operators["$limit"] = n;
    return this;
  }
  /**
   * Returns results ordered according to a sort specification.
   * @param {AnyObject} modifier an object of key and values specifying the sort order. 1 for ascending and -1 for descending
   * @return {Cursor} Returns the cursor, so you can chain this call.
   */
  sort(modifier) {
    this.#operators["$sort"] = modifier;
    return this;
  }
  /**
   * Specifies the collation for the cursor returned by the `mingo.Query.find`
   * @param {*} spec
   */
  collation(spec) {
    this.#options = { ...this.#options, collation: spec };
    return this;
  }
  /**
   * Returns the next document in a cursor.
   * @returns {AnyObject | Boolean}
   */
  next() {
    if (this.#buffer.length > 0) {
      return this.#buffer.pop();
    }
    const o = this.fetch().next();
    if (o.done) return;
    return o.value;
  }
  /**
   * Returns true if the cursor has documents and can be iterated.
   * @returns {boolean}
   */
  hasNext() {
    if (this.#buffer.length > 0) return true;
    const o = this.fetch().next();
    if (o.done) return false;
    this.#buffer.push(o.value);
    return true;
  }
  /**
   * Applies a function to each document in a cursor and collects the return values in an array.
   * @param fn
   * @returns {Array}
   */
  map(fn) {
    return this.all().map(fn);
  }
  /**
   * Applies a JavaScript function for every document in a cursor.
   * @param fn
   */
  forEach(fn) {
    this.all().forEach(fn);
  }
  [Symbol.iterator]() {
    return this.fetchAll();
  }
}
export {
  Cursor
};
