import { useStoreStateObject } from "@ariakit/react-store";
import {
  useBooleanEvent,
  useEvent,
  useId,
  useWrapElement,
  createElement,
  createHook,
  forwardRef,
  memo,
} from "@ariakit/react-utils";
import type { Props } from "@ariakit/react-utils";
import {
  disabledFromProps,
  getItemRoleByPopupRole,
  hasFocus,
  isTextField,
  isDownloading,
  isOpeningInNewTab,
  isApple,
  invariant,
} from "@ariakit/utils";
import type { BooleanOrCallback } from "@ariakit/utils";
import type { ElementType, KeyboardEvent, MouseEvent } from "react";
import { useCallback, useContext } 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 { getScrollItemIntoView } from "./__utils.ts";
import {
  ComboboxItemCheckedContext,
  ComboboxItemValueContext,
  ComboboxListRoleContext,
  useComboboxScopedContext,
} from "./combobox-context.tsx";
import type { ComboboxStore } from "./combobox-store.ts";

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

function isSelected(
  storeValue?: string | readonly string[],
  itemValue?: string,
) {
  if (itemValue == null) return;
  if (storeValue == null) return false;
  if (Array.isArray(storeValue)) {
    return storeValue.includes(itemValue);
  }
  return storeValue === itemValue;
}

/**
 * Returns the role for a combobox item based on the popup role.
 */
function getItemRole(popupRole?: string) {
  return getItemRoleByPopupRole(popupRole) ?? "option";
}

/**
 * Returns props to create a `ComboboxItem` component.
 * @see https://ariakit.com/components/combobox
 * @example
 * ```jsx
 * const store = useComboboxStore();
 * const props = useComboboxItem({ store, value: "value" });
 * <Role {...props} />
 * ```
 */
