import { useStoreState } from "@ariakit/react-store";
import {
  useAttribute,
  useBooleanEvent,
  useEvent,
  useId,
  useMergeRefs,
  useTransactionState,
  useWrapElement,
  createElement,
  createHook,
  forwardRef,
} from "@ariakit/react-utils";
import type { Props } from "@ariakit/react-utils";
import { isSelfTarget, invariant } from "@ariakit/utils";
import type { BooleanOrCallback } from "@ariakit/utils";
import type { ElementType, KeyboardEvent } from "react";
import { createContext, useContext, useEffect, useMemo, useState } from "react";
import type { CompositeTypeaheadOptions } from "../composite/composite-typeahead.tsx";
import { useCompositeTypeahead } from "../composite/composite-typeahead.tsx";
import type { CompositeOptions } from "../composite/composite.tsx";
import { useComposite } from "../composite/composite.tsx";
import type { DisclosureContentOptions } from "../disclosure/disclosure-content.tsx";
import { isHidden } from "../disclosure/disclosure-content.tsx";
import {
  SelectHeadingContext,
  SelectScopedContextProvider,
  useSelectContext,
} from "./select-context.tsx";
import type { SelectStore } from "./select-store.ts";

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

const SelectListContext = createContext<
  ((store: SelectStore | null) => void) | null
>(null);

/**
 * Returns props to create a `SelectList` component.
 * @deprecated Use `useComboboxList` instead.
 * @see https://ariakit.com/components/select
 * @example
 * ```jsx
 * const store = useSelectStore();
 * const props = useSelectList({ store });
 * <Role {...props}>
 *   <SelectItem value="Apple" />
 *   <SelectItem value="Orange" />
 * </Role>
 * ```
 */
export const useSelectList = createHook<TagName, SelectListOptions>(
  function useSelectList({
    store,
    resetOnEscape = true,
    hideOnEnter = true,
    composite,
    alwaysVisible,
    ...props
  }) {
    const context = useSelectContext();
    store = store || context;

    invariant(
      store,
      process.env.NODE_ENV !== "production" &&
        "SelectList must receive a `store` prop or be wrapped in a SelectProvider component.",
    );

    const id = useId(props.id);
    const value = useStoreState(store, "value");
    const multiSelectable = Array.isArray(value);
    const [defaultValue, setDefaultValue] = useState(value);

    const mounted = useStoreState(store, "mounted");

    // Stores the intial value so we can reset it later when Escape is pressed
    useEffect(() => {
      if (mounted) return;
      setDefaultValue(value);
    }, [mounted, value]);

    resetOnEscape = resetOnEscape && !multiSelectable;

    const onKeyDownProp = props.onKeyDown;
    const resetOnEscapeProp = useBooleanEvent(resetOnEscape);
    const hideOnEnterProp = useBooleanEvent(hideOnEnter);

    const onKeyDown = useEvent((event: KeyboardEvent<HTMLType>) => {
      onKeyDownProp?.(event);
      if (event.defaultPrevented) return;
      if (event.key === "Escape" && resetOnEscapeProp(event)) {
        store?.setValue(defaultValue);
      }
      if (event.key === " " || event.key === "Enter") {
        if (isSelfTarget(event) && hideOnEnterProp(event)) {
          event.preventDefault();
          store?.hide();
        }
      }
    });

    const headingContext = useContext(SelectHeadingContext);
    const headingState = useState<string>();
    const [headingId, setHeadingId] = headingContext || headingState;

    const headingContextValue: typeof headingState = useMemo(
      () => [headingId, setHeadingId],
      [headingId, setHeadingId],
    );

    const [childStore, setChildStore] = useState<SelectStore | null>(null);
    const setStore = useContext(SelectListContext);

    useEffect(() => {
      if (!setStore) return;
      setStore(store);
      return () => setStore(null);
    }, [setStore, store]);

    props = useWrapElement(
      props,
      (element) => (
        <SelectScopedContextProvider value={store}>
          <SelectListContext.Provider value={setChildStore}>
            <SelectHeadingContext.Provider value={headingContextValue}>
              {element}
            </SelectHeadingContext.Provider>
          </SelectListContext.Provider>
        </SelectScopedContextProvider>
      ),
      [store, headingContextValue],
    );

    const hasCombobox = !!store.combobox;
    composite = composite ?? (!hasCombobox && childStore !== store);

    const [, setElement] = useTransactionState(
      composite ? store.setListElement : null,
    );

    const hidden = isHidden(mounted, props.hidden, alwaysVisible);
    const style = hidden ? { ...props.style, display: "none" } : props.style;

    if (composite) {
      props = {
        role: "listbox",
        "aria-multiselectable": multiSelectable || undefined,
        ...props,
      };
    }

    const labelElement = useStoreState(store, ["labelElement"], (state) =>
      headingId ? null : state.labelElement,
    );
    useAttribute(labelElement, "id");
    const labelId = headingId || labelElement?.id;

    props = {
      "aria-labelledby": props["aria-label"] != null ? undefined : labelId,
      hidden,
      ...props,
      id,
      ref: useMergeRefs(setElement, props.ref),
      style,
      onKeyDown,
    };

    props = useComposite({ store, ...props, composite });
    props = useCompositeTypeahead({ store, typeahead: !hasCombobox, ...props });

    return props;
  },
);

