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

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

  interface AsyncGenerator<T = unknown, TReturn = any, TNext = unknown> {
    /**
     * Returns the last element of a sequence.
     * @returns The value at the last position in the source sequence.
     */
    last(): Promise<T>;
    /**
     * Returns the last element of a sequence that satisfies a specified condition.
     * @param predicate A function to test each element for a condition.
     * @returns The last element in the sequence that passes the test in the specified predicate function.
     */
    last(predicate: (item: T) => Promised<boolean>): Promise<T>;
    /**
     * Returns the last element of a sequence, or undefined if the sequence contains no elements.
     * @param allow_undefined Specify that undefined should be returned if no elements.
     * @returns undefined if the source sequence is empty; otherwise, the last element in the sequence.
     */
    last(allow_undefined: "or-default"): Promise<T | undefined>;
    /**
     * Returns the last element of a sequence that satisfies a
     * condition or undefined if no such element is found.
     * @param predicate A function to test each element for a condition.
     * @param allow_undefined Specify that undefined should be returned if no elements.
     * @returns undefined if the sequence is empty or if no elements pass the test in the predicate function; otherwise, the last element that passes the test in the predicate function.
     */
    last(
      predicate: (item: T) => Promised<boolean>,
      allow_undefined: "or-default"
    ): Promise<T | undefined>;
  }
}

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

    let last_match: unknown;
    let part = this.next();
    while (!part.done) {
      if (predicate(part.value)) {
        last_match = part.value;
      }

      part = this.next();
    }

    if (last_match === undefined && !allow_undefined) {
      throw new Error("No matches for sequence");
    }

    return last_match;
  },
  async function (predicate, allow_undefined) {
    if (IsString(predicate)) {
      allow_undefined = predicate as "or-default";
      predicate = (i): i is unknown => true;
    } else if (!predicate) {
      predicate = (i): i is unknown => true;
    }

    let last_match: unknown;
    let part = await this.next();
    while (!part.done) {
      if (predicate(part.value)) {
        last_match = part.value;
      }

      part = await this.next();
    }

    if (last_match === undefined && !allow_undefined) {
      throw new Error("No matches for sequence");
    }

    return last_match;
  }
);
