import { useStoreState } from "@ariakit/react-store";
import {
  useBooleanEvent,
  useEvent,
  createElement,
  createHook,
  forwardRef,
  memo,
} from "@ariakit/react-utils";
import type { Props } from "@ariakit/react-utils";
import {
  getDocument,
  getPopupItemRole,
  isDownloading,
  isOpeningInNewTab,
  hasFocusWithin,
  invariant,
} from "@ariakit/utils";
import type { BooleanOrCallback } from "@ariakit/utils";
import { useContext } from "react";
import type { ElementType, MouseEvent } from "react";
import type { CompositeHoverOptions } from "../composite/composite-hover.tsx";
import { useCompositeHover } from "../composite/composite-hover.tsx";
import type { CompositeItemOptions } from "../composite/composite-item.tsx";
import { useCompositeItem } from "../composite/composite-item.tsx";
import { useMenubarScopedContext } from "../menubar/menubar-context.tsx";
import type { MenubarStore } from "../menubar/menubar-store.ts";
import {
  MenuListHiddenContext,
  useMenuScopedContext,
} from "./menu-context.tsx";
import type { MenuStore, MenuStoreState } from "./menu-store.ts";

const TagName = "div" satisfies ElementType;
type TagName = typeof TagName;
type HTMLType = HTMLElementTagNameMap[TagName];

function menuHasFocus(
  compositeElement?: MenuStoreState["compositeElement"],
  items?: MenuStoreState["items"],
  currentTarget?: Element,
) {
  if (!compositeElement) return false;
  if (hasFocusWithin(compositeElement)) return true;
  const expandedItem = items?.find((item) => {
    if (item.element === currentTarget) return false;
    return item.element?.getAttribute("aria-expanded") === "true";
  });
  const expandedMenuId = expandedItem?.element?.getAttribute("aria-controls");
  if (!expandedMenuId) return false;
  const doc = getDocument(compositeElement);
  const expandedMenu = doc.getElementById(expandedMenuId);
  if (!expandedMenu) return false;
  if (hasFocusWithin(expandedMenu)) return true;
  return !!expandedMenu.querySelector("[role=menuitem][aria-expanded=true]");
}

/**
 * Returns props to create a `MenuItem` component.
 * @see https://ariakit.com/components/menu
 * @example
 * ```jsx
 * const store = useMenuStore();
 * const undo = useMenuItem({ store });
 * const redo = useMenuItem({ store });
 * <MenuButton store={store}>Edit</MenuButton>
 * <Menu store={store}>
 *   <Role {...undo}>Undo</Role>
 *   <Role {...redo}>Redo</Role>
 * </Menu>
 * ```
 */
