/* eslint-disable react/jsx-props-no-spreading */
import FocusTrap from 'focus-trap-react';
import { AriaRole, KeyboardEventHandler, ReactNode, useCallback, useEffect, useRef, useState } from 'react';
import { usePopper } from 'react-popper';
import { CSSTransition } from 'react-transition-group';

import { useTestIdAttribute } from '../../hooks/useTestIdAttribute';
import { TestIdProps } from '../../types';
import { assertEmptyObject } from '../../utils/assertEmptyObject';
import { Portal } from '../Portal/Portal';

import { StyledContainer, StyledPopper, animationDuration } from './styled';

/** Props for {@link Dropdown } */
export interface DropdownProps extends TestIdProps {
  /**
   * HTML id attribute that will be set to {@link Dropdown} container.
   *
   * This props should be REQUIRED because it should be used in `aria-controls`.
   */
  id: string;
  /** HTML role attribute that will be set to {@link Dropdown} container. */
  role?: AriaRole;
  /**
   * If `true` then {@link Dropdown} will be visible.
   *
   * Pay attention that {@link Dropdown} will be hidden with animation, that's
   * mean that if this property will be changed to `false` dropdown content will
   * be hidden after some delay.
   */
  visible: boolean;
  /**
   * Element that used as {@link Dropdown} trigger.
   *
   * It's important to know what element used as trigger to correct process
   * clicks event outside {@link Dropdown}. If we do not know trigger, and
   * user clicks by trigger, than {@link Dropdown} will be hidden and shown
   * immediately.
   */
  trigger: HTMLElement | null;
  /**
   * Options that will be passed to {@link usePopper} hook.
   *
   * Strategy should be always `fixed` to be sure that {@link Dropdown} works
   * well in popups.
   */
  popperOptions: Omit<NonNullable<Parameters<typeof usePopper>[2]>, 'strategy'>;
  /** Handler that should be run when user press ESC button. Usually it should hide {@link Dropdown} */
  onEscButtonPress(): void;
  /**
   * Handler that should be run when user click somewhere outside {@link Dropdown}.
   * Usually it should hide {@link Dropdown}
   */
  onOutsideClick(event: MouseEvent): void;
  children: ReactNode;
  focusTrapOptions?: FocusTrap.Props['focusTrapOptions'];
}

export function Dropdown(props: DropdownProps) {
  const {
    id,
    role,
    visible,
    trigger,
    onEscButtonPress,
    onOutsideClick,
    children,
    popperOptions,
    testId,
    focusTrapOptions,
    ...rest
  } = props;
  assertEmptyObject(rest);

  const testIdAttribute = useTestIdAttribute();
  const [popperElement, setPopperElement] = useState<HTMLElement | null>(null);
  const transitionNodeRef = useRef(null);
  const [isFocusTrapActive, setFocusTrapActive] = useState(false);

  const { styles, attributes, update } = usePopper(trigger, popperElement, {
    ...popperOptions,
    strategy: 'fixed',
  });

  useEffect(() => {
    if (visible && update) {
      update();
    }
  }, [visible, update]);

  // By the same reason Popper doesn't want to update their position when trigger element size changes.
  // Force to do it.
  useEffect(() => {
    const resizeObserver = new ResizeObserver(() => {
      /* istanbul ignore next */
      if (update) {
        update();
      }
    });

    if (trigger && update) {
      resizeObserver.observe(trigger);
    }

    return () => resizeObserver.disconnect();
  }, [trigger, update]);

  useEffect(() => {
    if (!visible) {
      return;
    }

    const handleOnClick = (event: MouseEvent) => {
      const target = event.composedPath()[0] as Node;

      if (trigger && trigger.contains(target)) {
        return;
      }

      if (popperElement && !popperElement.contains(target) && onOutsideClick) {
        onOutsideClick(event);
      }
    };

    document.addEventListener('click', handleOnClick, true);

    return () => {
      document.removeEventListener('click', handleOnClick, true);
    };
  }, [onEscButtonPress, onOutsideClick, popperElement, trigger, visible]);

  const handleOnKeyDown = useCallback<KeyboardEventHandler<HTMLDivElement>>(
    (event) => {
      if (event.key === 'Escape' && onEscButtonPress) {
        event.stopPropagation();
        onEscButtonPress();
      }
    },
    [onEscButtonPress],
  );

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

  return (
    <Portal>
      <CSSTransition
        in={visible}
        mountOnEnter
        nodeRef={transitionNodeRef}
        onEntered={handleOnEntered}
        onExit={handleOnExit}
        timeout={animationDuration}
        unmountOnExit
      >
        <FocusTrap active={isFocusTrapActive} focusTrapOptions={focusTrapOptions}>
          <StyledPopper
            ref={setPopperElement}
            id={id}
            role={role}
            style={styles.popper}
            {...attributes.popper}
            {...{ [testIdAttribute]: testId }}
          >
            <StyledContainer ref={transitionNodeRef} onKeyDown={handleOnKeyDown}>
              {children}
            </StyledContainer>
          </StyledPopper>
        </FocusTrap>
      </CSSTransition>
    </Portal>
  );
}
