import React from "react";
import { createPortal } from "react-dom";
import { useComboBoxState } from "react-stately";
import { useComboBox } from "react-aria";
import { styled, theme } from "../theme";

import type * as WPDS from "../theme";
import type { ComboBoxState, Key } from "react-stately";
import type { AriaListBoxOptions } from "react-aria";
import type { CollectionChildren } from "@react-types/shared";

type InputSearchContextProps = {
  state: ComboBoxState<object>;
  inputRef: React.MutableRefObject<null>;
  listBoxRef: React.MutableRefObject<null>;
  popoverRef: React.MutableRefObject<null>;
  containerRef: React.MutableRefObject<null>;
  inputProps: React.InputHTMLAttributes<HTMLInputElement>;
  listBoxProps: AriaListBoxOptions<object>;
  setCollectionChildren: React.Dispatch<
    React.SetStateAction<CollectionChildren<object> | undefined>
  >;
  setDisabledKeys: React.Dispatch<
    React.SetStateAction<Iterable<Key> | undefined>
  >;
  isDisabled?: boolean;
  setUsePortal: React.Dispatch<React.SetStateAction<boolean | undefined>>;
  setPortalDomNode: React.Dispatch<React.SetStateAction<HTMLElement | null>>;
};

export const InputSearchContext = React.createContext(
  {} as InputSearchContextProps
);

const StyledRoot = styled("div", {
  width: "100%",
  position: "relative",
  variants: {
    portal: {
      false: {
        "&:focus-within::after": {
          content: "",
          borderRadius: theme.radii["012"],
          border: `1px solid ${theme.colors.signal}`,
          inset: 0,
          position: "absolute",
          pointerEvents: "none",
          zIndex: 1,
        },
      },
    },
  },
});

export type InputSearchRootProps = {
  /** Defines a string value that labels the current element.  */
  "aria-label"?: string;
  /** Identifies the element (or elements) that labels the current element. */
  "aria-labelledby"?: string;
  /** InputSearch.Root expects to receive InputSearch.Input and InputSearch.Popover as children.*/
  children?: React.ReactNode;
  /** Override CSS */
  css?: WPDS.CSS;
  /** Whether the input field should be disabled or not */
  disabled?: boolean;
  /** If true, the popover opens when focus is on the text box. */
  openOnFocus?: boolean;
  /** Called with the selection value when the user makes a selection from the list. */
  onSelect?: (value: string) => void;
} & React.ComponentPropsWithoutRef<"div">;

export const InputSearchRoot = ({
  children,
  css,
  disabled,
  openOnFocus,
  onSelect,
  ...props
}: InputSearchRootProps) => {
  const [collectionChildren, setCollectionChildren] =
    React.useState<CollectionChildren<object>>();
  const [disabledKeys, setDisabledKeys] = React.useState<Iterable<Key>>();

  const state = useComboBoxState({
    children: collectionChildren,
    disabledKeys,
    allowsCustomValue: true,
    allowsEmptyCollection: true,
    menuTrigger: openOnFocus ? "focus" : "input",
    ...props,
  });

  const inputRef = React.useRef(null);
  const listBoxRef = React.useRef(null);
  const popoverRef = React.useRef(null);

  const { inputProps, listBoxProps } = useComboBox(
    {
      // Provide aria-label as fallback to satisfy react-aria's accessibility requirements.
      // When InputText provides a visible <label>, it will properly associate via for/id,
      // and that takes precedence. If aria-label or aria-labelledby are explicitly provided
      // via props, they'll override this default.
      "aria-label": "Search",
      ...props,
      inputRef,
      listBoxRef,
      popoverRef,
    },
    state
  );

  const prevSelectedKey = React.useRef(state.selectedKey);
  React.useEffect(() => {
    if (!onSelect) return;
    if (prevSelectedKey.current !== state.selectedKey) {
      if (state.selectedItem) {
        onSelect(
          state.selectedItem.textValue ||
            (state.selectedItem.rendered as string)
        );
      } else if (state.selectedItem === null) {
        onSelect("");
      }
      prevSelectedKey.current = state.selectedKey;
    }
  }, [state.selectedItem, onSelect]);

  const containerRef = React.useRef(null);

  const [usePortal, setUsePortal] = React.useState<boolean>();
  const [portalDomNode, setPortalDomNode] = React.useState<HTMLElement | null>(
    null
  );

  return (
    <InputSearchContext.Provider
      value={{
        state,
        inputRef,
        listBoxRef,
        popoverRef,
        containerRef,
        inputProps,
        listBoxProps,
        setCollectionChildren,
        setDisabledKeys,
        setUsePortal,
        setPortalDomNode,
        isDisabled: disabled,
      }}
    >
      <StyledRoot ref={containerRef} css={css} portal={usePortal} {...props}>
        {children}
        {/* 
        react-aria's ariaHideOutside utility assumes popover is visible when the global state.isOpen is true. This isn't always the case for conditional rendering, especially when async data is involved. Use root level containers to avoid errors and respond correctly to clicks
        */}
        {usePortal ? (
          portalDomNode !== null &&
          createPortal(
            <div data-testid="popover-container" ref={popoverRef} />,
            portalDomNode
          )
        ) : (
          <div data-testid="popover-container" ref={popoverRef} />
        )}
      </StyledRoot>
    </InputSearchContext.Provider>
  );
};

InputSearchRoot.displayName = "InputSearchRoot";
