import { dynamic } from "@solidjs/web";
import { createMemo, createSignal, omit, untrack } from "solid-js";
import { useBaseUiId } from "../../internals/base-ui-id.jsx";
import { createChangeEventDetails, createGenericEventDetails, REASONS } from "../../internals/change-event-details.jsx";
import { composeRefs, userRef } from "../../internals/compose-refs.jsx";
import { CompositeList } from "../../internals/composite/composite-list.jsx";
import { createControlledState } from "../../internals/create-controlled-state.jsx";
import { activeElement } from "../../internals/dom/active-element.jsx";
import { contains } from "../../internals/dom/contains.jsx";
import { ownerDocument } from "../../internals/dom/owner-document.jsx";
import { useFieldRootContext } from "../../internals/field-context.jsx";
import { useFormContext } from "../../internals/form-context.jsx";
import { LabelableProvider } from "../../internals/labelable-provider/labelable-provider.jsx";
import { useAriaLabelledBy } from "../../internals/labelable-provider/use-aria-labelled-by.jsx";
import { mergeRootProps } from "../../internals/merge-root-props.jsx";
import { getDefaultLabelId, resolveAriaLabelledBy } from "../../internals/resolve-aria-labelled-by.jsx";
import { resolveClassStyle } from "../../internals/resolve-class-style.jsx";
import { ariaLabelledBy as seedAriaLabelledBy } from "../../internals/seed-aria-attr.jsx";
import { areArraysEqual } from "../../internals/slider-utils/are-arrays-equal.jsx";
import { asc } from "../../internals/slider-utils/asc.jsx";
import { clamp } from "../../internals/slider-utils/clamp.jsx";
import { getSliderValue } from "../../internals/slider-utils/get-slider-value.jsx";
import { validateMinimumDistance } from "../../internals/slider-utils/validate-minimum-distance.jsx";
import { useFieldControlValidation } from "../../internals/use-field-control-validation/use-field-control-validation.jsx";
import { useIsoLayoutEffect } from "../../internals/use-iso-layout-effect.jsx";
import { useValueChanged } from "../../internals/use-value-changed.jsx";
import { SliderRootContext } from "../slider-root-context.jsx";
import { sliderDataAttrs } from "../utils/state-attrs.jsx";
function areValuesEqual(newValue, oldValue) {
  if (typeof newValue === "number" && typeof oldValue === "number") {
    return newValue === oldValue;
  }
  if (Array.isArray(newValue) && Array.isArray(oldValue)) {
    return areArraysEqual(newValue, oldValue);
  }
  return false;
}
function getSliderChangeEventReason(event) {
  return "key" in event ? REASONS.keyboard : REASONS.inputChange;
}

/**
 * Groups all parts of the slider. Renders a `<div role="group">`.
 *
 * Solid 2 adaptations of Base UI React's `SliderRoot`:
 *
 *  - No top-level reactive reads — every `props.X` access is wrapped in an
 *    accessor. Reactive state (`values`, `active`, `dragging`, `indicator
 *    Position`) is signal-backed.
 *  - The CompositeList is used for thumb registration (1:1 with React);
 *    `thumbMap` is exposed as an accessor via `onMapChange` so `<Slider.Value>`
 *    can subscribe to thumb-id changes reactively.
 *  - Field integration goes through `useFieldControlValidation` — registers
 *    the control into the surrounding `<Field.Root>` via the unified hook.
 *  - `LabelableProvider` is injected inside the `children` getter so
 *    `<Slider.Label>` + thumb inputs discover each other via the provider's
 *    context.
 *  - `thumbAlignment="edge"` / `"edge-client-only"` work on the client via
 *    a ResizeObserver in `<Slider.Thumb>` that writes `--position` /
 *    `--start-position` / `--relative-size` CSS vars. The SSR prehydration
 *    `<script>` from React (`SliderThumb.tsx:487-499`) is not yet ported.
 *    The `renderBeforeHydration` context flag is
 *    plumbed now so the future SSR change-set is purely additive.
 */
