// thread-canvas.jsx - the site's signature "technically cool" flourish.
// A living, woven THREAD of light strung between two soft nodes (you + your person).
// Many sine-modulated strands drift and breathe; a single accent core strand glows;
// a bisou (the real kiss-mark asset) travels the thread now and then, a heartbeat
// passing between two people. The thread bows toward the cursor. All on <canvas>,
// buttery via requestAnimationFrame, DPR-crisp, theme-aware, reduced-motion-safe.
(function () {
  const { useRef, useEffect } = React;

  // preload both kiss marks once (red for day, cream for night)
  const kissRed = new Image(); kissRed.src = R("site/assets/kiss-mark-red.png");
  const kissCream = new Image(); kissCream.src = R("site/assets/kiss-mark-cream.png");

  function ThreadCanvas({ height = 460 }) {
    const wrapRef = useRef(null);
    const canvasRef = useRef(null);

    useEffect(() => {
      const canvas = canvasRef.current;
      const wrap = wrapRef.current;
      if (!canvas || !wrap) return;
      const ctx = canvas.getContext("2d");
      const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;

      let w = 0, h = 0, dpr = 1, raf = 0;
      const pointer = { x: 0, y: 0, str: 0 }; // str = how "engaged" the cursor is (eased)
      let targetStr = 0;

      function readPalette() {
        const cs = getComputedStyle(document.documentElement);
        const grab = (v, f) => (cs.getPropertyValue(v).trim() || f);
        return {
          accent: grab("--accent", "#7E2A2A"),
          strand: grab("--text-secondary", "#8F8980"),
          node: grab("--text", "#1A1613"),
          night: document.documentElement.classList.contains("dark"),
        };
      }
      let pal = readPalette();

      function resize() {
        const rect = wrap.getBoundingClientRect();
        w = Math.max(320, rect.width);
        h = rect.height || height;
        dpr = Math.min(2, window.devicePixelRatio || 1);
        canvas.width = Math.round(w * dpr);
        canvas.height = Math.round(h * dpr);
        canvas.style.width = w + "px";
        canvas.style.height = h + "px";
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        pal = readPalette();
      }
      resize();

      // pointer tracked on window so the canvas never blocks page scroll
      function onMove(e) {
        const rect = canvas.getBoundingClientRect();
        pointer.x = e.clientX - rect.left;
        pointer.y = e.clientY - rect.top;
        const inside = pointer.x > -80 && pointer.x < w + 80 && pointer.y > -80 && pointer.y < h + 80;
        targetStr = inside ? 1 : 0;
      }
      function onLeave() { targetStr = 0; }
      window.addEventListener("mousemove", onMove, { passive: true });
      window.addEventListener("mouseout", onLeave, { passive: true });

      const N = 88;                     // points along the thread
      const STRANDS = 7;                // woven strands
      let t = 0;
      // travelling bisou: fires, crosses, then rests before the next
      let pulse = -0.5;                 // position 0..1, <0 means resting
      let pulseGap = reduce ? 9e9 : (1.2 + Math.random() * 1.6);
      let restTimer = 0;

      function nodes() {
        return {
          ax: w * 0.13, ay: h * 0.5,
          bx: w * 0.87, by: h * 0.5,
        };
      }

      // y of the thread at parametric p (0..1) at time t, with pointer bow
      function threadY(p, ax, ay, bx, by) {
        const x = ax + (bx - ax) * p;
        const anchor = Math.sin(p * Math.PI);          // 0 at ends, 1 in middle
        let y = ay + (by - ay) * p;
        y += Math.sin(p * Math.PI) * h * 0.02;          // faint resting sag
        y += Math.sin(p * 6.283 * 1.0 + t * 0.7) * h * 0.05 * anchor;
        y += Math.sin(p * 6.283 * 2.0 - t * 1.05) * h * 0.028 * anchor;
        y += Math.sin(p * 6.283 * 3.3 + t * 1.6) * h * 0.014 * anchor;
        // pointer bow - thread leans toward the cursor, ends stay pinned
        if (pointer.str > 0.001) {
          const infl = Math.exp(-Math.pow((x - pointer.x) / (w * 0.16), 2));
          y += (pointer.y - y) * infl * 0.55 * anchor * pointer.str;
        }
        return { x, y };
      }

      function drawStrand(offset, ax, ay, bx, by, color, width, alpha, glow) {
        ctx.beginPath();
        for (let i = 0; i <= N; i++) {
          const p = i / N;
          const pt = threadY(p, ax, ay, bx, by);
          const yy = pt.y + offset * Math.sin(p * Math.PI);
          if (i === 0) ctx.moveTo(pt.x, yy);
          else ctx.lineTo(pt.x, yy);
        }
        ctx.strokeStyle = color;
        ctx.lineWidth = width;
        ctx.lineCap = "round";
        ctx.globalAlpha = alpha;
        if (glow) { ctx.shadowColor = color; ctx.shadowBlur = glow; } else { ctx.shadowBlur = 0; }
        ctx.stroke();
        ctx.shadowBlur = 0;
        ctx.globalAlpha = 1;
      }

      function hexA(hex, a) {
        const s = hex.replace("#", "");
        const n = parseInt(s.length === 3 ? s.split("").map(c => c + c).join("") : s, 16);
        return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${a})`;
      }

      function drawNode(x, y, color, r) {
        // soft halo
        const g = ctx.createRadialGradient(x, y, 0, x, y, r * 4.5);
        g.addColorStop(0, hexA(color, pal.night ? 0.5 : 0.32));
        g.addColorStop(1, hexA(color, 0));
        ctx.fillStyle = g;
        ctx.beginPath(); ctx.arc(x, y, r * 4.5, 0, 6.2832); ctx.fill();
        // breathing ring
        const br = r * (1.7 + Math.sin(t * 1.4) * 0.12);
        ctx.strokeStyle = hexA(color, 0.5); ctx.lineWidth = 1;
        ctx.beginPath(); ctx.arc(x, y, br, 0, 6.2832); ctx.stroke();
        // core dot
        ctx.fillStyle = color;
        ctx.beginPath(); ctx.arc(x, y, r, 0, 6.2832); ctx.fill();
      }

      let last = performance.now();
      let running = false;
      function frame(now) {
        const dt = Math.min(0.05, (now - last) / 1000);
        last = now;
        t += dt;
        pointer.str += (targetStr - pointer.str) * Math.min(1, dt * 6);

        const { ax, ay, bx, by } = nodes();
        ctx.clearRect(0, 0, w, h);

        const strandCol = pal.strand;
        // woven neutral strands - fanned above & below the centre line
        for (let s = 0; s < STRANDS; s++) {
          const k = (s - (STRANDS - 1) / 2) / ((STRANDS - 1) / 2); // -1..1
          const offset = k * h * 0.06;
          const edge = Math.abs(k);
          drawStrand(offset, ax, ay, bx, by, strandCol, 1.0, (pal.night ? 0.22 : 0.16) * (1 - edge * 0.55), 0);
        }
        // luminous accent core - the single red the brand allows, kept thin
        drawStrand(0, ax, ay, bx, by, pal.accent, 1.6, pal.night ? 0.95 : 0.85, pal.night ? 14 : 9);

        // travelling bisou along the core
        if (!reduce) {
          if (pulse < 0) {
            restTimer += dt;
            if (restTimer >= pulseGap) { pulse = 0; restTimer = 0; }
          } else {
            pulse += dt / 2.6; // ~2.6s to cross
            if (pulse >= 1) { pulse = -0.5; pulseGap = 1.4 + Math.random() * 2.2; }
          }
        }
        if (pulse >= 0 && pulse <= 1) {
          const pt = threadY(pulse, ax, ay, bx, by);
          const fade = Math.sin(pulse * Math.PI);         // fade in/out at the ends
          const img = pal.night ? kissCream : kissRed;
          const size = 26 + Math.sin(pulse * Math.PI) * 8;
          // glow trail
          ctx.save();
          ctx.globalAlpha = 0.5 * fade;
          const g = ctx.createRadialGradient(pt.x, pt.y, 0, pt.x, pt.y, size * 1.6);
          g.addColorStop(0, hexA(pal.accent, 0.6));
          g.addColorStop(1, hexA(pal.accent, 0));
          ctx.fillStyle = g;
          ctx.beginPath(); ctx.arc(pt.x, pt.y, size * 1.6, 0, 6.2832); ctx.fill();
          ctx.restore();
          if (img.complete && img.naturalWidth) {
            ctx.save();
            ctx.globalAlpha = fade;
            ctx.translate(pt.x, pt.y);
            const asp = img.naturalHeight / img.naturalWidth || 1;
            ctx.drawImage(img, -size / 2, -size * asp / 2, size, size * asp);
            ctx.restore();
          }
        }

        // the two of you
        drawNode(ax, ay, pal.accent, 5);
        drawNode(bx, by, pal.accent, 5);

        if (running) raf = requestAnimationFrame(frame);
      }

      // Only animate while the thread is on screen (respects the brand's "no
      // perpetual decorative loops", saves CPU, and lets the page idle).
      function start() { if (running || reduce) return; running = true; last = performance.now(); raf = requestAnimationFrame(frame); }
      function stop() { running = false; cancelAnimationFrame(raf); }

      if (reduce) {
        // one calm static render
        const { ax, ay, bx, by } = nodes();
        ctx.clearRect(0, 0, w, h);
        for (let s = 0; s < STRANDS; s++) {
          const k = (s - (STRANDS - 1) / 2) / ((STRANDS - 1) / 2);
          drawStrand(k * h * 0.06, ax, ay, bx, by, pal.strand, 1, 0.14, 0);
        }
        drawStrand(0, ax, ay, bx, by, pal.accent, 1.6, 0.85, 0);
        drawNode(ax, ay, pal.accent, 5);
        drawNode(bx, by, pal.accent, 5);
      }

      const io = new IntersectionObserver((entries) => {
        entries.forEach((en) => { if (en.isIntersecting) start(); else stop(); });
      }, { threshold: 0.01 });
      io.observe(wrap);

      const ro = new ResizeObserver(resize);
      ro.observe(wrap);
      window.addEventListener("resize", resize, { passive: true });
      // repaint palette when the day/night class flips
      const mo = new MutationObserver(() => { pal = readPalette(); });
      mo.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "style"] });

      return () => {
        cancelAnimationFrame(raf);
        io.disconnect();
        ro.disconnect();
        mo.disconnect();
        window.removeEventListener("resize", resize);
        window.removeEventListener("mousemove", onMove);
        window.removeEventListener("mouseout", onLeave);
      };
    }, [height]);

    return (
      <div ref={wrapRef} className="thread-canvas-wrap" style={{ height }}>
        <canvas ref={canvasRef} className="thread-canvas" aria-hidden="true"></canvas>
      </div>
    );
  }

  window.BxThreadCanvas = ThreadCanvas;
})();
