/**
 * Stage 5 step 1 — Solid port of Base UI React's `FloatingPortal`
 * (`packages/react/src/floating-ui-react/components/FloatingPortal.tsx`).
 *
 * Responsibilities:
 *
 *  - Renders its `children` into a target element (by default `document.body`),
 *    via `@solidjs/web`'s `Portal`. The Solid wrap means we inherit
 *    framework-maintained Portal semantics (owner-tree scoping, hydration
 *    boundary, `<Portal>` unmount tear-down) for free.
 *
 *  - Exposes `FloatingPortalContext` (the React source calls the same context
 *    `PortalContext`) with `portalNode`, `setFocusManagerState`, and four
 *    sentinel refs (`beforeOutside`, `afterOutside`, `beforeInside`,
 *    `afterInside`). The `FloatingFocusManager` port (Stage 5 step 2) reads
 *    these to route tab traversal across the portal boundary.
 *
 *  - CSP-nonce aware. When a `<CSPProvider>` is present, the portal wrapper
 *    `<div data-quoin-portal>` and any injected sentinel `<span>`s carry
 *    the same `nonce=` attribute so strict-CSP consumers don't trip the
 *    browser's inline-element gate.
 *
 *  - Extended target resolution. The React source accepts an element, a ref,
 *    `null`, or nothing (→ `document.body`). The Solid port widens this to
 *    also accept a CSS selector string and a zero-argument function returning
 *    an element, matching the public `@floating-ui/react` `FloatingPortal`
 *    ergonomics.
 *
 *  - Optional auto-created shared root. When `id` is supplied and no element
 *    with that id already exists under the resolved container, the portal
 *    creates a `<div id="...">` inside `document.body`. A module-level
 *    ref-count keeps the auto-created root alive while any `FloatingPortal`
 *    with the same `id` is mounted, and tears it down on last unmount.
 *    External nodes that already live in the DOM are never removed.
 *
 * Port notes / deviations from React:
 *
 *  - React's `useFloatingPortalNode` returns `{ portalNode, portalSubtree }`
 *    so the caller can render the subtree in its JSX tree. The Solid port
 *    collapses that into a single `<Portal>` + inner wrapper div — Solid's
 *    Portal mounts its children into the target DOM subtree already, so the
 *    two-hop dance (React portals the wrapper, then portals the children
 *    into the wrapper) isn't needed.
 *  - `useFloatingPortalNode` is exposed as a hook that returns an accessor
 *    for the current portal node. It reads from `FloatingPortalContext`, so
 *    it's only meaningful inside a mounted `FloatingPortal`.
 *  - The FocusGuard helper's Safari `role="button"` dance is dropped — the
 *    Solid port doesn't ship `isSafari` under `detect-browser.ts` and the
 *    guards here are structurally `tabindex=0` visually-hidden spans. AT
 *    parity with Safari's VoiceOver can be restored when a shared Solid
 *    `FocusGuard` lands.
 *  - `focusInsideDisabledRef` / `enableFocusInside` / `disableFocusInside`
 *    wiring from the React source is preserved so focus traps can dock
 *    against the portal boundary.
 */

import { getParentNode, isElement, isNode } from "@floating-ui/utils/dom";
import { Portal } from "@solidjs/web";
import { createContext, createEffect, createSignal, useContext } from "solid-js";
import { useCspNonce } from "../../../csp-provider/csp-context.jsx";
import { createChangeEventDetails } from "../../change-event-details.jsx";
import { FocusGuard } from "../../popups/focus-guard.jsx";
import { useIsoLayoutEffect } from "../../use-iso-layout-effect.jsx";
import { createAttribute } from "../utils/create-attribute.jsx";
import { disableFocusInside, enableFocusInside, getNextTabbable, getPreviousTabbable, isOutsideEvent } from "../utils/tabbable.jsx";

/**
 * Upward-flowing state written by a `FloatingFocusManager` that is mounted
 * as a descendant of the portal. The portal uses this to decide whether to
 * render tab-guard sentinels and where to bounce focus on guard activation.
 */

/**
 * Context value shared with descendants of `<FloatingPortal>`.
 */

const FloatingPortalContext = createContext(null);

/**
 * Read the nearest `FloatingPortalContext`. Returns `null` when used
 * outside a mounted `<FloatingPortal>`.
 */
export function usePortalContext() {
  return useContext(FloatingPortalContext);
}

/**
 * Returns the resolved portal node (the wrapper `<div>`) of the nearest
 * `<FloatingPortal>`. Returns `null` when used outside a mounted portal or
 * before the portal has committed.
 */
