/**
 * Port of Base UI React's `@base-ui/utils/useScrollLock`
 * (`packages/utils/src/useScrollLock.ts`).
 *
 * Single global scroll lock with ref-counting, `<html>`/body inset-scrollbar
 * compensation, iOS overlay-scrollbar path, and resize handling. This
 * supersedes the minimal `use-anchored-popup-scroll-lock` overflow toggle —
 * that file is now a thin wrapper that forwards to this module so existing
 * popover / tooltip call sites keep working without changes.
 *
 * ## React → Solid deviations
 *
 * - The React hook body uses `useIsoLayoutEffect` — here it maps to
 *   `createRenderEffect` via the `useIsoLayoutEffect` alias. The compute
 *   half re-reads `enabled` / `referenceElement` and the apply half
 *   returns a disposer; Solid's effect machinery invokes the previous
 *   disposer on every re-run and on owner dispose, mirroring React's
 *   useEffect cleanup semantics.
 * - `Accessor<T> | T` is accepted for both inputs so consumers don't have
 *   to wrap bare values. Matches the hook-parameter shape elsewhere in
 *   the port.
 * - `addEventListener` is inlined (one-off use for the window resize
 *   listener) since this codebase doesn't have a dedicated helper.
 *
 * ## happy-dom gaps
 *
 * Real-browser-only paths that unit tests cannot exercise:
 *
 * - **Inset-scrollbar compensation**: `isOverflowElement(html)` + `body`
 *   height/width rewriting requires real layout metrics. happy-dom
 *   returns 0 for `offsetWidth` / `clientWidth` — the inset branch runs
 *   but writes zero-valued styles.
 * - **iOS overlay-scrollbar path**: gated on `isIOS`; test env reports
 *   `false` so the path is inert.
 * - **`scrollbar-gutter: stable` detection**: relies on `CSS.supports` +
 *   real `offsetWidth` diffs; result in happy-dom is environment-
 *   dependent.
 * - **`visualViewport.scale` pinch-zoom bail-out**: happy-dom has no
 *   `visualViewport`.
 * - **Window `resize` event**: happy-dom dispatches events but doesn't
 *   emit native layout changes.
 *
 * The unit tests in `test/internals/popups/use-scroll-lock.test.tsx`
 * cover the ref-counting state machine, the `overflow: hidden` / `clip`
 * site-author bailout, and basic enable/disable lifecycle.
 */

import { isOverflowElement } from "@floating-ui/utils/dom";
import { createRenderEffect } from "solid-js";
import { ownerDocument } from "../dom/owner-document.jsx";
import { ownerWindow } from "../dom/owner-window.jsx";
// `isSafari` stands in for `isWebKit` here — the Solid port's
// `detect-browser` only exports `isSafari` (navigator.vendor check), which
// overlaps the WebKit path for the purposes of the pinch-zoom bail-out.
// A dedicated `isWebKit` would land as a separate internals update.
import { isIOS, isSafari as isWebKit } from "../floating-ui/detect-browser.jsx";
import { AnimationFrame } from "../use-animation-frame.jsx";
import { Timeout } from "../use-timeout.jsx";
const NOOP = () => {};

// Module-global state — matches the React source's single `ScrollLocker`
// instance + top-level `originalHtmlStyles` / `originalBodyStyles`
// caches. Ref-counting is shared across all components that acquire a
// lock, so two simultaneously-open dialogs acquire the lock once and
// release it once.
let originalHtmlStyles = {};
let originalBodyStyles = {};
let originalHtmlScrollBehavior = "";
function hasInsetScrollbars(referenceElement) {
  if (typeof document === "undefined") {
    return false;
  }
  const doc = ownerDocument(referenceElement);
  const win = ownerWindow(doc.documentElement);
  return win.innerWidth - doc.documentElement.clientWidth > 0;
}
function supportsStableScrollbarGutter(referenceElement) {
  const supported = typeof CSS !== "undefined" && CSS.supports && CSS.supports("scrollbar-gutter", "stable");
  if (!supported || typeof document === "undefined") {
    return false;
  }
  const doc = ownerDocument(referenceElement);
  const html = doc.documentElement;
  const body = doc.body;
  const scrollContainer = isOverflowElement(html) ? html : body;
  const originalScrollContainerOverflowY = scrollContainer.style.overflowY;
  const originalHtmlStyleGutter = html.style.scrollbarGutter;
  html.style.scrollbarGutter = "stable";
  scrollContainer.style.overflowY = "scroll";
  const before = scrollContainer.offsetWidth;
  scrollContainer.style.overflowY = "hidden";
  const after = scrollContainer.offsetWidth;
  scrollContainer.style.overflowY = originalScrollContainerOverflowY;
  html.style.scrollbarGutter = originalHtmlStyleGutter;
  return before === after;
}

