import { A, O, T } from "ts-toolbelt";

// ///////////////////////////////////////////////////////////////////////////////////////
// TOOLS /////////////////////////////////////////////////////////////////////////////////

// Here lies a loose collection of tools that compute types for the functions in "index.d.ts"
// The goal of this file is to keep "index.d.ts" readable as well as hiding implementations

// WHEN ADDING A NEW TOOL
// - Add documentation for the tool you've created
// - Add <created by @username> on your tool's docs

// TODO
// - Types need proper descriptions, so that we know what they do

// ---------------------------------------------------------------------------------------
// A

/**
 * <needs description>
 */
export type Arity0Fn = () => any;

/**
 * <needs description>
 */
export type Arity1Fn = (a: any) => any;

/**
 * <needs description>
 */
export type Arity2Fn = (a: any, b: any) => any;

/**
 * <needs description>
 */
export interface ArrayLike {
    nodeType: number;
}

/**
 * <needs description>
 * @param K
 */
export type AssocPartialOne<K extends keyof any> =
    (<T>(val: T) => <U>(obj: U) => Record<K, T> & Omit<U, K>)
    & (<T, U>(val: T, obj: U) => Record<K, T> & Omit<U, K>);

// ---------------------------------------------------------------------------------------
// C

/**
 * <needs description>
 */
export interface CharList extends String {
    push(x: string): void;
}

/**
 * <needs description>
 * @param V0
 * @param R
 */
export type ComposeWithFns<V0, R> = [
    (x0: V0) => R
] | [
    (x: any) => R,
    (x: V0) => any
] | [
    (x: any) => R,
    (x: any) => any,
    (x: V0) => any
] | [
    (x: any) => R,
    (x: any) => any,
    (x: any) => any,
    (x: V0) => any
] | [
    (x: any) => R,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: V0) => any
] | [
    (x: any) => R,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: V0) => any
] | [
    (x: any) => R,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: V0) => any
] | [
    (x: any) => R,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: V0) => any
] | [
    (x: any) => R,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: V0) => any
] | [
    (x: any) => R,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: V0) => any
];

// ---------------------------------------------------------------------------------------
// D

/**
 * <needs description>
 * @param A
 */
export interface Dictionary<A> {
    [index: string]: A;
}

// ---------------------------------------------------------------------------------------
// E

/**
 * Represents all objects evolvable with Evolver E
 * @param E
 */
export type Evolvable<E extends Evolver> = {
    [P in keyof E]?: Evolved<E[P]>;
};

/**
 * <needs description>
 * @param O
 * @param E
 */
export type Evolve<O extends Evolvable<E>, E extends Evolver> = {
    [P in keyof O]: P extends keyof E
                    ? EvolveValue<O[P], E[P]>
                    : O[P];
};

/**
 * <needs description>
 * @param A
 */
type Evolved<A> =
    A extends (value: infer V) => any
    ? V
    : A extends Evolver
      ? Evolvable<A>
      : never;

/**
 * A set of transformation to run as part of an evolve
 * @param T - the type to be evolved
 */
export type Evolver<T extends Evolvable<any> = any> = {
    // if T[K] isn't evolvable, don't allow nesting for that property
    [key in keyof Partial<T>]: ((value: T[key]) => T[key]) | (T[key] extends Evolvable<any> ? Evolver<T[key]> : never);
};

/**
 * <needs description>
 * @param O
 * @param E
 */
type EvolveNestedValue<O, E extends Evolver> =
    O extends object
    ? O extends Evolvable<E>
      ? Evolve<O, E>
      : never
    : never;

/**
 * <needs description>
 * @param V
 * @param E
 */
type EvolveValue<V, E> =
    E extends (value: V) => any
    ? ReturnType<E>
    : E extends Evolver
      ? EvolveNestedValue<V, E>
      : never;

// ---------------------------------------------------------------------------------------
// F

/**
 * <needs description>
 */
export interface Find {
    <T, P extends T>(pred: (val: T) => val is P, list: readonly T[]): P | undefined;
    <T>(pred: (val: T) => boolean, list: readonly T[]): T | undefined;
    <T, P extends T>(pred: (val: T) => val is P): (list: readonly T[]) => P | undefined;
    <T>(pred: (val: T) => boolean): (list: readonly T[]) => T | undefined;
}

/**
 * <needs description>
 * @param A
 */
export type Functor<A> =
  | { ['fantasy-land/map']: <B>(fn: (a: A) => B) => Functor<B>; [key: string]: any }
  | { map: <B>(fn: (a: A) => B) => Functor<B>; [key: string]: any };

