import { twMerge } from "tailwind-merge";

/**
 * Takes a string, and returns boolean if it is "true" or "false".
 * Otherwise, returns the string.
 *
 * @example
 * ```
 * type BooleanString = BooleanString<"true" | "false" | "other">
 * // BooleanString = true | false | "other" = boolean | "other"
 * ```
 */
type BooleanString<T extends string> = T extends "true"
  ? true
  : T extends "false"
    ? false
    : T;

/**
 * A type that represents a variant object.
 *
 * @example
 * ```
 * const variant: VariantType = {
 *   opened: {
 *     true: "opacity-100",
 *     false: "opacity-0",
 *   }
 *   size: {
 *     sm: "small",
 *     md: "medium",
 *     lg: "large",
 *   },
 *   color: {
 *     red: "#ff0000",
 *     green: "#00ff00",
 *     blue: "#0000ff",
 *   },
 * }
 * ```
 */
type VariantType = Record<string, Record<string, string>>;

/**
 * Takes VariantType, and returns a type that represents the variant object.
 *
 * @example
 * ```
 * const kvs: VariantKV<VariantType> = {
 *   opened: true  // BooleanString<"true" | "false"> = boolean;
 *   size: "sm"    // BooleanString<"sm" | "md" | "lg"> = "sm" | "md" | "lg";
 *   color: "red"  // BooleanString<"red" | "green" | "blue"> = "red" | "green" | "blue";
 * }
 * ```
 */
type VariantKV<V extends VariantType> = {
  [VariantKey in keyof V]: BooleanString<keyof V[VariantKey] & string>;
};

/**
 * Used for safely casting `Object.entries(<VariantKV>)`
 */
type VariantKVEntry<V extends VariantType> = [
  keyof V,
  BooleanString<keyof V[keyof V] & string>,
][];

/**
 * Takes VariantKV as parameter, return className string.
 *
 * @example
 * vcn({
 *   /* ... *\/
 *   dynamics: [
 *     ({ a, b }) => {
 *       return a === "something" ? "asdf" : b
 *     },
 *   ]
 * })
 */
type DynamicClassName<V extends VariantType> = (
  variantProps: VariantKV<V>,
) => string;

/**
 * Takes VariantType, and returns a type that represents the preset object.
 *
 * @example
 * ```
 * const presets: PresetType<VariantType> = {
 *   preset1: {
 *     opened: true,
 *     size: "sm",
 *     color: "red",
 *   },
 *   preset2: {
 *     opened: false,
 *     size: "md",
 *     color: "green",
 *     className: "transition-opacity",
 *   },
 * }
 * ```
 */
type PresetType<V extends VariantType> = {
  [PresetName: string]: Partial<VariantKV<V>> & { className?: string };
};

/**
 * A utility function to provide variants and presets to the component
 *
 * @param param - Variant Configuration
 * @returns function (variantProps) -> class name,
 * @returns function (anyProps) -> [variantProps, otherProps]
 */
export function vcn<V extends VariantType>(param: {
  /**
   * First definition: without presets
   */
  base?: string | undefined;
  variants: V;
  dynamics?: DynamicClassName<V>[];
  defaults: VariantKV<V>;
  presets?: undefined;
}): [
  /**
   * Variant Props -> Class Name
   */
  (
    variantProps: Partial<VariantKV<V>> & {
      className?: string;
    },
  ) => string,
  /**
   * Any Props -> Variant Props, Other Props
   */
  // biome-ignore lint/suspicious/noExplicitAny: using unknown causes error `Index signature for type 'string' is missing in type --Props`.
  <AnyPropBeforeResolve extends Record<string, any>>(
    anyProps: AnyPropBeforeResolve,
  ) => [
    Partial<VariantKV<V>> & {
      className?: string;
    },
    Omit<AnyPropBeforeResolve, keyof Partial<VariantKV<V>> | "className">,
  ],
];
export function vcn<V extends VariantType, P extends PresetType<V>>(param: {
  /**
   * Second definition: with presets
   */
  base?: string | undefined;
  variants: V /* VariantType */;
  dynamics?: DynamicClassName<V>[];
  defaults: VariantKV<V>;
  presets: P;
}): [
  /**
   * Variant Props -> Class Name
   */
  (
    variantProps: Partial<VariantKV<V>> & {
      className?: string;
      preset?: keyof P;
    },
  ) => string,
  /**
   * Any Props -> Variant Props, Other Props
   */
  // biome-ignore lint/suspicious/noExplicitAny: using unknown causes error `Index signature for type 'string' is missing in type --Props`.
  <AnyPropBeforeResolve extends Record<string, any>>(
    anyProps: AnyPropBeforeResolve,
  ) => [
    Partial<VariantKV<V>> & {
      className?: string;
      preset?: keyof P;
    },
    Omit<
      AnyPropBeforeResolve,
      keyof Partial<VariantKV<V>> | "preset" | "className"
    >,
  ],
];
export function vcn<
  V extends VariantType,
  P extends PresetType<V> | undefined,
