import { createRenderEffect, createSignal, untrack } from "solid-js";
import { readOption } from "../maybe-accessor.jsx";
import { SNAPSHOT_KEYS, snapshotKeys } from "../observability/snapshot.jsx";
import { resetLayoutStyles } from "../reset-layout-styles.jsx";
import { useAnimationsFinished } from "../use-animations-finished.jsx";
import { useTransitionStatus } from "../use-transition-status/use-transition-status.jsx";

/** Props that `mergePanelProps` injects onto the panel element. */

/**
 * Options for {@link createMountTransition}.
 */

/**
 * Return value of {@link createMountTransition}.
 */

/**
 * Reusable animation-lifecycle primitive for any animated compound surface
 * (Collapsible, Dialog, Popover, Tooltip, etc.).
 *
 * Extracts:
 * - `useTransitionStatus` state machine (mounted / transitionStatus)
 * - Panel measurement (scrollHeight / scrollWidth → CSS vars)
 * - Exit-animation wait via `useAnimationsFinished`
 * - `beforematch` listener for `hiddenUntilFound`
 *
 * The caller owns the `open` signal; this helper wraps the rest.
 *
 * ### Design deviation from recommended signature
 *
 * The recommended interface included `setMounted` as an escape hatch. This
 * implementation omits it from the public surface — `setMounted` is an
 * internal implementation detail of the exit-animation path and should not be
 * called directly by callers. If callers need it, they should re-implement the
 * exit-animation logic. This keeps the surface smaller.
 *
 * `mergePanelProps` replaces the proposed `mergePanelProps<P>` function — same
 * idea, slightly renamed for clarity. The returned object uses getters so
 * Solid's compiled spread stays reactive after the merge.
 *
 * ### Solid 2 notes
 *
 * - `panelElement` signal uses `ownedWrite: true` so the ref callback (unowned
 *   scope) can write — same pattern as `renderElement`'s host-capture signal.
 * - Dimension signals use `ownedWrite: true` for the same reason (writes from
 *   animation-finished callbacks).
 * - Effects are `createRenderEffect` so dimension writes are synchronous with
 *   DOM updates (no paint flash).
 */
