/**
 * `<Drawer.Root>` — Solid 2 port of Base UI React's `DrawerRoot`
 * (`packages/react/src/drawer/root/DrawerRoot.tsx`).
 *
 * Composes over `<Dialog.Root>` + `IsDrawerContext(true)` to reuse
 * dialog's open/close/focus-trap/scroll-lock machinery. Adds drawer-
 * specific state:
 *
 *  - `swipeDirection` — dismissal direction (defaults `'down'`).
 *  - `snapPoints` / `snapPoint` / `defaultSnapPoint` — snap-point control.
 *  - Nested-drawer bookkeeping (`popupHeight`, `frontmostHeight`,
 *    `hasNestedDrawer`, `nestedSwiping`, `nestedSwipeProgressStore`).
 *  - `DrawerProvider` reporter — auto-registers the drawer's id with the
 *    enclosing `<Drawer.Provider>` and wires an Android `CloseWatcher` (if
 *    available) for back-gesture support.
 *
 * Papercuts avoided:
 *
 *  - P4: the `<DrawerRootContext>` wraps children via `<Dialog.Root>`'s
 *    children, which already evaluates under the dialog's provider at
 *    the right time.
 *  - P11: `DialogRoot` accepts `children` by value; we thread the drawer
 *    provider + reporter through its children JSX (no `omit` needed here
 *    because we never enter the `renderElement` path).
 */

import { createEffect, createSignal, onCleanup, untrack } from "solid-js";
import { IsDrawerContext, useDialogRootContext } from "../../dialog/dialog-root-context.jsx";
import { DialogRoot } from "../../dialog/root/dialog-root.jsx";
import { useBaseUiId } from "../../internals/base-ui-id.jsx";
import { createChangeEventDetails, REASONS } from "../../internals/change-event-details.jsx";
import { createControlledState } from "../../internals/create-controlled-state.jsx";
import { readOption } from "../../internals/maybe-accessor.jsx";
import { createDrawer } from "../create-drawer.jsx";
import { useDrawerProviderContext } from "../provider/drawer-provider-context.jsx";
import { DrawerRootContext, useDrawerRootContext } from "./drawer-root-context.jsx";

// biome-ignore lint/complexity/noBannedTypes: state is empty — matches React.

function createNestedSwipeProgressStore() {
  let progress = 0;
  const listeners = new Set();
  return {
    getSnapshot: () => progress,
    set(nextProgress) {
      const resolved = Number.isFinite(nextProgress) ? nextProgress : 0;
      if (resolved === progress) return;
      progress = resolved;
      for (const l of listeners) l();
    },
    subscribe(listener) {
      listeners.add(listener);
      return () => {
        listeners.delete(listener);
      };
    }
  };
}

/**
 * Groups all parts of the drawer. Doesn't render its own HTML element.
 */