// ---------------------------------------------------------------------------------------
// K

/**
 * <needs description>
 * @param K
 * @param V
 */
export type KeyValuePair<K, V> = [K, V];

// ---------------------------------------------------------------------------------------
// L

/**
 * <needs description>
 * @param S
 * @param A
 */
export type Lens<S, A> = (
    functorFactory: (a: A) => Functor<A>
) => (s: S) => Functor<S>;

// ---------------------------------------------------------------------------------------
// M

/**
 * Merge an object `O1` with `O2`
 * @param O1
 * @param O2
 * @param Depth
 *
 * `O1` & `O2` are intersected with `[]` so that we can
 * handle the scenario where we merge arrays (like ramda).
 * Ramda removes array props when merging arrays, and thus
 * only keeps own properties. This is what `ObjectOf` does.
 *
 * => ramda's `merge` functions are 100% properly typed.
 *
 * <created by @pirix-gh>
 */
export type Merge<O1 extends object, O2 extends object, Depth extends 'flat' | 'deep'> =
    O.MergeUp<T.ObjectOf<O1>, T.ObjectOf<O2>, Depth, 1>;

/**
 * Merge multiple objects `Os` with each other
 * @param Os
 *
 * It essentially works like [[Merge]], since the utility
 * `MergeUp` is used by `AssignUp` internally.
 *
 * <created by @pirix-gh>
 */
export type MergeAll<Os extends readonly object[]> =
    O.AssignUp<{}, Os, 'flat', 1> extends infer M
    ? {} extends M    // nothing merged => bcs no `as const`
      ? T.UnionOf<Os> // so we output the approximate types
      : M             // otherwise, we can get accurate types
    : never;

// ---------------------------------------------------------------------------------------
// O

/**
 * Predicate for an object containing the key.
 */
export type ObjPred<T = unknown> = (value: any, key: unknown extends T ? string : keyof T) => boolean;

/**
 * <needs description>
 */
export type Ord = number | string | boolean | Date;

/**
 * An object with at least one of its properties beeing of type `Key`.
 *
 * @example
 * ```
 * // $ExpectType { foo: unknown } | { bar: unknown }
 * type Foo = ObjectHavingSome<"foo" | "bar">
 * ```
 */
// Implementation taken from
// https://github.com/piotrwitek/utility-types/blob/df2502ef504c4ba8bd9de81a45baef112b7921d0/src/mapped-types.ts#L351-L362
export type ObjectHavingSome<Key extends string> = A.Clean<{
    [K in Key]: { [P in K]: unknown }
}[Key]>;

// ---------------------------------------------------------------------------------------
// P

/**
 * <needs description>
 */
export type Path = Array<(number | string)>;

/**
 * <needs description>
 */
export type Placeholder = A.x & {'@@functional/placeholder': true};

/**
 * <needs description>
 */
export type Pred = (...a: readonly any[]) => boolean;

/**
 * <needs description>
 * @param V0
 * @param R
 */
export type PipeWithFns<V0, R> = [
    (x0: V0) => R
] | [
    (x0: V0) => any,
    (x: any) => R
] | [
    (x0: V0) => any,
    (x: any) => any,
    (x: any) => R
] | [
    (x0: V0) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => R
] | [
    (x0: V0) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => R
] | [
    (x0: V0) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => R
] | [
    (x0: V0) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => R
] | [
    (x0: V0) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => R
] | [
    (x0: V0) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => R
] | [
    (x0: V0) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => any,
    (x: any) => R
];

// ---------------------------------------------------------------------------------------
// R

/**
 * <needs description>
 * @param A
 */
export interface Reduced<A> {
    '@@transducer/value': A;
    '@@transducer/reduced': true;
}

// ---------------------------------------------------------------------------------------
// S

/**
 * <needs description>
 * @param A
 */
export type SafePred<A> = (...a: readonly A[]) => boolean;

// ---------------------------------------------------------------------------------------
// V

/**
 * <needs description>
 * @param R
 */
export type ValueOfRecord<R> =
    R extends Record<any, infer T>
    ? T
    : never;

/**
 * If `T` is a union, `T[keyof T]` (cf. `map` and `values` in `index.d.ts`) contains the types of object values that are common across the union (i.e., an intersection).
 * Because we want to include the types of all values, including those that occur in some, but not all members of the union, we first define `ValueOfUnion`.
 * @see https://stackoverflow.com/a/60085683
 */
export type ValueOfUnion<T> = T extends infer U ? U[keyof U] : never;

export {};
