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

/**
 * scroll-progress — surfaces page or element scroll progress as a 0..1
 * attribute, CSS custom property, and event. Building block for sticky
 * headers, ToC highlighting, scroll-linked animations, and reading-time
 * indicators.
 *
 * Modes (via `data-scroll-progress-mode`):
 *   - "in-view" (default) — 0 = host top edge meets viewport bottom;
 *     1 = host bottom edge crosses viewport top. Useful for fade-in /
 *     parallax-on-scroll patterns.
 *   - "page" — 0 = page at top, 1 = page fully scrolled. Independent of
 *     host position.
 *   - "scrolled" — 0 = host content scrollTop is 0, 1 = fully scrolled.
 *     For overflow:auto containers.
 *
 * Container override (`data-scroll-container`) accepts a CSS selector;
 * resolves to the scrolling ancestor explicitly. Falls back to the
 * nearest auto/scroll overflow ancestor or `window`.
 *
 * Throttling — scroll events fire frequently. The trait coalesces
 * updates via rAF: at most one progress update per frame. Cleanup
 * cancels any pending rAF + removes the listener.
 */
export const scrollProgress = defineTrait({
  name: 'scroll-progress',
  category: 'layout-measurement',
  description: 'Page or element scroll progress as 0..1 attribute + CSS variable + event',
  attributes: ['data-scroll-progress-active', 'data-scroll-progress'],
  events: ['scroll-progress'],
  config: ['data-scroll-progress-mode', 'data-scroll-container'],
  setup({ host }) {
    const mode = host.getAttribute('data-scroll-progress-mode') || 'in-view';
    const containerSelector = host.getAttribute('data-scroll-container');

    // Resolve scroll container — explicit selector wins; otherwise walk
    // up looking for an overflow:auto/scroll ancestor; fall back to window.
    function resolveContainer() {
      if (containerSelector) {
        try {
          const root = host.getRootNode?.() || document;
          const node = root.querySelector?.(containerSelector)
            || document.querySelector(containerSelector);
          if (node) return node;
        } catch {
          // Invalid selector — fall through.
        }
      }
      let node = host.parentElement;
      while (node) {
        const style = (typeof getComputedStyle === 'function')
          ? getComputedStyle(node)
          : null;
        if (style) {
          const flow = style.overflow + style.overflowY + style.overflowX;
          if (/(auto|scroll)/.test(flow)) return node;
        }
        node = node.parentElement;
      }
      return (typeof window !== 'undefined') ? window : null;
    }

    const container = resolveContainer();
    if (!container) {
      // No place to listen — mark active and bail. Keeps cleanup
      // contract intact (attribute is still removed on disconnect).
      host.setAttribute('data-scroll-progress-active', '');
      return () => {
        host.removeAttribute('data-scroll-progress-active');
        host.removeAttribute('data-scroll-progress');
        host.style.removeProperty('--scroll-progress');
      };
    }

    const isWindow = container === (typeof window !== 'undefined' ? window : null);

    function clamp01(v) {
      if (!Number.isFinite(v)) return 0;
      if (v < 0) return 0;
      if (v > 1) return 1;
      return v;
    }

    function compute() {
      // "page" — scrollY / (scrollHeight - innerHeight)
      if (mode === 'page') {
        if (isWindow) {
          const max = (document.documentElement.scrollHeight || 0) - (window.innerHeight || 0);
          if (max <= 0) return 0;
          return clamp01((window.scrollY || 0) / max);
        }
        const max = (container.scrollHeight || 0) - (container.clientHeight || 0);
        if (max <= 0) return 0;
        return clamp01((container.scrollTop || 0) / max);
      }

      // "scrolled" — host's own scrollTop / max-scroll
      if (mode === 'scrolled') {
        const max = (host.scrollHeight || 0) - (host.clientHeight || 0);
        if (max <= 0) return 0;
        return clamp01((host.scrollTop || 0) / max);
      }

      // "in-view" (default) — 0 when host top meets viewport bottom,
      // 1 when host bottom crosses viewport top.
      // We measure the host rect against the container's viewport.
      if (typeof host.getBoundingClientRect !== 'function') return 0;
      const rect = host.getBoundingClientRect();
      let viewTop;
      let viewBottom;
      if (isWindow) {
        viewTop = 0;
        viewBottom = window.innerHeight || 0;
      } else if (typeof container.getBoundingClientRect === 'function') {
        const cRect = container.getBoundingClientRect();
        viewTop = cRect.top;
        viewBottom = cRect.bottom;
      } else {
        viewTop = 0;
        viewBottom = window.innerHeight || 0;
      }

      const viewHeight = viewBottom - viewTop;
      const total = viewHeight + (rect.height || 0);
      if (total <= 0) return 0;

      // Distance from "host fully below view" to current position.
      // When rect.top === viewBottom → 0. When rect.bottom === viewTop → 1.
      const distance = viewBottom - rect.top;
      return clamp01(distance / total);
    }

    let rafId = null;
    let lastWritten = -1;

    function update() {
      rafId = null;
      const progress = compute();
      // De-dup writes when nothing meaningful changed (3-decimal precision
      // is enough for any visual or reading-progress consumer).
      const rounded = Math.round(progress * 1000) / 1000;
      if (rounded === lastWritten) return;
      lastWritten = rounded;

      const str = rounded.toFixed(3);
      host.setAttribute('data-scroll-progress', str);
      host.style.setProperty('--scroll-progress', str);
      host.dispatchEvent(new CustomEvent('scroll-progress', {
        bubbles: true,
        detail: { progress: rounded },
      }));
    }

    function schedule() {
      if (rafId !== null) return;
      if (typeof requestAnimationFrame === 'function') {
        rafId = requestAnimationFrame(update);
      } else {
        // Fallback for environments without rAF — write synchronously.
        update();
      }
    }

    const target = container;
    const listenerOpts = { passive: true };

    target.addEventListener?.('scroll', schedule, listenerOpts);
    // For "scrolled" mode the host itself is the scroll surface — also
    // listen on host so progress updates without requiring the resolved
    // container to be the host.
    if (mode === 'scrolled' && target !== host) {
      host.addEventListener?.('scroll', schedule, listenerOpts);
    }
    // Window resize affects in-view + page math; update on resize too.
    if (typeof window !== 'undefined') {
      window.addEventListener?.('resize', schedule, listenerOpts);
    }

    host.setAttribute('data-scroll-progress-active', '');
    // Seed the initial value on next frame so first paint has the value.
    schedule();

    return () => {
      if (rafId !== null && typeof cancelAnimationFrame === 'function') {
        cancelAnimationFrame(rafId);
      }
      rafId = null;
      target.removeEventListener?.('scroll', schedule, listenerOpts);
      if (mode === 'scrolled' && target !== host) {
        host.removeEventListener?.('scroll', schedule, listenerOpts);
      }
      if (typeof window !== 'undefined') {
        window.removeEventListener?.('resize', schedule, listenerOpts);
      }
      host.removeAttribute('data-scroll-progress-active');
      host.removeAttribute('data-scroll-progress');
      host.style.removeProperty('--scroll-progress');
    };
  },
});
