/* Motion (motion.dev) — the site's unified animation layer.
 *
 * Imported by an Astro-PROCESSED <script> in Layout.astro, so Vite bundles it
 * into a self-hosted /_astro/[hash].js → stays within CSP `script-src 'self'`
 * (no inline, no CDN). Motion sets styles via JS (WAAPI / element.style), which
 * is allowed under `style-src` — so no `unsafe-inline` is introduced.
 *
 * Progressive enhancement: every animated element is VISIBLE with JS off. The
 * pre-paint head script (site.js) adds `html.js`; only THEN does CSS hide the
 * pre-reveal state (see global.css). Reduced-motion: we no-op and let the CSS
 * reduced-motion rules show everything immediately.
 */
import { animate, inView, scroll, stagger } from "motion";

const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// easeOutExpo-ish — confident, decelerating; good for entrances/reveals.
const EASE = [0.16, 1, 0.3, 1] as const;

if (!reduce) {
  // Fail open: if anything in the Motion layer throws, drop `html.js` so the CSS
  // pre-reveal hide releases and ALL content shows. A broken animation must
  // never leave the page blank.
  try {
    run();
  } catch {
    document.documentElement.classList.remove("js");
  }
}

function run(): void {
  /* 1. Hero — staggered fade-up on load. */
  const heroEls = document.querySelectorAll<HTMLElement>(".hero__inner > *");
  if (heroEls.length) {
    animate(
      heroEls,
      { opacity: [0, 1], y: [14, 0] },
      { duration: 0.6, delay: stagger(0.08), ease: EASE },
    );
  }

  /* 2. Scroll reveals — section heads, skill cards (+ staggered chips), CTAs.
   *    inView fires as each [data-reveal] enters; re-fires on re-entry. */
  inView(
    "[data-reveal]",
    (el) => {
      animate(el, { opacity: [0, 1], y: [16, 0] }, { duration: 0.55, ease: EASE });
      const chips = el.querySelectorAll<HTMLElement>(".chip");
      if (chips.length) {
        animate(
          chips,
          { opacity: [0, 1], y: [8, 0] },
          { duration: 0.42, delay: stagger(0.04, { startDelay: 0.12 }), ease: EASE },
        );
      }
    },
    { amount: 0.2 },
  );

  /* 3. Backdrop parallax — drive --bg-shift from scroll progress (replaces the
   *    hand-rolled rAF listener; Motion throttles via ScrollTimeline/rAF). */
  const bg = document.querySelectorAll<HTMLElement>(".site-bg__img");
  if (bg.length) {
    scroll((progress: number) => {
      const shift = ((0.5 - progress) * window.innerHeight * 0.7).toFixed(1);
      bg.forEach((img) => img.style.setProperty("--bg-shift", `${shift}px`));
    });
  }

  /* 4. Top scroll-progress bar — cross-browser (replaces the Chromium-only CSS
   *    animation-timeline). Drive scaleX STRAIGHT from scroll progress via the
   *    callback form: scroll(animate(...)) snapped the bar back to 0 at the very
   *    bottom (the progress=1 boundary); the callback maps 0→1 cleanly both ways. */
  const bar = document.querySelector<HTMLElement>(".scroll-progress");
  if (bar) {
    // Compute progress straight from scroll position so it reliably spans 0→1
    // (Motion's default scroll progress topped out ~0.89 at the true bottom).
    const setBar = (): void => {
      const max = document.documentElement.scrollHeight - window.innerHeight;
      const p = max > 0 ? Math.min(1, Math.max(0, window.scrollY / max)) : 0;
      bar.style.transform = `scaleX(${p})`;
    };
    scroll(setBar);
    setBar();
  }

  /* 5. Nav — animated active-section underline + tactile hover. */
  initNav();

  /* 6. Buttons — spring lift on hover, press on tap. */
  initButtons();
}

function initNav(): void {
  const links = Array.from(
    document.querySelectorAll<HTMLAnchorElement>(".nav__links a[data-nav-id]"),
  );
  if (!links.length) return;

  const spring = { type: "spring", stiffness: 380, damping: 30 } as const;
  let activeIdx = -1;

  // `fromRight` flips the grow/collapse origin so the underline animates in the
  // direction of travel: scrolling down it grows from the left (moves right);
  // scrolling back up it grows from the right (moves left).
  const under = (a: HTMLAnchorElement, on: boolean, fromRight = false): void => {
    const ul = a.querySelector<HTMLElement>(".nav__ul");
    if (!ul) return;
    ul.style.transformOrigin = fromRight ? "100% 50%" : "0% 50%";
    animate(ul, { scaleX: on ? 1 : 0 }, spring);
  };

  // Links that map to a section ON THIS PAGE. Projects/Blog point at the homepage
  // #projects / #writing sections so every nav item gets the underline; on their
  // own pages those sections don't exist and they simply drop out of the spy.
  const sectioned = links.filter(
    (a) => a.dataset.navId && document.getElementById(a.dataset.navId),
  );

  for (const a of links) {
    a.addEventListener("pointerenter", () => under(a, true));
    // On leave, keep the underline only if this link IS the active scroll-spy
    // section. Guard on activeIdx >= 0 so that when nothing is active — e.g. the
    // Projects/Blog pages, where none of these sections exist and activeIdx stays
    // -1 — a hovered tab doesn't stick lit via the -1 === indexOf(non-member) -1.
    a.addEventListener("pointerleave", () =>
      under(a, activeIdx >= 0 && sectioned.indexOf(a) === activeIdx),
    );
  }

  // Light up the link whose section is currently being read, recomputed from
  // scroll position so it tracks BOTH directions. Every change re-asserts ALL
  // underlines (active → 1, the rest → 0) so none can be left stranded, and the
  // grow/collapse origin follows the scroll direction.
  if (sectioned.length) {
    const setActive = (): void => {
      const vh = window.innerHeight;
      const ref = vh * 0.45; // a section whose top has passed this line is "active"
      let idx = -1;
      sectioned.forEach((a, i) => {
        const sec = document.getElementById(a.dataset.navId as string);
        if (sec && sec.getBoundingClientRect().top <= ref) idx = i;
      });
      // At the very bottom the last (often short) section may never push its top
      // above the line — force it so the final item lights up.
      if (window.scrollY + vh >= document.documentElement.scrollHeight - 4) {
        idx = sectioned.length - 1;
      }
      if (idx === activeIdx) return;
      const backward = idx < activeIdx; // scrolling up → travel left
      activeIdx = idx;
      // Coherent directional slide: the activating underline grows from its
      // trailing edge while the others collapse toward the leading edge, so the
      // whole motion follows the scroll direction (down → right, up → left)
      // instead of the active/old pair diverging.
      sectioned.forEach((a, i) => {
        const on = i === idx;
        under(a, on, on ? backward : !backward);
      });
    };
    scroll(setActive);
    setActive();
  }
}

function initButtons(): void {
  const lift = { type: "spring", stiffness: 400, damping: 26 } as const;
  const tap = { type: "spring", stiffness: 600, damping: 22 } as const;
  for (const b of document.querySelectorAll<HTMLElement>(".btn")) {
    b.addEventListener("pointerenter", () => animate(b, { y: -2, scale: 1 }, lift));
    b.addEventListener("pointerleave", () => animate(b, { y: 0, scale: 1 }, lift));
    b.addEventListener("pointerdown", () => animate(b, { scale: 0.95 }, tap));
    b.addEventListener("pointerup", () => animate(b, { scale: 1, y: -2 }, tap));
  }
}
