import { useFormikContext } from 'formik';
import {
  KeyboardEvent,
  KeyboardEventHandler,
  MouseEvent,
  MouseEventHandler,
  useCallback,
  useRef,
} from 'react';

import { Button, ButtonProps } from '../../components/Button/Button';
import { assertEmptyObject } from '../../utils/assertEmptyObject';
import { makeTestId } from '../../utils/makeTestId';

import { TunedLoader } from './styled';

export type FormikSubmitButtonProps = Omit<
  ButtonProps,
  'id' | 'type' | 'ariaLabel' | 'ariaErrorMessage' | 'ariaInvalid' | 'onMouseDown' | 'onKeyDown' | 'onClick'
> & {
  onClick?(event: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>): void;
  initiallyEnabled?: boolean;
};

/**
 * Button that support {@link Formik} submitting and validation behavior.
 *
 * Should be used inside {@link Formik}.
 *
 * @example
 * <Formik>
 *   // Some content
 *   <FormikSubmitButton>Save</FormikSubmitButton>
 * </Formik>
 */
export function FormikSubmitButton(props: FormikSubmitButtonProps) {
  const {
    ariaDescribedBy,
    children,
    icon,
    disabled,
    appearance,
    className,
    name,
    value,
    onClick,
    testId,
    initiallyEnabled,
    ...rest
  } = props;
  assertEmptyObject(rest);

  const submitCountRef = useRef(0);

  const { submitCount, handleSubmit, isValid, isSubmitting, dirty } = useFormikContext();

  /* A form can have several submit buttons, and the loading indicator should be shown only for one of them */
  const isProcessing = submitCountRef.current === submitCount && isSubmitting;

  const isDisabled = disabled || isSubmitting || !isValid || (!initiallyEnabled && !dirty);

  /**
   * When user has focus to some field they can click by SubmitButton, then
   * `onBlur` field event will be invoked and some error bellows the field can appear.
   * This error can move the SubmitButton down and then mouse cursor can be outside the button.
   * It will lead to `onClick` event will be triggered not on the SubmitButton.
   * And submitting will not invoke.
   * That's why we add custom onClick handling
   */
  const handleClick = useCallback(
    (event: MouseEvent<HTMLButtonElement> | KeyboardEvent<HTMLButtonElement>) => {
      onClick?.(event);
      if (!event.isDefaultPrevented()) {
        event.preventDefault();
        submitCountRef.current = submitCount + 1;
        handleSubmit();
      }
    },
    [onClick, handleSubmit, submitCount],
  );

  const handleMouseDown = useCallback<MouseEventHandler<HTMLButtonElement>>(
    (event) => {
      handleClick(event);
    },
    [handleClick],
  );

  const handleKeyDown = useCallback<KeyboardEventHandler<HTMLButtonElement>>(
    (event) => {
      if (event.key === ' ' || event.key === 'Enter') {
        handleClick(event);
      }
    },
    [handleClick],
  );

  const handleNativeClick = useCallback<MouseEventHandler<HTMLButtonElement>>((event) => {
    // prevent submit by button with type "submit"
    event.preventDefault();
  }, []);

  /**
   * When user has focus to some field they can click by SubmitButton, then
   * `onBlur` field event will be invoked and some error bellows the field can appear.
   * This error can move the SubmitButton down and then mouse cursor can be outside the button.
   * It will lead to `onClick` event will be triggered not on the SubmitButton.
   * And submitting will not invoke.
   * That's why we add submitting by onMouseDown event
   */
  return (
    <Button
      appearance={appearance}
      ariaDescribedBy={ariaDescribedBy}
      className={className}
      disabled={isDisabled}
      icon={isProcessing ? undefined : icon}
      name={name}
      onClick={handleNativeClick}
      onKeyDown={handleKeyDown}
      onMouseDown={handleMouseDown}
      testId={testId}
      type="submit"
      value={value}
    >
      {isProcessing && <TunedLoader testId={makeTestId(testId, 'loader')} />}
      {children}
    </Button>
  );
}
