import { onCleanup, untrack } from "solid-js";
import { createStableCallback } from "./stable-callback.jsx";
import { useAnimationFrame } from "./use-animation-frame.jsx";

/**
 * A `data-starting-style` marker attribute — matches the convention used by
 * Base UI React's `TransitionStatusDataAttributes.startingStyle`. Kept inline
 * here so this primitive doesn't need to depend on a
 * `stateAttributesMapping` module that may not yet exist in the Solid port.
 */
const STARTING_STYLE_ATTR = "data-starting-style";

/**
 * The element source accepted by {@link useAnimationsFinished}. Consumers may
 * pass either:
 *
 * - An accessor that returns the element (preferred — re-reading it lets the
 *   primitive pick up late-mounted elements).
 * - A plain `HTMLElement | null` reference captured via `ref`.
 */

function resolveElement(source) {
  if (source === null) return null;
  if (typeof source === "function") return source();
  return source;
}

/**
 * Returns a function that, given a callback, waits for all CSS
 * animations/transitions on the target element to settle before invoking it.
 *
 * Mirrors Base UI React's `useAnimationsFinished`
 * (`packages/react/src/internals/useAnimationsFinished.ts`), but without
 * `ReactDOM.flushSync` — Solid 2 uses `flush()` for synchronous application,
 * and consumers that need a synchronous apply can wrap the callback
 * themselves. The vast majority of callers just want "run after animations",
 * so we invoke the callback directly.
 *
 * Options:
 *
 * - `waitForStartingStyleRemoved`: if `true`, waits for
 *   `[data-starting-style]` to be removed from the element before probing
 *   `getAnimations()`. This matches React's behavior for "open" animations
 *   that aren't registered until the starting-style attribute is cleared.
 * - `treatAbortedAsFinished`: if `false`, when an animation is aborted the
 *   primitive re-probes for new animations instead of treating it as
 *   finished.
 *
 * The returned function is stabilized via {@link createStableCallback} so its
 * identity never changes — safe to pass through directives or capture in
 * long-lived subscriptions.
 *
 * Solid 2 notes:
 *
 * - Must be called from an owned scope (the internal `useAnimationFrame`
 *   registers an `onCleanup`).
 * - No DOM reads happen during setup; the element is resolved lazily each
 *   time the returned function is called, so it's safe to pass an accessor
 *   that returns `null` on the first call.
 */
