import { ConditionalPick, IsLiteral, OptionalKeysOf, Primitive, RequireAtLeastOne, RequiredKeysOf, Simplify, UnionToTuple } from "type-fest";
import { IsUnion } from "type-fest/source/internal";

//#region src/internals/helpers.d.ts
type IsTrue<T> = true extends T ? true : false;
type IfTrue<T, OnTrue, OnFalse> = true extends T ? OnTrue : OnFalse;
type Union<A, B> = [B] extends [A] ? A : [A] extends [B] ? B : A | B;
type IsLiteral$1<T> = IsLiteral<T> extends true ? true : T extends null | undefined ? true : false;
interface MatchError<_i> {
  __: never;
}

//#endregion
//#region src/internals/case.d.ts
type HasCases<T> = IsTrue<T extends unknown ? T extends Primitive | null | undefined ? true : false : false>;
type PickCase<T, U> = IsLiteral$1<U> extends true ? U : Extract<T, U>;
type DropCase<T, U> = IsLiteral$1<U> extends true ? Exclude<T, U> : T;
type CasePatterns<T> = T extends unknown ? T extends Primitive ? T : never : never;
type JoinMatchedCases<T, U> = IsLiteral$1<U> extends true ? T | U : T;
type ExcludeMatchedCases<T, U> = Exclude<T, U>;

//#endregion
//#region src/internals/shape.d.ts
type IsPlainObject<T> = T extends object ? T extends any[] ? false : T extends Function ? false : true : false;
type HasShapes<T> = IsTrue<T extends unknown ? IsPlainObject<T> extends true ? true : false : false>;
type PickShape<T, U> = T extends unknown ? IsPlainObject<T> extends true ? Readonly<Simplify<MatchShape<T, U> & U>> : never : never;
type DropShape<T, U> = T extends unknown ? IsPlainObject<T> extends true ? [MatchShape<T, U>] extends [never] ? T : DropLiteralFields<T, U> : T : never;
type DropLiteralFields<T, U> = HasLiteralFields<U> extends true ? LiteralFieldsOf<U> extends infer V ? Exclude<ShapeVariations<T, V>, V> : never : T;
type ShapeVariations<T, U, V = UnionKeysOf<T, U>> = V extends [infer Key, ...infer Rest] ? Key extends keyof T ? ShapeVariations<FieldVariations<T, Key>, U, Rest> : never : T;
type FieldVariations<T, K extends keyof T, V = UnionToTuple<T[K]>> = V extends [infer Value, ...infer Rest] ? T & Record<K, Value> | FieldVariations<T, K, Rest> : never;
type UnionKeysOf<T, U> = UnionToTuple<keyof { [K in keyof U]: K extends keyof T ? IsLiteral$1<U[K]> extends true ? IsUnion<T[K]> extends true ? K : never : never : never }>;
type ShapePatterns<T> = T extends unknown ? T extends object ? AtLeastOneField<CleanOptionals<ConditionalPick<T, Primitive>>> : never : never;
type JoinMatchedShapes<T, U> = HasLiteralFields<U> extends true ? T | U : T;
type ExcludeMatchedShapes<T, U> = [U] extends [never] ? T : U extends unknown ? [MatchShape<U, T>] extends [never] ? T : never : never;
type MatchShape<T, U> = IfTrue<{ [K in keyof U]: K extends keyof T ? U[K] extends T[K] ? true : false : false }[keyof U], T, never>;
type CleanOptionals<T extends object> = Simplify<{ [K in RequiredKeysOf<T>]: T[K] } & { [K in OptionalKeysOf<T>]: T[K] | undefined }>;
type LiteralFieldsOf<T> = { [K in keyof T as IsLiteral$1<T[K]> extends true ? K : never]: T[K] };
type HasLiteralFields<T> = [keyof LiteralFieldsOf<T>] extends [never] ? false : true;
type AtLeastOneField<T> = RequireAtLeastOne<T, keyof T>;

