import { Input } from 'antd';
import type { InputProps, InputRef } from 'antd';
import type { FocusEvent, KeyboardEvent } from 'react';
import { forwardRef, useEffect, useState } from 'react';
import type { InputCommitPlugin, InputCommitTiming } from './types';

type AntdInputChangeEvent = Parameters<NonNullable<InputProps['onChange']>>[0];

export type InputUIProps = Omit<InputProps, 'onChange'> & {
  onChange?: InputProps['onChange'];
  commitPlugins?: ReadonlyArray<InputCommitPlugin>;
};

function normalizeValue(value: InputProps['value'] | InputProps['defaultValue']) {
  if (value === undefined || value === null) {
    return '';
  }

  return String(value);
}

function toCommittedChangeEvent(
  event: KeyboardEvent<HTMLInputElement> | FocusEvent<HTMLInputElement>,
): AntdInputChangeEvent {
  return {
    ...event,
    target: event.currentTarget,
    currentTarget: event.currentTarget,
  } as AntdInputChangeEvent;
}

export const InputUI = forwardRef<InputRef, InputUIProps>(function InputUI(
  { value, defaultValue, onChange, onBlur, onPressEnter, commitPlugins = [], ...restProps },
  ref,
) {
  const [draftValue, setDraftValue] = useState(() => normalizeValue(value ?? defaultValue));
  const [committedValue, setCommittedValue] = useState(() => normalizeValue(value ?? defaultValue));

  useEffect(() => {
    if (value !== undefined) {
      const normalizedValue = normalizeValue(value);

      setDraftValue(normalizedValue);
      setCommittedValue(normalizedValue);
    }
  }, [value]);

  const tryCommit = (
    event: KeyboardEvent<HTMLInputElement> | FocusEvent<HTMLInputElement>,
    timing: InputCommitTiming,
  ) => {
    const nextValue = event.currentTarget.value;
    const isValid = commitPlugins.every((plugin) =>
      plugin({
        nextValue,
        previousValue: committedValue,
        timing,
      }),
    );

    if (!isValid) {
      setDraftValue(committedValue);
      return;
    }

    setCommittedValue(nextValue);
    onChange?.(toCommittedChangeEvent(event));
  };

  return (
    <Input
      {...restProps}
      ref={ref}
      value={draftValue}
      onChange={(event) => {
        setDraftValue(event.target.value);
      }}
      onPressEnter={(event) => {
        tryCommit(event, 'press-enter');
        onPressEnter?.(event);
      }}
      onBlur={(event) => {
        tryCommit(event, 'blur');
        onBlur?.(event);
      }}
    />
  );
});