export function DrawerRoot(props) {
  const parentDrawer = useDrawerRootContext(true);
  const notifyParentSwipeProgressChange = parentDrawer?.onNestedSwipeProgressChange;
  const notifyParentFrontmostHeight = parentDrawer?.onNestedFrontmostHeightChange;
  const notifyParentSwipingChange = parentDrawer?.onNestedSwipingChange;
  const notifyParentHasNestedDrawer = parentDrawer?.onNestedDrawerPresenceChange;

  // Controlled / uncontrolled open state (Base UI `open` / `defaultOpen`
  // contract): the `open` prop wins while its resolved value is defined;
  // otherwise an internal signal (seeded once from `defaultOpen`) backs
  // the state and `handleOpenChange` (below) writes it. In React Base UI
  // this resolution lives in `Dialog.Root` (Drawer passes `open` /
  // `defaultOpen` straight through); here the drawer owns it so its
  // headless primitive sees the resolved open and the inner `<DialogRoot>`
  // stays controlled by it.
  const [resolvedOpen, setUncontrolledOpen] = createControlledState({
    controlled: () => {
      const controlled = readOption(props.open);
      return controlled === undefined ? undefined : Boolean(controlled);
    },
    default: () => readOption(props.defaultOpen),
    fallback: false,
    name: "<Drawer.Root>",
    state: "open"
  });

  // Compose the headless Drawer primitive — owns logical popup state (open,
  // disabled, modal). DOM machinery (DialogRoot's floating tree, swipe
  // gesture, snap-points, transitions, portal, dismiss) stays in this
  // wrapper. `onOpenChange` is NOT wired here — `handleOpenChange` (below)
  // owns the open-change pipeline so it can run snap-point reset bookkeeping.
  const drawerPrimitive = createDrawer({
    open: resolvedOpen,
    modal: () => readOption(props.modal) ?? true
  });
  const swipeDirection = () => props.swipeDirection ?? "down";
  const snapToSequentialPoints = () => props.snapToSequentialPoints ?? false;
  const snapPoints = () => props.snapPoints;

  // Consumer-owned snap-point accessor. Falls back to first snap point when
  // the consumer has not passed a value. No internal signal.
  const activeSnapPointAccessor = () => {
    if (props.snapPoint !== undefined) return props.snapPoint ?? null;
    return props.snapPoints?.[0] ?? null;
  };
  const [popupHeight, setPopupHeight] = createSignal(0, {
    ownedWrite: true
  });
  const [frontmostHeight, setFrontmostHeight] = createSignal(0, {
    ownedWrite: true
  });
  const [hasNestedDrawer, setHasNestedDrawer] = createSignal(false, {
    ownedWrite: true
  });
  const [nestedSwiping, setNestedSwiping] = createSignal(false, {
    ownedWrite: true
  });
  const nestedSwipeProgressStore = createNestedSwipeProgressStore();

  // Track whether a nested drawer is currently open (for frontmost-height
  // bookkeeping) — a plain `let` matches React's `useRef(false)`.
  let isNestedDrawerOpenRef = false;
  const setActiveSnapPoint = (nextSnapPoint, details) => {
    const eventDetails = details ?? createChangeEventDetails(REASONS.none);
    props.onSnapPointChange?.(nextSnapPoint, eventDetails);
    // Consumer's onSnapPointChange handler is the only writer — no internal
    // write. `eventDetails.isCanceled` check preserved for the cancel path.
  };

  // Read through the consumer's accessor — no internal copy.
  const resolvedActiveSnapPoint = () => activeSnapPointAccessor();
  const onPopupHeightChange = height => {
    setPopupHeight(height);
    if (!isNestedDrawerOpenRef && height > 0) setFrontmostHeight(height);
  };

  // Invoked from a child drawer's effect-apply (`drawer-popup` notifies on
  // frontmost-height change). The `popupHeight()` read is a snapshot —
  // wrap in `untrack` so it doesn't trip `[STRICT_READ_UNTRACKED]`.
  const onNestedFrontmostHeightChange = height => untrack(() => {
    if (height > 0) {
      isNestedDrawerOpenRef = true;
      setFrontmostHeight(height);
      return;
    }
    isNestedDrawerOpenRef = false;
    if (popupHeight() > 0) setFrontmostHeight(popupHeight());
  });
  const onNestedDrawerPresenceChange = present => {
    setHasNestedDrawer(present);
  };
  const onNestedSwipeProgressChange = progress => {
    nestedSwipeProgressStore.set(progress);
    notifyParentSwipeProgressChange?.(progress);
  };
  const onNestedSwipingChange = swiping => {
    setNestedSwiping(swiping);
    notifyParentSwipingChange?.(swiping);
  };

  // Every open writer (trigger press, Escape, outside press, close button,
  // swipe dismissal, CloseWatcher, imperative handle) routes through the
  // inner dialog's `setOpen` → `setOpenInternal` → this callback (wired as
  // `<DialogRoot onOpenChange>`). Mirrors Base UI's drawer pipeline: fire
  // the consumer's `onOpenChange`, honor `details.cancel()`, run the
  // snap-point reset, then write the uncontrolled open state — a no-op
  // for reads while the `open` prop is controlled.
  const handleOpenChange = (nextOpen, details) => {
    props.onOpenChange?.(nextOpen, details);
    if (details.isCanceled) return;
    const pts = snapPoints();
    if (!nextOpen && pts && pts.length > 0) {
      // On close, reset to first snap point by notifying the consumer.
      setActiveSnapPoint(pts[0] ?? null, createChangeEventDetails(details.reason, details.event, details.trigger));
    }
    setUncontrolledOpen(nextOpen);
  };
  const contextValue = {
    swipeDirection,
    snapToSequentialPoints,
    snapPoints,
    activeSnapPoint: resolvedActiveSnapPoint,
    setOpen: handleOpenChange,
    setActiveSnapPoint,
    frontmostHeight,
    popupHeight,
    hasNestedDrawer,
    nestedSwiping,
    nestedSwipeProgressStore,
    onPopupHeightChange,
    onNestedFrontmostHeightChange,
    onNestedDrawerPresenceChange,
    onNestedSwipingChange,
    onNestedSwipeProgressChange,
    notifyParentFrontmostHeight,
    notifyParentSwipingChange,
    notifyParentSwipeProgressChange,
    notifyParentHasNestedDrawer
  };
  return <DrawerRootContext value={contextValue}>
			<IsDrawerContext value={true}>
				<DialogRoot open={drawerPrimitive.open()} onOpenChange={handleOpenChange} onOpenChangeComplete={props.onOpenChangeComplete} disablePointerDismissal={props.disablePointerDismissal} modal={drawerPrimitive.modal()} actionsRef={props.actionsRef} handle={props.handle} triggerId={props.triggerId}>
					{
        // If the consumer passed a payload render-function, wrap it so
        // we can inject the provider reporter alongside the resolved
        // JSX. Otherwise render children + reporter directly.
        typeof props.children === "function" ? ctx => <>
									<DrawerProviderReporter />
									{props.children(ctx)}
								</> : <>
								<DrawerProviderReporter />
								{props.children}
							</>}
				</DialogRoot>
			</IsDrawerContext>
		</DrawerRootContext>;
}