export function useFloatingPortalNode() {
  const ctx = usePortalContext();
  return () => ctx?.portalNode ?? null;
}
const PORTAL_ATTR = createAttribute("portal");

/**
 * Logical portal ancestry — the Solid analogue of Base UI React's
 * `insideReactTree` latch. React marks presses via synthetic capture
 * handlers on the popup (`onClickCapture` etc. in `useDismiss`'s floating
 * bag), and those fire even for presses inside PORTALED descendants
 * because React portals propagate synthetic events through the component
 * tree. Solid events follow the DOM tree, so a press inside a nested
 * portaled popup (a Select popup inside a Dialog, say) never reaches the
 * dialog popup's handlers and would read as an outside press, dismissing
 * the ancestor overlay.
 *
 * Instead, every `<FloatingPortal>` records its logical parent portal —
 * the nearest portal in the COMPONENT tree, which context preserves even
 * across DOM portals — and stamps its wrapper element with that chain.
 * Dismissal hooks then ask "is the press target inside a portal that is a
 * logical DESCENDANT of my own portal?" (`isInsideNestedPortal`),
 * recovering the logical-tree walk Base UI gets from React for free.
 */

const portalAncestry = new WeakMap();

/**
 * Walk up from `start` (crossing shadow boundaries) to the nearest
 * `<FloatingPortal>` wrapper and return its ancestry node, or `null` when
 * the element is not inside any quoin portal.
 */
function nearestPortalAncestryNode(start) {
  let el = start;
  while (el) {
    const node = portalAncestry.get(el);
    if (node) {
      return node;
    }
    const parent = getParentNode(el);
    if (parent === el || !isElement(parent)) {
      return null;
    }
    el = parent;
  }
  return null;
}

/**
 * Whether `target` sits inside a portal that is a logical (component-tree)
 * descendant of the portal hosting `ownElement`. Strict descendants only:
 * a press inside our own portal wrapper but not in a nested portal (e.g.
 * on the dialog's internal backdrop or viewport) is still an outside
 * press for the popup itself.
 */
export function isInsideNestedPortal(target, ownElement) {
  if (!ownElement || !isElement(target)) {
    return false;
  }
  const own = nearestPortalAncestryNode(ownElement);
  if (!own) {
    return false;
  }
  const targetNode = nearestPortalAncestryNode(target);
  if (!targetNode || targetNode === own) {
    return false;
  }
  for (let node = targetNode.parent; node; node = node.parent) {
    if (node === own) {
      return true;
    }
  }
  return false;
}

/**
 * Target the user can supply. Ports the React source's `container` shape
 * (element / ref / null / undefined) and adds:
 *
 *   - a CSS selector string (resolved via `document.querySelector`);
 *   - a zero-arg function returning any of the above.
 */

// Module-level ref-count for auto-created `id` roots. Keyed by id; value is
// the count of `FloatingPortal` instances currently mounted that are using
// an auto-created root with that id. The fourth tuple entry is the element
// itself so we can tear it down when the count drops to zero.
const autoRootRegistry = new Map();
function isShadowRoot(value) {
  return typeof ShadowRoot !== "undefined" && value instanceof ShadowRoot;
}
function resolveTarget(target) {
  if (target == null) {
    return null;
  }

  // Function form — unwrap once, then re-resolve.
  if (typeof target === "function") {
    const inner = target();
    return resolveTarget(inner);
  }
  if (typeof target === "string") {
    if (typeof document === "undefined") {
      return null;
    }
    const found = document.querySelector(target);
    return found ?? null;
  }
  if (isNode(target) || isShadowRoot(target)) {
    return target;
  }

  // Ref-like object with `current`.
  if ("current" in target) {
    const current = target.current;
    if (current == null) {
      return null;
    }
    return current;
  }
  return null;
}
/**
 * Portals the floating element into the body (or a supplied target),
 * carrying CSP nonce and focus-guard plumbing for strict-CSP consumers and
 * `FloatingFocusManager` tab traversal.
 *
 * @see https://floating-ui.com/docs/FloatingPortal
 */