export function SliderRoot(props) {
  const field = useFieldRootContext();
  const {
    clearErrors
  } = useFormContext();
  const id = useBaseUiId(untrack(() => typeof props.id === "string" ? props.id : undefined));
  const defaultLabelId = getDefaultLabelId(id);
  const [labelId, setLabelIdState] = createSignal(undefined, {
    ownedWrite: true
  });
  const min = () => props.min ?? 0;
  const max = () => props.max ?? 100;
  const step = () => props.step ?? 1;
  const largeStep = () => props.largeStep ?? 10;
  const minStepsBetweenValues = () => props.minStepsBetweenValues ?? 0;
  const orientation = () => props.orientation ?? "horizontal";
  const thumbCollisionBehavior = () => props.thumbCollisionBehavior ?? "push";
  const disabled = () => field.disabled() || props.disabled === true;
  const name = () => field.name() ?? props.name;
  const form = () => props.form;
  const format = () => props.format;
  const locale = () => props.locale;

  // Controlled / uncontrolled value (Base UI `value` / `defaultValue`
  // contract): the `value` prop wins while not `undefined`; otherwise an
  // internal signal (seeded once from `defaultValue ?? min`, exactly like
  // React's `useControlled({ default: defaultValue ?? min })`) backs the
  // state and `setValue` below writes it.
  const [valueUnwrapped, setUncontrolledValue] = createControlledState({
    controlled: () => props.value,
    default: () => props.defaultValue ?? min(),
    fallback: 0,
    name: "<Slider.Root>",
    state: "value"
  });
  const controlRef = {
    current: null
  };
  const sliderRef = {
    current: null
  };
  const thumbRefs = {
    current: []
  };
  const pressedInputRef = {
    current: null
  };
  const pressedThumbCenterOffsetRef = {
    current: null
  };
  const pressedThumbIndexRef = {
    current: -1
  };
  const pressedValuesRef = {
    current: null
  };
  const lastChangedValueRef = {
    current: null
  };
  const lastChangeReasonRef = {
    current: "none"
  };
  const [active, setActiveState] = createSignal(-1, {
    ownedWrite: true
  });
  const [lastUsedThumbIndex, setLastUsedThumbIndex] = createSignal(-1, {
    ownedWrite: true
  });
  const [dragging, setDragging] = createSignal(false, {
    ownedWrite: true
  });

  // Edge-alignment geometry: first slot = start thumb position, second =
  // end thumb position, both as percent strings written out by the thumb's
  // ResizeObserver callback. Undefined until the first measurement lands;
  // `equals: false` ensures observer writes that produce the same tuple
  // identity still flush so the indicator re-reads.
  const [indicatorPosition, setIndicatorPositionState] = createSignal([undefined, undefined], {
    ownedWrite: true,
    equals: false
  });
  function setIndicatorPosition(updater) {
    setIndicatorPositionState(updater(indicatorPosition()));
  }
  const thumbAlignment = () => props.thumbAlignment ?? "center";
  const inset = () => thumbAlignment() !== "center";
  const renderBeforeHydration = () => thumbAlignment() === "edge";
  const [thumbMap, setThumbMap] = createSignal(new Map(), {
    ownedWrite: true,
    equals: false
  });
  function setActive(value) {
    setActiveState(value);
    if (value !== -1) {
      setLastUsedThumbIndex(value);
    }
  }
  const range = () => Array.isArray(valueUnwrapped());
  const values = createMemo(() => {
    const current = valueUnwrapped();
    const lo = min();
    const hi = max();
    if (!Array.isArray(current)) {
      return [clamp(current ?? lo, lo, hi)];
    }
    return current.slice().sort(asc);
  });
  const registerThumbMap = map => {
    setThumbMap(map);
  };

  // --- Field validation + registration --------------------------------------
  // Hook reads accessor inputs synchronously at body time to seed its
  // initial state — wrap in `untrack` so the snapshot reads don't trip
  // the component-body strict-read shell. Reactivity flows through the
  // retained accessor closures.
  const validation = untrack(() => useFieldControlValidation({
    value: () => valueUnwrapped(),
    id: () => id,
    controlRef,
    getValue: () => valueUnwrapped()
  }));
  function registerFieldControlRef(element) {
    if (element) controlRef.current = element;
  }
  useValueChanged(() => valueUnwrapped(), () => {
    const current = valueUnwrapped();
    clearErrors(name());
    if (field.shouldValidateOnChange()) {
      void validation.commit(current);
    } else {
      void validation.commit(current, true);
    }
    const initialValue = field.validityData().initialValue;
    let isDirty;
    if (Array.isArray(current) && Array.isArray(initialValue)) {
      isDirty = !areArraysEqual(current, initialValue);
    } else {
      isDirty = current !== initialValue;
    }
    field.setDirty(isDirty);
  });
  if (process.env.NODE_ENV !== "production") {
    // Dev-only. Matches React `SliderRoot.tsx:273-277`. Reads with
    // `untrack` so re-reads during later updates don't retrigger the
    // warning — React's warn only fires on the first invalid render too.
    untrack(() => {
      if (min() >= max()) {
        console.warn("Slider `max` must be greater than `min`.");
      }
    });
  }

  // Blur any focused descendant when disabled flips on — some browsers leave
  // focus on a disabled form control which is both visually confusing and
  // breaks keyboard-escape patterns.
  useIsoLayoutEffect(() => disabled(), isDisabled => {
    if (!isDisabled) return undefined;
    const root = sliderRef.current;
    if (!root) return undefined;
    const activeEl = activeElement(ownerDocument(root));
    if (contains(root, activeEl)) {
      activeEl.blur?.();
    }
    return undefined;
  });

  // SSR-only: React emits a pre-hydration `<script>` from the last thumb
  // (see `SliderThumb.tsx:487-499`) to fix up `--position` CSS vars before
  // hydration. Skipped until the port gains SSR — the `renderBeforeHydration`
  // context flag is already plumbed so the future SSR agent's change set
  // is purely additive.

  function setValue(newValue, details) {
    const prev = valueUnwrapped();
    if (Number.isNaN(newValue) || prev !== undefined && areValuesEqual(newValue, prev)) {
      return;
    }
    const changeDetails = details ?? createChangeEventDetails(REASONS.none, undefined, undefined, {
      activeThumbIndex: -1
    });
    lastChangeReasonRef.current = changeDetails.reason;
    lastChangedValueRef.current = newValue;

    // Form-library interop (matches React `SliderRoot.tsx` ~226-239):
    // clone the native event and redefine `event.target` to `{ value, name }`
    // so libraries reading `event.target.value` / `event.target.name` in
    // `onValueChange` see the slider's value and name rather than whatever
    // raw DOM node dispatched the event. See
    // https://github.com/mui/material-ui/issues/13485#issuecomment-676048492.
    const nativeEvent = changeDetails.event;
    if (nativeEvent !== undefined) {
      try {
        const EventCtor = nativeEvent.constructor ?? Event;
        const cloned = new EventCtor(nativeEvent.type, nativeEvent);
        Object.defineProperty(cloned, "target", {
          writable: true,
          value: {
            value: newValue,
            name: name()
          }
        });
        changeDetails.event = cloned;
      } catch {
        // Some event types cannot be cloned (e.g. synthetic test events
        // with nonstandard constructors). Fall through silently — the
        // original event is still attached to `changeDetails`.
      }
    }
    if (props.onValueChange) {
      props.onValueChange(newValue, changeDetails);
    }
    // Cancel gate matches React Base UI `SliderRoot.setValue`:
    // `details.cancel()` keeps the uncontrolled state unchanged.
    if (changeDetails.isCanceled) return;
    setUncontrolledValue(newValue);
  }
  function handleInputChange(valueInput, index, event) {
    const currentValues = values();
    const newValue = getSliderValue(valueInput, index, min(), max(), range(), currentValues);
    if (validateMinimumDistance(newValue, step(), minStepsBetweenValues())) {
      const reason = getSliderChangeEventReason(event);
      const details = createChangeEventDetails(reason, event, undefined, {
        activeThumbIndex: index
      });
      setValue(newValue, details);
      field.setTouched(true);
      const committed = lastChangedValueRef.current ?? newValue;
      onValueCommitted(committed, createGenericEventDetails(reason, event));
    }
  }
  function onValueCommitted(committed, details) {
    if (props.onValueCommitted) {
      props.onValueCommitted(committed, details);
    }
  }

  // Read the parent field's labelId once per access; `resolveAriaLabelledBy`
  // prefers the field label over a locally-registered `<Slider.Label>`.
  const fieldLabelId = () => {
    const fs = field.state();
    return fs && "labelId" in fs ? fs.labelId ?? undefined : undefined;
  };
  const resolvedRootLabelId = () => resolveAriaLabelledBy(fieldLabelId(), labelId());

  // External aria-labelledby wins position-wise; merged with registered
  // labels via `useAriaLabelledBy`. The hook initialises against its
  // argument at body time — `untrack` keeps that snapshot read out of
  // the component's strict-read shell.
  const composedAriaLabelledBy = untrack(() => useAriaLabelledBy(() => {
    const external = props["aria-labelledby"];
    if (typeof external === "string" && external.length > 0) return external;
    return resolvedRootLabelId();
  }));
  const state = createMemo(() => ({
    ...field.state(),
    activeThumbIndex: active(),
    disabled: disabled(),
    dragging: dragging(),
    orientation: orientation(),
    max: max(),
    min: min(),
    minStepsBetweenValues: minStepsBetweenValues(),
    step: step(),
    values: values()
  }));
  const contextValue = {
    active,
    lastUsedThumbIndex,
    controlRef,
    thumbRefs,
    dragging,
    setDragging,
    disabled,
    formatOptions: format,
    locale,
    largeStep,
    step,
    min,
    max,
    minStepsBetweenValues,
    orientation,
    thumbCollisionBehavior,
    form,
    name,
    values,
    state,
    labelId: composedAriaLabelledBy,
    rootLabelId: () => defaultLabelId,
    setLabelId: setLabelIdState,
    setActive,
    handleInputChange,
    setValue,
    onValueCommitted,
    registerThumbMap,
    thumbMap,
    pressedInputRef,
    pressedThumbCenterOffsetRef,
    pressedThumbIndexRef,
    pressedValuesRef,
    lastChangedValueRef,
    lastChangeReasonRef,
    commitValidation: validation.commit,
    registerFieldControlRef,
    inset,
    renderBeforeHydration,
    indicatorPosition,
    setIndicatorPosition
  };

  // Resolve `class`/`style` here so function values (stateful styling) are
  // invoked with the slider state instead of being spread to the DOM raw.
  const userClassStyle = resolveClassStyle(props, () => state());
  const userProps = omit(props, "as", "aria-labelledby", "children", "class", "style", "attrs", "defaultValue", "disabled", "form", "format", "largeStep", "locale", "max", "min", "minStepsBetweenValues", "name", "onValueChange", "onValueCommitted", "orientation", "step", "thumbAlignment", "thumbCollisionBehavior", "value");
  const onMapChange = map => {
    registerThumbMap(map);
  };
  const localRef = el => {
    sliderRef.current = el;
  };
  const internalSlot = {
    id,
    role: "group",
    get children() {
      return <SliderRootContext value={contextValue}>
					<LabelableProvider>
						<CompositeList elementsRef={thumbRefs.current} onMapChange={onMapChange}>
							{props.children}
						</CompositeList>
					</LabelableProvider>
				</SliderRootContext>;
    }
  };
  let merged = mergeRootProps(internalSlot, seedAriaLabelledBy(composedAriaLabelledBy));
  merged = mergeRootProps(merged, sliderDataAttrs(state));
  merged = mergeRootProps(merged, untrack(() => validation.getAriaInvalidProps()));
  merged = mergeRootProps(merged, userClassStyle);
  merged = mergeRootProps(merged, userProps);
  const Tag = dynamic(() => props.as ?? "div");
  return <Tag {...merged} ref={composeRefs(localRef, userRef(props))} />;
}
//# sourceMappingURL=slider-root.jsx.map
