import {
  createStore,
  mergeStore,
  omit,
  setup,
  sync,
  throwOnConflictingProps,
} from "@ariakit/store";
import type { Store, StoreOptions, StoreProps } from "@ariakit/store";
import { defaultValue } from "@ariakit/utils";
import type { SetState } from "@ariakit/utils";
import type {
  DialogStoreFunctions,
  DialogStoreOptions,
  DialogStoreState,
} from "../dialog/dialog-store.ts";
import { createDialogStore } from "../dialog/dialog-store.ts";

type BasePlacement = "top" | "bottom" | "left" | "right";

type Placement =
  | BasePlacement
  | `${BasePlacement}-start`
  | `${BasePlacement}-end`;

/**
 * Creates a popover store.
 */
export function createPopoverStore({
  popover: otherPopover,
  ...props
}: PopoverStoreProps = {}): PopoverStore {
  const store = mergeStore(
    props.store,
    omit(otherPopover, [
      "arrowElement",
      "anchorElement",
      "contentElement",
      "popoverElement",
      "disclosureElement",
      // Two popovers sharing a store are still positioned independently.
      "unstable_placing",
    ]),
  );

  throwOnConflictingProps(props, store);

  const syncState = store?.getState();

  const dialog = createDialogStore({ ...props, store });

  const placement = defaultValue(
    props.placement,
    syncState?.placement,
    "bottom" as const,
  );

  const initialState: PopoverStoreState = {
    ...dialog.getState(),
    placement,
    currentPlacement: placement,
    anchorElement: defaultValue(syncState?.anchorElement, null),
    popoverElement: defaultValue(syncState?.popoverElement, null),
    arrowElement: defaultValue(syncState?.arrowElement, null),
    rendered: Symbol("rendered"),
    unstable_placing: false,
  };
  const popover = createStore(initialState, dialog, store);
  let syncedAnchorElement =
    initialState.anchorElement === initialState.disclosureElement
      ? initialState.anchorElement
      : null;

  setup(popover, () =>
    sync(popover, ["anchorElement", "disclosureElement"], (state) => {
      if (state.anchorElement && state.anchorElement !== syncedAnchorElement) {
        syncedAnchorElement = null;
        return;
      }
      syncedAnchorElement = state.disclosureElement;
      popover.setState("anchorElement", syncedAnchorElement);
    }),
  );

  return {
    ...dialog,
    ...popover,
    setAnchorElement: (element) => popover.setState("anchorElement", element),
    setPopoverElement: (element) => popover.setState("popoverElement", element),
    setArrowElement: (element) => popover.setState("arrowElement", element),
    render: () => popover.setState("rendered", Symbol("rendered")),
  };
}

export interface PopoverStoreState extends DialogStoreState {
  /**
   * The anchor element.
   *
   * Live examples:
   * - [Navigation Menubar](https://ariakit.com/examples/menubar-navigation)
   */
  anchorElement: HTMLElement | null;
  /**
   * The popover element that will render the placement attributes.
   *
   * Live examples:
   * - [Form with Select](https://ariakit.com/examples/form-select)
   * - [Sliding Menu](https://ariakit.com/examples/menu-slide)
   * - [Responsive Popover](https://ariakit.com/examples/popover-responsive)
   */
  popoverElement: HTMLElement | null;
  /**
   * The arrow element.
   */
  arrowElement: HTMLElement | null;
  /**
   * The current temporary position of the popover. This might differ from the
   * [`placement`](https://ariakit.com/reference/popover-provider#placement)
   * state if the popover has had to adjust its position dynamically.
   *
   * Live examples:
   * - [Tooltip with
   *   Motion](https://ariakit.com/examples/tooltip-framer-motion)
   */
  currentPlacement: Placement;
  /**
   * The placement of the popover.
   *
   * Live examples:
   * - [Submenu with
   *   Combobox](https://ariakit.com/examples/menu-nested-combobox)
   * - [Sliding Menu](https://ariakit.com/examples/menu-slide)
   * - [Navigation Menubar](https://ariakit.com/examples/menubar-navigation)
   * - [Selection Popover](https://ariakit.com/examples/popover-selection)
   * - [Standalone Popover](https://ariakit.com/examples/popover-standalone)
   * - [Select Grid](https://ariakit.com/examples/select-grid)
   * @default "bottom"
   */
  placement: Placement;
  /**
   * A symbol that's used to recompute the popover position when the
   * [`render`](https://ariakit.com/reference/use-popover-store#render) method
   * is called.
   */
  rendered: symbol;
  /**
   * Whether the popover is showing and hasn't settled at the position its
   * current positioning pass will leave it at. Every pass a commit starts
   * asserts it, not just the one that follows the popover being shown: showing
   * publishes it as soon as the store changes, while every other commit-started
   * pass publishes it with its commit, so code that re-anchors and reads this
   * in the same turn still sees the previous value. The `autoUpdate` runs that
   * follow a scroll or a resize reposition an already placed popover without
   * asserting it again.
   *
   * Components that move focus or scroll into the popup wait for this to become
   * `false`, otherwise they act on an element that's still at its
   * pre-placement origin, or at a position it's about to leave, and drag the
   * page along with it.
   * @private
   */
  unstable_placing: boolean;
}

export interface PopoverStoreFunctions extends DialogStoreFunctions {
  /**
   * Sets the anchor element.
   *
   * Live examples:
   * - [Navigation Menubar](https://ariakit.com/examples/menubar-navigation)
   */
  setAnchorElement: SetState<PopoverStoreState["anchorElement"]>;
  /**
   * Sets the popover element.
   */
  setPopoverElement: SetState<PopoverStoreState["popoverElement"]>;
  /**
   * Sets the arrow element.
   */
  setArrowElement: SetState<PopoverStoreState["arrowElement"]>;
  /**
   * A function that can be used to recompute the popover position. This is
   * useful when the popover anchor changes in a way that affects the popover
   * position.
   *
   * Live examples:
   * - [Textarea with inline
   *   Combobox](https://ariakit.com/examples/combobox-textarea)
   * - [Selection Popover](https://ariakit.com/examples/popover-selection)
   */
  render: () => void;
}

export interface PopoverStoreOptions
  extends DialogStoreOptions, StoreOptions<PopoverStoreState, "placement"> {
  /**
   * A reference to another popover store that's controlling another popover to
   * keep them in sync.
   */
  popover?: PopoverStore | null;
}

export interface PopoverStoreProps
  extends PopoverStoreOptions, StoreProps<PopoverStoreState> {}

export interface PopoverStore
  extends PopoverStoreFunctions, Store<PopoverStoreState> {}
