import { dynamic } from "@solidjs/web";
import { createEffect, createRenderEffect, createSignal, omit } from "solid-js";
import { useDirection } from "../../direction-provider/direction-provider-context.jsx";
import { composeRefs, userRef } from "../../internals/compose-refs.jsx";
import { mergeRootProps } from "../../internals/merge-root-props.jsx";
import { resolveClassStyle } from "../../internals/resolve-class-style.jsx";
import { normalizeScrollOffset } from "../../internals/scroll-area-utils/scroll-edges.jsx";
import { DISABLE_SCROLLBAR_CLASS_NAME, styleDisableScrollbar } from "../../internals/scroll-area-utils/style-disable-scrollbar.jsx";
import { clamp } from "../../internals/slider-utils/clamp.jsx";
import { createStableCallback } from "../../internals/stable-callback.jsx";
import { useTimeout } from "../../internals/use-timeout.jsx";
import { MIN_THUMB_SIZE } from "../constants.jsx";
import { useScrollAreaRootContext } from "../root/scroll-area-root-context.jsx";
import { getOffset } from "../utils/get-offset.jsx";
import { scrollAreaDataAttrs } from "../utils/state-attrs.jsx";
import { ScrollAreaViewportContext } from "./scroll-area-viewport-context.jsx";
import { ScrollAreaViewportCssVars } from "./scroll-area-viewport-css-vars.jsx";
// Cross-component latch. `CSS.registerProperty` is global, so we guard
// against re-registering when multiple scroll areas mount.
let scrollAreaOverflowVarsRegistered = false;

/**
 * Opts the scroll-area overflow CSS variables out of inheritance. With
 * `inherits: false`, descendants that want to read these vars must opt in
 * explicitly via `inherit`. Safari (WebKit) has bugs around that opt-in so
 * we skip the optimization there.
 */
function removeCSSVariableInheritance() {
  if (scrollAreaOverflowVarsRegistered) return;
  // Feature-detect WebKit via navigator — happy-dom exposes
  // `navigator.userAgent` without the WebKit markers, so the check is a
  // no-op in tests which is correct.
  const isWebKit = typeof navigator !== "undefined" && /AppleWebKit/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
  if (isWebKit) return;
  if (typeof CSS === "undefined" || !("registerProperty" in CSS)) {
    scrollAreaOverflowVarsRegistered = true;
    return;
  }
  for (const name of [ScrollAreaViewportCssVars.scrollAreaOverflowXStart, ScrollAreaViewportCssVars.scrollAreaOverflowXEnd, ScrollAreaViewportCssVars.scrollAreaOverflowYStart, ScrollAreaViewportCssVars.scrollAreaOverflowYEnd]) {
    try {
      CSS.registerProperty({
        name,
        syntax: "<length>",
        inherits: false,
        initialValue: "0px"
      });
    } catch {
      // Already registered — ignore.
    }
  }
  scrollAreaOverflowVarsRegistered = true;
}
function getHiddenState(viewport) {
  const y = viewport.clientHeight >= viewport.scrollHeight;
  const x = viewport.clientWidth >= viewport.scrollWidth;
  return {
    y,
    x,
    corner: y || x
  };
}
function mergeHiddenState(prev, next) {
  if (prev.y === next.y && prev.x === next.x && prev.corner === next.corner) {
    return prev;
  }
  return next;
}

/**
 * The scrollable container of the scroll area. Renders a `<div>` element.
 *
 * Responsibilities:
 *
 *  - Hides the native scrollbars via the shared `.base-ui-disable-scrollbar`
 *    class (the root injects the matching stylesheet).
 *  - Publishes thumb/corner sizes, per-axis hidden state, overflow-edge
 *    flags, and per-edge scroll-offset CSS vars for descendant scrollbars.
 *  - Observes the viewport with a `ResizeObserver` and recomputes the thumb
 *    on size changes. Also recomputes after in-progress animations settle.
 */