/**
 * Overlay-scrollbar path — used on iOS and whenever the document has no
 * inset scrollbars. Locks the overflowing element's `overflow-x` /
 * `overflow-y` to `hidden` without attempting body geometry compensation.
 */
function preventScrollOverlayScrollbars(referenceElement) {
  const doc = ownerDocument(referenceElement);
  const html = doc.documentElement;
  const body = doc.body;
  const elementToLock = isOverflowElement(html) ? html : body;
  const originalElementToLockStyles = {
    overflowY: elementToLock.style.overflowY,
    overflowX: elementToLock.style.overflowX
  };
  Object.assign(elementToLock.style, {
    overflowY: "hidden",
    overflowX: "hidden"
  });
  return () => {
    Object.assign(elementToLock.style, originalElementToLockStyles);
  };
}

/**
 * Inset-scrollbar path — used when the document has classic inset
 * scrollbars (most desktops). Rewrites `<html>` and `<body>` to lock the
 * scroll position while preserving scrollbar width through either
 * `scrollbar-gutter: stable` or a `calc(100dvh - ...)` / `calc(100vw - ...)`
 * body clamp. Mirrors the React source 1:1.
 */
function preventScrollInsetScrollbars(referenceElement) {
  const doc = ownerDocument(referenceElement);
  const html = doc.documentElement;
  const body = doc.body;
  const win = ownerWindow(html);
  let scrollTop = 0;
  let scrollLeft = 0;
  let updateGutterOnly = false;
  const resizeFrame = AnimationFrame.create();

  // Pinch-zoom in Safari causes a shift. Just don't lock scroll if there's any pinch-zoom.
  if (isWebKit && (win.visualViewport?.scale ?? 1) !== 1) {
    return NOOP;
  }
  function lockScroll() {
    /* DOM reads: */
    const htmlStyles = win.getComputedStyle(html);
    const bodyStyles = win.getComputedStyle(body);
    const htmlScrollbarGutterValue = htmlStyles.scrollbarGutter || "";
    const hasBothEdges = htmlScrollbarGutterValue.includes("both-edges");
    const scrollbarGutterValue = hasBothEdges ? "stable both-edges" : "stable";
    scrollTop = html.scrollTop;
    scrollLeft = html.scrollLeft;
    originalHtmlStyles = {
      scrollbarGutter: html.style.scrollbarGutter,
      overflowY: html.style.overflowY,
      overflowX: html.style.overflowX
    };
    originalHtmlScrollBehavior = html.style.scrollBehavior;
    originalBodyStyles = {
      position: body.style.position,
      height: body.style.height,
      width: body.style.width,
      boxSizing: body.style.boxSizing,
      overflowY: body.style.overflowY,
      overflowX: body.style.overflowX,
      scrollBehavior: body.style.scrollBehavior
    };
    const isScrollableY = html.scrollHeight > html.clientHeight;
    const isScrollableX = html.scrollWidth > html.clientWidth;
    const hasConstantOverflowY = htmlStyles.overflowY === "scroll" || bodyStyles.overflowY === "scroll";
    const hasConstantOverflowX = htmlStyles.overflowX === "scroll" || bodyStyles.overflowX === "scroll";

    // Values can be negative in Firefox
    const scrollbarWidth = Math.max(0, win.innerWidth - body.clientWidth);
    const scrollbarHeight = Math.max(0, win.innerHeight - body.clientHeight);
    const marginY = parseFloat(bodyStyles.marginTop) + parseFloat(bodyStyles.marginBottom);
    const marginX = parseFloat(bodyStyles.marginLeft) + parseFloat(bodyStyles.marginRight);
    const elementToLock = isOverflowElement(html) ? html : body;
    updateGutterOnly = supportsStableScrollbarGutter(referenceElement);

    /*
     * DOM writes:
     * Do not read the DOM past this point!
     */

    if (updateGutterOnly) {
      html.style.scrollbarGutter = scrollbarGutterValue;
      elementToLock.style.overflowY = "hidden";
      elementToLock.style.overflowX = "hidden";
      return;
    }
    Object.assign(html.style, {
      scrollbarGutter: scrollbarGutterValue,
      overflowY: "hidden",
      overflowX: "hidden"
    });
    if (isScrollableY || hasConstantOverflowY) {
      html.style.overflowY = "scroll";
    }
    if (isScrollableX || hasConstantOverflowX) {
      html.style.overflowX = "scroll";
    }
    Object.assign(body.style, {
      position: "relative",
      height: marginY || scrollbarHeight ? `calc(100dvh - ${marginY + scrollbarHeight}px)` : "100dvh",
      width: marginX || scrollbarWidth ? `calc(100vw - ${marginX + scrollbarWidth}px)` : "100vw",
      boxSizing: "border-box",
      overflow: "hidden",
      scrollBehavior: "unset"
    });
    body.scrollTop = scrollTop;
    body.scrollLeft = scrollLeft;
    html.setAttribute("data-quoin-scroll-locked", "");
    html.style.scrollBehavior = "unset";
  }
  function cleanup() {
    Object.assign(html.style, originalHtmlStyles);
    Object.assign(body.style, originalBodyStyles);
    if (!updateGutterOnly) {
      html.scrollTop = scrollTop;
      html.scrollLeft = scrollLeft;
      html.removeAttribute("data-quoin-scroll-locked");
      html.style.scrollBehavior = originalHtmlScrollBehavior;
    }
  }
  function handleResize() {
    cleanup();
    resizeFrame.request(lockScroll);
  }
  lockScroll();
  win.addEventListener("resize", handleResize);
  return () => {
    resizeFrame.cancel();
    cleanup();
    // Sometimes this cleanup can run after test teardown. Guard so we
    // don't crash.
    if (typeof win.removeEventListener === "function") {
      win.removeEventListener("resize", handleResize);
    }
  };
}

