import { dynamic, isServer } from "@solidjs/web";
import { omit, onCleanup, untrack } from "solid-js";
import { useDirection } from "../../direction-provider/direction-provider-context.jsx";
import { createChangeEventDetails, createGenericEventDetails, REASONS } from "../../internals/change-event-details.jsx";
import { composeRefs, userRef } from "../../internals/compose-refs.jsx";
import { activeElement } from "../../internals/dom/active-element.jsx";
import { contains } from "../../internals/dom/contains.jsx";
import { getTarget } from "../../internals/dom/get-target.jsx";
import { ownerDocument } from "../../internals/dom/owner-document.jsx";
import { ownerWindow } from "../../internals/dom/owner-window.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 { resolveThumbCollision } from "../../internals/slider-utils/resolve-thumb-collision.jsx";
import { roundValueToStep } from "../../internals/slider-utils/round-value-to-step.jsx";
import { validateMinimumDistance } from "../../internals/slider-utils/validate-minimum-distance.jsx";
import { useAnimationFrame } from "../../internals/use-animation-frame.jsx";
import { useSliderRootContext } from "../slider-root-context.jsx";
import { sliderDataAttrs } from "../utils/state-attrs.jsx";
const INTENTIONAL_DRAG_COUNT_THRESHOLD = 2;
function getControlOffset(styles, vertical) {
  if (!styles) return {
    start: 0,
    end: 0
  };
  function parseSize(value) {
    const parsed = value != null ? Number.parseFloat(value) : 0;
    return Number.isNaN(parsed) ? 0 : parsed;
  }
  const start = vertical ? "Top" : "InlineStart";
  const end = vertical ? "Bottom" : "InlineEnd";
  // Indexing via bracket notation because React's port reads the same
  // camelCase border/padding keys off CSSStyleDeclaration.
  return {
    start: parseSize(styles[`border${start}Width`]) + parseSize(styles[`padding${start}`]),
    end: parseSize(styles[`border${end}Width`]) + parseSize(styles[`padding${end}`])
  };
}
function getFingerCoords(event, touchIdRef) {
  if (touchIdRef.current != null && event.changedTouches != null) {
    const touchEvent = event;
    for (let i = 0; i < touchEvent.changedTouches.length; i += 1) {
      const touch = touchEvent.changedTouches[i];
      if (touch && touch.identifier === touchIdRef.current) {
        return {
          x: touch.clientX,
          y: touch.clientY
        };
      }
    }
    return null;
  }
  const pointer = event;
  return {
    x: pointer.clientX,
    y: pointer.clientY
  };
}
/**
 * The clickable, interactive part of the slider. Renders a `<div>`.
 *
 * Owns the pointer/touch-drag state machine:
 *
 *  - `pointerdown` on the track picks the closest thumb, computes the finger
 *    position in value space, and begins listening for `pointermove` and
 *    `pointerup` on the ambient document.
 *  - `pointermove` applies collision resolution (push / swap / none) and
 *    commits a `drag` change.
 *  - `pointerup` commits the final value and releases listeners.
 *  - Touch events follow the same pipeline via `touchstart` / `touchmove` /
 *    `touchend`, keyed by the active touch identifier.
 *
 * Pointer-drag integration behavior is limited in happy-dom; covered in unit
 * tests where possible and scheduled for browser-level coverage later.
 */
