import { dynamic } from "@solidjs/web";
import { createMemo, createSignal, Show, untrack } from "solid-js";
import { useCspNonce } from "../../csp-provider/csp-context.jsx";
import { useDirection } from "../../direction-provider/direction-provider-context.jsx";
import { useBaseUiId } from "../../internals/base-ui-id.jsx";
import { composeRefs, userRef } from "../../internals/compose-refs.jsx";
import { useCompositeListItem } from "../../internals/composite/composite-list.jsx";
import { ownerWindow } from "../../internals/dom/owner-window.jsx";
import { useFieldRootContext } from "../../internals/field-context.jsx";
import { isHydrating } from "../../internals/is-hydrating.jsx";
import { useAriaLabelledBy } from "../../internals/labelable-provider/use-aria-labelled-by.jsx";
import { useLabelableId } from "../../internals/labelable-provider/use-labelable-id.jsx";
import { mergePropsN } from "../../internals/merge-props-n.jsx";
import { mergeRootProps } from "../../internals/merge-root-props.jsx";
import { resolveClassStyle } from "../../internals/resolve-class-style.jsx";
import { clamp } from "../../internals/slider-utils/clamp.jsx";
import { getMidpoint } from "../../internals/slider-utils/get-midpoint.jsx";
import { getSliderValue } from "../../internals/slider-utils/get-slider-value.jsx";
import { getDecimalPrecision, roundValueToStep } from "../../internals/slider-utils/round-value-to-step.jsx";
import { useIsoLayoutEffect } from "../../internals/use-iso-layout-effect.jsx";
import { valueToPercent } from "../../internals/value-to-percent.jsx";
import { VISUALLY_HIDDEN_STYLE } from "../../internals/visually-hidden-style.jsx";
import { useSliderRootContext } from "../slider-root-context.jsx";
import { sliderDataAttrs } from "../utils/state-attrs.jsx";
import { sliderPrehydrationScript } from "./prehydration-script.jsx";
const ARROW_UP = "ArrowUp";
const ARROW_DOWN = "ArrowDown";
const ARROW_LEFT = "ArrowLeft";
const ARROW_RIGHT = "ArrowRight";
const HOME = "Home";
const END = "End";
const PAGE_UP = "PageUp";
const PAGE_DOWN = "PageDown";
const HANDLED_KEYS = new Set([ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, HOME, END, PAGE_UP, PAGE_DOWN]);

// Keys that Base UI's composite primitives also consume (arrows / Home / End).
// React stops propagation for these so a parent `Menu`/`Listbox`/composite
// doesn't double-handle a slider's own keyboard event. Mirrors
// `@base-ui/react`'s `COMPOSITE_KEYS` constant.
const COMPOSITE_KEYS = new Set([ARROW_UP, ARROW_DOWN, ARROW_LEFT, ARROW_RIGHT, HOME, END]);
function formatNumber(value, locale, format) {
  if (!format) return String(value);
  try {
    return new Intl.NumberFormat(locale, format).format(value);
  } catch {
    return String(value);
  }
}
function getDefaultAriaValueText(values, index, format, locale) {
  if (index < 0) return undefined;
  if (values.length === 2) {
    if (index === 0) {
      return `${formatNumber(values[index] ?? 0, locale, format)} start range`;
    }
    return `${formatNumber(values[index] ?? 0, locale, format)} end range`;
  }
  return format ? formatNumber(values[index] ?? 0, locale, format) : undefined;
}
function getNewValue(thumbValue, increment, direction, min, max) {
  const next = direction === 1 ? thumbValue + increment : thumbValue - increment;
  const precision = Math.max(getDecimalPrecision(thumbValue), getDecimalPrecision(increment), getDecimalPrecision(min));
  return clamp(Number(next.toFixed(precision)), min, max);
}

/**
 * The draggable part of the slider at the tip of the indicator. Renders a
 * `<div>` with a nested `<input type="range">` for form participation and
 * keyboard navigation.
 *
 * Keyboard contract (matches React Base UI):
 *
 *   - Arrow keys: ±step (or ±largeStep with Shift). Horizontal arrows honor
 *     RTL via the direction context.
 *   - Page Up / Page Down: ±largeStep.
 *   - Home / End: jumps to the range edge (or the adjacent thumb's value +/-
 *     `step * minStepsBetweenValues` in range mode).
 *
 * Solid 2 notes:
 *
 *   - `index` is resolved through `useCompositeListItem` when the consumer
 *     doesn't pass `index` explicitly — matches React's "auto-index" branch.
 *   - `aria-labelledby` is seeded with `""` so Solid 2's DOM spread keeps the
 *     key in `ownKeys` (see `test/internals/merge-props-undefined-attr.test.tsx`).
 *   - The input ref is exposed through `props.inputRef` — consumers compose
 *     with Solid's native array refs on the outer thumb via `ref`.
 */
