import React, {
  useCallback,
  useEffect,
  ReactElement,
  ReactNode,
  useState,
  useContext,
} from 'react';
import { usePopper } from 'react-popper';
import { Placement } from '@popperjs/core';
import { ThemeContext } from 'styled-components';
import css from '../../utils/css';

import {
  DropdownWrapper,
  DropdownPopper,
  ContentWrapper,
} from './StyledDropdown';

import { CommonProps } from '../common';
import { useResizeObserver } from '../../utils/hooks';
import assert from '../../utils/assert';

export interface DropdownProps extends CommonProps {
  /**
   * Content of the dropdown, usually a menu.
   */
  content: ReactNode;
  /**
   * Whether the dropdown takes up the entire line.
   */
  display?: 'inline-block' | 'block';
  /**
   * Whether to keep dropdown content mounted when dropdown is closed.
   */
  keepContentMounted?: boolean;
  /**
   * Closing callback.
   */
  onClose: () => void;
  /**
   * Whether the dropdown is open.
   */
  open: boolean;
  /**
   * Placement of dropdown content to the target.
   */
  placement?: 'bottom-left' | 'bottom-right';
  /**
   * Target element that dropdown menu is relative to.
   */
  target: ReactElement;
}

const PLACEMENT_MAP: {
  'bottom-left': 'bottom-start';
  'bottom-right': 'bottom-end';
} = {
  'bottom-left': 'bottom-start',
  'bottom-right': 'bottom-end',
};

const DISPLAY_PROPERTIES = ['inline-block', 'block'];

const FALLBACK_PLACEMENT_MAP: {
  'bottom-left': Placement[];
  'bottom-right': Placement[];
} = {
  'bottom-left': ['bottom-end', 'top-start', 'top-end'],
  'bottom-right': ['bottom-start', 'top-end', 'top-start'],
};

const checkIfDropdownContainTarget = (
  dropdownWrapperElement: HTMLDivElement | null,
  target: Node
): boolean | undefined => {
  if (dropdownWrapperElement === null) {
    return undefined;
  }

  return dropdownWrapperElement.contains(target);
};

const Dropdown = ({
  open,
  content,
  target,
  keepContentMounted = false,
  placement = 'bottom-left',
  display = 'inline-block',
  onClose,
  id,
  className,
  style,
  sx = {},
  'data-test-id': dataTestId,
}: DropdownProps): ReactElement => {
  assert(
    DISPLAY_PROPERTIES.includes(display),
    `[Dropdown] display:${display} isn't among the supported values (block, inline-block)`
  );

  const [wrapperElement, setWrapperElement] = useState<HTMLDivElement | null>(
    null
  );
  const [dropdownElement, setDropdownElement] = useState<HTMLDivElement | null>(
    null
  );
  const [popperUpdateDone, setPopperUpdateDone] = useState<boolean>(false);
  const [dropDownMinWidth, setDropdownMinWidth] = useState<number>();
  const theme = useContext(ThemeContext);
  const isDropdownOpen = open && popperUpdateDone;

  const { styles, attributes, update, forceUpdate } = usePopper(
    wrapperElement,
    dropdownElement,
    {
      placement: PLACEMENT_MAP[placement],
      strategy: 'fixed',
      modifiers: [
        {
          name: 'offset',
          options: {
            offset: [0, theme.space.dropdown.margin],
          },
        },
        {
          name: 'flip',
          options: {
            fallbackPlacements: FALLBACK_PLACEMENT_MAP[placement],
          },
        },
        {
          name: 'computeStyles',
          options: {
            adaptive: false,
            gpuAcceleration: false,
          },
        },
      ],
    }
  );

  const clickOutside = useCallback(
    e => {
      const clickInWrapper = checkIfDropdownContainTarget(
        wrapperElement,
        e.target
      );

      if (clickInWrapper === false) {
        onClose();
      }
    },
    [onClose, wrapperElement]
  );

  const wrapperResizeCallback = useCallback(
    ({ width }) => {
      setDropdownMinWidth(width);
      if (forceUpdate !== null && open === true) forceUpdate();
    },
    [setDropdownMinWidth, forceUpdate, open]
  );

  const dropdownResizeCallback = useCallback(() => {
    if (forceUpdate !== null && open === true) forceUpdate();
  }, [forceUpdate, open]);

  useEffect(() => {
    if (update !== null && open === true) {
      update().then(state => {
        if (state !== undefined) {
          setPopperUpdateDone(true);
        }
      });
    }
  }, [open, update]);

  useEffect(() => {
    if (open === true) {
      document.addEventListener('click', clickOutside, true);
    }

    return (): void => {
      document.removeEventListener('click', clickOutside, true);
    };
  }, [open, clickOutside]);

  useResizeObserver(wrapperResizeCallback, wrapperElement);
  useResizeObserver(dropdownResizeCallback, dropdownElement);

  return (
    <DropdownWrapper ref={setWrapperElement} themeDisplay={display}>
      {target}
      {(open === true || keepContentMounted === true) && (
        <DropdownPopper
          ref={setDropdownElement}
          style={styles['popper']}
          open={isDropdownOpen}
          {...attributes['popper']}
        >
          <ContentWrapper
            style={{ minWidth: dropDownMinWidth, ...style, ...css(sx) }}
            id={id}
            className={className}
            data-test-id={dataTestId}
          >
            {content}
          </ContentWrapper>
        </DropdownPopper>
      )}
    </DropdownWrapper>
  );
};

export default Dropdown;
