import { Deferred, deferred } from "../deps.js";

export class DeferredStack<T> {
  #array: Array<T>;
  #creator?: () => Promise<T>;
  #max_size: number;
  #queue: Array<Deferred<T>>;
  #size: number;

  constructor(
    max?: number,
    ls?: Iterable<T>,
    creator?: () => Promise<T>,
  ) {
    this.#array = ls ? [...ls] : [];
    this.#creator = creator;
    this.#max_size = max || 10;
    this.#queue = [];
    this.#size = this.#array.length;
  }

  get available(): number {
    return this.#array.length;
  }

  async pop(): Promise<T> {
    if (this.#array.length > 0) {
      return this.#array.pop()!;
    } else if (this.#size < this.#max_size && this.#creator) {
      this.#size++;
      return await this.#creator();
    }
    const d = deferred<T>();
    this.#queue.push(d);
    await d;
    return this.#array.pop()!;
  }

  push(value: T): void {
    this.#array.push(value);
    if (this.#queue.length > 0) {
      const d = this.#queue.shift()!;
      d.resolve();
    }
  }

  get size(): number {
    return this.#size;
  }
}

/**
 * The DeferredAccessStack provides access to a series of elements provided on the stack creation,
 * but with the caveat that they require an initialization of sorts before they can be used
 *
 * Instead of providing a `creator` function as you would with the `DeferredStack`, you provide
 * an initialization callback to execute for each element that is retrieved from the stack and a check
 * callback to determine if the element requires initialization and return a count of the initialized
 * elements
 */
export class DeferredAccessStack<T> {
  #elements: Array<T>;
  #initializeElement: (element: T) => Promise<void>;
  #checkElementInitialization: (element: T) => Promise<boolean> | boolean;
  #queue: Array<Deferred<undefined>>;
  #size: number;

  get available(): number {
    return this.#elements.length;
  }

  /**
   * The max number of elements that can be contained in the stack a time
   */
  get size(): number {
    return this.#size;
  }

  /**
   * @param initialize This function will execute for each element that hasn't been initialized when requested from the stack
   */
  constructor(
    elements: T[],
    initCallback: (element: T) => Promise<void>,
    checkInitCallback: (element: T) => Promise<boolean> | boolean,
  ) {
    this.#checkElementInitialization = checkInitCallback;
    this.#elements = elements;
    this.#initializeElement = initCallback;
    this.#queue = [];
    this.#size = elements.length;
  }

  /**
   * Will execute the check for initialization on each element of the stack
   * and then return the number of initialized elements that pass the check
   */
  async initialized(): Promise<number> {
    const initialized = await Promise.all(
      this.#elements.map((e) => this.#checkElementInitialization(e)),
    );

    return initialized
      .filter((initialized) => initialized === true)
      .length;
  }

  async pop(): Promise<T> {
    let element: T;
    if (this.available > 0) {
      element = this.#elements.pop()!;
    } else {
      // If there are not elements left in the stack, it will await the call until
      // at least one is restored and then return it
      const d = deferred<undefined>();
      this.#queue.push(d);
      await d;
      element = this.#elements.pop()!;
    }

    if (!await this.#checkElementInitialization(element)) {
      await this.#initializeElement(element);
    }
    return element;
  }

  push(value: T): void {
    this.#elements.push(value);
    // If an element has been requested while the stack was empty, indicate
    // that an element has been restored
    if (this.#queue.length > 0) {
      const d = this.#queue.shift()!;
      d.resolve();
    }
  }
}
