import { IsString } from "@paulpopat/safe-type";
import { Build, Promised } from "./utils";

declare global {
  interface Generator<T = unknown, TReturn = any, TNext = unknown> {
    /**
     * Returns the first element of a sequence.
     * @returns The first element in the specified sequence.
     */
    first(): T;
    /**
     * Returns the first element of a sequence, or undefined if the sequence contains no elements.
     * @param allow_empty Specify that the sequence may be empty.
     * @returns The first element in the specified sequence, or undefined if the sequence is empty.
     */
    first(allow_empty: "or-default"): T | undefined;
    /**
     * Returns the first element in a sequence that satisfies a specified condition.
     * @param predicate A function to test each element for a condition.
     * @returns The first element in the sequence that passes the test in the specified predicate function.
     */
    first(predicate: (item: T) => boolean): T;
    /**
     * Returns the first element of the sequence that satisfies a condition or a default value if no such element is found.
     * @param predicate A function to test each element for a condition.
     * @param allow_empty Specify that the sequence may be empty.
     * @returns The first element in the sequence that passes the test in the specified predicate function.
     */
    first(
      predicate: (item: T) => boolean,
      allow_empty: "or-default"
    ): T | undefined;
    /**
     * Returns the first element in a sequence that matches the type specified in the type checker.
     * @param predicate A function to test each element for a type.
     * @returns The first element in the sequence that passes the test in the specified predicate function.
     */
    first<TRes extends T>(predicate: (item: T) => item is TRes): TRes;
    /**
     * Returns the first element of the sequence that matches the type specified in the type checker or a default value if no such element is found.
     * @param predicate A function to test each element for a type.
     * @param allow_empty Specify that the sequence may be empty.
     * @returns The first element in the sequence that passes the test in the specified predicate function.
     */
    first<TRes extends T>(
      predicate: (item: T) => item is TRes,
      allow_empty: "or-default"
    ): TRes | undefined;
  }

  interface AsyncGenerator<T = unknown, TReturn = any, TNext = unknown> {
    /**
     * Returns the first element of a sequence.
     * @returns The first element in the specified sequence.
     */
    first(): Promise<T>;
    /**
     * Returns the first element of a sequence, or undefined if the sequence contains no elements.
     * @param allow_empty Specify that the sequence may be empty.
     * @returns The first element in the specified sequence, or undefined if the sequence is empty.
     */
    first(allow_empty: "or-default"): Promise<T | undefined>;
    /**
     * Returns the first element in a sequence that satisfies a specified condition.
     * @param predicate A function to test each element for a condition.
     * @returns The first element in the sequence that passes the test in the specified predicate function.
     */
    first(predicate: (item: T) => Promised<boolean>): Promise<T>;
    /**
     * Returns the first element of the sequence that satisfies a condition or a default value if no such element is found.
     * @param predicate A function to test each element for a condition.
     * @param allow_empty Specify that the sequence may be empty.
     * @returns The first element in the sequence that passes the test in the specified predicate function.
     */
    first(
      predicate: (item: T) => Promised<boolean>,
      allow_empty: "or-default"
    ): Promise<T | undefined>;
    /**
     * Returns the first element in a sequence that matches the type specified in the type checker.
     * @param predicate A function to test each element for a type.
     * @returns The first element in the sequence that passes the test in the specified predicate function.
     */
    first<TRes extends T>(predicate: (item: T) => item is TRes): Promise<TRes>;
    /**
     * Returns the first element of the sequence that matches the type specified in the type checker or a default value if no such element is found.
     * @param predicate A function to test each element for a type.
     * @param allow_empty Specify that the sequence may be empty.
     * @returns The first element in the sequence that passes the test in the specified predicate function.
     */
    first<TRes extends T>(
      predicate: (item: T) => item is TRes,
      allow_empty: "or-default"
    ): Promise<TRes | undefined>;
  }
}

Build(
  "first",
  function (predicate, allow_empty) {
    if (IsString(predicate)) {
      allow_empty = predicate as "or-default";
      predicate = (i): i is unknown => true;
    } else if (!predicate) {
      predicate = (i): i is unknown => true;
    }

    for (const item of this) {
      if (predicate(item)) {
        return item;
      }
    }

    if (allow_empty) {
      return undefined;
    }

    throw new Error("No matching elements in sequence.");
  },
  async function (predicate, allow_empty) {
    if (IsString(predicate)) {
      allow_empty = predicate as "or-default";
      predicate = (i): i is unknown => true;
    } else if (!predicate) {
      predicate = (i): i is unknown => true;
    }

    for await (const item of this) {
      if (await Promise.resolve(predicate(item))) {
        return item;
      }
    }

    if (allow_empty) {
      return undefined;
    }

    throw new Error("No matching elements in sequence.");
  }
);
