/** @license MIT | @cube-dev/ui-kit v0.142.7 | Cube Dev Team */
import { EventBusContext, useEventBus } from "./useEventBus.js";
import { useCallback, useContext, useEffect, useRef } from "react";

//#region src/utils/react/usePopoverSync.ts
/**
* Module-level registry of currently open popovers. Lets us resolve the
* LOGICAL parent of an arbitrary element across portal boundaries.
*
* Popover content is portaled to a shared root (`document.body` by default —
* see `Overlay.tsx`), so a grandchild popover's trigger lives inside a
* sibling portal rather than physically inside its grandparent's container.
* A naive `container.contains(triggerEl)` check therefore misses the
* relationship and closes the grandparent — the bug that surfaces with 3+
* levels of `SubMenuTrigger` nesting.
*
* The registry stores closures over the host's refs so reads always see the
* latest `.current` value without forcing a re-register per render.
*/
const openPopovers = /* @__PURE__ */ new Map();
/**
* Find the open popover whose container directly contains `target`. Returns
* `null` for elements that live outside every registered popover (e.g. a
* top-level trigger button rendered next to its overlay root).
*/
function findOwningPopover(target) {
	if (!target) return null;
	for (const entry of openPopovers) if (entry[1].getContainerEl()?.contains(target)) return entry;
	return null;
}
/**
* Returns `true` when `target` is nested inside the overlay identified by
* `ancestorMenuId` — either as a direct DOM descendant of `ancestorContainer`
* or as a descendant of a chain of popovers whose triggers eventually land
* inside it. Used by `popover:open` peers to avoid closing themselves when a
* grand-child overlay opens through a portal.
*/
function isLogicalDescendantOf(target, ancestorMenuId, ancestorContainer) {
	if (ancestorContainer && target && ancestorContainer.contains(target)) return true;
	let cur = target;
	const visited = /* @__PURE__ */ new Set();
	while (cur) {
		const owner = findOwningPopover(cur);
		if (!owner) return false;
		const [id, entry] = owner;
		if (visited.has(id)) return false;
		visited.add(id);
		if (id === ancestorMenuId) return true;
		cur = entry.getTriggerEl();
	}
	return false;
}
/**
* Coordinates the "only one popover open at a time" invariant via the EventBus.
*
* - When `isOpen` flips `false -> true`, emits `popover:open` once.
* - While open, listens for peers' `popover:open` events and calls `onClose`.
*
* Implementation notes (ALL of these matter — losing any one re-introduces a
* race that surfaces only under rapid trigger switching, which is hard to
* reproduce in tests):
*
* 1. `isOpen` and `onClose` are read through refs inside the listener, so the
*    subscription effect's dep array does NOT include `isOpen`/`onClose`. This
*    keeps the listener identity stable across open/close transitions and
*    avoids the unsubscribe-emit-resubscribe window where an emit can be
*    delivered to a stale listener (or no listener).
* 2. The emit fires only on the `false -> true` transition, gated by
*    `wasOpenRef`. A re-render where `isOpen` is still `true` must NOT
*    re-emit, otherwise it could re-trigger listeners on peers that just
*    opened in the same render flush.
* 3. The `enabled` flag short-circuits both effects symmetrically. When it
*    flips off, `wasOpenRef` is reset so re-enabling later still emits if
*    `isOpen` is true at that moment.
*/
function usePopoverSync({ menuId, isOpen, onClose, enabled = true, triggerRef, containerRef, dismissOnInnerButtonPress = true, closeOnPeerOpen = true }) {
	const { emit, on } = useEventBus();
	const isOpenRef = useRef(isOpen);
	useEffect(() => {
		isOpenRef.current = isOpen;
	}, [isOpen]);
	const onCloseRef = useRef(onClose);
	useEffect(() => {
		onCloseRef.current = onClose;
	}, [onClose]);
	const containerRefRef = useRef(containerRef);
	useEffect(() => {
		containerRefRef.current = containerRef;
	}, [containerRef]);
	const triggerRefRef = useRef(triggerRef);
	useEffect(() => {
		triggerRefRef.current = triggerRef;
	}, [triggerRef]);
	useEffect(() => {
		if (!enabled) return;
		const offOpen = closeOnPeerOpen ? on("popover:open", (data) => {
			if (data.menuId === menuId || !isOpenRef.current) return;
			const container = containerRefRef.current?.current ?? null;
			if (isLogicalDescendantOf(data.triggerEl, menuId, container)) return;
			onCloseRef.current();
		}) : null;
		const offDismiss = dismissOnInnerButtonPress ? on("popover:dismiss-ancestor", (data) => {
			if (!isOpenRef.current) return;
			const container = containerRefRef.current?.current;
			const from = data?.from;
			if (!container || !from) return;
			if (container.contains(from)) onCloseRef.current();
		}) : null;
		return () => {
			offOpen?.();
			offDismiss?.();
		};
	}, [
		on,
		menuId,
		enabled,
		dismissOnInnerButtonPress,
		closeOnPeerOpen
	]);
	const wasOpenRef = useRef(false);
	useEffect(() => {
		if (!enabled) {
			if (wasOpenRef.current) openPopovers.delete(menuId);
			wasOpenRef.current = false;
			return;
		}
		if (isOpen && !wasOpenRef.current) {
			wasOpenRef.current = true;
			openPopovers.set(menuId, {
				getContainerEl: () => containerRefRef.current?.current ?? null,
				getTriggerEl: () => triggerRefRef.current?.current ?? null
			});
			emit("popover:open", {
				menuId,
				triggerEl: triggerRefRef.current?.current ?? null
			});
		} else if (!isOpen && wasOpenRef.current) {
			wasOpenRef.current = false;
			openPopovers.delete(menuId);
		}
	}, [
		isOpen,
		emit,
		menuId,
		enabled
	]);
	useEffect(() => {
		return () => {
			openPopovers.delete(menuId);
		};
	}, [menuId]);
}
/**
* Hook that returns a dispatcher to close the popover that contains a given
* DOM element. Used by `Button` / `ItemButton` to implement the default
* "press inside a popover closes the popover" behaviour. Custom (non-Cube)
* interactive controls can call this directly:
*
* ```tsx
* const dismiss = useDismissParentPopover();
* <MyCustomPressable onPress={(e) => { doThing(); dismiss(e.currentTarget); }} />
* ```
*
* The actual dismiss is dispatched through the EventBus, which defers via
* `setTimeout(0)` — so the user's synchronous handler (and any React state
* updates it triggers) flushes BEFORE the popover closes. This is critical
* for the "open a hoisted modal from a popover footer" case: the modal
* mounts first, then the popover closes.
*
* Only popover-type containers (those that pass `dismissOnInnerButtonPress`
* as `true`, the default) react to the event. Modal/tray/fullscreen Dialog
* containers explicitly opt out so a Button inside their content does not
* auto-close them.
*
* When called outside an `EventBusProvider` (e.g. in unit tests that render
* a Button without wrapping in `<Root>`), the returned function is a no-op —
* the dismiss flow gracefully degrades rather than throwing.
*/
function useDismissParentPopover() {
	const emit = useContext(EventBusContext)?.emit;
	return useCallback((from) => {
		if (!from || !emit) return;
		emit("popover:dismiss-ancestor", { from });
	}, [emit]);
}

//#endregion
export { useDismissParentPopover, usePopoverSync };
//# sourceMappingURL=usePopoverSync.js.map