import { IsFunction } from "@paulpopat/safe-type";
import { FakeHash } from "../fake-hash";
import { Build, Promised } from "./utils";

declare global {
  interface Generator<T = unknown, TReturn = any, TNext = unknown> {
    /**
     * Groups the elements of a sequence according to a specified key selector function.
     * @param builder A function to extract the key for each element.
     * @returns A collection of elements of the type specified where each element represents a projection over a group and its key.
     */
    group_by<
      TKey extends string | number | boolean,
      TElement = T,
      TResult = [TKey, Generator<TElement>]
    >(
      builder: (item: T) => TKey
    ): Generator<TResult, TReturn, TNext>;
    /**
     * Groups the elements of a sequence according to a specified key selector function
     * and projects the elements for each group by using a specified function.
     * @param builder.key A function to extract the key for each element.
     * @param builder.element A function to map each source element to an element in an grouping.
     * @returns A collection of elements of the type specified where each element represents a projection over a group and its key.
     */
    group_by<
      TKey extends string | number | boolean,
      TElement = T,
      TResult = [TKey, Generator<TElement>]
    >(builder: {
      key: (item: T) => TKey;
      element?: (item: T) => TElement;
    }): Generator<TResult, TReturn, TNext>;
    /**
     * Groups the elements of a sequence according to a key selector function.
     * The keys are compared by using a comparer and each group's elements are
     * projected by using a specified function.
     * @param builder.key A function to extract the key for each element.
     * @param builder.element A function to map each source element to an element in an grouping.
     * @param comparitor A comparitor function to compare keys with.
     * @returns A collection of elements of the type specified where each element represents a projection over a group and its key.
     */
    group_by<TKey, TElement = T, TResult = [TKey, Generator<TElement>]>(
      builder: {
        key: (item: T) => TKey;
        element?: (item: T) => TElement;
      },
      comparitor: (a: TKey, b: TKey) => boolean
    ): Generator<TResult, TReturn, TNext>;
    /**
     * Groups the elements of a sequence according to a specified key selector
     * function and compares the keys by using a specified comparer.
     * @param builder A function to extract the key for each element.
     * @param comparitor A comparitor function to compare keys with.
     * @returns A collection of elements of the type specified where each element represents a projection over a group and its key.
     */
    group_by<TKey, TElement = T, TResult = [TKey, Generator<TElement>]>(
      builder: (item: T) => TKey,
      comparitor: (a: TKey, b: TKey) => boolean
    ): Generator<TResult, TReturn, TNext>;
  }

  interface AsyncGenerator<T = unknown, TReturn = any, TNext = unknown> {
    /**
     * Groups the elements of a sequence according to a specified key selector function.
     * @param builder A function to extract the key for each element.
     * @returns A collection of elements of the type specified where each element represents a projection over a group and its key.
     */
    group_by<
      TKey extends string | number | boolean,
      TElement = T,
      TResult = [TKey, AsyncGenerator<TElement>]
    >(
      builder: (item: T) => Promised<TKey>
    ): AsyncGenerator<TResult, TReturn, TNext>;
    /**
     * Groups the elements of a sequence according to a specified key selector function
     * and projects the elements for each group by using a specified function.
     * @param builder.key A function to extract the key for each element.
     * @param builder.element A function to map each source element to an element in an grouping.
     * @returns A collection of elements of the type specified where each element represents a projection over a group and its key.
     */
    group_by<
      TKey extends string | number | boolean,
      TElement = T,
      TResult = [TKey, AsyncGenerator<TElement>]
    >(builder: {
      key: (item: T) => Promised<TKey>;
      element?: (item: T) => Promised<TElement>;
    }): AsyncGenerator<TResult, TReturn, TNext>;
    /**
     * Groups the elements of a sequence according to a key selector function.
     * The keys are compared by using a comparer and each group's elements are
     * projected by using a specified function.
     * @param builder.key A function to extract the key for each element.
     * @param builder.element A function to map each source element to an element in an grouping.
     * @param comparitor A comparitor function to compare keys with.
     * @returns A collection of elements of the type specified where each element represents a projection over a group and its key.
     */
    group_by<TKey, TElement = T, TResult = [TKey, AsyncGenerator<TElement>]>(
      builder: (item: T) => Promised<TKey>,
      comparitor: (a: TKey, b: TKey) => boolean
    ): AsyncGenerator<TResult, TReturn, TNext>;
    /**
     * Groups the elements of a sequence according to a specified key selector
     * function and compares the keys by using a specified comparer.
     * @param builder A function to extract the key for each element.
     * @param comparitor A comparitor function to compare keys with.
     * @returns A collection of elements of the type specified where each element represents a projection over a group and its key.
     */
    group_by<TKey, TElement = T, TResult = [TKey, AsyncGenerator<TElement>]>(
      builder: {
        key: (item: T) => Promised<TKey>;
        element?: (item: T) => Promised<TElement>;
      },
      comparitor: (a: TKey, b: TKey) => boolean
    ): AsyncGenerator<TResult, TReturn, TNext>;
  }
}

function CreateBuilder(
  builder:
    | ((item: unknown) => unknown)
    | {
        key: (item: unknown) => unknown;
        element?: (item: unknown) => unknown;
      }
): {
  key: (item: unknown) => unknown;
  element: (item: unknown) => unknown;
};
function CreateBuilder(
  builder:
    | ((item: unknown) => Promised<unknown>)
    | {
        key: (item: unknown) => Promised<unknown>;
        element?: (item: unknown) => Promised<unknown>;
      }
): {
  key: (item: unknown) => Promised<unknown>;
  element: (item: unknown) => Promised<unknown>;
};
function CreateBuilder(builder: any) {
  return IsFunction(builder)
    ? {
        key: builder,
        element: (i: any) => i,
      }
    : {
        key: builder.key,
        element: builder.element ?? ((i: any) => i),
      };
}

Build(
  "group_by",
  function* (builder, comparitor = (a, b) => a === b) {
    const internal = this.array();
    const b = CreateBuilder(builder);
    const hash = FakeHash(comparitor);
    for (const item of internal) {
      const key = b.key(item);
      if (hash.contains(key)) {
        continue;
      }

      hash.add(key);

      yield [
        key,
        (function* () {
          for (let i = 0; i < internal.length; i++) {
            const item = internal[i];
            const bkey = b.key(item);
            if (comparitor(key, bkey)) {
              yield b.element(item);
            }
          }
        })(),
      ];
    }
  },
  async function* (builder, comparitor = (a, b) => a === b) {
    const internal = await this.array();
    const b = CreateBuilder(builder);
    const hash = FakeHash(comparitor);
    for (const item of internal) {
      const key = await Promise.resolve(b.key(item));
      if (hash.contains(key)) {
        continue;
      }

      hash.add(key);

      yield [
        key,
        (async function* () {
          for (let i = 0; i < internal.length; i++) {
            const item = internal[i];
            const bkey = b.key(item);
            if (comparitor(key, bkey)) {
              yield await Promise.resolve(b.element(item));
            }
          }
        })(),
      ];
    }
  }
);
