import {
  ComponentPropsWithoutRef,
  FC,
  forwardRef,
  ReactElement,
  ReactNode,
  Ref,
} from "react";
import { useCallbackRef, useMergeRefs } from "use-callback-ref";
import { twJoin } from "tailwind-merge";

import { Icon, type IconProps } from "../icon/Icon";
import { InputLabel } from "../inputLabel/InputLabel";
import { ErrorIcon } from "./ErrorIcon";
import { RequiredIndicator } from "./RequiredIndicator";
import { useTrackedValue } from "./useTrackedValue";
import { InputCounter } from "./InputCounter";
import { InputButton } from "./InputButton";
import { ClearButton } from "./ClearButton";
import { fieldClasses, inputClasses } from "./Input.styles";
import { twMerge } from "../../../tailwind.config";

export interface InputProps extends ComponentPropsWithoutRef<"input"> {
  /**
   * A string or a custom label describing this field.
   *
   * Use `Input.Label` to access the original styled label.
   *
   * Note: if you're using a custom label, it is your responsibility
   * to pass the `htmlFor={id}` prop to it with the ID that matches
   * the one passed to the `<Input />` itself.
   */
  label?: string | ReactNode;
  /** @default "40" */
  height?: "32" | "40";
  /** @default "outlined" */
  variant?: "secondary" | "outlined";
  /** Whether the field should display an error state. */
  error?: boolean;
  /** A tooltip to explain the error. */
  errorText?: string;
  fullWidth?: boolean;
  leftIcon?: ReactElement | IconProps["name"];
  rightIcon?: ReactElement | IconProps["name"];
  maxLength?: number;
  ref?: Ref<HTMLInputElement>;
  clearButton?: boolean;
  onClear?: () => void;
  ESCAPE_className?: string;
}

export type InputComponent = FC<InputProps> & {
  Button: typeof InputButton;
  Label: typeof InputLabel;
};

// False positive because of the type assertion
// eslint-disable-next-line react/display-name
export const Input = forwardRef<HTMLInputElement, InputProps>(
  (
    {
      height = "40",
      id,
      variant = "outlined",
      label,
      error = false,
      required = false,
      fullWidth = false,
      errorText,
      leftIcon,
      rightIcon,
      maxLength,
      disabled = false,
      clearButton = true,
      onChange,
      onClear,
      ESCAPE_className,
      ...restProps
    },
    ref
  ) => {
    const [value, takeValueSnapshot, handleChange] = useTrackedValue(onChange);
    const localRef = useCallbackRef<HTMLInputElement>(null, takeValueSnapshot);
    const mergedRef = useMergeRefs([localRef, ref]);

    return (
      <div
        className={twMerge(
          fullWidth ? "flex" : "inline-flex",
          "flex-col",
          ESCAPE_className
        )}
      >
        {typeof label === "string" ? (
          <InputLabel htmlFor={id}>{label}</InputLabel>
        ) : (
          label
        )}
        <div
          className={inputClasses({
            variant,
            height,
            disabled,
            error,
          })}
        >
          {leftIcon !== undefined && (
            <InputIcon icon={leftIcon} disabled={disabled} />
          )}
          <input
            ref={mergedRef}
            id={id}
            onChange={handleChange}
            disabled={disabled}
            className={fieldClasses({ variant })}
            maxLength={maxLength}
            {...restProps}
          />
          {clearButton && !disabled && (
            <ClearButton
              hidden={value === ""}
              inputRef={localRef}
              onChange={handleChange}
              onClear={onClear}
            />
          )}
          {!error && rightIcon !== undefined && (
            <InputIcon icon={rightIcon} disabled={disabled} />
          )}
          {error && <ErrorIcon errorText={errorText} />}
          {required && <RequiredIndicator />}
          {maxLength !== undefined ? (
            <InputCounter maxLength={maxLength} value={value} />
          ) : null}
        </div>
      </div>
    );
  }
  // ↓ Required to assign `InputButton`
) as unknown as InputComponent;

Input.displayName = "Input";

Input.Button = InputButton;
Input.Label = InputLabel;

export const InputIcon = ({
  icon,
  disabled,
}: {
  icon: ReactElement | IconProps["name"];
  disabled: boolean;
}) =>
  typeof icon === "string" ? (
    <div className={twJoin("flex", disabled ? "text-N60" : "text-N100")}>
      <Icon name={icon} size="20" weight="medium" />
    </div>
  ) : (
    icon
  );