//#endregion
//#region src/internals/MatchStrict.d.ts
interface MatchStrict<Input, Output = never, MatchedLiterals = never, MatchedShapes = never> {
  returnType: MatchError<"Calling `.returnType<T>()` is only allowed directly after `match(...)`">;
  /**
   * Matches a primitive value (`===`). Returns result if matched.
   *
   * ```ts
   * const value = 2 as number
   *
   * match(value)
   *     .case(1, "one")
   *     .case(2, "two")
   *     .case(3, "three")
   *     .orThrow() //=> "two"
   * ```
   */
  case: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasCases<Input> extends true ? <const I extends CasePatterns<Input>>(value: ExcludeMatchedCases<I, MatchedLiterals>, result: Output) => MatchStrict<DropCase<Input, I>, Output, JoinMatchedCases<MatchedLiterals, I>, MatchedShapes> : MatchError<"No primitives left to match against">;
  /**
   * Like `.case`, but calls `fn(value)` if matched. Useful for expensive computations.
   *
   * ```ts
   * const value = 2 as number;
   *
   * match(value)
   *     .onCase(1, (num) => num * -1)
   *     .onCase(2, (num) => num * -2)
   *     .onCase(3, (num) => num * -3)
   *     .orThrow(); //=> -4
   * ```
   */
  onCase: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasCases<Input> extends true ? <const I extends CasePatterns<Input>>(value: ExcludeMatchedCases<I, MatchedLiterals>, fn: (value: PickCase<Input, I>) => Output) => MatchStrict<DropCase<Input, I>, Output, JoinMatchedCases<MatchedLiterals, I>, MatchedShapes> : MatchError<"No primitives left to match against">;
  /**
   * Matches a shallow object shape. All fields must match (`===`), only supports matching primitives.
   *
   * ```ts
   * type Rectangle = {
   *     x: number
   *     y: number
   *     width: number
   *     height: number
   * }
   *
   * type Circle = {
   *     x: number
   *     y: number
   *     radius: number
   * }
   *
   * const value: Rectangle = {
   *     x: 0,
   *     y: 0,
   *     width: 0,
   *     height: 0,
   * }
   *
   * const isEmpty = match(value as Rectangle | Circle)
   *     .shape({ width: 0, height: 0 }, true)
   *     .shape({ radius: 0 }, true)
   *     .or(false) //=> true
   * ```
   */
  shape: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasShapes<Input> extends true ? <const I extends ShapePatterns<Input>>(value: ExcludeMatchedShapes<I, MatchedShapes>, result: Output) => MatchStrict<DropShape<Input, I>, Output, MatchedLiterals, JoinMatchedShapes<MatchedShapes, I>> : MatchError<"No shapes left to match against">;
  /**
   * Like `.shape`, but calls `fn(value)` if matched. Useful for expensive computations.
   *
   * ```ts
   * type Rectangle = {
   *     kind: "rectangle"
   *     width: number
   *     height: number
   * }
   *
   * type Circle = {
   *     kind: "circle"
   *     radius: number
   * }
   *
   * const value: Rectangle = {
   *     kind: "rectangle",
   *     width: 10,
   *     height: 10,
   * }
   *
   * const area = match(value as Rectangle | Circle)
   *     .shape({ kind: "rectangle" }, rect => rect.width * rect.height)
   *     .shape({ kind: "circle" }, circ => Math.PI * circ.radius ** 2)
   *     .orThrow() //=> 100
   * ```
   */
  onShape: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasShapes<Input> extends true ? <const I extends ShapePatterns<Input>>(value: ExcludeMatchedShapes<I, MatchedShapes>, fn: (value: PickShape<Input, I>) => Output) => MatchStrict<DropShape<Input, I>, Output, MatchedLiterals, JoinMatchedShapes<MatchedShapes, I>> : MatchError<"No shapes left to match against">;
  /**
   * Matches if `predicate(value)` is truthy.
   *
   * ```ts
   * match(10 as number)
   *     .cond((num) => num > 0, "positive")
   *     .cond((num) => num < 0, "negative")
   *     .or("zero"); //=> "positive"
   * ```
   */
  cond: [Input] extends [never] ? MatchError<"Match is exhaustive"> : (<const I extends Input>(predicate: (value: Input) => value is I, result: Output) => MatchStrict<Exclude<Input, I>, Output, MatchedLiterals, MatchedShapes>) & ((predicate: (value: Input) => boolean, result: Output) => MatchStrict<Input, Output, MatchedLiterals, MatchedShapes>);
  /**
   * Like `.cond`, but calls `fn(value)` if matched.
   *
   * <!-- prettier-ignore -->
   * ```ts
   * match("Hello world!" as string)
   *     .onCond(msg => msg.length > 250, msg => `Message "${msg}" is too long`)
   *     .onCond(msg => msg.length < 100, msg => `Message "${msg}" is too short`)
   *     .or(false); //=> `Message "Hello world!" is too short`
   * ```
   */
  onCond: [Input] extends [never] ? MatchError<"Match is exhaustive"> : (<const I extends Input>(predicate: (value: Input) => value is I, fn: (value: I) => Output) => MatchStrict<Exclude<Input, I>, Output, MatchedLiterals, MatchedShapes>) & ((predicate: (value: Input) => boolean, fn: (value: Input) => Output) => MatchStrict<Input, Output, MatchedLiterals, MatchedShapes>);
  /**
   * Returns the result, otherwise the given fallback.
   *
   * ```ts
   * import { match } from "@monstermann/match";
   *
   * match(3 as number)
   *     .case(1, "one")
   *     .case(2, "two")
   *     .or("other"); //=> "other"
   * ```
   */
  or: [Input] extends [never] ? MatchError<"Match is exhaustive"> : (fallback: Output) => Output;
  /**
   * Returns the result, otherwise calls `fn(value)`.
   *
   * ```ts
   * import { match } from "@monstermann/match";
   *
   * match(3 as number)
   *     .case(1, "one")
   *     .case(2, "two")
   *     .orElse((num) => String(num)); //=> "3"
   * ```
   */
  orElse: [Input] extends [never] ? MatchError<"Match is exhaustive"> : (fallback: (value: Readonly<Input>) => Output) => Output;
  /**
   * Returns the result, or throws an exception at runtime. Enforces exhaustiveness at compile time.
   *
   * ```ts
   * import { match } from "@monstermann/match";
   *
   * match(3 as number)
   *     .case(1, "one")
   *     .case(2, "two")
   *     .orThrow(); //=> Error
   * //   ~~~~~~~ ❌ Type 'MatchError<3>' has no call signatures.
   * ```
   */
  orThrow: [Input] extends [never] ? () => Output : MatchError<Input>;
}