export function useAnimationsFinished(elementSource, waitForStartingStyleRemoved = false, treatAbortedAsFinished = true) {
  const frame = useAnimationFrame();

  // Owner-dispose cleanup. The frame-scheduler + any external `signal` each
  // clean themselves up, but a MutationObserver kicked off in the
  // `waitForStartingStyleRemoved` branch has no owner tie: if the owning
  // scope disposes before the attribute is removed (and no signal is
  // passed), the observer would leak. We track the live observer and the
  // live per-call AbortController so `onCleanup` can tear everything down
  // in one shot.
  let activeObserver = null;
  let activeInternalAbort = null;
  onCleanup(() => {
    activeObserver?.disconnect();
    activeObserver = null;
    activeInternalAbort?.abort();
    activeInternalAbort = null;
  });
  const runWhenAnimationsFinished = (fnToExecute, signal = null) =>
  // Side-effect helper — re-runs are scheduled by the caller, not by
  // this body's reads. Wrapping the entry in `untrack` keeps the
  // elementSource accessor read AND the consumer callback invocation
  // from tripping `[STRICT_READ_UNTRACKED]` when the helper is invoked
  // from an effect's apply scope (e.g. `useOpenChangeComplete`).
  untrack(() => {
    frame.cancel();
    // New invocation — stop any prior observer/listener we may have set
    // up for a previous call's `waitForStartingStyleRemoved` cycle AND
    // abort any in-flight `Promise.all(animations.finished)` from a
    // previous call, so a stale `.then` can't fire `fnToExecute` after
    // we've started a fresh cycle (stale state writes on rapid toggle).
    activeObserver?.disconnect();
    activeObserver = null;
    activeInternalAbort?.abort();
    activeInternalAbort = null;
    const element = resolveElement(elementSource);
    if (element == null) return;
    const resolvedElement = element;

    // Tests and SSR-like environments may omit `getAnimations`; and the
    // global kill-switch disables animation waiting entirely.
    if (typeof resolvedElement.getAnimations !== "function" || globalThis.QUOIN_ANIMATIONS_DISABLED) {
      // `untrack` so consumer callbacks reading reactive state during
      // the synchronous fast-path don't trip `[STRICT_READ_UNTRACKED]`
      // when this helper is invoked from inside an effect's apply scope
      // (e.g. `useOpenChangeComplete`'s setOpen-driven effect calling
      // the popup's `onComplete` straight through). The async path
      // below already runs after a microtask so it's outside the apply
      // scope by definition; wrapping `done()` consistently keeps the
      // synchronous and async semantics identical from the consumer's
      // perspective.
      untrack(fnToExecute);
      return;
    }

    // Fresh internal abort controller for this invocation. The
    // `Promise.all` chain below checks `cycleAbort.signal.aborted`
    // before calling `done()`, so a later invocation that fires
    // `cycleAbort.abort()` cancels this cycle's callback. Without this,
    // a rapid open/close cycle can land the previous close's
    // `fnToExecute` AFTER the new open's measure has committed.
    const cycleAbort = new AbortController();
    activeInternalAbort = cycleAbort;
    const done = () => {
      untrack(fnToExecute);
    };
    function exec() {
      Promise.all(resolvedElement.getAnimations().map(animation => animation.finished)).then(() => {
        if (cycleAbort.signal.aborted) return;
        if (!signal?.aborted) done();
      }).catch(() => {
        if (cycleAbort.signal.aborted) return;
        if (treatAbortedAsFinished) {
          if (!signal?.aborted) done();
          return;
        }
        const currentAnimations = resolvedElement.getAnimations();
        if (!signal?.aborted && currentAnimations.length > 0 && currentAnimations.some(animation => animation.pending || animation.playState !== "finished")) {
          // Animation was aborted because an underlying property changed
          // mid-flight; re-probe so we wait on the replacement.
          exec();
        }
      });
    }
    if (waitForStartingStyleRemoved) {
      // If the attribute isn't present at call-time, fall back to waiting
      // one frame so late-registered "open" animations get a chance to
      // show up in `getAnimations()`.
      if (!resolvedElement.hasAttribute(STARTING_STYLE_ATTR)) {
        frame.request(exec);
        return;
      }
      const attributeObserver = new MutationObserver(() => {
        if (!resolvedElement.hasAttribute(STARTING_STYLE_ATTR)) {
          attributeObserver.disconnect();
          if (activeObserver === attributeObserver) activeObserver = null;
          exec();
        }
      });
      attributeObserver.observe(resolvedElement, {
        attributes: true,
        attributeFilter: [STARTING_STYLE_ATTR]
      });

      // Track so owner-dispose can disconnect us. `cycleAbort` is this
      // invocation's abort controller — aborting it (on owner dispose
      // or on the next invocation of `runWhenAnimationsFinished`)
      // tears down the observer, and the `Promise.all` branch above
      // short-circuits `done()` when `cycleAbort.signal.aborted`.
      activeObserver = attributeObserver;
      const teardown = () => {
        attributeObserver.disconnect();
        if (activeObserver === attributeObserver) activeObserver = null;
      };
      cycleAbort.signal.addEventListener("abort", teardown, {
        once: true
      });
      signal?.addEventListener("abort", teardown, {
        once: true
      });
      return;
    }
    frame.request(exec);
  });

  // Expose via `createStableCallback` so the returned function's identity is
  // stable (matches React's `useStableCallback` contract). The inner
  // function is already a single closure for this hook's lifetime, but we
  // keep the trampoline so call-site shape matches React.
  return createStableCallback(() => runWhenAnimationsFinished);
}
//# sourceMappingURL=use-animations-finished.jsx.map
