import { defineTrait } from '../define.js';
import { prefersReducedMotion } from '../motion.js';

/**
 * layout-animation — FLIP (First / Last / Invert / Play) layout transitions.
 *
 * Captures the host's bounding rect BEFORE a layout change, reads the new rect
 * AFTER, then applies an inverse `transform: translate(dx, dy) scale(sx, sy)`
 * that visually pins the host at its old position — finally animating the
 * transform back to identity over a configurable duration. The host's actual
 * layout-driven position (set by Grid / Flex / DOM order / class flips) is
 * what triggers the animation.
 *
 * Trigger model — manual + observer combo:
 *   1. MutationObserver on host.parentElement watching for childList changes.
 *      When the host is reordered among its siblings, FLIP runs.
 *   2. `data-layout-animate-trigger` — set any value to fire FLIP comparing
 *      the cached snapshot to the current rect. Useful for class-driven
 *      layout shifts that don't reorder DOM nodes.
 *
 * Snapshots are captured on connect, after every observer-driven FLIP, and
 * every time the trigger attribute changes. The snapshot read is rAF-debounced
 * so a burst of mutations only schedules one capture.
 *
 * Reduced-motion: skip the inversion entirely; the host snaps to its new
 * position. The done event still fires synchronously so caller logic gated on
 * settle doesn't stall.
 */
