import React, {
  Fragment,
  ReactNode,
  MouseEvent,
  useState,
  useLayoutEffect,
} from "react";
import { useId } from "@reach/auto-id";
import FocusLock from "react-focus-lock";
import { usePopper } from "react-popper-2";
import { animated, useTransition } from "react-spring";
import { easeCubicInOut } from "d3-ease";
import { useDisclosure, useKeyPress, useOutsideClick } from "../../hooks";
import { KEY_CODE, PopperPlacement } from "../../types";
import { Box, BoxProps } from "../Box";
import { Portal } from "../Portal";
import { MenuContext, MenuContextProps, RenderProps } from "./context";

interface TriggerProps {
  id: string;
  onClick: (event: Event | MouseEvent<HTMLButtonElement>) => void;
  "aria-haspopup": "menu";
  "aria-expanded": boolean;
  "aria-controls": string;
  role: string;
  tabIndex: number;
}

interface TriggerState {
  isOpen: boolean;
  onClose: () => void;
}

type TriggerFunction = (props: TriggerProps, state: TriggerState) => ReactNode;
type RenderFunction = (props: RenderProps) => ReactNode;

export interface MenuProps extends BoxProps {
  /**
   * Render function to create the menu button; `triggerProps` props must be
   * spread onto the element that's being returned.
   */
  trigger: TriggerFunction;
  /**
   * Render function to create the menu list and options
   */
  children: RenderFunction | ReactNode;
  /**
   * Where the menu list popover will be placed relative to the button
   */
  placement?: PopperPlacement;
  /**
   * Tell the menu list popover how to be positioned
   */
  position?: "absolute" | "fixed";
  /**
   * Popper.js modifiers
   */
  popperModifiers?: Record<string, unknown>[];
  /**
   * Whether or not to set focus to the first menu item when the menu appears
   */
  autoFocus?: boolean;
  /**
   * Whether or not to render the popover within a portal; useful when dealing
   * with competing z-index values or other portaled components. This will
   * render the menu at the highest z-index value (z-toast).
   */
  hasPortal?: boolean;
  /**
   * Allow the menu list to be open when the component mounts
   */
  isOpen?: boolean;
  /**
   * Called whenever the menu is closed
   */
  onClose?: () => void;
}

const MENU_OFFSET = 8;
const TRANSITION_START = {
  transform: `translateY(-${MENU_OFFSET}px)`,
  opacity: 0,
};
const TRANSITION_END = { transform: "translateY(0)", opacity: 1 };

export const MenuRenderer = ({
  children,
  className,
  placement = "bottom-start",
  position = "absolute",
  trigger,
  popperModifiers = [],
  autoFocus = false,
  hasPortal = false,
  isOpen: isOpenProp = false,
  onClose: onCloseProp,
  ...rest
}: MenuProps) => {
  const { isOpen, onOpen, onClose, onToggle } = useDisclosure(isOpenProp);
  const menuElement = React.useRef<HTMLDivElement>(null);
  const menuListElement = React.useRef<HTMLDivElement>(null);
  const id = `menu:${useId()}`;
  const menuId = `${id}-menu`;
  const buttonId = `${id}-menubutton`;
  const defaultPopperModifiers = [
    {
      name: "offset",
      options: {
        offset: [0, MENU_OFFSET],
      },
    },
  ];

  const [minWidth, setMinWidth] = useState(0);
  const [referenceElement, setReferenceElement] = useState(null);
  const [popperElement, setPopperElement] = useState(null);
  const { styles, attributes, update } = usePopper(
    referenceElement,
    popperElement,
    {
      placement,
      modifiers: [...defaultPopperModifiers, ...popperModifiers],
      strategy: position,
    },
  );

  const triggerProps: TriggerProps = {
    id: buttonId,
    onClick: (event: Event | MouseEvent<HTMLButtonElement>) => {
      event.stopPropagation();
      event.preventDefault();
      onToggle();
    },
    "aria-haspopup": "menu",
    "aria-expanded": isOpen,
    "aria-controls": menuId,
    role: "button",
    tabIndex: 0,
  };

  const handleClose = () => {
    if (onCloseProp) {
      onCloseProp();
    }
    onClose();
  };

  const triggerState: TriggerState = {
    isOpen,
    onClose: handleClose,
  };

  const listProps = {
    id: menuId,
    role: "menu",
    tabIndex: -1,
    "aria-labelledby": buttonId,
  };

  const itemProps = {
    tabIndex: 0,
    role: "menuitem",
  };

  const renderProps = {
    listProps,
    itemProps,
    isOpen,
    onClose: handleClose,
  };

  const context: MenuContextProps = {
    menuId,
    buttonId,
    onToggle,
    onOpen,
    placement,
    ...renderProps,
  };

  useOutsideClick([menuElement, menuListElement], () => {
    handleClose();
  });

  useKeyPress(KEY_CODE.ESC, () => {
    handleClose();
  });

  useLayoutEffect(() => {
    if (menuElement && menuElement.current) {
      const calculatedMinWidth = Math.round(
        menuElement.current.getBoundingClientRect().width,
      );
      setMinWidth(calculatedMinWidth);
    }
  }, []);

  /**
   * Call Popper’s update function to reposition the popover when the
   * dimensions of the children change.
   */
  useLayoutEffect(() => {
    if (typeof update === "function") {
      update();
    }
  }, [children, update]);

  const PortalComponent = hasPortal ? Portal : Fragment;

  const transitions = useTransition(isOpen, null, {
    from: TRANSITION_START,
    enter: TRANSITION_END,
    leave: TRANSITION_START,
    config: {
      duration: 250,
      easing: easeCubicInOut,
    },
  });

  return (
    <Box className={className} ref={menuElement} {...rest}>
      <Box ref={setReferenceElement}>{trigger(triggerProps, triggerState)}</Box>
      {transitions.map(
        ({ item, key, props: transitionProps }) =>
          item && (
            <PortalComponent key={key}>
              <MenuContext.Provider value={context}>
                <Box
                  className={hasPortal ? "z-toast" : "z-menu"}
                  ref={setPopperElement}
                  style={{ minWidth: `${minWidth}px`, ...styles.popper }}
                  {...attributes.popper}
                >
                  <FocusLock autoFocus={autoFocus}>
                    <animated.div
                      key={key}
                      style={transitionProps}
                      ref={menuListElement}
                    >
                      {typeof children === "function"
                        ? children(renderProps)
                        : children}
                    </animated.div>
                  </FocusLock>
                </Box>
              </MenuContext.Provider>
            </PortalComponent>
          ),
      )}
    </Box>
  );
};
