import FocusTrap from 'focus-trap-react';
import {
  KeyboardEventHandler,
  MouseEventHandler,
  ReactNode,
  useCallback,
  useMemo,
  useRef,
  useState,
} from 'react';
import { CSSTransition } from 'react-transition-group';

import { Portal } from '../../components/Portal/Portal';
import { useId } from '../../hooks/useId';
import { useTestIdAttribute } from '../../hooks/useTestIdAttribute';
import { CommonProps } from '../../types';
import { assertEmptyObject } from '../../utils/assertEmptyObject';
import { makeTestId } from '../../utils/makeTestId';

import { ModalDialogSize } from './constants';
import { ModalDialogContextProvider } from './contexts/ModalDialogContext';
import {
  StyledModalBackdrop,
  StyledModalDialog,
  hideAnimationDuration,
  showAnimationDuration,
} from './styled';

/** Props for {@link ModalDialog} */
export interface ModalDialogProps extends CommonProps {
  /** If `true` then dialog will be visible. */
  visible?: boolean;
  /**
   * Size of dialog
   *
   * @default {@link ModalDialogSize.Default}
   */
  size?: ModalDialogSize;
  /**
   * Invokes when the user presses the ESC key or clicks on a dialog backdrop
   */
  onDismiss(): void;
  children: ReactNode;
}

/**
 * Component that show common modal dialog with scrollable content.
 *
 * ```tsx
 * <ModalDialog visible={visible} onDismiss={() => setVisible(false)}>
 *   <ModalHeader>
 *     <ModalTitle>Title</ModalTitle>
 *     <ModalSubTitle>Subtitle</ModalSubTitle>
 *   </ModalHeader>
 *   <ModalBody>
 *     Some content
 *   </ModalBody>
 *   <ModalFooter>
 *     <ModalFooterButtonGroup>
 *       <Button appearance={ButtonAppearance.Subtle}>Left Button</Button>
 *     </ModalFooterButtonGroup>
 *     <ModalFooterButtonGroup>
 *       <Button>Close</Button>
 *       <Button appearance={ButtonAppearance.Primary}>Ok</Button>
 *     </ModalFooterButtonGroup>
 *   </ModalFooter>
 * </ModalDialog>
 * ```
 *
 * ### Dialog closing
 *
 * The {@link ModalDialog} has `onDismiss` callback that invokes when the user presses the ESC key or clicks on a dialog backdrop.
 *
 * According to [accessibility recommendations](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/), a dialog should be closed by the ESC keypress.
 * Therefore in most cases, you should define this logic in your code.
 *
 * ```tsx
 * const [visible, setVisible] = useState(false);
 * return <ModalDialog visible={visible} onDismiss={() => setVisible(false)}></ModalDialog>
 * ```
 *
 * If you want to forbid closing a dialog and do it, for example, only when a form inside the dialog was submitted, you can use a stub for `onDismiss.`
 *
 * ```tsx
 * const [visible, setVisible] = useState(false);
 * return <ModalDialog visible={visible} onDismiss={() => {}}></ModalDialog>
 * ```
 */
export function ModalDialog(props: ModalDialogProps) {
  const { visible, size, onDismiss, children, className, testId, ariaDescribedBy, ...rest } = props;
  assertEmptyObject(rest);

  const testIdAttribute = useTestIdAttribute();
  const backdropRef = useRef<HTMLDivElement | null>(null);
  const id = useId();
  const [isFocusTrapActive, setFocusTrapActive] = useState(false);

  const handleBackdropKeyDown = useCallback<KeyboardEventHandler<HTMLDivElement>>(
    (event) => {
      if (event.key === 'Escape') {
        onDismiss();
      }
    },
    [onDismiss],
  );

  const handleBackdropClick = useCallback<MouseEventHandler<HTMLDivElement>>(
    (event) => {
      if (event.target === backdropRef.current) {
        onDismiss();
      }
    },
    [onDismiss],
  );

  const focusTrapOptions = useMemo<FocusTrap.Props['focusTrapOptions']>(() => {
    return {
      fallbackFocus() {
        return backdropRef.current!;
      },
    };
  }, []);

  const handleOnEntered = useCallback(() => setFocusTrapActive(true), []);
  const handleOnExit = useCallback(() => setFocusTrapActive(false), []);

  const timeoutOptions = useMemo(() => {
    return {
      enter: showAnimationDuration,
      exit: hideAnimationDuration,
    };
  }, []);

  return (
    <Portal>
      <CSSTransition
        in={visible}
        mountOnEnter
        nodeRef={backdropRef}
        onEntered={handleOnEntered}
        onExit={handleOnExit}
        timeout={timeoutOptions}
        unmountOnExit
      >
        <ModalDialogContextProvider id={id}>
          <FocusTrap active={isFocusTrapActive} focusTrapOptions={focusTrapOptions}>
            <StyledModalBackdrop
              ref={backdropRef}
              className={className}
              onClick={handleBackdropClick}
              onKeyDown={handleBackdropKeyDown}
              tabIndex={-1}
              {...{ [testIdAttribute]: testId }}
            >
              <StyledModalDialog
                $size={size ?? ModalDialogSize.Default}
                aria-describedby={ariaDescribedBy}
                aria-hidden={!visible}
                aria-labelledby={id}
                aria-modal="true"
                role="dialog"
                {...{ [testIdAttribute]: makeTestId(testId, 'dialog') }}
              >
                {children}
              </StyledModalDialog>
            </StyledModalBackdrop>
          </FocusTrap>
        </ModalDialogContextProvider>
      </CSSTransition>
    </Portal>
  );
}