export function SliderControl(props) {
  const context = useSliderRootContext();
  const direction = useDirection();
  const focusFrame = useAnimationFrame();
  const controlRef = {
    current: null
  };
  const touchIdRef = {
    current: null
  };
  const moveCountRef = {
    current: 0
  };
  // Seed the latest-values cache with a one-shot snapshot — the
  // pointer-handler effects below subscribe to subsequent `context.values()`
  // changes and refresh this ref. `untrack` keeps the snapshot read out of
  // the component-body strict-read shell.
  const latestValuesRef = {
    current: untrack(() => context.values())
  };
  // Computed style of the control, captured once on mount. Used by
  // `getControlOffset` to subtract border + padding from the usable drag
  // width. Grabbed via `ownerWindow` for shadow-DOM / iframe safety.
  const stylesRef = {
    current: null
  };
  // Half the thumb's main-axis size, maintained by `<Slider.Thumb>`'s
  // ResizeObserver when `thumbAlignment !== 'center'`. In edge mode the
  // usable drag region shrinks by `2 * insetThumbOffset` so the thumb's
  // edge (not its center) aligns with the control endpoints.
  const insetThumbOffsetRef = {
    current: 0
  };
  function updatePressedThumb(nextIndex) {
    if (context.pressedThumbIndexRef.current !== nextIndex) {
      context.pressedThumbIndexRef.current = nextIndex;
    }
    const thumbElement = context.thumbRefs.current[nextIndex];
    if (!thumbElement) {
      context.pressedThumbCenterOffsetRef.current = null;
      context.pressedInputRef.current = null;
      return;
    }
    context.pressedInputRef.current = thumbElement.querySelector('input[type="range"]');
  }
  function getFingerState(fingerCoords) {
    const control = controlRef.current;
    if (!control) return null;
    const vertical = context.orientation() === "vertical";
    const rect = control.getBoundingClientRect();
    const {
      width,
      height,
      bottom,
      left,
      right
    } = rect;
    const controlOffset = getControlOffset(stylesRef.current, vertical);
    const insetThumbOffset = insetThumbOffsetRef.current;
    const controlSize = (vertical ? height : width) - controlOffset.start - controlOffset.end - insetThumbOffset * 2;
    const thumbCenterOffset = context.pressedThumbCenterOffsetRef.current ?? 0;
    const fingerX = fingerCoords.x - thumbCenterOffset;
    const fingerY = fingerCoords.y - thumbCenterOffset;
    const rtl = direction() === "rtl";
    const valueSize = vertical ? bottom - fingerY - controlOffset.end : (rtl ? right - fingerX : fingerX - left) - controlOffset.start;
    const valueRescaled = clamp((valueSize - insetThumbOffset) / (controlSize || 1), 0, 1);
    const min = context.min();
    const max = context.max();
    const step = context.step();
    let newValue = (max - min) * valueRescaled + min;
    newValue = roundValueToStep(newValue, step, min);
    newValue = clamp(newValue, min, max);
    const values = context.values();
    const range = values.length > 1;
    if (!range) {
      return {
        value: newValue,
        thumbIndex: 0,
        didSwap: false
      };
    }
    const thumbIndex = context.pressedThumbIndexRef.current;
    if (thumbIndex < 0) return null;
    const collisionResult = resolveThumbCollision({
      behavior: context.thumbCollisionBehavior(),
      values,
      currentValues: latestValuesRef.current,
      initialValues: context.pressedValuesRef.current,
      pressedIndex: thumbIndex,
      nextValue: newValue,
      min,
      max,
      step,
      minStepsBetweenValues: context.minStepsBetweenValues()
    });
    if (context.thumbCollisionBehavior() === "swap" && collisionResult.didSwap) {
      updatePressedThumb(collisionResult.thumbIndex);
    } else {
      context.pressedThumbIndexRef.current = collisionResult.thumbIndex;
    }
    return collisionResult;
  }
  function startPressing(fingerCoords) {
    const values = context.values();
    context.pressedValuesRef.current = values.length > 1 ? values.slice() : null;
    latestValuesRef.current = values;
    const pressedThumbIndex = context.pressedThumbIndexRef.current;
    let closestThumbIndex = pressedThumbIndex;
    if (!(pressedThumbIndex > -1 && pressedThumbIndex < values.length)) {
      const vertical = context.orientation() === "vertical";
      const axis = vertical ? "y" : "x";
      let minDistance;
      closestThumbIndex = -1;
      for (let i = 0; i < context.thumbRefs.current.length; i += 1) {
        const thumbEl = context.thumbRefs.current[i];
        if (thumbEl instanceof Element) {
          const midpoint = getMidpoint(thumbEl);
          const distance = Math.abs(fingerCoords[axis] - midpoint[axis]);
          if (minDistance === undefined || distance <= minDistance) {
            closestThumbIndex = i;
            minDistance = distance;
          }
        }
      }
    }
    if (closestThumbIndex > -1 && closestThumbIndex !== pressedThumbIndex) {
      updatePressedThumb(closestThumbIndex);
    }
    if (context.inset()) {
      const thumbEl = context.thumbRefs.current[closestThumbIndex];
      if (thumbEl instanceof Element) {
        const vertical = context.orientation() === "vertical";
        const thumbRect = thumbEl.getBoundingClientRect();
        insetThumbOffsetRef.current = (vertical ? thumbRect.height : thumbRect.width) / 2;
      }
    }
  }
  function focusThumb(thumbIndex) {
    const thumb = context.thumbRefs.current[thumbIndex];
    const input = thumb?.querySelector('input[type="range"]');
    input?.focus({
      preventScroll: true
    });
  }
  function handleTouchMove(nativeEvent) {
    const fingerCoords = getFingerCoords(nativeEvent, touchIdRef);
    if (fingerCoords == null) return;
    moveCountRef.current += 1;
    if (nativeEvent.type === "pointermove" && nativeEvent.buttons === 0) {
      handleTouchEnd(nativeEvent);
      return;
    }
    const finger = getFingerState(fingerCoords);
    if (finger == null) return;
    if (validateMinimumDistance(finger.value, context.step(), context.minStepsBetweenValues())) {
      if (!context.dragging() && moveCountRef.current > INTENTIONAL_DRAG_COUNT_THRESHOLD) {
        context.setDragging(true);
      }
      const details = createChangeEventDetails(REASONS.drag, nativeEvent, undefined, {
        activeThumbIndex: finger.thumbIndex
      });
      context.setValue(finger.value, details);
      latestValuesRef.current = Array.isArray(finger.value) ? finger.value : [finger.value];
      if (finger.didSwap) focusThumb(finger.thumbIndex);
    }
  }
  function handleTouchEnd(nativeEvent) {
    context.setActive(-1);
    context.setDragging(false);
    context.pressedInputRef.current = null;
    context.pressedThumbCenterOffsetRef.current = null;
    const fingerCoords = getFingerCoords(nativeEvent, touchIdRef);
    const finger = fingerCoords != null ? getFingerState(fingerCoords) : null;
    if (finger != null) {
      const commitReason = context.lastChangeReasonRef.current;
      context.onValueCommitted(context.lastChangedValueRef.current ?? finger.value, createGenericEventDetails(commitReason, nativeEvent));
    }
    if ("pointerType" in nativeEvent) {
      const pointerId = nativeEvent.pointerId;
      if (controlRef.current?.hasPointerCapture?.(pointerId)) {
        controlRef.current.releasePointerCapture(pointerId);
      }
    }
    context.pressedThumbIndexRef.current = -1;
    touchIdRef.current = null;
    context.pressedValuesRef.current = null;
    stopListening();
  }
  function stopListening() {
    // Reached on the server via the setup `onCleanup` when a render scope is
    // disposed (e.g. during `renderToString`). No listeners were ever
    // bound server-side, and `ownerDocument(null)` would fall through to the
    // absent `document` global — so there is nothing to detach.
    if (isServer) return;
    const doc = ownerDocument(controlRef.current);
    doc.removeEventListener("pointermove", handleTouchMove);
    doc.removeEventListener("pointerup", handleTouchEnd);
    doc.removeEventListener("touchmove", handleTouchMove);
    doc.removeEventListener("touchend", handleTouchEnd);
    context.pressedValuesRef.current = null;
  }
  function handleTouchStart(nativeEvent) {
    if (context.disabled()) return;
    const touch = nativeEvent.changedTouches[0];
    if (touch != null) touchIdRef.current = touch.identifier;
    const fingerCoords = getFingerCoords(nativeEvent, touchIdRef);
    if (fingerCoords != null) {
      startPressing(fingerCoords);
      const finger = getFingerState(fingerCoords);
      if (finger == null) return;
      focusThumb(finger.thumbIndex);
      const details = createChangeEventDetails(REASONS.trackPress, nativeEvent, undefined, {
        activeThumbIndex: finger.thumbIndex
      });
      context.setValue(finger.value, details);
      latestValuesRef.current = Array.isArray(finger.value) ? finger.value : [finger.value];
      if (finger.didSwap) focusThumb(finger.thumbIndex);
    }
    moveCountRef.current = 0;
    const doc = ownerDocument(controlRef.current);
    doc.addEventListener("touchmove", handleTouchMove, {
      passive: true
    });
    doc.addEventListener("touchend", handleTouchEnd, {
      passive: true
    });
  }
  function bindTouchStart(el) {
    el.addEventListener("touchstart", handleTouchStart, {
      passive: true
    });
    return () => {
      el.removeEventListener("touchstart", handleTouchStart);
    };
  }
  onCleanup(() => {
    stopListening();
    focusFrame.cancel();
  });
  let unbindTouch = null;
  const localRef = el => {
    if (unbindTouch) {
      unbindTouch();
      unbindTouch = null;
    }
    controlRef.current = el;
    context.registerFieldControlRef(el);
    if (el) {
      // React captures `getComputedStyle` once on mount (the live
      // CSSStyleDeclaration re-reads on every property access). We mirror
      // that here — `ownerWindow` keeps realm checks correct for iframe /
      // shadow DOM hosts.
      if (stylesRef.current == null) {
        stylesRef.current = ownerWindow(el).getComputedStyle(el);
      }
      unbindTouch = bindTouchStart(el);
    }
  };

  // Resolve `class`/`style` here so function values (stateful styling) are
  // invoked with the slider state — otherwise a `class={(state) => …}` is
  // spread to the DOM raw and never applied (no class attribute at all),
  // collapsing the control's layout.
  const userClassStyle = resolveClassStyle(props, () => context.state());
  const userProps = omit(props, "as", "class", "style", "attrs");
  const internalSlot = {
    // SSR hook for the deferred prehydration script. React emits
    // this data attr only when `renderBeforeHydration` is true so
    // the script can `querySelectorAll` matching controls. We seed
    // `""` so Solid 2's DOM spread keeps the key in `ownKeys` even
    // when the flag is off (see
    // `test/internals/merge-props-undefined-attr.test.tsx`).
    get "data-quoin-slider-control"() {
      return context.renderBeforeHydration() ? "" : "";
    },
    onPointerDown(event) {
      const control = controlRef.current;
      const target = getTarget(event);
      if (!control || context.disabled() || event.defaultPrevented || !(target instanceof Element) || event.button !== 0) {
        return;
      }
      const fingerCoords = getFingerCoords(event, touchIdRef);
      if (fingerCoords != null) {
        startPressing(fingerCoords);
        const finger = getFingerState(fingerCoords);
        if (finger == null) return;
        const pressedOnFocusedThumb = contains(context.thumbRefs.current[finger.thumbIndex], activeElement(ownerDocument(control)));
        if (pressedOnFocusedThumb) {
          event.preventDefault();
        } else {
          focusFrame.request(() => {
            focusThumb(finger.thumbIndex);
          });
        }
        context.setDragging(true);
        const pressedOnAnyThumb = context.pressedThumbCenterOffsetRef.current != null;
        if (!pressedOnAnyThumb) {
          const details = createChangeEventDetails(REASONS.trackPress, event, undefined, {
            activeThumbIndex: finger.thumbIndex
          });
          context.setValue(finger.value, details);
          latestValuesRef.current = Array.isArray(finger.value) ? finger.value : [finger.value];
          if (finger.didSwap) focusThumb(finger.thumbIndex);
        }
      }
      if (event.pointerId) {
        control.setPointerCapture(event.pointerId);
      }
      moveCountRef.current = 0;
      const doc = ownerDocument(controlRef.current);
      doc.addEventListener("pointermove", handleTouchMove, {
        passive: true
      });
      doc.addEventListener("pointerup", handleTouchEnd, {
        once: true
      });
    }
  };
  let merged = mergeRootProps(internalSlot, sliderDataAttrs(context.state));
  merged = mergeRootProps(merged, userClassStyle);
  merged = mergeRootProps(merged, userProps);
  const Tag = dynamic(() => props.as ?? "div");
  return <Tag {...merged} ref={composeRefs(localRef, userRef(props))} />;
}
//# sourceMappingURL=slider-control.jsx.map
