/**
 * Stage 4.4 port of Base UI React's `useHoverReferenceInteraction`.
 *
 * React source:
 *   `base-ui/packages/react/src/floating-ui-react/hooks/useHoverReferenceInteraction.ts`
 *
 * Attaches the hover event handlers that belong on the reference (trigger)
 * element. Coordinates with the floating-side counterpart (below) via
 * `HoverInteraction`, the shared mutable bag on `dataRef`.
 *
 * Returns a prop bag (`onPointerDown`, `onPointerEnter`, `onMouseMove`)
 * applied via the `reference` slot of `useInteractions`. The heavy
 * `mouseenter` / `mouseleave` wiring is installed imperatively with
 * `addEventListener` so disabled-element gaps don't swallow the events
 * (React's delegation system has the same quirk).
 *
 * ### React → Solid deviations
 *
 * 1. `useRef` → plain `MutableRef<T>` objects or closure `let`s. The
 *    `HoverInteraction` shared bag is pure mutables so no signals are
 *    involved — the hover logic runs entirely in event callbacks and
 *    effect bodies.
 * 2. `useValueAsRef` → not needed for options read inside event
 *    handlers — we capture them at setup. Options are read at event
 *    time off the captured `props` reference so late-bound changes
 *    observed by callers still apply (plain JS behaviour).
 * 3. `useStableCallback` → `createStableCallback` from
 *    `src/internals/stable-callback.ts`. Same identity-stability
 *    guarantee — ensures `addEventListener` / `removeEventListener`
 *    pair up across re-installations.
 * 4. React's `useEffect` with dep arrays → `createEffect` in the
 *    split `(compute, apply)` form. The compute branch returns a
 *    tracked snapshot keyed on `enabled` / the signals we want to
 *    re-run on; the apply branch installs listeners and registers
 *    `onCleanup`.
 * 5. `ReactDOM.flushSync` (used on the touch open path) → Solid's
 *    `flush()` from `solid-js`. Same semantics: apply all pending
 *    updates before the next tick, so downstream reactive readers
 *    see the touch-opened state synchronously.
 * 6. React returns `HTMLProps | undefined` from `useMemo`; the Solid
 *    port returns a plain object since the body runs once.
 */

import { isElement } from "@floating-ui/utils/dom";
import { createEffect, flush, onCleanup, untrack } from "solid-js";
import { createChangeEventDetails, REASONS } from "../../change-event-details.jsx";
import { ownerDocument } from "../../dom/owner-document.jsx";
import { readOption } from "../../maybe-accessor.jsx";
import { createStableCallback } from "../../stable-callback.jsx";
import { useFloatingTree } from "../components/floating-tree.jsx";
import { contains, getTarget, isTargetInsideEnabledTrigger } from "../utils/element.jsx";
import { isMouseLikePointerType } from "../utils/event.jsx";
import { applySafePolygonPointerEventsMutation, clearSafePolygonPointerEventsMutation, useHoverInteractionSharedState } from "./use-hover-interaction-shared-state.jsx";
import { getDelay, getRestMs, isClickLikeOpenEvent as isClickLikeOpenEventShared } from "./use-hover-shared.jsx";
const EMPTY_REF = {
  current: null
};
/**
 * Provides hover interactions that should be attached to reference or trigger
 * elements.
 */