>({
  base,
  variants,
  dynamics = [],
  defaults,
  presets,
}: {
  base?: string | undefined;
  variants: V;
  dynamics?: DynamicClassName<V>[];
  defaults: VariantKV<V>;
  presets?: P;
}) {
  /**
   * --Internal utility function--
   * After transforming props to final version (which means "after overriding default, preset, and variant props sent via component props")
   * It turns final version of variant props to className
   */
  function __transformer__(
    final: VariantKV<V>,
    dynamics: string[],
    propClassName?: string,
  ): string {
    const classNames: string[] = [];

    for (const [variantName, variantKey] of Object.entries(
      final,
    ) as VariantKVEntry<V>) {
      classNames.push(variants[variantName][variantKey.toString()]);
    }

    return twMerge(base, ...classNames, ...dynamics, propClassName);
  }

  return [
    /**
     * Takes any props (including className), and returns the class name.
     * If there is no variant specified in props, then it will fallback to preset, and then default.
     *
     *
     * Process priority of variant will be:
     *
     * --- Processed as string
     * 1. Base
     *
     * --- Processed as object (it will ignore rest of "not duplicated classname" in lower priority)
     * 2. Default
     * 3. Preset (overriding default)
     * 4. Variant props via component (overriding preset)
     *
     * --- Processed as string
     * 5. Dynamic classNames using variant props
     * 6. User's className (overriding dynamic)
     *
     *
     * @param variantProps - The variant props including className.
     * @returns The class name.
     */
    (
      variantProps: { className?: string; preset?: keyof P } & Partial<
        VariantKV<V>
      >,
    ) => {
      const { className, preset, ..._otherVariantProps } = variantProps;

      // Omit<Partial<VariantKV<V>> & { className; preset; }, className | preset> = Partial<VariantKV<V>> (safe to cast)
      // We all know `keyof V` = string, right? (but typescript says it's not, so.. attacking typescript with unknown lol)
      const otherVariantProps = _otherVariantProps as unknown as Partial<
        VariantKV<V>
      >;

      const kv: VariantKV<V> = { ...defaults };

      // Preset Processing
      if (presets && preset && preset in presets) {
        for (const [variantName, variantKey] of Object.entries(
          // typescript bug (casting to NonNullable<P> required)
          (presets as NonNullable<P>)[preset],
        ) as VariantKVEntry<V>) {
          kv[variantName] = variantKey;
        }
      }

      // VariantProps Processing
      for (const [variantName, variantKey] of Object.entries(
        otherVariantProps,
      ) as VariantKVEntry<V>) {
        kv[variantName] = variantKey;
      }

      // make dynamics result
      const dynamicClasses: string[] = [];
      for (const dynamicFunction of dynamics) {
        dynamicClasses.push(dynamicFunction(kv));
      }

      return __transformer__(kv, dynamicClasses, className);
    },

    /**
     * Takes any props, parse variant props and other props.
     * If `options.excludeA` is true, then it will parse `A` as "other" props.
     * Otherwise, it will parse A as variant props.
     *
     * @param anyProps - Any props that have passed to the component.
     * @returns [variantProps, otherProps]
     */
    <AnyPropBeforeResolve extends Record<string, unknown>>(
      anyProps: AnyPropBeforeResolve,
    ) => {
      const variantKeys = Object.keys(variants) as (keyof V)[];

      return Object.entries(anyProps).reduce(
        ([variantProps, otherProps], [key, value]) => {
          if (
            variantKeys.includes(key) ||
            key === "className" ||
            key === "preset"
          ) {
            return [{ ...variantProps, [key]: value }, otherProps];
          }
          return [variantProps, { ...otherProps, [key]: value }];
        },
        [{}, {}],
      ) as [
        Partial<VariantKV<V>> & {
          className?: string;
          preset?: keyof P;
        },
        Omit<
          typeof anyProps,
          keyof Partial<VariantKV<V>> | "preset" | "className"
        >,
      ];
    },
  ];
}

/**
 * Extract the props type from return value of `vcn` function.
 *
 * @example
 * ```
 * const [variantProps, otherProps] = vcn({ ... })
 * interface Props
 *   extends VariantProps<typeof variantProps>, OtherProps { ... }
 *
 * function Component(props: Props) {
 *   ...
 * }
 * ```
 */
export type VariantProps<F extends (props: Record<string, unknown>) => string> =
  F extends (props: infer P) => string ? { [key in keyof P]: P[key] } : never;