/**
 * Shared particle-stage for confetti + confetti-burst traits.
 *
 * Strategy: a session-long body-level singleton `<div popover="manual">`
 * that the Popover API auto-promotes to the browser's top-layer once
 * `showPopover()` is called. Particles spawn as absolutely-positioned
 * `<span>` children of this stage with viewport-relative coordinates
 * read from each host's `getBoundingClientRect()`. Effect: bursts
 * escape `overflow: hidden` ancestors and render above modals,
 * drawers, and any z-stacking context — same anchoring as a
 * top-layer popover.
 *
 * The stage is created lazily on first call, never removed. Both
 * `confetti` and `confetti-burst` import `getStage()` and share the
 * singleton — there's no per-trait container, no z-index war, and no
 * tear-down race when bursts overlap.
 *
 * Notes:
 * - happy-dom stubs `showPopover` to a no-op (see test-setup.js), so
 *   the try/catch only matters in browsers without the Popover API
 *   (none of our baseline browsers since 2026; Chromium 125+, Safari
 *   17+, Firefox 125+ all ship it).
 * - Each particle self-removes on `animationend`. The stage is never
 *   torn down — it's a session-long shared resource.
 */

const STAGE_ID = 'adia-confetti-stage';

let stage = null;
let stylesInjected = false;

/**
 * Lazily create + return the singleton popover stage. Idempotent —
 * subsequent calls return the cached node (or rehydrate if it was
 * removed externally, e.g. by a hot-reload).
 */
export function getStage() {
  if (stage && stage.isConnected) return stage;

  // Existing instance from a prior session / hot-reload?
  const existing = document.getElementById(STAGE_ID);
  if (existing) {
    stage = existing;
    showSilently(stage);
    ensureStyles();
    return stage;
  }

  const el = document.createElement('div');
  el.id = STAGE_ID;
  el.setAttribute('aria-hidden', 'true');
  // popover="manual" — auto-promoted to the top-layer once shown,
  // and immune to light-dismiss (no spurious close on outside click).
  if ('popover' in HTMLElement.prototype) el.setAttribute('popover', 'manual');
  el.style.cssText =
    'position:fixed;inset:0;margin:0;padding:0;border:0;background:transparent;' +
    'pointer-events:none;overflow:visible;z-index:99999;';
  document.body.appendChild(el);
  showSilently(el);
  stage = el;
  ensureStyles();
  return stage;
}

function showSilently(el) {
  // showPopover throws InvalidStateError if already shown. Guard with
  // try/catch — cheaper than checking matches(':popover-open'), which
  // happy-dom doesn't implement either.
  try { el.showPopover?.(); } catch { /* already shown or unsupported */ }
}

/**
 * Compute a host's center in viewport coordinates. Returns null if
 * the host is detached (rect = 0×0 at 0,0 — bursts on a detached
 * host would render at the viewport corner, so callers should bail).
 */
export function viewportCenterOf(host) {
  const r = host.getBoundingClientRect();
  // Detached / display:none host — width+height both 0 with all-zero
  // coords. Treat as "no anchor" and let the trait fall back.
  if (r.width === 0 && r.height === 0 && r.left === 0 && r.top === 0) return null;
  return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
}

/**
 * Spawn a single particle <span> into the stage. Caller passes the
 * starting viewport coords (from `viewportCenterOf(host)` plus any
 * per-particle jitter the physics layer adds) and the visual props.
 *
 * The returned element self-removes on `animationend`. Caller is
 * responsible for triggering motion (CSS transition, Web Animations
 * API keyframes, or an inline `animation: …` rule).
 */
export function spawnParticle(stage, x, y, { size = 6, color = '#f44' } = {}) {
  const p = document.createElement('span');
  p.style.cssText =
    'position:absolute;' +
    `left:${x}px;top:${y}px;` +
    `width:${size}px;height:${size}px;background:${color};` +
    'border-radius:1px;pointer-events:none;will-change:transform,opacity;';
  stage.appendChild(p);
  p.addEventListener('animationend', () => p.remove(), { once: true });
  return p;
}

/**
 * Inject the @keyframes blocks the particles animate on. One-time,
 * idempotent — guarded by a module flag. The keyframes drift each
 * particle along a random vector; per-particle randomness is set via
 * inline CSS custom properties (`--dx`, `--dy`, `--rot`).
 */
function ensureStyles() {
  if (stylesInjected) return;
  if (typeof document === 'undefined') return;
  const styleEl = document.createElement('style');
  styleEl.id = 'adia-confetti-stage-styles';
  styleEl.textContent = `
    @keyframes adia-confetti-fountain {
      0%   { transform: translate(0, 0) rotate(0deg);            opacity: 1; }
      100% { transform: translate(var(--dx, 0), var(--dy, 0))    rotate(var(--rot, 0deg)); opacity: 0; }
    }
    @keyframes adia-confetti-fall {
      0%   { transform: translate(0, 0) rotate(0deg);            opacity: 1; }
      100% { transform: translate(var(--dx, 0), var(--dy, 0))    rotate(var(--rot, 0deg)); opacity: 0; }
    }
  `;
  document.head?.appendChild(styleEl);
  stylesInjected = true;
}

export const SHARED_COLORS = ['#f44', '#4a4', '#44f', '#ff4', '#f4f', '#4ff'];

/**
 * Test-only: tear down the stage so test suites don't leak particles
 * across cases. Not part of the public surface.
 */
export function _resetStage() {
  if (stage && stage.parentNode) stage.remove();
  const styleEl = document.getElementById('adia-confetti-stage-styles');
  if (styleEl) styleEl.remove();
  stage = null;
  stylesInjected = false;
}
