// Living starfield backdrop for the Cosmology page.
//
// Three depth layers + a few soft nebula patches. Stars drift slowly in
// individual random directions (very slow — a star takes minutes to cross
// the screen) and twinkle on their own timing. Tiny CPU footprint when
// the Cosmology view is hidden — the rAF loop is killed.
//
// Honors prefers-reduced-motion: static field, no twinkle, no drift.

(function () {
  const canvas = document.getElementById("starfield");
  if (!canvas) return;
  const ctx = canvas.getContext("2d");

  const reduceMotion = window.matchMedia
    && window.matchMedia("(prefers-reduced-motion: reduce)").matches;

  // Density relative to viewport area — cap so dense displays don't explode.
  function targetStarCount() {
    const area = window.innerWidth * window.innerHeight;
    return Math.min(420, Math.max(150, Math.round(area / 5500)));
  }

  // Star color palette — mostly warm white, sometimes blue, gold, pink.
  const COLORS = [
    "255, 250, 240", "255, 250, 240", "255, 250, 240", "255, 250, 240",
    "255, 250, 240", "255, 250, 240", "255, 250, 240",
    "205, 220, 255", "205, 220, 255",
    "255, 225, 195",
    "230, 200, 255",
  ];
  const NEBULA_COLORS = [
    "rgba(140, 80, 220, 0.07)",
    "rgba(80, 130, 220, 0.05)",
    "rgba(220, 100, 180, 0.05)",
    "rgba(100, 80, 200, 0.06)",
  ];

  let stars = [];
  let nebulae = [];
  let width = 0, height = 0;
  let dpr = 1;
  let rafId = null;
  let lastTs = null;
  let time = 0;

  function sizeCanvas() {
    dpr = window.devicePixelRatio || 1;
    width = window.innerWidth;
    height = window.innerHeight;
    canvas.width  = Math.round(width  * dpr);
    canvas.height = Math.round(height * dpr);
    canvas.style.width  = width  + "px";
    canvas.style.height = height + "px";
    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
  }

  function rand(min, max) { return min + Math.random() * (max - min); }

  function makeStar() {
    // 0 = far/dim/slow, 1 = near/bright/fast
    const r = Math.random();
    let depth;
    if      (r < 0.55) depth = rand(0.10, 0.40);   // background
    else if (r < 0.88) depth = rand(0.40, 0.70);   // mid
    else                depth = rand(0.70, 1.00);  // foreground
    const angle = Math.random() * Math.PI * 2;
    const baseSpeed = 0.25 + depth * 3.0;   // pixels per second
    return {
      x: Math.random() * width,
      y: Math.random() * height,
      size: 0.4 + depth * 2.0,
      brightness: 0.30 + depth * 0.65,
      color: COLORS[Math.floor(Math.random() * COLORS.length)],
      dx: Math.cos(angle) * baseSpeed,
      dy: Math.sin(angle) * baseSpeed,
      twinkleOff: Math.random() * Math.PI * 2,
      twinkleSpd: 0.25 + Math.random() * 0.9,
      glow: depth > 0.7,
    };
  }

  function makeNebula() {
    return {
      x: Math.random() * width,
      y: Math.random() * height,
      r: rand(220, 460),
      color: NEBULA_COLORS[Math.floor(Math.random() * NEBULA_COLORS.length)],
      dx: rand(-0.05, 0.05),
      dy: rand(-0.05, 0.05),
    };
  }

  function init() {
    const n = targetStarCount();
    stars = [];
    for (let i = 0; i < n; i++) stars.push(makeStar());
    nebulae = [];
    const nebCount = 4 + Math.floor(Math.min(width, height) / 600);
    for (let i = 0; i < nebCount; i++) nebulae.push(makeNebula());
  }

  function wrap(value, max) {
    // Soft wrap with a margin so a star doesn't blink in at the edge.
    const margin = 40;
    if (value > max + margin) return -margin;
    if (value < -margin)      return max + margin;
    return value;
  }

  function draw(dt) {
    ctx.clearRect(0, 0, width, height);

    // Nebula layer (drawn first, sits behind stars).
    for (const n of nebulae) {
      n.x += n.dx * dt;
      n.y += n.dy * dt;
      n.x = wrap(n.x, width);
      n.y = wrap(n.y, height);
      const g = ctx.createRadialGradient(n.x, n.y, 0, n.x, n.y, n.r);
      g.addColorStop(0, n.color);
      g.addColorStop(1, "transparent");
      ctx.fillStyle = g;
      ctx.fillRect(n.x - n.r, n.y - n.r, n.r * 2, n.r * 2);
    }

    // Stars.
    for (const s of stars) {
      if (!reduceMotion) {
        s.x += s.dx * dt;
        s.y += s.dy * dt;
        s.x = wrap(s.x, width);
        s.y = wrap(s.y, height);
      }
      const twinkle = reduceMotion
        ? 1
        : (0.55 + 0.45 * Math.sin(time * s.twinkleSpd + s.twinkleOff));
      const alpha = s.brightness * twinkle;

      if (s.glow) {
        // Soft glow halo for the few brightest stars.
        ctx.fillStyle = "rgba(" + s.color + ", " + (alpha * 0.18).toFixed(3) + ")";
        ctx.beginPath();
        ctx.arc(s.x, s.y, s.size * 4, 0, Math.PI * 2);
        ctx.fill();
      }
      ctx.fillStyle = "rgba(" + s.color + ", " + alpha.toFixed(3) + ")";
      ctx.beginPath();
      ctx.arc(s.x, s.y, s.size, 0, Math.PI * 2);
      ctx.fill();
    }
  }

  function loop(ts) {
    if (lastTs == null) lastTs = ts;
    const dt = (ts - lastTs) / 1000;
    lastTs = ts;
    time += dt;
    draw(dt);
    rafId = requestAnimationFrame(loop);
  }

  function start() {
    if (rafId) return;
    sizeCanvas();
    if (stars.length === 0) init();
    lastTs = null;
    if (reduceMotion) {
      // One static paint — no loop.
      draw(0);
    } else {
      rafId = requestAnimationFrame(loop);
    }
  }
  function stop() {
    if (rafId) cancelAnimationFrame(rafId);
    rafId = null;
  }

  // Resize: re-seed so density matches the new viewport area.
  let resizeT = null;
  window.addEventListener("resize", () => {
    if (resizeT) clearTimeout(resizeT);
    resizeT = setTimeout(() => {
      sizeCanvas();
      init();
      if (!reduceMotion && rafId) draw(0);
    }, 120);
  });

  // Pause when off-screen or hidden.
  const cosmoView = document.querySelector('.view[data-view="cosmology"]');
  function isActive() { return cosmoView && cosmoView.classList.contains("active"); }
  if (cosmoView && "MutationObserver" in window) {
    new MutationObserver(() => { if (isActive()) start(); else stop(); })
      .observe(cosmoView, { attributes: true, attributeFilter: ["class"] });
    if (isActive()) start();
  }
  document.addEventListener("visibilitychange", () => {
    if (document.hidden) stop();
    else if (isActive()) start();
  });
})();