export function useHoverReferenceInteraction(context, props = {}) {
  const store = "rootStore" in context ? context.rootStore : context;
  const {
    dataRef,
    events
  } = store.context;

  // Untrack the body-time prop destructure: parents may pass
  // signal-derived values via JSX, which makes each `props.foo`
  // access a tracked read and fires `[STRICT_READ_UNTRACKED]` under
  // the strict-read scope set up by `devComponent(...)`. These fields
  // are typed as plain values (not `MaybeAccessor`), so a one-shot
  // snapshot here matches the React fork's semantics.
  const {
    delay = 0,
    handleClose = null,
    mouseOnly = false,
    restMs = 0,
    move = true,
    triggerElementRef = EMPTY_REF,
    externalTree,
    getHandleCloseContext,
    isClosing
  } = untrack(() => ({
    delay: props.delay,
    handleClose: props.handleClose,
    mouseOnly: props.mouseOnly,
    restMs: props.restMs,
    move: props.move,
    triggerElementRef: props.triggerElementRef,
    externalTree: props.externalTree,
    getHandleCloseContext: props.getHandleCloseContext,
    isClosing: props.isClosing
  }));
  const readEnabled = () => readOption(props.enabled) ?? true;
  const readIsActiveTrigger = () => readOption(props.isActiveTrigger) ?? true;
  const readTriggerElement = () => readOption(props.triggerElement) ?? null;
  const tree = useFloatingTree(externalTree);
  const instance = useHoverInteractionSharedState(store);
  const isHoverCloseActiveRef = {
    current: false
  };
  createEffect(() => readIsActiveTrigger(), active => {
    if (active) {
      instance.handleCloseOptions = handleClose?.__options;
    }
  });
  const isClickLikeOpenEvent = createStableCallback(() => () => isClickLikeOpenEventShared(dataRef.current.openEvent?.type, instance.interactedInside));
  const isRelatedTargetInsideEnabledTrigger = createStableCallback(() => target => {
    return isTargetInsideEnabledTrigger(target, store.context.triggerElements);
  });
  const isOverInactiveTrigger = createStableCallback(() => (currentDomReference, currentTarget, target) => {
    const allTriggers = store.context.triggerElements;
    if (allTriggers.hasElement(currentTarget)) {
      return !currentDomReference || !contains(currentDomReference, currentTarget);
    }
    if (!isElement(target)) {
      return false;
    }
    const targetElement = target;
    return allTriggers.hasMatchingElement(trigger => contains(trigger, targetElement)) && (!currentDomReference || !contains(currentDomReference, targetElement));
  });
  const closeWithDelay = createStableCallback(() => (event, runElseBranch = true) => {
    const closeDelay = getDelay(delay, "close", instance.pointerType);
    if (closeDelay) {
      instance.openChangeTimeout.start(closeDelay, () => {
        store.setOpen(false, createChangeEventDetails(REASONS.triggerHover, event));
        tree?.events.emit("floating.closed", event);
      });
    } else if (runElseBranch) {
      instance.openChangeTimeout.clear();
      store.setOpen(false, createChangeEventDetails(REASONS.triggerHover, event));
      tree?.events.emit("floating.closed", event);
    }
  });
  const cleanupMouseMoveHandler = createStableCallback(() => () => {
    if (!instance.handler) {
      return;
    }
    const doc = ownerDocument(store.state.domReferenceElement);
    doc.removeEventListener("mousemove", instance.handler);
    instance.handler = undefined;
  });
  const clearPointerEvents = createStableCallback(() => () => {
    clearSafePolygonPointerEventsMutation(instance);
  });

  // Single-shot unmount-style cleanup — matches React's
  // `React.useEffect(() => cleanupMouseMoveHandler, [...])`.
  onCleanup(() => {
    cleanupMouseMoveHandler();
  });

  // `openchange` listener: clears pending hover timers whenever the
  // popup closes so a delayed hover-open can't fight an external
  // `setOpen(false)`. Matches React's subscriber.
  createEffect(() => readEnabled(), isEnabled => {
    if (!isEnabled) {
      return;
    }
    function onOpenChangeLocal(details) {
      if (!details.open) {
        isHoverCloseActiveRef.current = details.reason === REASONS.triggerHover;
        cleanupMouseMoveHandler();
        instance.openChangeTimeout.clear();
        instance.restTimeout.clear();
        instance.blockMouseMove = true;
        instance.restTimeoutPending = false;
      } else {
        isHoverCloseActiveRef.current = false;
      }
    }
    events.on("openchange", onOpenChangeLocal);
    return () => {
      events.off("openchange", onOpenChangeLocal);
    };
  });

  // Main reference-element listener effect. Re-runs when the reference
  // element identity changes (so `addEventListener` latches on the new
  // node), when the trigger ref signal updates, or when `enabled` flips.
  createEffect(() => {
    const isActiveTrigger = readIsActiveTrigger();
    const trigger = readTriggerElement() ?? triggerElementRef.current ?? (isActiveTrigger ? store.state.domReferenceElement : null);
    return {
      trigger,
      enabled: readEnabled()
    };
  }, ({
    trigger,
    enabled
  }) => {
    if (!enabled) {
      return;
    }
    if (!isElement(trigger)) {
      return;
    }
    // `isElement` narrows to `Element`, but the listeners below use the
    // `HTMLElementEventMap` overloads; bind a non-null `HTMLElement` so the
    // typed handlers resolve and the narrowing survives nested closures.
    const triggerEl = trigger;
    let referenceHasPointer = false;
    function onPointerEnterNative(event) {
      instance.pointerType = event.pointerType;
    }
    function onMouseEnter(event) {
      referenceHasPointer = true;
      instance.openChangeTimeout.clear();
      instance.blockMouseMove = false;

      // `mouseenter` can fire before JSX `onPointerEnter` runs (or when
      // only imperative listeners are active). Treat it as mouse-like.
      if (mouseOnly && !isMouseLikePointerType(instance.pointerType) && event.type === "mouseenter") {
        instance.pointerType = "mouse";
      }
      if (mouseOnly && !isMouseLikePointerType(instance.pointerType)) {
        return;
      }

      // Only rest delay is set; there's no fallback delay.
      // This will be handled by `onMouseMove`.
      const restMsValue = getRestMs(restMs);
      const openDelay = getDelay(delay, "open", instance.pointerType);
      const eventTarget = getTarget(event);
      const currentTarget = event.currentTarget ?? null;
      const currentDomReference = store.state.domReferenceElement;
      let triggerNode = currentTarget;

      // Wrapper/delegated mode: resolve the actual trigger from the event target.
      if (isElement(eventTarget) && !store.context.triggerElements.hasElement(eventTarget)) {
        for (const triggerElement of store.context.triggerElements.elements()) {
          if (contains(triggerElement, eventTarget)) {
            triggerNode = triggerElement;
            break;
          }
        }
      }

      // Wrapper/delegated mode fallback.
      if (isElement(currentTarget) && isElement(currentDomReference) && !store.context.triggerElements.hasElement(currentTarget) && contains(currentTarget, currentDomReference)) {
        triggerNode = currentDomReference;
      }
      const isOverInactive = triggerNode == null ? false : isOverInactiveTrigger(currentDomReference, triggerNode, eventTarget);
      const isOpen = store.state.open;
      const isInClosingTransition = isClosing?.() ?? store.state.transitionStatus === "ending";
      const isHoverCloseTransition = !isOpen && isInClosingTransition && isHoverCloseActiveRef.current;
      const isReenteringSameTriggerDuringCloseTransition = !isOverInactive && isElement(triggerNode) && isElement(currentDomReference) && contains(currentDomReference, triggerNode) && isHoverCloseTransition;
      const isRestOnlyDelay = restMsValue > 0 && !openDelay;
      const shouldOpenImmediately = isOverInactive && (isOpen || isHoverCloseTransition) || isReenteringSameTriggerDuringCloseTransition;
      const shouldOpen = !isOpen || isOverInactive;
      if (shouldOpenImmediately) {
        store.setOpen(true, createChangeEventDetails(REASONS.triggerHover, event, triggerNode ?? undefined));
        return;
      }
      if (isRestOnlyDelay) {
        return;
      }
      if (openDelay) {
        instance.openChangeTimeout.start(openDelay, () => {
          if (!shouldOpen || !referenceHasPointer) {
            return;
          }
          if (!store.state.open) {
            store.setOpen(true, createChangeEventDetails(REASONS.triggerHover, event, triggerNode ?? undefined));
          }
        });
      } else if (shouldOpen) {
        store.setOpen(true, createChangeEventDetails(REASONS.triggerHover, event, triggerNode ?? undefined));
      }
    }
    function onMouseLeave(event) {
      referenceHasPointer = false;
      if (isClickLikeOpenEvent()) {
        clearPointerEvents();
        return;
      }

      // Cancel a pending hover-open when the pointer leaves before the
      // delay elapses. This must run even when `floatingContext` is not
      // wired yet (popup still mounting) — otherwise the timer fires
      // after leave and the surface blips visible briefly.
      if (!store.state.open) {
        instance.openChangeTimeout.clear();
      }
      cleanupMouseMoveHandler();
      const domReferenceElement = store.state.domReferenceElement;
      const doc = ownerDocument(domReferenceElement);
      instance.restTimeout.clear();
      instance.restTimeoutPending = false;
      const handleCloseContextBase = dataRef.current.floatingContext ?? getHandleCloseContext?.();
      const ignoreRelatedTargetTrigger = isRelatedTargetInsideEnabledTrigger(event.relatedTarget);
      if (ignoreRelatedTargetTrigger) {
        return;
      }
      if (handleClose && handleCloseContextBase) {
        if (!store.state.open) {
          instance.openChangeTimeout.clear();
        }
        const currentTrigger = triggerElementRef.current;
        instance.handler = handleClose({
          ...handleCloseContextBase,
          tree,
          x: event.clientX,
          y: event.clientY,
          onClose() {
            clearPointerEvents();
            cleanupMouseMoveHandler();
            if (readEnabled() && !isClickLikeOpenEvent() && currentTrigger === store.state.domReferenceElement) {
              closeWithDelay(event, true);
            }
          }
        });
        doc.addEventListener("mousemove", instance.handler);
        instance.handler(event);
        return;
      }
      if (!store.state.open) {
        return;
      }
      const shouldClose = instance.pointerType === "touch" ? !contains(store.state.floatingElement, event.relatedTarget) : true;
      if (shouldClose) {
        closeWithDelay(event);
      }
    }
    function syncHoverWhileAlreadyOver() {
      if (store.state.open || instance.openChangeTimeout.isStarted()) {
        return;
      }
      let hovered = false;
      try {
        hovered = triggerEl.matches(":hover");
      } catch {
        hovered = false;
      }
      if (!hovered) {
        return;
      }
      onMouseEnter(new MouseEvent("mouseenter", {
        bubbles: true,
        cancelable: true
      }));
    }
    const cleanups = [];
    if (move) {
      const onceHandler = evt => onMouseEnter(evt);
      triggerEl.addEventListener("mousemove", onceHandler, {
        once: true
      });
      cleanups.push(() => {
        triggerEl.removeEventListener("mousemove", onceHandler);
      });
    }
    triggerEl.addEventListener("pointerenter", onPointerEnterNative);
    triggerEl.addEventListener("mouseenter", onMouseEnter);
    triggerEl.addEventListener("mouseleave", onMouseLeave);
    cleanups.push(() => {
      triggerEl.removeEventListener("pointerenter", onPointerEnterNative);
      triggerEl.removeEventListener("mouseenter", onMouseEnter);
      triggerEl.removeEventListener("mouseleave", onMouseLeave);
    });
    queueMicrotask(syncHoverWhileAlreadyOver);
    return () => {
      for (const cleanup of cleanups) {
        cleanup();
      }
    };
  });
  function setPointerRef(event) {
    instance.pointerType = event.pointerType;
  }
  return {
    onPointerDown: setPointerRef,
    onPointerEnter: setPointerRef,
    onMouseMove(event) {
      const nativeEvent = event;
      const trigger = event.currentTarget;
      const currentDomReference = store.state.domReferenceElement;
      const currentOpen = store.state.open;
      const isOverInactive = isOverInactiveTrigger(currentDomReference, trigger, event.target);
      if (mouseOnly && !isMouseLikePointerType(instance.pointerType)) {
        return;
      }
      if (currentOpen && isOverInactive && instance.handleCloseOptions?.blockPointerEvents) {
        const floatingElement = store.state.floatingElement;
        if (floatingElement) {
          const scopeElement = instance.handleCloseOptions?.getScope?.() ?? trigger.ownerDocument.body;
          applySafePolygonPointerEventsMutation(instance, {
            scopeElement,
            referenceElement: trigger,
            floatingElement
          });
        }
      }
      const restMsValue = getRestMs(restMs);
      if (currentOpen && !isOverInactive || restMsValue === 0) {
        return;
      }
      const movementX = event.movementX;
      const movementY = event.movementY;
      if (!isOverInactive && instance.restTimeoutPending && (movementX ?? 0) ** 2 + (movementY ?? 0) ** 2 < 2) {
        return;
      }
      instance.restTimeout.clear();
      function handleMouseMove() {
        instance.restTimeoutPending = false;

        // A delayed hover open should not override a click-like open that
        // happened while the hover delay was pending.
        if (isClickLikeOpenEvent()) {
          return;
        }
        const latestOpen = store.state.open;
        if (!instance.blockMouseMove && (!latestOpen || isOverInactive)) {
          store.setOpen(true, createChangeEventDetails(REASONS.triggerHover, nativeEvent, trigger));
        }
      }
      if (instance.pointerType === "touch") {
        // React uses `ReactDOM.flushSync` to apply the touch-open
        // synchronously so downstream reactive readers see it in
        // the same tick. Solid's `flush()` is the direct analogue.
        handleMouseMove();
        flush();
      } else if (isOverInactive && currentOpen) {
        handleMouseMove();
      } else {
        instance.restTimeoutPending = true;
        instance.restTimeout.start(restMsValue, handleMouseMove);
      }
    }
  };
}
//# sourceMappingURL=use-hover-reference-interaction.jsx.map