/**
 * Renders a wrapper for
 * [`SelectItem`](https://ariakit.com/reference/select-item) elements. This
 * component may be rendered within a
 * [`SelectPopover`](https://ariakit.com/reference/select-popover) component if
 * there are other non-item elements inside the popover.
 *
 * The `aria-labelledby` prop is set to the
 * [`Select`](https://ariakit.com/reference/select) element's `id` by default.
 * @deprecated Use
 * [`ComboboxList`](https://ariakit.com/reference/combobox-list) instead.
 * @see https://ariakit.com/components/select
 * @example
 * ```jsx {5-8}
 * <SelectProvider>
 *   <Select />
 *   <SelectPopover>
 *     <SelectDismiss />
 *     <SelectList>
 *       <SelectItem value="Apple" />
 *       <SelectItem value="Orange" />
 *     </SelectList>
 *   </SelectPopover>
 * </SelectProvider>
 * ```
 */
export const SelectList = forwardRef(function SelectList(
  props: SelectListProps,
) {
  const htmlProps = useSelectList(props);
  return createElement(TagName, htmlProps);
});

/** @deprecated Use `ComboboxListOptions` instead. */
export interface SelectListOptions<T extends ElementType = TagName>
  extends
    CompositeOptions<T>,
    CompositeTypeaheadOptions<T>,
    Pick<DisclosureContentOptions, "alwaysVisible"> {
  /**
   * Object returned by the
   * [`useSelectStore`](https://ariakit.com/reference/use-select-store) hook. If
   * not provided, the closest
   * [`SelectProvider`](https://ariakit.com/reference/select-provider)
   * component's context will be used.
   */
  store?: SelectStore;
  /**
   * Whether the select value should be reset to the value before the list got
   * shown when Escape is pressed. This has effect only when
   * [`setValueOnMove`](https://ariakit.com/reference/select-provider#setvalueonmove)
   * is `true`.
   * @default true
   */
  resetOnEscape?: BooleanOrCallback<KeyboardEvent<HTMLElement>>;
  /**
   * Whether the [`SelectList`](https://ariakit.com/reference/select-list) or
   * [`SelectPopover`](https://ariakit.com/reference/select-popover) components
   * should be hidden when the user presses Enter or Space while the list
   * element is focused and no item is active.
   * @default true
   */
  hideOnEnter?: BooleanOrCallback<KeyboardEvent<HTMLElement>>;
}

/** @deprecated Use `ComboboxListProps` instead. */
export type SelectListProps<T extends ElementType = TagName> = Props<
  T,
  SelectListOptions<T>
>;