//#endregion
//#region src/internals/Match.d.ts
interface Match<Input, Output = never, MatchedLiterals = never, MatchedShapes = never> {
  /**
   * By default, the return type is inferred from your cases. You can enforce a specific type:
   *
   * ```ts
   * match(foo)
   *     .returnType<"a" | "b" | "c">()
   *     .case(1, "a")
   *     .case(2, "b")
   *     .or("c")
   * ```
   */
  returnType: [Output] extends [never] ? <O>() => MatchStrict<Input, O> : MatchError<"Calling `.returnType<T>()` is only allowed directly after `match(...)`">;
  /**
   * Matches a primitive value (`===`). Returns result if matched.
   *
   * ```ts
   * const value = 2 as number
   *
   * match(value)
   *     .case(1, "one")
   *     .case(2, "two")
   *     .case(3, "three")
   *     .orThrow() //=> "two"
   * ```
   */
  case: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasCases<Input> extends true ? <const I extends CasePatterns<Input>, O>(value: ExcludeMatchedCases<I, MatchedLiterals>, result: O) => Match<DropCase<Input, I>, Union<Output, O>, JoinMatchedCases<MatchedLiterals, I>, MatchedShapes> : MatchError<"No primitives left to match against">;
  /**
   * Like `.case`, but calls `fn(value)` if matched. Useful for expensive computations.
   *
   * ```ts
   * const value = 2 as number;
   *
   * match(value)
   *     .onCase(1, (num) => num * -1)
   *     .onCase(2, (num) => num * -2)
   *     .onCase(3, (num) => num * -3)
   *     .orThrow(); //=> -4
   * ```
   */
  onCase: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasCases<Input> extends true ? <const I extends CasePatterns<Input>, O>(value: ExcludeMatchedCases<I, MatchedLiterals>, fn: (value: PickCase<Input, I>) => O) => Match<DropCase<Input, I>, Union<Output, O>, JoinMatchedCases<MatchedLiterals, I>, MatchedShapes> : MatchError<"No primitives left to match against">;
  /**
   * Matches a shallow object shape. All fields must match (`===`), only supports matching primitives.
   *
   * ```ts
   * type Rectangle = {
   *     x: number
   *     y: number
   *     width: number
   *     height: number
   * }
   *
   * type Circle = {
   *     x: number
   *     y: number
   *     radius: number
   * }
   *
   * const value: Rectangle = {
   *     x: 0,
   *     y: 0,
   *     width: 0,
   *     height: 0,
   * }
   *
   * const isEmpty = match(value as Rectangle | Circle)
   *     .shape({ width: 0, height: 0 }, true)
   *     .shape({ radius: 0 }, true)
   *     .or(false) //=> true
   * ```
   */
  shape: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasShapes<Input> extends true ? <const I extends ShapePatterns<Input>, O>(value: ExcludeMatchedShapes<I, MatchedShapes>, result: O) => Match<DropShape<Input, I>, Union<Output, O>, MatchedLiterals, JoinMatchedShapes<MatchedShapes, I>> : MatchError<"No shapes left to match against">;
  /**
   * Like `.shape`, but calls `fn(value)` if matched. Useful for expensive computations.
   *
   * ```ts
   * type Rectangle = {
   *     kind: "rectangle"
   *     width: number
   *     height: number
   * }
   *
   * type Circle = {
   *     kind: "circle"
   *     radius: number
   * }
   *
   * const value: Rectangle = {
   *     kind: "rectangle",
   *     width: 10,
   *     height: 10,
   * }
   *
   * const area = match(value as Rectangle | Circle)
   *     .shape({ kind: "rectangle" }, rect => rect.width * rect.height)
   *     .shape({ kind: "circle" }, circ => Math.PI * circ.radius ** 2)
   *     .orThrow() //=> 100
   * ```
   */
  onShape: [Input] extends [never] ? MatchError<"Match is exhaustive"> : HasShapes<Input> extends true ? <const I extends ShapePatterns<Input>, O>(value: ExcludeMatchedShapes<I, MatchedShapes>, fn: (value: PickShape<Input, I>) => O) => Match<DropShape<Input, I>, Union<Output, O>, MatchedLiterals, JoinMatchedShapes<MatchedShapes, I>> : MatchError<"No shapes left to match against">;
  /**
   * Matches if `predicate(value)` is truthy.
   *
   * ```ts
   * match(10 as number)
   *     .cond((num) => num > 0, "positive")
   *     .cond((num) => num < 0, "negative")
   *     .or("zero"); //=> "positive"
   * ```
   */
  cond: [Input] extends [never] ? MatchError<"Match is exhaustive"> : (<const I extends Input, O>(predicate: (value: Input) => value is I, result: O) => Match<Exclude<Input, I>, Union<Output, O>, MatchedLiterals, MatchedShapes>) & (<O>(predicate: (value: Input) => boolean, result: O) => Match<Input, Union<Output, O>, MatchedLiterals, MatchedShapes>);
  /**
   * Like `.cond`, but calls `fn(value)` if matched.
   *
   * <!-- prettier-ignore -->
   * ```ts
   * match("Hello world!" as string)
   *     .onCond(msg => msg.length > 250, msg => `Message "${msg}" is too long`)
   *     .onCond(msg => msg.length < 100, msg => `Message "${msg}" is too short`)
   *     .or(false); //=> `Message "Hello world!" is too short`
   * ```
   */
  onCond: [Input] extends [never] ? MatchError<"Match is exhaustive"> : (<const I extends Input, O>(predicate: (value: Input) => value is I, fn: (value: I) => O) => Match<Exclude<Input, I>, Union<Output, O>, MatchedLiterals, MatchedShapes>) & (<O>(predicate: (value: Input) => boolean, fn: (value: Input) => O) => Match<Input, Union<Output, O>, MatchedLiterals, MatchedShapes>);
  /**
   * Returns the result, otherwise the given fallback.
   *
   * ```ts
   * import { match } from "@monstermann/match";
   *
   * match(3 as number)
   *     .case(1, "one")
   *     .case(2, "two")
   *     .or("other"); //=> "other"
   * ```
   */
  or: [Input] extends [never] ? MatchError<"Match is exhaustive"> : <O>(fallback: O) => Union<Output, O>;
  /**
   * Returns the result, otherwise calls `fn(value)`.
   *
   * ```ts
   * import { match } from "@monstermann/match";
   *
   * match(3 as number)
   *     .case(1, "one")
   *     .case(2, "two")
   *     .orElse((num) => String(num)); //=> "3"
   * ```
   */
  orElse: [Input] extends [never] ? MatchError<"Match is exhaustive"> : <O>(fallback: (value: Readonly<Input>) => O) => Union<Output, O>;
  /**
   * Returns the result, or throws an exception at runtime. Enforces exhaustiveness at compile time.
   *
   * ```ts
   * import { match } from "@monstermann/match";
   *
   * match(3 as number)
   *     .case(1, "one")
   *     .case(2, "two")
   *     .orThrow(); //=> Error
   * //   ~~~~~~~ ❌ Type 'MatchError<3>' has no call signatures.
   * ```
   */
  orThrow: [Input] extends [never] ? () => Output : MatchError<Input>;
}

//#endregion
//#region src/match.d.ts
declare function match<const T>(value: T): Match<T>;

//#endregion
export { match };