/**
 * Singleton ref-counted lock. First acquire installs the lock (via a
 * `setTimeout(fn, 0)` so simultaneous opens coalesce); last release
 * schedules cleanup on another 0-delay timeout so enter/exit animations
 * with brief re-enables don't thrash.
 */
class ScrollLocker {
  lockCount = 0;
  restore = null;
  timeoutLock = Timeout.create();
  timeoutUnlock = Timeout.create();
  acquire(referenceElement) {
    this.lockCount += 1;
    if (this.lockCount === 1 && this.restore === null) {
      this.timeoutLock.start(0, () => this.lock(referenceElement));
    }
    return this.release;
  }
  release = () => {
    this.lockCount -= 1;
    if (this.lockCount === 0 && this.restore) {
      this.timeoutUnlock.start(0, this.unlock);
    }
  };
  unlock = () => {
    if (this.lockCount === 0 && this.restore) {
      this.restore();
      this.restore = null;
    }
  };
  lock(referenceElement) {
    if (this.lockCount === 0 || this.restore !== null) {
      return;
    }
    if (typeof document === "undefined") {
      this.restore = NOOP;
      return;
    }
    const doc = ownerDocument(referenceElement);
    const html = doc.documentElement;
    const htmlOverflowY = ownerWindow(html).getComputedStyle(html).overflowY;

    // If the site author already hid overflow on <html>, respect it and bail.
    if (htmlOverflowY === "hidden" || htmlOverflowY === "clip") {
      this.restore = NOOP;
      return;
    }
    const hasOverlayScrollbars = isIOS || !hasInsetScrollbars(referenceElement);

    // On iOS, scroll locking does not work if the navbar is collapsed.
    // See the React source's comment for the history of issues that
    // gate iOS-specific behavior.
    this.restore = hasOverlayScrollbars ? preventScrollOverlayScrollbars(referenceElement) : preventScrollInsetScrollbars(referenceElement);
  }
}
const SCROLL_LOCKER = new ScrollLocker();

/**
 * Inputs for `useScrollLock`. Both parameters accept a raw value or a
 * Solid `Accessor<T>` — passing an accessor lets the hook react to prop
 * changes without the caller needing to set up their own effect.
 */

function resolve(input) {
  return typeof input === "function" ? input() : input;
}

/**
 * Locks the scroll of the document when `enabled` is true. Ref-counted
 * globally — multiple simultaneous callers share one lock/unlock cycle.
 *
 * Usage:
 *
 * ```ts
 * useScrollLock({
 *   enabled: () => props.modal === true && open(),
 *   referenceElement: popupElement,
 * });
 * ```
 */
export function useScrollLock(parameters = {}) {
  createRenderEffect(() => ({
    enabled: resolve(parameters.enabled ?? true),
    reference: resolve(parameters.referenceElement ?? null)
  }), ({
    enabled,
    reference
  }) => {
    if (!enabled) return undefined;
    const release = SCROLL_LOCKER.acquire(reference);
    return release;
  });
}

/**
 * Test-only peek into the lock state. Not part of the public hook
 * surface; tests import it directly.
 *
 * @internal
 */
export function __peekScrollLockerState() {
  return {
    count: SCROLL_LOCKER.lockCount,
    locked: SCROLL_LOCKER.restore !== null
  };
}

/**
 * Test-only reset. Forces the singleton back to its pristine state so
 * tests don't leak ref counts across each other.
 *
 * @internal
 */
export function __resetScrollLockerForTests() {
  SCROLL_LOCKER.timeoutLock.clear();
  SCROLL_LOCKER.timeoutUnlock.clear();
  if (SCROLL_LOCKER.restore) {
    SCROLL_LOCKER.restore();
    SCROLL_LOCKER.restore = null;
  }
  SCROLL_LOCKER.lockCount = 0;
}
//# sourceMappingURL=use-scroll-lock.jsx.map