export const useMenuItem = createHook<TagName, MenuItemOptions>(
  function useMenuItem({
    store,
    hideOnClick = true,
    preventScrollOnKeyDown = true,
    focusOnHover,
    blurOnHoverEnd,
    ...props
  }) {
    // Only use the scoped menu context here. The unscoped MenuProvider context
    // is intentionally skipped so MenuItem can bind to the menubar store when
    // it renders a MenuButton inside a Menubar.
    const menuContext = useMenuScopedContext(true);
    const menubarContext = useMenubarScopedContext();
    store = store || menuContext || (menubarContext as any);

    invariant(
      store,
      process.env.NODE_ENV !== "production" &&
        "MenuItem must be wrapped in a MenuList, Menu or Menubar component",
    );

    const onClickProp = props.onClick;
    const hideOnClickProp = useBooleanEvent(hideOnClick);
    const hideMenu = "hideAll" in store ? store.hideAll : undefined;
    const isWithinMenu = !!hideMenu;

    const onClick = useEvent((event: MouseEvent<HTMLType>) => {
      onClickProp?.(event);
      if (event.defaultPrevented) return;
      if (isDownloading(event)) return;
      if (isOpeningInNewTab(event)) return;
      if (!hideMenu) return;
      // If this item is also a submenu button or any other disclosure, we
      // don't want to hide the menu.
      const popupType = event.currentTarget.getAttribute("aria-haspopup");
      if (popupType && popupType !== "false") return;
      if (!hideOnClickProp(event)) return;
      hideMenu();
    });

    const contentElement = useStoreState(store, ["contentElement"], (state) =>
      "contentElement" in state ? state.contentElement : null,
    );

    // Whether the enclosing menu list is currently hidden (provided by
    // `MenuList`; see `MenuListHiddenContext`).
    const menuListHidden = useContext(MenuListHiddenContext);

    const role = getPopupItemRole(contentElement, "menuitem");

    props = {
      role,
      ...props,
      onClick,
    };

    props = useCompositeItem<TagName>({
      store,
      preventScrollOnKeyDown,
      ...props,
      // Hidden menus do not need registered items, and registering many can
      // cause recursive mount updates. Use actual visibility so `alwaysVisible`
      // and `hidden={false}` menus remain registered.
      // https://github.com/ariakit/ariakit/issues/3214
      shouldRegisterItem: menuListHidden ? false : props.shouldRegisterItem,
    });

    props = useCompositeHover({
      store,
      ...props,
      focusOnHover(event) {
        const getFocusOnHover = () => {
          if (typeof focusOnHover === "function") return focusOnHover(event);
          if (focusOnHover != null) return focusOnHover;
          return true;
        };
        if (!store) return false;
        if (!getFocusOnHover()) return false;
        const { compositeElement, items } = store.getState();
        // Hovering is not a request to move the page or menu; the pointer is
        // already on what the user cares about. Keyboard navigation presents
        // items through the composite's own scroll step.
        // If the menu item is also a submenu button, we should move actual DOM
        // focus to it so that the submenu will not close when the user moves
        // the cursor back to the menu button.
        if (isWithinMenu) {
          if (event.currentTarget.hasAttribute("aria-expanded")) {
            event.currentTarget.focus({ preventScroll: true });
          }
          return true;
        }
        // If the menu item is inside a menu bar, we should move DOM focus to
        // the menu item if focus is somewhere on the widget. Without this, the
        // open menus in the menu bar wouldn't close.
        if (menuHasFocus(compositeElement, items, event.currentTarget)) {
          event.currentTarget.focus({ preventScroll: true });
          return true;
        }
        return false;
      },
      blurOnHoverEnd(event) {
        if (typeof blurOnHoverEnd === "function") return blurOnHoverEnd(event);
        if (blurOnHoverEnd != null) return blurOnHoverEnd;
        // The menu container should be focused on mouseleave only if the menu
        // item is inside a menu, not a menu bar.
        return isWithinMenu;
      },
    });

    return props;
  },
);

/**
 * Renders a menu item inside
 * [`MenuList`](https://ariakit.com/reference/menu-list) or
 * [`Menu`](https://ariakit.com/reference/menu)
 * components.
 * @see https://ariakit.com/components/menu
 * @example
 * ```jsx {4-5}
 * <MenuProvider>
 *   <MenuButton>Edit</MenuButton>
 *   <Menu>
 *     <MenuItem>Undo</MenuItem>
 *     <MenuItem>Redo</MenuItem>
 *   </Menu>
 * </MenuProvider>
 * ```
 */
export const MenuItem = memo(
  forwardRef(function MenuItem(props: MenuItemProps) {
    const htmlProps = useMenuItem(props);
    return createElement(TagName, htmlProps);
  }),
);

export interface MenuItemOptions<T extends ElementType = TagName>
  extends CompositeItemOptions<T>, CompositeHoverOptions<T> {
  /**
   * Object returned by the
   * [`useMenuStore`](https://ariakit.com/reference/use-menu-store) or
   * [`useMenubarStore`](https://ariakit.com/reference/use-menubar-store)
   * hooks. If not provided, the closest
   * [`Menu`](https://ariakit.com/reference/menu),
   * [`MenuList`](https://ariakit.com/reference/menu-list),
   * [`Menubar`](https://ariakit.com/reference/menubar), or
   * [`MenubarProvider`](https://ariakit.com/reference/menubar-provider)
   * components' context will be used.
   */
  store?: MenubarStore | MenuStore;
  /**
   * Determines if the menu should hide when this item is clicked.
   *
   * **Note**: This behavior isn't triggered if this menu item is rendered as a
   * link and modifier keys are used to either open the link in a new tab or
   * download it.
   *
   * Live examples:
   * - [Sliding Menu](https://ariakit.com/examples/menu-slide)
   * @default true
   */
  hideOnClick?: BooleanOrCallback<MouseEvent<HTMLElement>>;
}

export type MenuItemProps<T extends ElementType = TagName> = Props<
  T,
  MenuItemOptions<T>
>;