export const useComboboxItem = createHook<TagName, ComboboxItemOptions>(
  function useComboboxItem({
    store,
    value,
    hideOnClick,
    setValueOnClick,
    selectValueOnClick = true,
    resetValueOnSelect,
    focusOnHover,
    moveOnKeyPress = true,
    preventScrollOnKeyDown,
    getItem: getItemProp,
    ...props
  }) {
    const context = useComboboxScopedContext();
    store = store || context;

    invariant(
      store,
      process.env.NODE_ENV !== "production" &&
        "ComboboxItem must be wrapped in a ComboboxList or ComboboxPopover component.",
    );

    const id = useId(props.id);

    const {
      resetValueOnSelectState,
      multiSelectable,
      selected,
      autoFocusSelected,
      selectElement,
    } = useStoreStateObject(store, ["selectedValue"], {
      resetValueOnSelectState: "resetValueOnSelect",
      multiSelectable(state) {
        return Array.isArray(state.selectedValue);
      },
      selected(state) {
        return isSelected(state.selectedValue, value);
      },
      autoFocusSelected(state) {
        if (value == null) return false;
        if (!Array.isArray(state.selectedValue)) {
          return state.selectedValue === value;
        }
        return state.selectedValue[state.selectedValue.length - 1] === value;
      },
      selectElement: "selectElement",
    });

    // The selected item marks the initial presentation target. Derive the
    // marker from the selected value so items don't subscribe to movement.
    const autoFocusSelectedItem = !!selectElement;

    const selectMode = !!selectElement;
    const disabled = disabledFromProps(props);

    const getItem = useCallback<NonNullable<CompositeItemOptions["getItem"]>>(
      (item) => {
        const nextItem = {
          ...item,
          value: selectMode && disabled ? undefined : value,
        };
        if (getItemProp) {
          return getItemProp(nextItem);
        }
        return nextItem;
      },
      [selectMode, disabled, value, getItemProp],
    );

    setValueOnClick = setValueOnClick ?? (!selectMode && !multiSelectable);
    hideOnClick = hideOnClick ?? (value != null && !multiSelectable);
    preventScrollOnKeyDown = preventScrollOnKeyDown ?? selectMode;
    const popupRole = useContext(ComboboxListRoleContext);
    const role = getItemRole(popupRole);

    const onClickProp = props.onClick;
    const setValueOnClickProp = useBooleanEvent(setValueOnClick);
    const selectValueOnClickProp = useBooleanEvent(selectValueOnClick);
    const resetValueOnSelectProp = useBooleanEvent(
      resetValueOnSelect ?? resetValueOnSelectState,
    );
    const hideOnClickProp = useBooleanEvent(hideOnClick);

    const onClick = useEvent((event: MouseEvent<HTMLType>) => {
      onClickProp?.(event);
      if (event.defaultPrevented) return;
      if (isDownloading(event)) return;
      if (isOpeningInNewTab(event)) return;
      if (value != null) {
        if (selectValueOnClickProp(event)) {
          if (resetValueOnSelectProp(event)) {
            store?.resetInputValue();
          }
          store?.setSelectedValue((prevValue) => {
            if (!Array.isArray(prevValue)) return value;
            if (prevValue.includes(value)) {
              return prevValue.filter((v) => v !== value);
            }
            return [...prevValue, value];
          });
        }
        if (setValueOnClickProp(event)) {
          store?.setInputValue(value);
        }
      }
      if (hideOnClickProp(event)) {
        store?.hide();
      }
    });

    const onKeyDownProp = props.onKeyDown;

    const onKeyDown = useEvent((event: KeyboardEvent<HTMLType>) => {
      onKeyDownProp?.(event);
      if (event.defaultPrevented) return;
      const compositeElement = store?.getState().compositeElement;
      if (!compositeElement) return;
      if (hasFocus(compositeElement)) return;
      // When the combobox is not working with virtual focus, the items will
      // receive DOM focus. Therefore, pressing printable keys will not fill
      // the text field. So we need to programmatically focus on the text
      // field when the user presses printable keys.
      const printable =
        event.key.length === 1 && !event.ctrlKey && !event.metaKey;
      const pc = !isApple();
      const modifier = pc ? event.ctrlKey : event.metaKey;
      // If it's cmd/ctrl+v, focus on the text field so the value is pasted
      // there.
      const paste = modifier && event.key.toLowerCase() === "v";
      const deleteKey = event.key === "Backspace" || event.key === "Delete";
      if (printable || paste || deleteKey) {
        // In select mode, the composite element is the select button rather
        // than a text field, so focusing it would move focus out of the open
        // list.
        if (compositeElement === selectElement) return;
        if (isTextField(compositeElement)) {
          queueMicrotask(() => compositeElement.focus());
          store?.setInputValue(compositeElement.value);
          return;
        }
        compositeElement.focus();
        if (
          "value" in compositeElement &&
          typeof compositeElement.value === "string"
        ) {
          // Update the store value with the current element's value. This is
          // necessary because the value may temporarily change based on the
          // currently selected item, but it'll be reset to the original value
          // when the combobox input is focused.
          store?.setInputValue(compositeElement.value);
        }
      }
    });

    if ((selectMode || multiSelectable) && selected != null) {
      props = {
        "aria-selected": selected,
        ...props,
      };
    }

    props = useWrapElement(
      props,
      (element) => (
        <ComboboxItemValueContext.Provider value={value}>
          <ComboboxItemCheckedContext.Provider value={selected ?? false}>
            {element}
          </ComboboxItemCheckedContext.Provider>
        </ComboboxItemValueContext.Provider>
      ),
      [value, selected],
    );

    const defaultAutoFocus = autoFocusSelectedItem
      ? autoFocusSelected
      : undefined;

    props = {
      role,
      children: value,
      ...props,
      id,
      onClick,
      onKeyDown,
    };
    if (props.autoFocus === undefined && defaultAutoFocus !== undefined) {
      // Keep the marker for Dialog, but avoid a re-mounted selected item
      // stealing focus after the initial focus effect has run.
      props.autoFocus = false;
      props["data-autofocus"] = defaultAutoFocus || undefined;
    }

    const moveOnKeyPressProp = useBooleanEvent(moveOnKeyPress);
    const scrollItemIntoView = getScrollItemIntoView(store);

    props = useCompositeItem<TagName>({
      store,
      unstable_scrollIntoView: scrollItemIntoView,
      ...props,
      getItem,
      preventScrollOnKeyDown,
      // Dispatch a custom event on the combobox input when moving to an item
      // with the keyboard so the Combobox component can enable inline
      // autocompletion.
      moveOnKeyPress: (event) => {
        if (!moveOnKeyPressProp(event)) return false;
        const moveEvent = new Event("combobox-item-move");
        const compositeElement = store?.getState().compositeElement;
        compositeElement?.dispatchEvent(moveEvent);
        return true;
      },
    });

    const focusOnHoverProp = useBooleanEvent(focusOnHover ?? false);

    props = useCompositeHover({
      store,
      ...props,
      focusOnHover(event) {
        if (focusOnHover !== undefined) {
          return focusOnHoverProp(event);
        }
        return selectMode && store.getState().open;
      },
    });

    return props;
  },
);

/**
 * Renders a combobox item inside
 * [`ComboboxList`](https://ariakit.com/reference/combobox-list) or
 * [`ComboboxPopover`](https://ariakit.com/reference/combobox-popover)
 * components.
 *
 * By default, the [`value`](https://ariakit.com/reference/combobox-item#value)
 * prop will be rendered as the children, but this can be overriden.
 * @see https://ariakit.com/components/combobox
 * @example
 * ```jsx {4-6}
 * <ComboboxProvider>
 *   <Combobox />
 *   <ComboboxPopover>
 *     <ComboboxItem value="Apple" />
 *     <ComboboxItem value="Banana" />
 *     <ComboboxItem value="Orange" />
 *   </ComboboxPopover>
 * </ComboboxProvider>
 * ```
 */
export const ComboboxItem = memo(
  forwardRef(function ComboboxItem(props: ComboboxItemProps) {
    const htmlProps = useComboboxItem(props);
    return createElement(TagName, htmlProps);
  }),
);