export const layoutAnimation = defineTrait({
  name: 'layout-animation',
  category: 'motion-positioning',
  description: 'FLIP-style layout transition: animate from old to new bounds without explicit coordinates',
  attributes: ['data-layout-animation-active'],
  events: ['layout-animation-done'],
  config: [
    'data-layout-animate-duration',
    'data-layout-animate-easing',
    'data-layout-animate-trigger',
  ],
  setup({ host }) {
    let snapshot = null;          // last captured DOMRect-shaped {x,y,w,h}
    let activeAnimation = null;   // the in-flight Web Animation
    let captureRafId = null;      // debounced rAF for snapshot capture
    let mutationObserver = null;
    let attrObserver = null;

    function readDuration() {
      const raw = host.getAttribute('data-layout-animate-duration');
      const n = parseInt(raw, 10);
      return Number.isFinite(n) && n > 0 ? n : 300;
    }

    function readEasing() {
      return host.getAttribute('data-layout-animate-easing') || 'cubic-bezier(0.2, 0.8, 0.2, 1)';
    }

    function rectOf(el) {
      // getBoundingClientRect is unavailable in some test environments
      // (jsdom is fine; happy-dom returns zeros). Fall back to a zero rect
      // — the FLIP becomes a no-op rather than throwing.
      if (typeof el.getBoundingClientRect !== 'function') {
        return { x: 0, y: 0, width: 0, height: 0 };
      }
      const r = el.getBoundingClientRect();
      return { x: r.left, y: r.top, width: r.width, height: r.height };
    }

    function captureSnapshot() {
      snapshot = rectOf(host);
    }

    function scheduleCapture() {
      if (captureRafId != null) return;
      if (typeof requestAnimationFrame !== 'function') {
        captureSnapshot();
        return;
      }
      captureRafId = requestAnimationFrame(() => {
        captureRafId = null;
        captureSnapshot();
      });
    }

    function fireDone() {
      host.dispatchEvent(new CustomEvent('layout-animation-done', { bubbles: true }));
    }

    function flip() {
      // No prior snapshot — this is the first observation; just record and exit.
      if (!snapshot) {
        captureSnapshot();
        return;
      }

      const last = rectOf(host);
      const dx = snapshot.x - last.x;
      const dy = snapshot.y - last.y;
      const sx = snapshot.width  > 0 && last.width  > 0 ? snapshot.width  / last.width  : 1;
      const sy = snapshot.height > 0 && last.height > 0 ? snapshot.height / last.height : 1;

      // No meaningful delta — skip the play and re-baseline.
      if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5 && Math.abs(sx - 1) < 0.01 && Math.abs(sy - 1) < 0.01) {
        snapshot = last;
        return;
      }

      // Reduced-motion: snap to the new position; fire done synchronously.
      if (prefersReducedMotion()) {
        snapshot = last;
        host.setAttribute('data-layout-animation-active', '');
        queueMicrotask(() => {
          host.removeAttribute('data-layout-animation-active');
          fireDone();
        });
        return;
      }

      // Cancel any previous in-flight FLIP so they don't stack.
      if (activeAnimation && typeof activeAnimation.cancel === 'function') {
        try { activeAnimation.cancel(); } catch { /* noop */ }
        activeAnimation = null;
      }

      const duration = readDuration();
      const easing = readEasing();

      // Bail gracefully when Web Animations API isn't available (SSR, JSDOM,
      // happy-dom). Snap + fire done so caller logic doesn't stall.
      if (typeof host.animate !== 'function') {
        snapshot = last;
        host.setAttribute('data-layout-animation-active', '');
        queueMicrotask(() => {
          host.removeAttribute('data-layout-animation-active');
          fireDone();
        });
        return;
      }

      host.setAttribute('data-layout-animation-active', '');

      const fromTransform = `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`;
      const toTransform   = 'translate(0px, 0px) scale(1, 1)';

      try {
        activeAnimation = host.animate(
          [
            { transform: fromTransform, transformOrigin: 'top left' },
            { transform: toTransform,   transformOrigin: 'top left' },
          ],
          { duration, easing, fill: 'none' },
        );
      } catch {
        snapshot = last;
        host.removeAttribute('data-layout-animation-active');
        queueMicrotask(fireDone);
        return;
      }

      const onFinish = () => {
        activeAnimation = null;
        snapshot = rectOf(host);
        host.removeAttribute('data-layout-animation-active');
        fireDone();
      };

      activeAnimation.addEventListener?.('finish', onFinish, { once: true });
      activeAnimation.addEventListener?.('cancel', () => {
        activeAnimation = null;
        host.removeAttribute('data-layout-animation-active');
      }, { once: true });

      // Re-baseline the snapshot to the new rect for the next FLIP.
      snapshot = last;
    }

    // ── Observers ──────────────────────────────────────────────────────────

    if (typeof MutationObserver !== 'undefined') {
      const parent = host.parentElement;
      if (parent) {
        mutationObserver = new MutationObserver((records) => {
          // Only react when the host's siblings actually shifted.
          let relevant = false;
          for (const r of records) {
            if (r.type === 'childList') { relevant = true; break; }
          }
          if (relevant) flip();
        });
        mutationObserver.observe(parent, { childList: true });
      }

      // Watch our own trigger / config attrs. The trigger attr changing is
      // the explicit "run a FLIP now" signal; duration/easing changes are
      // read on the next FLIP without needing an observer entry.
      attrObserver = new MutationObserver((records) => {
        for (const r of records) {
          if (r.type === 'attributes' && r.attributeName === 'data-layout-animate-trigger') {
            flip();
            return;
          }
        }
      });
      attrObserver.observe(host, {
        attributes: true,
        attributeFilter: ['data-layout-animate-trigger'],
      });
    }

    // Capture an initial snapshot once the host has settled into its first
    // layout pass. Using rAF avoids reading offsetTop / getBoundingClientRect
    // before the first paint, which is unreliable for some flex containers.
    scheduleCapture();

    return () => {
      if (captureRafId != null && typeof cancelAnimationFrame === 'function') {
        cancelAnimationFrame(captureRafId);
        captureRafId = null;
      }
      if (mutationObserver) { mutationObserver.disconnect(); mutationObserver = null; }
      if (attrObserver)     { attrObserver.disconnect();     attrObserver = null; }
      if (activeAnimation && typeof activeAnimation.cancel === 'function') {
        try { activeAnimation.cancel(); } catch { /* noop */ }
        activeAnimation = null;
      }
      snapshot = null;
      host.removeAttribute('data-layout-animation-active');
    };
  },
});
