/**
 * Player-height spacer observer.
 *
 * The audio player docks `position: fixed` to the viewport bottom, so it sits
 * outside normal flow and would overlap page content. A spacer div in the layout
 * reserves the equivalent space — but the player's height is not constant: it
 * reflows across the four breakpoints and grows when an error banner appears. A
 * static height can't track that, so we mirror the player's live border-box
 * height into the `--player-height` custom property on :root and let the spacer
 * read it. One observer at a time, re-pointed on each `observe` call; the var
 * resets to 0 on `unobserve` (player minimized / disposed) so the spacer
 * collapses.
 *
 * COALESCING (Phase 20 theater-flash fix). `--player-height` has two consumers:
 * the layout spacer div AND the ambient WaveformVisualizer backdrop, whose
 * `bottom` inset is this var (WaveformVisualizer.razor.css `.mix-waveform-bg`).
 * Moving that inset changes the visualizer canvas's CSS box, which fires the
 * renderer's own canvas ResizeObserver — and a GL resize CLEARS the backing
 * store. That is correct and cheap for a discrete bar-height change (breakpoint
 * reflow, minimize/expand, error banner). But Theater Mode eases the player bar's
 * "now showing" band open/closed over ~0.45s via a CSS grid-rows transition, so
 * the bar height changes EVERY FRAME of the ease. Mirroring each intermediate
 * frame here would re-clear the GL backing store ~27×, reading as a flash.
 *
 * The fix coalesces the publish with a LEADING + TRAILING edge: the first change
 * after a quiet period is written immediately (so a discrete jump — the common
 * case — has zero added latency and the clip never lags), then a rapid STREAM of
 * further changes (an animated transition) is debounced and only its SETTLED
 * end-state is written. So a Theater ease resizes the visualizer at most twice
 * (leading 1px move + final settle) instead of once per frame. The settled value
 * is always the last write, so at-rest sizing/clip stays exact; and this remains
 * the SOLE writer of `--player-height`, so the renderer's ResizeObserver stays the
 * sole canvas size writer (its invariant is untouched).
 */

const HEIGHT_VAR = '--player-height';

/**
 * Quiet window (ms) after which a pending settled height is flushed. One change
 * then silence (a discrete reflow) flushes after this delay but was ALSO written
 * on the leading edge, so the trailing flush is a no-op — discrete jumps pay no
 * latency. A continuous transition keeps resetting this timer until it ends, then
 * flushes the final height once. ~80ms comfortably exceeds a frame interval (so a
 * mid-ease frame never trips an early flush) yet settles promptly after the ease.
 */
const SETTLE_MS = 80;

let observer: ResizeObserver | null = null;
let lastWritten = -1;
let pendingHeight = -1;
let settleTimer: number | null = null;

function setVar(px: number): void {
    // Round up so sub-pixel heights never leave a hairline of overlap.
    const rounded = Math.ceil(px);
    if (rounded === lastWritten) return;
    lastWritten = rounded;
    document.documentElement.style.setProperty(HEIGHT_VAR, `${rounded}px`);
}

/**
 * Publish a measured height with leading + trailing coalescing. Leading: if no
 * settle is pending, this is the first change after a quiet period — write it now.
 * Trailing: (re)arm the settle timer so the final value of a rapid stream lands
 * once the stream stops.
 */
function publishHeight(px: number): void {
    pendingHeight = px;
    if (settleTimer === null) {
        // Leading edge — discrete jumps land immediately; the first frame of a
        // transition lands too (one resize), then the rest is debounced below.
        setVar(px);
    }
    if (settleTimer !== null) {
        clearTimeout(settleTimer);
    }
    settleTimer = window.setTimeout(() => {
        settleTimer = null;
        setVar(pendingHeight);
    }, SETTLE_MS);
}

function measure(entry: ResizeObserverEntry): number {
    // Prefer the border-box measurement; fall back to contentRect on the
    // (older) engines that don't populate borderBoxSize.
    const box = entry.borderBoxSize?.[0];
    return box ? box.blockSize : entry.contentRect.height;
}

export function observe(element: Element): void {
    unobserve();
    if (!element) return;

    observer = new ResizeObserver(entries => {
        const entry = entries[0];
        if (!entry) return;
        publishHeight(measure(entry));
    });
    observer.observe(element);

    // Seed synchronously so the spacer is correct on this frame, before the
    // first ResizeObserver callback fires. A fresh observe target is a discrete
    // change, so write it straight through (bypassing the debounce) — re-pointing
    // the observer (e.g. expanded <-> minimized) must not lag behind a settle.
    if (settleTimer !== null) {
        clearTimeout(settleTimer);
        settleTimer = null;
    }
    setVar(element.getBoundingClientRect().height);
}

export function unobserve(): void {
    observer?.disconnect();
    observer = null;
    if (settleTimer !== null) {
        clearTimeout(settleTimer);
        settleTimer = null;
    }
    pendingHeight = -1;
    setVar(0);
}