export function SliderThumb(props) {
  const context = useSliderRootContext();
  const field = useFieldRootContext();
  const direction = useDirection();
  const nonce = useCspNonce();
  const id = useBaseUiId(untrack(() => typeof props.id === "string" ? props.id : undefined));
  const disabled = () => context.disabled() || props.disabled === true;
  const range = () => context.values().length > 1;
  const defaultInputId = useBaseUiId();
  const {
    id: labelableInputId
  } = useLabelableId();
  const inputId = () => range() ? defaultInputId : labelableInputId;
  const metadata = createMemo(() => ({
    inputId: inputId()
  }));

  // `useCompositeListItem` reads its metadata input once at body time to
  // register with the parent list — wrap in `untrack` so the snapshot
  // read of `metadata()` (and the cascading `inputId()` / `range()` /
  // `context.values()` reads behind it) doesn't trip Solid 2's component
  // strict-read shell. Reactivity flows through the registry callbacks.
  const {
    ref: listItemRef,
    index: compositeIndex
  } = untrack(() => useCompositeListItem({
    metadata: metadata()
  }));
  const index = () => {
    if (!range()) return 0;
    if (typeof props.index === "number") return props.index;
    return compositeIndex();
  };
  const thumbValue = () => {
    const values = context.values();
    const i = index();
    return values[i] ?? 0;
  };
  const thumbValuePercent = () => valueToPercent(thumbValue(), context.min(), context.max());

  // The composed `aria-labelledby` for the hidden input. Prefers an explicit
  // `aria-label` on the thumb, falling back to ids registered via the
  // labelable provider above.
  const ariaLabel = () => {
    const getAriaLabel = props.getAriaLabel;
    if (typeof getAriaLabel === "function") return getAriaLabel(index());
    const raw = props["aria-label"];
    return typeof raw === "string" ? raw : undefined;
  };
  const externalAriaLabelledBy = () => {
    const v = props["aria-labelledby"];
    return typeof v === "string" && v.length > 0 ? v : undefined;
  };

  // Hook seeds its initial state from the argument at body time; wrap
  // in `untrack` to keep that snapshot out of the strict-read shell.
  const composedLabelledBy = untrack(() => useAriaLabelledBy(externalAriaLabelledBy));
  const thumbRef = {
    current: null
  };
  const inputRef = {
    current: null
  };
  // Element-valued signal driving the ResizeObserver effect below. The
  // render ref callback mirrors into both `thumbRef.current` (for
  // synchronous reads in handlers) and this signal (for effect re-runs).
  const [thumbElement, setThumbElement] = createSignal(null, {
    ownedWrite: true
  });

  // Edge-alignment geometry (React `SliderThumb.tsx:196-223`). Written by
  // the ResizeObserver callback and consumed by the thumb's style getter;
  // `undefined` until the first measurement lands or when `inset` is off.
  // This signal lives outside Solid's reactive graph on the measurement
  // side — the ResizeObserver callback is the only writer — so we expose
  // only the accessor to consumers.
  const [positionPercent, setPositionPercent] = createSignal(undefined, {
    ownedWrite: true
  });
  const isLastThumb = () => index() === context.values().length - 1;
  function getInsetPosition() {
    // `untrack` so reads of `context.orientation()` / `thumbValuePercent()`
    // / `index()` / `isLastThumb()` don't trip `[STRICT_READ_UNTRACKED]`
    // when this is called from inside an effect's apply callback. The
    // reads here are snapshots driving DOM math + setIndicatorPosition;
    // re-runs are scheduled by the effects that own this function and by
    // the ResizeObserver wired up below.
    untrack(() => {
      const control = context.controlRef.current;
      const thumb = thumbRef.current;
      if (!control || !thumb) return;
      const thumbRect = thumb.getBoundingClientRect();
      const controlRect = control.getBoundingClientRect();
      const vertical = context.orientation() === "vertical";
      const side = vertical ? "height" : "width";

      // Usable travel = control side minus one thumb side (thumb is
      // inset by half its size at each endpoint, so its *edge* lands on
      // the control edge at min/max).
      const controlSize = controlRect[side] - thumbRect[side];
      const thumbOffsetFromControlEdge = thumbRect[side] / 2 + controlSize * thumbValuePercent() / 100;
      const nextPositionPercent = thumbOffsetFromControlEdge / controlRect[side] * 100;
      const nextInsetPosition = Number.isFinite(nextPositionPercent) ? nextPositionPercent : undefined;
      setPositionPercent(nextInsetPosition);
      const i = index();
      if (i === 0) {
        context.setIndicatorPosition(prev => [nextInsetPosition, prev[1]]);
      }
      if (isLastThumb()) {
        context.setIndicatorPosition(prev => [prev[0], nextInsetPosition]);
      }
    });
  }

  // Initial measurement on mount. Queueing via microtask matches React's
  // `queueMicrotask` — lets the thumb + control settle in the DOM before
  // we read their rects.
  useIsoLayoutEffect(() => [context.inset(), thumbElement()], ([inset, el]) => {
    if (!inset || !el) return undefined;
    queueMicrotask(getInsetPosition);
    return undefined;
  });

  // Re-measure whenever the thumb's value percent changes. Size is
  // stable but its target position isn't.
  useIsoLayoutEffect(() => [context.inset(), thumbValuePercent(), thumbElement()], ([inset,, el]) => {
    if (!inset || !el) return undefined;
    getInsetPosition();
    return undefined;
  });

  // Two ResizeObservers (thumb + control) re-run the geometry whenever
  // either element resizes. Matches React's `SliderThumb.tsx:237-257`.
  // `ownerWindow` grabs the correct ResizeObserver constructor for
  // shadow-DOM / iframe hosts — don't reach for the global directly.
  useIsoLayoutEffect(() => [context.inset(), thumbElement()], ([inset, thumbEl]) => {
    const controlEl = context.controlRef.current;
    if (!inset || !thumbEl || !controlEl) return undefined;
    const win = ownerWindow(thumbEl);
    const ROConstructor = win.ResizeObserver;
    if (typeof ROConstructor !== "function") return undefined;
    const observer = new ROConstructor(() => {
      getInsetPosition();
    });
    observer.observe(thumbEl);
    observer.observe(controlEl);
    return () => {
      observer.disconnect();
    };
  });
  function handleKeyDown(event) {
    if (!HANDLED_KEYS.has(event.key)) return;
    // Matches React `SliderThumb.tsx:383-385`: stop the event from bubbling
    // to a surrounding `Menu`/composite so the same arrow key doesn't also
    // drive the outer focus ring.
    if (COMPOSITE_KEYS.has(event.key)) {
      event.stopPropagation();
    }
    const input = event.currentTarget;
    let newValue = null;
    const min = context.min();
    const max = context.max();
    const step = context.step();
    const largeStep = context.largeStep();
    const minSteps = context.minStepsBetweenValues();
    const rtl = direction() === "rtl";
    const i = index();
    const sliderValues = context.values();
    const roundedValue = roundValueToStep(thumbValue(), step, min);
    const increment = event.shiftKey ? largeStep : step;
    switch (event.key) {
      case ARROW_UP:
        newValue = getNewValue(roundedValue, increment, 1, min, max);
        break;
      case ARROW_RIGHT:
        newValue = getNewValue(roundedValue, increment, rtl ? -1 : 1, min, max);
        break;
      case ARROW_DOWN:
        newValue = getNewValue(roundedValue, increment, -1, min, max);
        break;
      case ARROW_LEFT:
        newValue = getNewValue(roundedValue, increment, rtl ? 1 : -1, min, max);
        break;
      case PAGE_UP:
        newValue = getNewValue(roundedValue, largeStep, 1, min, max);
        break;
      case PAGE_DOWN:
        newValue = getNewValue(roundedValue, largeStep, -1, min, max);
        break;
      case END:
        {
          const neighbor = sliderValues[i + 1];
          newValue = range() && Number.isFinite(neighbor) ? neighbor - step * minSteps : max;
          break;
        }
      case HOME:
        {
          const neighbor = sliderValues[i - 1];
          newValue = range() && Number.isFinite(neighbor) ? neighbor + step * minSteps : min;
          break;
        }
      default:
        break;
    }
    if (newValue !== null) {
      context.handleInputChange(newValue, i, event);
      event.preventDefault();
    }
    input.focus({
      preventScroll: true
    });
  }
  function handleInputChange(event) {
    context.handleInputChange(event.currentTarget.valueAsNumber, index(), event);
  }
  function handlePointerDown(event) {
    const i = index();
    context.pressedThumbIndexRef.current = i;
    const thumbEl = thumbRef.current;
    if (thumbEl != null) {
      const vertical = context.orientation() === "vertical";
      const midpoint = getMidpoint(thumbEl);
      const offset = vertical ? event.clientY - midpoint.y : event.clientX - midpoint.x;
      context.pressedThumbCenterOffsetRef.current = offset;
    }
    if (inputRef.current != null && context.pressedInputRef.current !== inputRef.current) {
      context.pressedInputRef.current = inputRef.current;
    }
  }
  function getThumbStyle() {
    const vertical = context.orientation() === "vertical";
    const rtl = direction() === "rtl";
    const startEdge = vertical ? "bottom" : "inset-inline-start";
    const crossOffsetProperty = vertical ? "left" : "top";
    const percent = thumbValuePercent();
    const activeIndex = context.active();
    const i = index();
    const isRange = range();
    const last = context.lastUsedThumbIndex();
    const isInset = context.inset();
    let zIndex;
    if (isRange) {
      if (activeIndex === i) zIndex = 2;else if (last === i) zIndex = 1;
    } else if (activeIndex === i) {
      zIndex = 1;
    }
    if (!isInset) {
      if (!Number.isFinite(percent)) {
        return {
          ...VISUALLY_HIDDEN_STYLE
        };
      }
      return {
        position: "absolute",
        [startEdge]: `${percent}%`,
        [crossOffsetProperty]: "50%",
        translate: `${(vertical || !rtl ? -1 : 1) * 50}% ${(vertical ? 1 : -1) * 50}%`,
        "z-index": zIndex
      };
    }

    // Edge mode: position the thumb from the measured `positionPercent`
    // via a CSS custom property so the prehydration script (see SSR
    // deferral below) can overwrite it before hydration without
    // touching inline styles directly. Hide the thumb until the first
    // measurement lands or until hydration completes — prevents a
    // one-frame flash at a wrong position.
    const pos = positionPercent();
    const renderBH = context.renderBeforeHydration();
    return {
      "--position": `${pos ?? 0}%`,
      visibility: renderBH && isHydrating() || pos === undefined ? "hidden" : undefined,
      position: "absolute",
      [startEdge]: "var(--position)",
      [crossOffsetProperty]: "50%",
      translate: `${(vertical || !rtl ? -1 : 1) * 50}% ${(vertical ? 1 : -1) * 50}%`,
      "z-index": zIndex
    };
  }
  const onInputFocus = event => {
    context.setActive(index());
    field.setFocused(true);
    props.onFocus?.(event);
  };
  const onInputBlur = event => {
    if (thumbRef.current == null) return;
    context.setActive(-1);
    // Matches React `SliderThumb.tsx:371-378`: on blur, flip the field
    // `touched` + `focused` latches and, when validationMode is "onBlur",
    // commit the thumb's current value into the validation pipeline.
    field.setTouched(true);
    field.setFocused(false);
    if (field.validationMode() === "onBlur") {
      const i = index();
      void context.commitValidation(getSliderValue(thumbValue(), i, context.min(), context.max(), range(), context.values()));
    }
    props.onBlur?.(event);
  };
  const forwardedProps = () => {
    // Strip the props we redirect onto the <input>. Keep `id` off the
    // forwarded set — we emit it explicitly on the root wrapper.
    const out = {};
    for (const key of Object.keys(props)) {
      if (key === "as" || key === "disabled" || key === "getAriaLabel" || key === "getAriaValueText" || key === "index" || key === "inputRef" || key === "tabIndex" || key === "onBlur" || key === "onFocus" || key === "id" || key === "class" || key === "style" || key === "attrs" || key === "aria-label" || key === "aria-labelledby" || key === "aria-describedby") {
        continue;
      }
      const desc = Object.getOwnPropertyDescriptor(props, key);
      if (desc) Object.defineProperty(out, key, desc);
    }
    return out;
  };
  const cssWritingMode = () => {
    if (context.orientation() !== "vertical") return undefined;
    return direction() === "rtl" ? "vertical-rl" : "vertical-lr";
  };
  function renderHiddenInput() {
    const inputStyle = () => ({
      ...VISUALLY_HIDDEN_STYLE,
      width: "100%",
      height: "100%",
      "writing-mode": cssWritingMode()
    });
    const inputRefCallback = el => {
      inputRef.current = el;
      // Bridge to the field validation inputRef so native validation
      // (e.g. `element.validity`) flows through `useFieldValidation`.
      field.validation.inputRef.current = el;
      props.inputRef?.(el);
    };
    const externalDescribedBy = props["aria-describedby"];
    const valueTextAccessor = () => {
      const gt = props.getAriaValueText;
      if (typeof gt === "function") {
        return gt(formatNumber(thumbValue(), context.locale(), context.formatOptions()), thumbValue(), index());
      }
      return getDefaultAriaValueText(context.values(), index(), context.formatOptions(), context.locale());
    };
    const labelledBy = () => ariaLabel() != null ? "" : composedLabelledBy() ?? "";

    // Pull the field's input-validation props and surface them on the
    // rendered `<input>`. The shape includes `onInput` (native validation
    // commit bridge), plus `aria-invalid` / `aria-describedby` carried
    // over from `getValidationProps`. Matches React `SliderThumb.tsx:472`.
    const validationInputProps = mergePropsN([field.validation.getInputValidationProps()]);
    const onValidationInput = validationInputProps.onInput;
    function handleInput(event) {
      // Bridge to the field validation pipeline. Chain order matches
      // React's merged props — validation runs on every native `input`,
      // and our slider value update continues to fire via `onChange`.
      onValidationInput?.(event);
    }

    // Compose `aria-describedby`: caller's explicit describedBy takes
    // precedence; otherwise fall back to whatever validation supplies.
    const composedDescribedBy = () => externalDescribedBy ?? validationInputProps["aria-describedby"];
    const ariaInvalid = () => {
      // `getValidationProps` emits literal strings (`"true"` / `"false"`)
      // or `undefined`; normalize any stray boolean values from external
      // sources.
      const v = validationInputProps["aria-invalid"];
      if (v === undefined) return undefined;
      if (v === true || v === "true") return "true";
      if (v === false || v === "false") return "false";
      if (v === "grammar" || v === "spelling") return v;
      return undefined;
    };
    return <input ref={inputRefCallback} type="range" aria-label={ariaLabel()} aria-labelledby={labelledBy()} aria-describedby={composedDescribedBy()} aria-invalid={ariaInvalid()} aria-orientation={context.orientation()} aria-valuenow={thumbValue()} aria-valuetext={valueTextAccessor()} disabled={disabled()} form={context.form()} id={inputId()} max={context.max()} min={context.min()} name={context.name()} step={context.step()} tabindex={props.tabIndex} value={thumbValue()} onKeyDown={handleKeyDown} onInput={handleInput} onChange={handleInputChange} onFocus={onInputFocus} onBlur={onInputBlur} style={inputStyle()} />;
  }
  const localRef = el => {
    thumbRef.current = el;
    setThumbElement(el);
    listItemRef(el);
  };
  const internalSlot = {
    id,
    get "data-index"() {
      return String(index());
    },
    get style() {
      return getThumbStyle();
    },
    onPointerDown: handlePointerDown,
    get children() {
      // SSR prehydration slot (mirrors React `SliderThumb.tsx:487-499`):
      // emit the inline `<script>` that sets the thumb/indicator CSS
      // position vars before hydration, gated on
      // `inset && isHydrating && renderBeforeHydration && isLastThumb`.
      // It must render with the LAST thumb so every preceding thumb is
      // already in the DOM when `document.querySelectorAll` runs. The
      // `<script>` is bound as a plain element (not a spread of an inline
      // call) so Solid's SSR compiler doesn't thunk it.
      const showScript = () => context.inset() && isHydrating() && context.renderBeforeHydration() && isLastThumb();
      return <>
					{props.children}
					{renderHiddenInput()}
					<Show when={showScript()}>
						<script nonce={nonce()} innerHTML={sliderPrehydrationScript} />
					</Show>
				</>;
    }
  };

  // Resolve `class`/`style` here so function values (stateful styling) are
  // invoked with the slider state instead of being spread to the DOM raw.
  // The internal `style` (the thumb position) is merged first below, so a
  // user-supplied style composes with it rather than replacing it.
  const userClassStyle = resolveClassStyle(props, () => context.state());
  let merged = mergeRootProps(internalSlot, sliderDataAttrs(context.state));
  merged = mergeRootProps(merged, userClassStyle);
  merged = mergeRootProps(merged, forwardedProps());
  const Tag = dynamic(() => props.as ?? "div");
  return <Tag {...merged} ref={composeRefs(localRef, userRef(props))} />;
}
//# sourceMappingURL=slider-thumb.jsx.map