export function FloatingPortal(props) {
  const parentPortal = usePortalContext();
  const nonce = useCspNonce();

  // Mutable mirror of the portal node — reads in the context getter return
  // the latest value even before Solid 2's signal flush lands (same pattern
  // as the toast store). The signal exists to wake reactive
  // consumers (effects, memos) when the mount completes.
  const portalNodeRef = {
    current: null
  };
  const [portalNodeSignal, setPortalNodeSignal] = createSignal(null, {
    equals: false
  });
  const setPortalNode = node => {
    portalNodeRef.current = node;
    setPortalNodeSignal(node);
  };
  const portalNode = () => portalNodeSignal();

  // `ownedWrite: true` — `setFocusManagerState` is invoked from a descendant
  // `<FloatingFocusManager>`'s `useIsoLayoutEffect` (a `createRenderEffect`
  // apply function), which is owned scope. Under Solid 2's bundled runtime,
  // writing to a signal from owned scope during the initial insert of the
  // surrounding `<Portal>`'s children aborts the pending `insert()` — the
  // wrapper `<div>`'s ref callback fires but the rest of the subtree never
  // attaches to `document.body`. This is the root cause of an issue where
  // the portal looks like it's mounting (trigger flips
  // `data-popup-open`, `mounted()` is true, `<Show>` is true, FloatingPortal
  // body runs) but the portal subtree never materializes. Opting into
  // `ownedWrite` lets the descendant-effect write land without disturbing
  // the ongoing insert.
  const [focusManagerState, setFocusManagerState] = createSignal(null, {
    ownedWrite: true
  });

  // Sentinel refs — mutable slots so the focus-manager descendant can
  // read them without a reactive dependency.
  const beforeOutsideRef = {
    current: null
  };
  const afterOutsideRef = {
    current: null
  };
  const beforeInsideRef = {
    current: null
  };
  const afterInsideRef = {
    current: null
  };
  const focusInsideDisabledRef = {
    current: false
  };

  // Resolve mount target reactively — string selectors / thunks re-run on
  // each access, so if the prop value flips between renders we see it.
  const resolveMount = () => {
    const explicit = props.root ?? props.container;
    // Walk up to the parent portal node if nothing explicit and a parent
    // portal is mounted — matches React's `parentPortalNode ?? body`.
    const resolved = resolveTarget(explicit) ?? parentPortal?.portalNode ?? (typeof document !== "undefined" ? document.body : null);
    if (!resolved) {
      return undefined;
    }

    // Solid's `<Portal>` accepts `Element`; when a ShadowRoot is handed
    // in we coerce via `.host`-less `as Element` cast because happy-dom
    // treats ShadowRoot as Element-alike.
    return resolved;
  };

  // If an `id` is supplied, find-or-create the shared root under the
  // resolved target, ref-counted so the last-unmount tears down the
  // auto-created node. Handled in a layout effect so the DOM work runs
  // before children mount into the wrapper.
  let autoCreatedId = null;
  useIsoLayoutEffect(() => undefined, () => {
    const id = props.id;
    if (!id || typeof document === "undefined") {
      return;
    }
    const existing = document.getElementById(id);
    if (existing && !autoRootRegistry.has(id)) {
      // External node (not one we've auto-created). Leave it alone
      // on cleanup — first-creator-wins.
      return;
    }
    const registryEntry = autoRootRegistry.get(id);
    if (registryEntry) {
      registryEntry.refCount += 1;
      autoCreatedId = id;
    } else {
      const target = resolveMount();
      const parent = target ?? document.body;
      const div = document.createElement("div");
      div.id = id;
      const nonceValue = nonce();
      if (nonceValue) {
        div.setAttribute("nonce", nonceValue);
      }
      parent.appendChild(div);
      autoRootRegistry.set(id, {
        element: div,
        refCount: 1,
        ownerDocument: document
      });
      autoCreatedId = id;
    }
    return () => {
      if (!autoCreatedId) {
        return;
      }
      const entry = autoRootRegistry.get(autoCreatedId);
      if (!entry) {
        return;
      }
      entry.refCount -= 1;
      if (entry.refCount <= 0) {
        entry.element.parentNode?.removeChild(entry.element);
        autoRootRegistry.delete(autoCreatedId);
      }
    };
  });

  // Logical parent link for nested-portal dismissal checks. The parent is
  // resolved through context, so the chain follows the component tree even
  // though the wrapper DOM lands wherever `resolveMount()` points.
  const ancestryNode = {
    parent: parentPortal?.ancestryNode ?? null
  };

  // Ref callback for the portal wrapper `<div>`. Fires once the element
  // is attached to the DOM subtree inside the Solid `<Portal>`.
  const attachWrapper = element => {
    portalAncestry.set(element, ancestryNode);
    setPortalNode(element);
  };

  // React's focusin/focusout capture — disable focus inside the portal
  // until it's actually entered via tabbing/mouse.
  createEffect(() => ({
    node: portalNode(),
    modal: focusManagerState()?.modal
  }), ({
    node,
    modal
  }) => {
    if (!node || modal) {
      return;
    }
    function onFocus(event) {
      if (node && event.relatedTarget && isOutsideEvent(event)) {
        if (event.type === "focusin") {
          if (focusInsideDisabledRef.current) {
            enableFocusInside(node);
            focusInsideDisabledRef.current = false;
          }
        } else {
          disableFocusInside(node);
          focusInsideDisabledRef.current = true;
        }
      }
    }
    node.addEventListener("focusin", onFocus, true);
    node.addEventListener("focusout", onFocus, true);
    return () => {
      node.removeEventListener("focusin", onFocus, true);
      node.removeEventListener("focusout", onFocus, true);
    };
  });

  // Mirror React's `open === false` path: re-enable focus when the
  // managed surface closes.
  createEffect(() => ({
    node: portalNode(),
    open: focusManagerState()?.open
  }), ({
    node,
    open
  }) => {
    if (!node || open !== false) {
      return;
    }
    enableFocusInside(node);
    focusInsideDisabledRef.current = false;
  });
  const shouldRenderGuards = () => {
    if (typeof props.renderGuards === "boolean") {
      return props.renderGuards;
    }
    const state = focusManagerState();
    return !!state && !state.modal && state.open && portalNode() !== null;
  };
  const contextValue = {
    // Read through the mutable mirror so downstream consumers that peek
    // synchronously (e.g. `useFloatingPortalNode` called from a probe)
    // see the value as soon as the ref callback has fired, without
    // waiting for Solid 2's signal flush.
    get portalNode() {
      return portalNodeRef.current;
    },
    setFocusManagerState: state => setFocusManagerState(state),
    beforeOutsideRef,
    afterOutsideRef,
    beforeInsideRef,
    afterInsideRef,
    ancestryNode
  };

  // Apply nonce to injected visually-hidden sentinel spans. Kept as an
  // inline style literal (Solid spreads JSX.CSSProperties) — matches
  // React's `ownerVisuallyHidden` import. The focus-guard sentinels
  // themselves now come from the shared `FocusGuard` module (which
  // uses `VISUALLY_HIDDEN_STYLE` internally); only the non-guard
  // `aria-owns` span below uses this inline literal.
  const hiddenStyle = {
    border: 0,
    clip: "rect(0 0 0 0)",
    height: "1px",
    margin: "-1px",
    overflow: "hidden",
    padding: 0,
    position: "absolute",
    "white-space": "nowrap",
    width: "1px"
  };
  return <Portal mount={resolveMount()}>
			<FloatingPortalContext value={contextValue}>
				{shouldRenderGuards() ? <FocusGuard ref={el => {
        beforeOutsideRef.current = el;
      }} data-type="outside" nonce={nonce()} onFocus={event => {
        const node = portalNode();
        if (!node) {
          return;
        }
        if (isOutsideEvent(event, node)) {
          beforeInsideRef.current?.focus();
        } else {
          const state = focusManagerState();
          const domReference = state ? state.domReference : null;
          const prevTabbable = getPreviousTabbable(domReference);
          prevTabbable?.focus();
        }
      }} /> : null}
				{shouldRenderGuards() && portalNode() ? <span aria-owns={portalNode()?.id} style={hiddenStyle} nonce={nonce()} /> : null}
				<div ref={attachWrapper} id={props.id} {...{
        [PORTAL_ATTR]: ""
      }} nonce={nonce()} {...props.wrapperProps ?? {}}>
					{props.children}
				</div>
				{shouldRenderGuards() ? <FocusGuard ref={el => {
        afterOutsideRef.current = el;
      }} data-type="outside" nonce={nonce()} onFocus={event => {
        const node = portalNode();
        if (!node) {
          return;
        }
        if (isOutsideEvent(event, node)) {
          afterInsideRef.current?.focus();
        } else {
          const state = focusManagerState();
          const domReference = state ? state.domReference : null;
          const nextTabbable = getNextTabbable(domReference);
          nextTabbable?.focus();
          if (state?.closeOnFocusOut) {
            state.onOpenChange(false, createChangeEventDetails("focus-out", event));
          }
        }
      }} /> : null}
			</FloatingPortalContext>
		</Portal>;
}
export { FloatingPortalContext };
//# sourceMappingURL=floating-portal.jsx.map
