import * as React from 'react';
import { TextControl } from '@tesseract-design/web-base';
import clsx from 'clsx';
import plugin from 'tailwindcss/plugin';
import { useBrowser, useClientSide, useFallbackId } from '@modal-sh/react-utils';

/**
 * Derived HTML element of the {@link NumberSpinner} component.
 */
export type NumberSpinnerDerivedElement = HTMLInputElement;

/**
 * Props of the {@link NumberSpinner} component.
 */
export interface NumberSpinnerProps extends Omit<React.HTMLProps<NumberSpinnerDerivedElement>, 'size' | 'type' | 'label'> {
  /**
   * Short textual description indicating the nature of the component's value.
   */
  label?: React.ReactNode,
  /**
   * Short textual description as guidelines for valid input values.
   */
  hint?: React.ReactNode,
  /**
   * Size of the component.
   */
  size?: TextControl.Size,
  /**
   * Should the component display a border?
   */
  border?: boolean,
  /**
   * Should the component occupy the whole width of its parent?
   */
  block?: boolean,
  /**
   * Style of the component.
   */
  variant?: TextControl.Variant,
  /**
   * Is the label hidden?
   */
  hiddenLabel?: boolean,
  /**
   * Visual length of the input.
   */
  length?: number,
  /**
   * Should the component be enhanced?
   */
  enhanced?: boolean,
  /**
   * Interval between steps in milliseconds.
   */
  stepInterval?: number,
  /**
   * Delay before the first step in milliseconds.
   */
  initialStepDelay?: number,
}

export const numberSpinnerPlugin = plugin(({ addComponents, }) => {
  addComponents({
    '.number-spinner': {
      '&[data-browser="chrome"] > input::-webkit-inner-spin-button': {
        'position': 'absolute',
        'top': '0',
        'right': '0',
        'height': '100%',
        'width': '1.5rem',
        'z-index': '2',
      },

      '&[data-browser="chrome"][data-enhanced] > input::-webkit-inner-spin-button': {
        'display': 'none',
      },

      '&[data-browser="firefox"][data-enhanced] > input[type="number"]': {
        'appearance': 'textfield',
      },
    },
  });
});

/**
 * Component for inputting discrete numeric values.
 */