export function ScrollAreaViewport(props) {
  const context = useScrollAreaRootContext();
  const direction = useDirection();

  // Tracks whether the most-recent scroll was user-driven. Flipped to
  // false by any interaction that implies user intent; reset to true
  // after ~100ms of no scroll events (matches React's programmatic-scroll
  // detection).
  let programmaticScroll = true;
  const lastMeasuredViewportMetrics = [Number.NaN, Number.NaN, Number.NaN, Number.NaN];
  const scrollEndTimeout = useTimeout();
  const waitForAnimationsTimeout = useTimeout();

  // Local element signal so effects retrigger when the viewport mounts.
  // Context refs are imperative containers, not signals.
  const [viewportEl, setViewportEl] = createSignal(null, {
    ownedWrite: true
  });
  const computeThumbPosition = createStableCallback(() => () => {
    const viewportEl = context.viewportRef.current;
    const scrollbarYEl = context.scrollbarYRef.current;
    const scrollbarXEl = context.scrollbarXRef.current;
    const thumbYEl = context.thumbYRef.current;
    const thumbXEl = context.thumbXRef.current;
    const cornerEl = context.cornerRef.current;
    if (!viewportEl) return;
    const scrollableContentHeight = viewportEl.scrollHeight;
    const scrollableContentWidth = viewportEl.scrollWidth;
    const viewportHeight = viewportEl.clientHeight;
    const viewportWidth = viewportEl.clientWidth;
    const scrollTop = viewportEl.scrollTop;
    const scrollLeft = viewportEl.scrollLeft;
    const isFirstMeasurement = Number.isNaN(lastMeasuredViewportMetrics[0]);
    lastMeasuredViewportMetrics[0] = viewportHeight;
    lastMeasuredViewportMetrics[1] = scrollableContentHeight;
    lastMeasuredViewportMetrics[2] = viewportWidth;
    lastMeasuredViewportMetrics[3] = scrollableContentWidth;
    if (isFirstMeasurement) {
      context.setHasMeasuredScrollbar(true);
    }
    if (scrollableContentHeight === 0 || scrollableContentWidth === 0) {
      // Matches React `ScrollAreaViewport.tsx:142-144` — with zero
      // content there's no meaningful hidden-state to publish, so bail
      // before doing any further work. A later ResizeObserver callback
      // will re-run this once the content mounts.
      return;
    }
    const nextHiddenState = getHiddenState(viewportEl);
    const scrollbarYHidden = nextHiddenState.y;
    const scrollbarXHidden = nextHiddenState.x;
    const ratioX = viewportWidth / scrollableContentWidth;
    const ratioY = viewportHeight / scrollableContentHeight;
    const maxScrollLeft = Math.max(0, scrollableContentWidth - viewportWidth);
    const maxScrollTop = Math.max(0, scrollableContentHeight - viewportHeight);
    let scrollLeftFromStart = 0;
    let scrollLeftFromEnd = 0;
    if (!scrollbarXHidden) {
      const rawScrollLeftFromStart = direction() === "rtl" ? clamp(-scrollLeft, 0, maxScrollLeft) : clamp(scrollLeft, 0, maxScrollLeft);
      scrollLeftFromStart = normalizeScrollOffset(rawScrollLeftFromStart, maxScrollLeft);
      scrollLeftFromEnd = maxScrollLeft - scrollLeftFromStart;
    }
    const rawScrollTopFromStart = !scrollbarYHidden ? clamp(scrollTop, 0, maxScrollTop) : 0;
    const scrollTopFromStart = !scrollbarYHidden ? normalizeScrollOffset(rawScrollTopFromStart, maxScrollTop) : 0;
    const scrollTopFromEnd = !scrollbarYHidden ? maxScrollTop - scrollTopFromStart : 0;
    const nextWidth = scrollbarXHidden ? 0 : viewportWidth;
    const nextHeight = scrollbarYHidden ? 0 : viewportHeight;
    let nextCornerWidth = 0;
    let nextCornerHeight = 0;
    if (!scrollbarXHidden && !scrollbarYHidden) {
      nextCornerWidth = scrollbarYEl?.offsetWidth ?? 0;
      nextCornerHeight = scrollbarXEl?.offsetHeight ?? 0;
    }

    // Only subtract corner size from scrollbar dimensions on the first
    // pass, before the layout has been told about the corner. After
    // that, layout already accounts for it.
    const cornerSize = context.cornerSize();
    const cornerNotYetSized = cornerSize.width === 0 && cornerSize.height === 0;
    const cornerWidthOffset = cornerNotYetSized ? nextCornerWidth : 0;
    const cornerHeightOffset = cornerNotYetSized ? nextCornerHeight : 0;
    const scrollbarXOffset = getOffset(scrollbarXEl, "padding", "x");
    const scrollbarYOffset = getOffset(scrollbarYEl, "padding", "y");
    const thumbXOffset = getOffset(thumbXEl, "margin", "x");
    const thumbYOffset = getOffset(thumbYEl, "margin", "y");
    const idealNextWidth = nextWidth - scrollbarXOffset - thumbXOffset;
    const idealNextHeight = nextHeight - scrollbarYOffset - thumbYOffset;
    const maxNextWidth = scrollbarXEl ? Math.min(scrollbarXEl.offsetWidth - cornerWidthOffset, idealNextWidth) : idealNextWidth;
    const maxNextHeight = scrollbarYEl ? Math.min(scrollbarYEl.offsetHeight - cornerHeightOffset, idealNextHeight) : idealNextHeight;
    const clampedNextWidth = Math.max(MIN_THUMB_SIZE, maxNextWidth * ratioX);
    const clampedNextHeight = Math.max(MIN_THUMB_SIZE, maxNextHeight * ratioY);
    context.setThumbSize(prev => {
      if (prev.height === clampedNextHeight && prev.width === clampedNextWidth) {
        return prev;
      }
      return {
        width: clampedNextWidth,
        height: clampedNextHeight
      };
    });

    // Position the vertical thumb. NB: this uses `Math.min/Math.max`
    // nesting rather than `clamp` — on a tiny track where the max offset
    // goes negative the two diverge (this branch yields the negative
    // offset, the horizontal branch's `clamp` yields 0); preserve as-is.
    function applyVerticalThumbTransform() {
      if (!scrollbarYEl || !thumbYEl) return;
      const maxThumbOffsetY = scrollbarYEl.offsetHeight - clampedNextHeight - scrollbarYOffset - thumbYOffset;
      const scrollRangeY = scrollableContentHeight - viewportHeight;
      const scrollRatioY = scrollRangeY === 0 ? 0 : scrollTop / scrollRangeY;
      const thumbOffsetY = Math.min(maxThumbOffsetY, Math.max(0, scrollRatioY * maxThumbOffsetY));
      thumbYEl.style.transform = `translate3d(0,${thumbOffsetY}px,0)`;
    }
    function applyHorizontalThumbTransform() {
      if (!scrollbarXEl || !thumbXEl) return;
      const maxThumbOffsetX = scrollbarXEl.offsetWidth - clampedNextWidth - scrollbarXOffset - thumbXOffset;
      const scrollRangeX = scrollableContentWidth - viewportWidth;
      const scrollRatioX = scrollRangeX === 0 ? 0 : scrollLeft / scrollRangeX;
      const thumbOffsetX = direction() === "rtl" ? clamp(scrollRatioX * maxThumbOffsetX, -maxThumbOffsetX, 0) : clamp(scrollRatioX * maxThumbOffsetX, 0, maxThumbOffsetX);
      thumbXEl.style.transform = `translate3d(${thumbOffsetX}px,0,0)`;
    }

    // Publish overflow offsets (as CSS vars), corner size, hidden state,
    // and the past-threshold overflow-edge flags back to the context.
    function commitOverflowAndCornerState() {
      if (!viewportEl) return;
      const overflowMetricsPx = [[ScrollAreaViewportCssVars.scrollAreaOverflowXStart, scrollLeftFromStart], [ScrollAreaViewportCssVars.scrollAreaOverflowXEnd, scrollLeftFromEnd], [ScrollAreaViewportCssVars.scrollAreaOverflowYStart, scrollTopFromStart], [ScrollAreaViewportCssVars.scrollAreaOverflowYEnd, scrollTopFromEnd]];
      for (const [cssVar, value] of overflowMetricsPx) {
        viewportEl.style.setProperty(cssVar, `${value}px`);
      }
      if (cornerEl) {
        if (scrollbarXHidden || scrollbarYHidden) {
          context.setCornerSize({
            width: 0,
            height: 0
          });
        } else {
          context.setCornerSize({
            width: nextCornerWidth,
            height: nextCornerHeight
          });
        }
      }
      context.setHiddenState(prev => mergeHiddenState(prev, nextHiddenState));
      const threshold = context.overflowEdgeThreshold;
      const nextOverflowEdges = {
        xStart: !scrollbarXHidden && scrollLeftFromStart > threshold.xStart,
        xEnd: !scrollbarXHidden && scrollLeftFromEnd > threshold.xEnd,
        yStart: !scrollbarYHidden && scrollTopFromStart > threshold.yStart,
        yEnd: !scrollbarYHidden && scrollTopFromEnd > threshold.yEnd
      };
      context.setOverflowEdges(prev => {
        if (prev.xStart === nextOverflowEdges.xStart && prev.xEnd === nextOverflowEdges.xEnd && prev.yStart === nextOverflowEdges.yStart && prev.yEnd === nextOverflowEdges.yEnd) {
          return prev;
        }
        return nextOverflowEdges;
      });
    }
    applyVerticalThumbTransform();
    applyHorizontalThumbTransform();
    commitOverflowAndCornerState();
  });

  // One-time setup: register the CSS vars as non-inheriting.
  createRenderEffect(() => viewportEl(), el => {
    if (!el) return;
    removeCSSVariableInheritance();
  });

  // Recompute whenever hidden-state or direction flips. `queueMicrotask`
  // so the scrollbar/thumb refs are populated before we measure them.
  createRenderEffect(() => ({
    hidden: context.hiddenState(),
    dir: direction()
  }), () => {
    queueMicrotask(computeThumbPosition);
  });

  // Initial hover catch: `onMouseEnter` doesn't fire on mount, so if the
  // viewport is already being hovered when it mounts, flip the flag.
  createRenderEffect(() => viewportEl(), el => {
    if (el?.matches?.(":hover")) {
      context.setHovering(true);
    }
  });

  // ResizeObserver wiring. Fires on viewport resize, which also covers
  // most font-loading / layout-shift cases.
  createEffect(() => viewportEl(), viewport => {
    if (typeof ResizeObserver === "undefined" || !viewport) return;
    let hasInitialized = false;
    const ro = new ResizeObserver(() => {
      // Skip the initial observe call when the metrics match what the
      // mount-time pass already measured.
      if (!hasInitialized) {
        hasInitialized = true;
        if (lastMeasuredViewportMetrics[0] === viewport.clientHeight && lastMeasuredViewportMetrics[1] === viewport.scrollHeight && lastMeasuredViewportMetrics[2] === viewport.clientWidth && lastMeasuredViewportMetrics[3] === viewport.scrollWidth) {
          return;
        }
      }
      computeThumbPosition();
    });
    ro.observe(viewport);

    // Wait for any in-flight animations (dialog open, etc.) to settle
    // and recompute so the thumb size isn't stuck at mid-animation.
    waitForAnimationsTimeout.start(0, () => {
      if (typeof viewport.getAnimations !== "function") return;
      const animations = viewport.getAnimations({
        subtree: true
      });
      if (animations.length === 0) return;
      Promise.allSettled(animations.map(animation => animation.finished)).then(() => computeThumbPosition()).catch(() => {});
    });
    return () => {
      ro.disconnect();
      waitForAnimationsTimeout.clear();
    };
  });

  // The root's derived state is identical to what this part needs for its
  // `data-*` projection — read it straight from context instead of rebuilding
  // the memo. (`ScrollAreaViewportState` is structurally `ScrollAreaRootState`.)
  const viewportState = context.viewportState;
  const contextValue = {
    computeThumbPosition
  };
  function handleUserInteraction() {
    programmaticScroll = false;
  }
  const userStyling = resolveClassStyle(props, () => context.viewportState());
  const userProps = omit(props, "class", "style", "attrs", "as", "children");
  const localRef = el => {
    context.viewportRef.current = el;
    setViewportEl(el);
  };
  const internalSlot = {
    role: "presentation",
    "data-id": `${context.rootId}-viewport`,
    // Non-scrollable viewports don't belong in tab order.
    get tabIndex() {
      const h = context.hiddenState();
      return h.x && h.y ? -1 : 0;
    },
    class: DISABLE_SCROLLBAR_CLASS_NAME,
    style: {
      overflow: "scroll"
    },
    onScroll: () => {
      const viewportEl = context.viewportRef.current;
      if (!viewportEl) return;
      computeThumbPosition();
      if (!programmaticScroll) {
        context.handleScroll({
          x: viewportEl.scrollLeft,
          y: viewportEl.scrollTop
        });
      }
      // Debounce the programmatic-scroll reset so momentum scrolling
      // (where no user-interaction events fire after the initial
      // gesture) is still treated as user-driven.
      scrollEndTimeout.start(100, () => {
        programmaticScroll = true;
      });
    },
    // `wheel` / `touchmove` are scroll-blocking events: a plain `onWheel` /
    // `onTouchMove` prop registers a NON-passive listener (Solid can't know
    // the handler won't `preventDefault`), which Chrome flags as a perf
    // violation. `handleUserInteraction` only flips a flag, so register them
    // passive via Solid's `on:` namespace (a non-function value is forwarded
    // as the `addEventListener` options). The non-scroll-blocking handlers
    // below stay as ordinary props.
    "on:wheel": {
      handleEvent: handleUserInteraction,
      passive: true
    },
    "on:touchmove": {
      handleEvent: handleUserInteraction,
      passive: true
    },
    onPointerMove: handleUserInteraction,
    onPointerEnter: handleUserInteraction,
    onKeyDown: handleUserInteraction,
    get children() {
      return <ScrollAreaViewportContext value={contextValue}>
					{props.children}
				</ScrollAreaViewportContext>;
    }
  };
  const styledUserProps = mergeRootProps(userStyling, userProps);
  let merged = mergeRootProps(internalSlot, scrollAreaDataAttrs(viewportState));
  merged = mergeRootProps(merged, styledUserProps);
  const Tag = dynamic(() => props.as ?? "div");
  return <Tag {...merged} ref={composeRefs(localRef, userRef(props))} />;
}

// Re-export the class name constant for consumers needing to reach the
// underlying style token (rare — normally handled by the root).
export { styleDisableScrollbar };
//# sourceMappingURL=scroll-area-viewport.jsx.map