export function createMountTransition(open, options = {}) {
  // Enter settle must not drop `data-starting-style` until measured height is
  // committed — otherwise `--*-height` stays `0px`/`auto` and open is instant.
  const [enterMeasureReady, setEnterMeasureReady] = createSignal(false, {
    ownedWrite: true,
    name: "mount-transition.enterMeasureReady"
  });
  createRenderEffect(() => open(), (isOpen, wasOpen) => {
    // Only reset on false → true. Resetting on every `open === true`
    // re-apply would clear `enterMeasureReady` right after measure commits.
    if (isOpen && wasOpen !== true) {
      setEnterMeasureReady(false);
    }
  });

  // ── Transition state machine ──────────────────────────────────────────────
  // idle-state enabled (stays at "idle" while open) + deferred ending (fires
  // on next frame after close). Matches the Collapsible root's call signature.
  const {
    mounted,
    transitionStatus,
    setMounted
  } = useTransitionStatus(open, () => true, () => true, enterMeasureReady);

  // ── Panel element reference ───────────────────────────────────────────────
  // If the caller passes in a pre-owned accessor we read from it; otherwise
  // we create our own signal.
  const [ownedPanelElement, setOwnedPanelElement] = createSignal(null, {
    ownedWrite: true,
    name: "mount-transition.panelElement"
  });
  const panelElement = options.panelElement ?? ownedPanelElement;
  const panelRef = options.panelElement ? _el => {} : el => {
    setOwnedPanelElement(el);
  };

  // ── Dimensions ───────────────────────────────────────────────────────────
  const [height, setHeight] = createSignal(undefined, {
    ownedWrite: true,
    name: "mount-transition.height"
  });
  const [width, setWidth] = createSignal(undefined, {
    ownedWrite: true,
    name: "mount-transition.width"
  });

  // Open settle mirrors React `useOpenChangeComplete({ open: true })`:
  // wait for `[data-starting-style]` removal before probing animations.
  // Close uses `false` for `waitForStartingStyleRemoved` and
  // `treatAbortedAsFinished: false` — matches `useCollapsiblePanel`.
  const runOnceOpenAnimationsFinish = useAnimationsFinished(panelElement, true);
  const runOnceCloseAnimationsFinish = useAnimationsFinished(panelElement, false, false);
  function measure(el) {
    setHeight(el.scrollHeight);
    setWidth(el.scrollWidth);
  }

  /**
   * Measure expanded size for the enter animation. Returns `true` when a
   * non-zero size was committed (or dimensions were already settled).
   * Mirrors React `useCollapsiblePanel` open pass: `resetLayoutStyles` then
   * `getDimensions`.
   */
  function measureForOpen(el) {
    // Drop stale `0` from the previous close cycle so the first open frame
    // does not lock `--*-height` to `0px` before we read scroll sizes.
    if (untrack(height) === 0) {
      setHeight(undefined);
      setWidth(undefined);
    }
    const restoreLayout = resetLayoutStyles(el);
    const nextHeight = el.scrollHeight;
    const nextWidth = el.scrollWidth;
    restoreLayout();
    if (nextHeight === 0 && nextWidth === 0) {
      return false;
    }
    setHeight(nextHeight);
    setWidth(nextWidth);
    // Commit the starting frame (height: 0 via `data-starting-style`)
    // before `useTransitionStatus` advances to idle on the next rAF(s).
    void el.offsetHeight;
    setEnterMeasureReady(true);
    return true;
  }

  // Enter: measure natural size during `"starting"` so the CSS var transitions
  // from 0 → natural height. Mirrors React `useCollapsiblePanel` which only
  // measures while `transitionStatus === 'starting'`. Measuring on every
  // `open+mounted` tick would fight the post-animation clear to `auto`.
  //
  // Solid mounts conditional panel DOM in the same commit as `starting`; the
  // first synchronous read often sees `scrollHeight === 0` because the ref
  // fires before the subtree is connected (especially under `<Show>`). By the
  // end of the flush the element IS connected, so a microtask retry lands in
  // the SAME frame as the click — matching React's layout-effect timing. This
  // keeps simultaneous enter/exit transitions in lockstep (accordion item
  // switch: total height stays constant). rAF retries remain as fallback for
  // layouts that genuinely need a frame.
  createRenderEffect(() => ({
    isOpen: open(),
    isMounted: mounted(),
    status: transitionStatus(),
    el: panelElement()
  }), ({
    isOpen,
    isMounted,
    status,
    el
  }) => {
    if (!el || !isOpen || !isMounted || status !== "starting") {
      return undefined;
    }
    if (measureForOpen(el)) {
      return undefined;
    }
    let cancelled = false;
    let outerId = 0;
    let innerId = 0;
    queueMicrotask(() => {
      if (cancelled || measureForOpen(el)) {
        return;
      }
      outerId = requestAnimationFrame(() => {
        if (measureForOpen(el)) {
          return;
        }
        innerId = requestAnimationFrame(() => {
          if (!measureForOpen(el)) {
            // Empty panel or layout never reported a size — still
            // advance so we do not stick in `starting` forever.
            setEnterMeasureReady(true);
          }
        });
      });
    });
    return () => {
      cancelled = true;
      if (outerId !== 0) cancelAnimationFrame(outerId);
      if (innerId !== 0) cancelAnimationFrame(innerId);
    };
  });

  // Exit: capture current size as starting frame, wait for anim, then collapse.
  // When no panel element is registered, runOnceAnimationsFinish returns early
  // (null-element check in useAnimationsFinished) without calling the callback.
  // In that case setMounted(false) never fires and the panel stays "mounted"
  // indefinitely — which is acceptable because there's nothing to animate or
  // hide. Callers that need unmount without a panel element should pass an open
  // signal that never reaches false while the panel is absent.
  createRenderEffect(() => ({
    isOpen: open(),
    el: panelElement()
  }), ({
    isOpen,
    el
  }) => {
    if (!el) return;
    if (!isOpen && untrack(mounted)) {
      measure(el);
      runOnceCloseAnimationsFinish(() => {
        setHeight(0);
        setWidth(0);
        setMounted(false);
      });
    }
  });

  // Once open and enter animations settle, clear dimensions so the panel
  // reverts to auto sizing (resize-aware). Mirrors React
  // `useOpenChangeComplete({ enabled: open && mounted && status === 'idle' })`.
  // Clearing while still in `"starting"` drops the measured CSS var before the
  // browser paints the 0 → natural-height transition.
  createRenderEffect(() => ({
    isOpen: open(),
    isMounted: mounted(),
    status: transitionStatus()
  }), ({
    isOpen,
    isMounted,
    status
  }) => {
    if (!isOpen || !isMounted || status !== "idle") {
      return undefined;
    }
    const el = untrack(panelElement);
    if (!el) return undefined;
    const abortController = new AbortController();
    runOnceOpenAnimationsFinish(() => {
      if (!open()) return;
      setHeight(undefined);
      setWidth(undefined);
    }, abortController.signal);
    return () => {
      abortController.abort();
    };
  });

  // ── beforematch listener ──────────────────────────────────────────────────
  createRenderEffect(() => ({
    el: panelElement(),
    enabled: readOption(options.hiddenUntilFound ?? false)
  }), ({
    el,
    enabled
  }) => {
    if (!el || !enabled) return undefined;
    const handler = event => {
      options.onBeforematch?.(event);
    };
    el.addEventListener("beforematch", handler);
    return () => {
      el.removeEventListener("beforematch", handler);
    };
  });

  // ── Derived accessors ─────────────────────────────────────────────────────
  const keepMounted = () => readOption(options.keepMounted ?? false);
  const hiddenUntilFound = () => readOption(options.hiddenUntilFound ?? false);
  const shouldRender = () => mounted() || keepMounted() || hiddenUntilFound();

  /**
   * `hidden` semantics:
   * - Open panel → `undefined` (no attribute)
   * - hiddenUntilFound + closed → `"until-found"` (special browser value)
   * - keepMounted + closed (after anim) → `true`
   * - Unmounted (`!shouldRender`) → irrelevant; element isn't in the DOM
   */
  const hidden = () => {
    if (open()) return undefined;
    if (!mounted()) {
      // Fully settled closed state
      if (hiddenUntilFound()) return "until-found";
      if (keepMounted()) return true;
      // Element should not render at all; this value won't be used.
      return true;
    }
    // Still mounted (exit animation in progress)
    return undefined;
  };

  // ── CSS var style bag ────────────────────────────────────────────────────
  const prefix = options.cssVarPrefix ?? "collapsible-panel";
  const heightVar = `--${prefix}-height`;
  const widthVar = `--${prefix}-width`;
  function dimensionStyle() {
    const h = height();
    const w = width();
    return {
      [heightVar]: h === undefined ? "auto" : `${h}px`,
      [widthVar]: w === undefined ? "auto" : `${w}px`
    };
  }
  function mergePanelProps(userProps) {
    const base = userProps ?? {};
    return Object.defineProperties(Object.assign({}, base), {
      ref: {
        get: () => panelRef,
        enumerable: true,
        configurable: true
      },
      hidden: {
        get: hidden,
        enumerable: true,
        configurable: true
      },
      style: {
        get: dimensionStyle,
        enumerable: true,
        configurable: true
      }
    });
  }
  const api = {
    mounted,
    shouldRender,
    transitionStatus,
    height,
    width,
    hidden,
    panelRef,
    mergePanelProps
  };
  api[SNAPSHOT_KEYS] = MOUNT_TRANSITION_SNAPSHOT_KEYS;
  return api;
}
const MOUNT_TRANSITION_SNAPSHOT_KEYS = snapshotKeys("mounted", "shouldRender", "transitionStatus", "height", "width", "hidden");
//# sourceMappingURL=use-mount-transition.jsx.map