/**
 * Reports the drawer's open state to the enclosing `<Drawer.Provider>`
 * (if any) and installs an Android `CloseWatcher` for the back gesture.
 *
 * 1:1 port of the React `DrawerProviderReporter`. Skips the `CloseWatcher`
 * wiring when not on Android — the React source scopes it the same way
 * to avoid interfering with `useDismiss`'s Escape + nesting semantics.
 */
function DrawerProviderReporter() {
  const generatedId = useBaseUiId();
  const drawerId = () => generatedId;
  const providerContext = useDrawerProviderContext(true);
  const dialogRootContext = useDialogRootContext();
  const open = dialogRootContext.open;
  const nestedOpenDialogCount = dialogRootContext.nestedOpenDialogCount;
  const popupElement = dialogRootContext.popupElement;
  const isTopmost = () => nestedOpenDialogCount() === 0;

  // Cleanup registration on unmount.
  onCleanup(() => {
    const id = drawerId();
    if (providerContext && id) providerContext.removeDrawer(id);
  });

  // Forward open-state to the enclosing provider.
  createEffect(() => ({
    o: open(),
    id: drawerId()
  }), ({
    o,
    id
  }) => {
    if (!id) return;
    providerContext?.setDrawerOpen(id, o);
  });

  // Android `CloseWatcher` wiring. Chromium-only; kept Android-only to
  // avoid interfering with Escape handling on desktop.
  createEffect(() => ({
    o: open(),
    topmost: isTopmost(),
    popup: popupElement()
  }), ({
    o,
    topmost,
    popup
  }) => {
    if (!o || !topmost) return undefined;
    const ua = typeof navigator !== "undefined" ? navigator.userAgent : "";
    const isAndroid = /Android/i.test(ua);
    if (!isAndroid) return undefined;
    const win = popup?.ownerDocument?.defaultView ?? globalThis.window;
    const CloseWatcherCtor = win.CloseWatcher;
    if (!CloseWatcherCtor) return undefined;
    const handleCloseWatcher = event => {
      if (!dialogRootContext.open()) return;
      dialogRootContext.setOpen(false, createChangeEventDetails(REASONS.closeWatcher, event));
    };
    const closeWatcher = new CloseWatcherCtor();
    closeWatcher.addEventListener("close", handleCloseWatcher);
    return () => {
      closeWatcher.removeEventListener("close", handleCloseWatcher);
      closeWatcher.destroy?.();
    };
  });
  return null;
}
//# sourceMappingURL=drawer-root.jsx.map