export interface ComboboxItemOptions<T extends ElementType = TagName>
  extends CompositeItemOptions<T>, CompositeHoverOptions<T> {
  /**
   * Object returned by the
   * [`useComboboxStore`](https://ariakit.com/reference/use-combobox-store)
   * hook. If not provided, the closest
   * [`ComboboxList`](https://ariakit.com/reference/combobox-list) or
   * [`ComboboxPopover`](https://ariakit.com/reference/combobox-popover)
   * components' context will be used.
   *
   * Live examples:
   * - [Navigation Menubar](https://ariakit.com/examples/menubar-navigation)
   */
  store?: ComboboxStore;
  /**
   * The value of the item. This will be rendered as the children by default.
   * - If
   *   [`setValueOnClick`](https://ariakit.com/reference/combobox-item#setvalueonclick)
   *   is set to `true`, this will be the value of the combobox input when the
   *   user clicks on this item.
   * - If
   *   [`selectValueOnClick`](https://ariakit.com/reference/combobox-item#selectvalueonclick)
   *   is set to `true`, this will be the value of the
   *   [`selectedValue`](https://ariakit.com/reference/combobox-provider#selectedvalue)
   *   state.
   * - If the
   *   [`autoComplete`](https://ariakit.com/reference/combobox#autocomplete)
   *   prop on the [`Combobox`](https://ariakit.com/reference/combobox)
   *   component is set to `both` or `inline`, this will be the value of the
   *   combobox input when the combobox loses focus.
   *
   * Live examples:
   * - [Animated Combobox](https://ariakit.com/examples/combobox-animated)
   * - [ComboboxCancel](https://ariakit.com/examples/combobox-cancel)
   * - [ComboboxDisclosure](https://ariakit.com/examples/combobox-disclosure)
   * - [Combobox filtering](https://ariakit.com/examples/combobox-filtering)
   * - [ComboboxGroup](https://ariakit.com/examples/combobox-group)
   * - [Textarea with inline
   *   Combobox](https://ariakit.com/examples/combobox-textarea)
   */
  value?: string;
  /**
   * Whether to hide the combobox when this item is clicked. By default, the
   * combobox will be hidden when the user clicks on an item with a
   * [`value`](https://ariakit.com/reference/combobox-item#value) prop, unless
   * the combobox is
   * [multi-selectable](https://ariakit.com/examples/combobox-multiple).
   *
   * Live examples:
   * - [Combobox with links](https://ariakit.com/examples/combobox-links)
   * - [Submenu with
   *   Combobox](https://ariakit.com/examples/menu-nested-combobox)
   * - [Command Menu](https://ariakit.com/examples/dialog-combobox-command-menu)
   * - [Command Menu with
   *   Tabs](https://ariakit.com/examples/dialog-combobox-tab-command-menu)
   */
  hideOnClick?: BooleanOrCallback<MouseEvent<HTMLElement>>;
  /**
   * Whether to set the combobox
   * [`inputValue`](https://ariakit.com/reference/combobox-provider#inputvalue)
   * state using this item's
   * [`value`](https://ariakit.com/reference/combobox-item#value) when the item
   * is clicked. The default is `true`, unless
   * [`ComboboxSelect`](https://ariakit.com/reference/combobox-select) is
   * rendered or the combobox is
   * [multi-selectable](https://ariakit.com/examples/combobox-multiple).
   *
   * Live examples:
   * - [Menu with Combobox](https://ariakit.com/examples/menu-combobox)
   * - [Submenu with
   *   Combobox](https://ariakit.com/examples/menu-nested-combobox)
   */
  setValueOnClick?: BooleanOrCallback<MouseEvent<HTMLElement>>;
  /**
   * Whether to set the
   * [`selectedValue`](https://ariakit.com/reference/combobox-provider#selectedvalue)
   * state using this item's
   * [`value`](https://ariakit.com/reference/combobox-item#value) when the item
   * is clicked. If a callback is provided, it will only be invoked if the item
   * has a value.
   *
   * Live examples:
   * - [Submenu with
   *   Combobox](https://ariakit.com/examples/menu-nested-combobox)
   * @default true
   */
  selectValueOnClick?: BooleanOrCallback<MouseEvent<HTMLElement>>;
  /**
   * Whether to reset the combobox input value when this item is selected or
   * unselected by click. This prop is set to `true` by default when
   * [`ComboboxSelect`](https://ariakit.com/reference/combobox-select) is
   * rendered, or when the combobox supports multiple selections, that is, when
   * the
   * [`selectedValue`](https://ariakit.com/reference/combobox-provider#selectedvalue)
   * or
   * [`defaultSelectedValue`](https://ariakit.com/reference/combobox-provider#defaultselectedvalue)
   * props are arrays.
   */
  resetValueOnSelect?: BooleanOrCallback<MouseEvent<HTMLElement>>;
  /**
   * Defaults to `false`, or `true` when the item is used with a
   * [`ComboboxSelect`](https://ariakit.com/reference/combobox-select).
   */
  focusOnHover?: CompositeHoverOptions["focusOnHover"];
}

export type ComboboxItemProps<T extends ElementType = TagName> = Props<
  T,
  ComboboxItemOptions<T>
>;
