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

/**
 * `view-transition` — declarative wrapper around the modern
 * View Transitions API (`document.startViewTransition()`).
 *
 * Sets `view-transition-name: <slug>` on the host so the browser tags
 * this element as a participant in any same-document transition. Reads
 * configuration from data attributes — `data-view-transition-name`
 * (the CSS slug), `data-view-transition-duration` (--vt-duration), and
 * `data-view-transition-easing` (--vt-easing). The trait also installs
 * a `host.startTransition(callback)` method so consumers can fire a
 * transition without importing `document.startViewTransition` directly.
 *
 * Browser-support shape (project floor at Chromium 125+, Safari 18.0+,
 * Firefox 129+): same-document is universal, cross-document is
 * Chromium-only and explicitly out of scope. When the browser lacks
 * the API, `startTransition()` runs the callback synchronously and
 * fires start+end back-to-back so caller logic still resolves.
 *
 * Reduced-motion is honored automatically by the user-agent CSS
 * (`view-transition-name: none` under `prefers-reduced-motion: reduce`),
 * but the trait also detects the preference and runs the no-animation
 * lifecycle synchronously so consumers can rely on the events.
 *
 *   <article-ui traits="view-transition" data-view-transition-name="card-42">
 *     …
 *   </article-ui>
 *
 *   const article = document.querySelector('article-ui');
 *   article.startTransition(() => {
 *     article.classList.add('detail-view');
 *   });
 */
export const viewTransition = defineTrait({
  name: 'view-transition',
  category: 'motion-positioning',
  description:
    'Wraps document.startViewTransition() for morph animations between DOM states',
  attributes: ['data-view-transition-active'],
  events: ['view-transition-start', 'view-transition-end'],
  config: [
    'data-view-transition-name',
    'data-view-transition-duration',
    'data-view-transition-easing',
  ],
  setup({ host }) {
    // ── Apply CSS configuration ─────────────────────────────────────────
    // The browser keys participants by the CSS `view-transition-name`
    // property; we mirror the data-attribute onto the host's inline
    // style so the value can be set declaratively in markup.
    const name = host.getAttribute('data-view-transition-name');
    const duration = host.getAttribute('data-view-transition-duration');
    const easing = host.getAttribute('data-view-transition-easing');

    const prevName = host.style.viewTransitionName;
    const prevDur = host.style.getPropertyValue('--vt-duration');
    const prevEase = host.style.getPropertyValue('--vt-easing');

    if (name) host.style.viewTransitionName = name;
    if (duration) host.style.setProperty('--vt-duration', duration);
    if (easing) host.style.setProperty('--vt-easing', easing);

    // ── start / end event helpers ───────────────────────────────────────
    function fireStart() {
      host.setAttribute('data-view-transition-active', '');
      host.dispatchEvent(
        new CustomEvent('view-transition-start', { bubbles: true }),
      );
    }
    function fireEnd() {
      host.removeAttribute('data-view-transition-active');
      host.dispatchEvent(
        new CustomEvent('view-transition-end', { bubbles: true }),
      );
    }

    // ── host.startTransition(callback) ──────────────────────────────────
    // The unusual-for-a-trait method install. Returns a thin object that
    // exposes `.finished` so consumers can await transition completion
    // — matching the shape of `document.startViewTransition()` itself.
    const supported =
      typeof document !== 'undefined' &&
      'startViewTransition' in document;

    function startTransition(callback) {
      const cb = typeof callback === 'function' ? callback : () => {};
      const reduced = prefersReducedMotion();

      // Graceful-degrade: no API support OR user prefers reduced motion.
      // Run the mutation now; emit start+end synchronously so consumers
      // can rely on the lifecycle even when no animation will play.
      if (!supported || reduced) {
        fireStart();
        try { cb(); } catch (err) { fireEnd(); throw err; }
        // Use queueMicrotask so callers attaching a one-shot 'view-transition-end'
        // listener immediately AFTER startTransition() still see the event.
        const finished = Promise.resolve().then(() => { fireEnd(); });
        return { finished, ready: finished, updateCallbackDone: finished, skipTransition() {} };
      }

      // Real path. The browser's startViewTransition() takes the callback,
      // snapshots the old state, runs the mutation, snapshots the new
      // state, then morphs between them. We dispatch start synchronously
      // (the browser has already snapshotted by the time we resume) and
      // end once the transition resolves.
      fireStart();
      const transition = document.startViewTransition(() => cb());
      transition.finished
        .then(() => fireEnd())
        .catch(() => fireEnd()); // user-skipped or thrown — still emit end
      return transition;
    }

    // Attach the method. We track whether the host already had one so we
    // can restore it on disconnect (rare, but keeps cleanup symmetric).
    const hadStartTransition = Object.prototype.hasOwnProperty.call(
      host,
      'startTransition',
    );
    const prevStartTransition = host.startTransition;
    host.startTransition = startTransition;

    return () => {
      // Restore inline styles
      host.style.viewTransitionName = prevName;
      if (prevDur) host.style.setProperty('--vt-duration', prevDur);
      else host.style.removeProperty('--vt-duration');
      if (prevEase) host.style.setProperty('--vt-easing', prevEase);
      else host.style.removeProperty('--vt-easing');

      // Restore the method (or delete if we added it fresh)
      if (hadStartTransition) host.startTransition = prevStartTransition;
      else delete host.startTransition;

      host.removeAttribute('data-view-transition-active');
    };
  },
});