export const NumberSpinner = React.forwardRef<NumberSpinnerDerivedElement, NumberSpinnerProps>((
  {
    label,
    hint,
    size = 'medium' as const,
    border = false as const,
    block = false as const,
    variant = 'default' as const,
    hiddenLabel = false as const,
    className,
    id: idProp,
    style,
    length,
    enhanced: enhancedProp = false as const,
    stepInterval = 100 as const,
    initialStepDelay = 400 as const,
    ...etcProps
  }: NumberSpinnerProps,
  forwardedRef,
) => {
  const { clientSide: indicator } = useClientSide({ clientSide: enhancedProp });
  const labelId = React.useId();
  const id = useFallbackId(idProp);
  const browser = useBrowser();
  const defaultRef = React.useRef<NumberSpinnerDerivedElement>(null);
  const ref = forwardedRef ?? defaultRef;
  const intervalRef = React.useRef<number | undefined>();
  const clickYRef = React.useRef<number>();
  const spinnerYRef = React.useRef<number>();
  const spinEventSource = React.useRef<'mouse' | 'keyboard'>();
  const [displayStep, setDisplayStep] = React.useState<boolean>();

  const performStepMouse = (
    input: NumberSpinnerDerivedElement,
    theStepUpMode?: boolean,
  ) => {
    if (typeof theStepUpMode !== 'boolean') {
      return;
    }
    const current = input;
    const theStep = current.step ?? 'any';
    current.step = theStep === 'any' ? '1' : theStep;
    if (theStepUpMode) {
      current.stepUp();
    } else {
      current.stepDown();
    }
    current.step = theStep;
    current.focus();
  };

  const windowMouseMove = (e: MouseEvent) => {
    if (spinEventSource.current !== 'mouse') {
      return;
    }
    clickYRef.current = e.pageY;
  };

  const checkMouseStepUpMode = () => {
    if (typeof clickYRef.current !== 'number' || typeof spinnerYRef.current !== 'number') {
      return undefined;
    }
    return clickYRef.current < spinnerYRef.current;
  };

  const doStepMouse: React.MouseEventHandler<HTMLButtonElement> = (e) => {
    if (spinEventSource.current === 'keyboard') {
      return;
    }
    const { current } = typeof ref === 'object' ? ref : defaultRef;
    if (!current) {
      return;
    }
    spinEventSource.current = 'mouse';
    const { top, bottom } = e.currentTarget.getBoundingClientRect();
    const { pageY } = e;
    spinnerYRef.current = top + ((bottom - top) / 2);
    clickYRef.current = pageY;
    window.addEventListener('mousemove', windowMouseMove);
    setTimeout(() => {
      clearInterval(intervalRef.current);
      const stepUpMode = checkMouseStepUpMode();
      setDisplayStep(stepUpMode);
      performStepMouse(current, stepUpMode);
      intervalRef.current = window.setTimeout(() => {
        const stepUpMode = checkMouseStepUpMode();
        setDisplayStep(stepUpMode);
        performStepMouse(current, stepUpMode);
        intervalRef.current = window.setInterval(() => {
          const stepUpMode = checkMouseStepUpMode();
          setDisplayStep(stepUpMode);
          performStepMouse(current, stepUpMode);
        }, stepInterval);
      }, initialStepDelay);
    });
  };

  const doStepKeyboard: React.KeyboardEventHandler<NumberSpinnerDerivedElement> = (e) => {
    if (spinEventSource.current === 'mouse') {
      return;
    }
    if (!(e.code === 'ArrowUp' || e.code === 'ArrowDown')) {
      return;
    }
    e.preventDefault();
    spinEventSource.current = 'keyboard';
    const current = e.currentTarget;
    const theStepUpMode = e.code === 'ArrowUp';
    setDisplayStep(theStepUpMode);
    setTimeout(() => {
      clearInterval(intervalRef.current);
      performStepMouse(current, theStepUpMode);
      intervalRef.current = window.setTimeout(() => {
        performStepMouse(current, theStepUpMode);
        intervalRef.current = window.setInterval(() => {
          performStepMouse(current, theStepUpMode);
        }, stepInterval);
      }, initialStepDelay);
    });
  };

  React.useEffect(() => {
    const stopStepMouse = () => {
      if (spinEventSource.current === 'keyboard') {
        return;
      }
      clearInterval(intervalRef.current);
      clickYRef.current = undefined;
      spinnerYRef.current = undefined;
      window.removeEventListener('mousemove', windowMouseMove);
      spinEventSource.current = undefined;
      setDisplayStep(undefined);
    };

    const stopStepKeyboard = (e: KeyboardEvent) => {
      if (spinEventSource.current === 'mouse') {
        return;
      }
      if (!(e.code === 'ArrowUp' || e.code === 'ArrowDown')) {
        return;
      }
      clearInterval(intervalRef.current);
      spinEventSource.current = undefined;
      setDisplayStep(undefined);
    };

    window.addEventListener('mouseup', stopStepMouse, { capture: true });
    window.addEventListener('keyup', stopStepKeyboard, { capture: true });
    return () => {
      window.removeEventListener('mouseup', stopStepMouse, { capture: true });
      window.addEventListener('keyup', stopStepKeyboard, { capture: true });
    };
  }, []);

  return (
    <div
      className={clsx(
        'relative rounded ring-secondary/50 group has-[:disabled]:opacity-50',
        'focus-within:ring-4',
        'number-spinner',
        {
          'block': block,
          'inline-block align-middle': !block,
        },
        className,
      )}
      style={style}
      data-testid="base"
      data-browser={browser}
      data-enhanced={indicator}
    >
      {label && (
        <>
          <label
            data-testid="label"
            id={labelId}
            htmlFor={id}
            className={clsx(
              'absolute z-[1] w-full top-0.5 left-0 pointer-events-none pl-1 text-xxs font-bold group-focus-within:text-secondary text-primary leading-none bg-negative select-none',
              {
                'sr-only': hiddenLabel,
              },
              {
                'pr-1': !indicator,
              },
              {
                'pr-10': indicator && size === 'small',
                'pr-12': indicator && size === 'medium',
                'pr-16': indicator && size === 'large',
              },
            )}
          >
            <span className="block w-full whitespace-nowrap h-[1.1em] overflow-hidden text-ellipsis">
              {label}
            </span>
          </label>
          {' '}
        </>
      )}
      <input
        {...etcProps}
        size={length}
        ref={typeof ref === 'function' ? defaultRef : ref}
        id={id}
        aria-labelledby={labelId}
        type="number"
        data-testid="input"
        onKeyDown={doStepKeyboard}
        className={clsx(
          'bg-negative rounded-inherit w-full peer block tabular-nums font-inherit relative',
          'focus:outline-0',
          'disabled:opacity-50 disabled:cursor-not-allowed',
          {
            'text-xxs': size === 'small',
            'text-xs': size === 'medium',
          },
          {
            'pl-4': variant === 'default',
            'pl-1.5': variant === 'alternate',
          },
          {
            'pt-4': variant === 'alternate',
          },
          {
            'pr-4': variant === 'default' && !indicator,
            'pr-1.5': variant === 'alternate' && !indicator,
          },
          {
            'pr-10': indicator && size === 'small',
            'pr-12': indicator && size === 'medium',
            'pr-16': indicator && size === 'large',
          },
          {
            'h-10': size === 'small',
            'h-12': size === 'medium',
            'h-16': size === 'large',
          },
        )}
      />
      {hint && (
        <div
          data-testid="hint"
          className={clsx(
            'absolute left-0 px-1 pointer-events-none text-xxs leading-none w-full bg-negative select-none text-primary',
            {
              'bottom-0 pl-4 pb-1': variant === 'default',
              'top-0.5': variant === 'alternate',
            },
            {
              'pt-2': variant === 'alternate' && size === 'small',
              'pt-3': variant === 'alternate' && size !== 'small',
            },
            {
              'pr-4': !indicator && variant === 'default',
              'pr-1': !indicator && variant === 'alternate',
            },
            {
              'pr-10': indicator && size === 'small',
              'pr-12': indicator && size === 'medium',
              'pr-16': indicator && size === 'large',
            },
          )}
        >
          <div
            className="opacity-50 whitespace-nowrap w-full h-[1.1em] overflow-hidden text-ellipsis"
          >
            {hint}
          </div>
        </div>
      )}
      {indicator && (
        <button
          data-testid="indicator"
          type="button"
          className={clsx(
            'text-center z-[1] flex flex-col items-stretch justify-center aspect-square absolute bottom-0 right-0 select-none text-primary group-focus-within:text-secondary',
            {
              'w-10': size === 'small',
              'w-12': size === 'medium',
              'w-16': size === 'large',
            },
          )}
          onMouseDown={doStepMouse}
        >
          <span
            className={clsx(
              'h-0 flex-auto flex justify-center items-end overflow-hidden',
              {
                'text-tertiary': displayStep === true,
                'text-inherit': displayStep !== true,
              },
            )}
          >
            <svg
              className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round"
              viewBox="0 0 24 24"
              role="presentation"
            >
              <polyline points="18 15 12 9 6 15" />
            </svg>
            <span className="sr-only">
              Step Up
            </span>
          </span>
          <span className="sr-only">
            /
          </span>
          <span
            className={clsx(
              'h-0 flex-auto flex justify-center items-end overflow-hidden',
              {
                'text-tertiary': displayStep === false,
                'text-inherit': displayStep !== false,
              },
            )}
          >
            <svg
              className="w-6 h-6 fill-none stroke-current stroke-2 linejoin-round linecap-round"
              viewBox="0 0 24 24"
              role="presentation"
            >
              <polyline points="6 9 12 15 18 9" />
            </svg>
            <span className="sr-only">
              Step Down
            </span>
          </span>
        </button>
      )}
      {border && (
        <span
          data-testid="border"
          className="absolute z-[1] inset-0 rounded-inherit border-2 border-primary pointer-events-none group-focus-within:border-secondary"
        />
      )}
    </div>
  );
});

NumberSpinner.displayName = 'NumberSpinner';

NumberSpinner.defaultProps = {
  label: undefined,
  hint: undefined,
  border: false as const,
  block: false as const,
  hiddenLabel: false as const,
  size: 'medium' as const,
  variant: 'default' as const,
  length: undefined,
  enhanced: false as const,
  stepInterval: 100 as const,
  initialStepDelay: 400 as const,
};
