/* loop-scene3.jsx — Chapter 3 closed-loop charge sim (power & heat).
   Single battery + one pinch (resistor). Same spectrum charges as Ch2
   (blue=full -> amber -> red=spent), but tuned for the POWER/HEAT lesson:
   the pinch glows hotter the more power it burns, and the centre readout
   finally names POWER in watts (P = V x I). Self-contained; names fl3-/
   LoopScene3 prefixed to avoid cross-file collisions. */

function fl3HexRgb(h) { h = (h || "").trim().replace("#", ""); if (h.length === 3) h = h.split("").map(c => c + c).join(""); return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]; }
function fl3MixHex(a, b, t) { const A = fl3HexRgb(a), B = fl3HexRgb(b); return `rgb(${Math.round(A[0] + (B[0] - A[0]) * t)},${Math.round(A[1] + (B[1] - A[1]) * t)},${Math.round(A[2] + (B[2] - A[2]) * t)})`; }
function fl3Smooth(t) { t = Math.max(0, Math.min(1, t)); return t * t * (3 - 2 * t); }
function fl3OpenFromR(R) { return Math.max(0.14, Math.min(1, 1.04 - (R - 1) / 9 * 0.9)); }
const FL3_BLUE = "#2f6db0", FL3_AMBER = "#e0a32e", FL3_RED = "#c0392b";
function fl3Spectrum(e) { e = Math.max(0, Math.min(1, e)); return e >= 0.5 ? fl3MixHex(FL3_AMBER, FL3_BLUE, (e - 0.5) / 0.5) : fl3MixHex(FL3_RED, FL3_AMBER, e / 0.5); }

const FL3 = (() => {
  const X0 = 130, X1 = 690, Y0 = 110, Y1 = 350, rc = 64;
  function rectPts(perCorner) {
    const arc = (cx, cy, a0, a1) => { const o = []; for (let i = 0; i <= perCorner; i++) { const a = a0 + (a1 - a0) * (i / perCorner); o.push([cx + rc * Math.cos(a), cy + rc * Math.sin(a)]); } return o; };
    const pts = [[(X0 + X1) / 2, Y1], [X0 + rc, Y1]];
    pts.push(...arc(X0 + rc, Y1 - rc, Math.PI / 2, Math.PI), [X0, Y0 + rc]);
    pts.push(...arc(X0 + rc, Y0 + rc, Math.PI, 1.5 * Math.PI), [X1 - rc, Y0]);
    pts.push(...arc(X1 - rc, Y0 + rc, -Math.PI / 2, 0), [X1, Y1 - rc]);
    pts.push(...arc(X1 - rc, Y1 - rc, 0, Math.PI / 2), [(X0 + X1) / 2, Y1]);
    return pts;
  }
  let pts = rectPts(12);
  // densify
  const dense = [pts[0].slice()];
  for (let i = 1; i < pts.length; i++) { const a = pts[i - 1], b = pts[i], d = Math.hypot(b[0] - a[0], b[1] - a[1]), k = Math.max(1, Math.round(d / 6)); for (let j = 1; j <= k; j++) { const t = j / k; dense.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]); } }
  pts = dense;
  const n = pts.length, cum = [0];
  for (let i = 1; i < n; i++) cum.push(cum[i - 1] + Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]));
  const P = cum[n - 1];
  function at(s) { s = ((s % P) + P) % P; let i = 1; while (i < n && cum[i] < s) i++; const a = pts[i - 1], b = pts[i] || pts[0], seg = cum[i] - cum[i - 1] || 1, f = (s - cum[i - 1]) / seg; let tx = b[0] - a[0], ty = b[1] - a[1]; const tl = Math.hypot(tx, ty) || 1; return { x: a[0] + (b[0] - a[0]) * f, y: a[1] + (b[1] - a[1]) * f, tx: tx / tl, ty: ty / tl }; }
  function sAt(px, py) { let best = 0, bd = Infinity; for (let i = 0; i < n; i++) { const d = (pts[i][0] - px) ** 2 + (pts[i][1] - py) ** 2; if (d < bd) { bd = d; best = cum[i]; } } return best; }
  return { P, at, sAt, pts, cum, X0, X1, Y0, Y1 };
})();

function LoopScene3({ v, r, kids }) {
  const cv = React.useRef(null);
  const propRef = React.useRef({ v, r, kids });
  propRef.current = { v, r, kids };

  React.useEffect(() => {
    const canvas = cv.current, ctx = canvas.getContext("2d");
    const VBW = 820, VBH = 460;
    const dpr = Math.min(2, window.devicePixelRatio || 1);
    let scale = 1;
    function fit() { const cw = canvas.clientWidth || VBW; scale = cw / VBW; canvas.width = Math.round(cw * dpr); canvas.height = Math.round(VBH * scale * dpr); ctx.setTransform(dpr * scale, 0, 0, dpr * scale, 0, 0); }
    fit(); window.addEventListener("resize", fit);

    const cs = getComputedStyle(document.body);
    const C = { ink: cs.getPropertyValue("--ink").trim() || "#2c2a26", faint: cs.getPropertyValue("--ink-faint").trim() || "#a79f90", card: cs.getPropertyValue("--bg-card").trim() || "#faf6ec", deep: cs.getPropertyValue("--bg-deeper").trim() || "#e6dfca" };
    const hWide = 17, rad = 4.6;

    const pinchS = FL3.sAt((FL3.X0 + FL3.X1) / 2, FL3.Y0);
    const battS = FL3.sAt((FL3.X0 + FL3.X1) / 2, FL3.Y1);
    const pinchHalf = 42, battHalf = 46;
    const pinchPt = FL3.at(pinchS), battPt = FL3.at(battS);

    function halfW(s) { const m = Math.abs(((s - pinchS + FL3.P / 2 + FL3.P) % FL3.P) - FL3.P / 2); return hWide * (m < pinchHalf ? 1 - (1 - fl3OpenFromR(propRef.current.r)) * fl3Smooth(1 - m / pinchHalf) : 1); }
    function winFrac(ph, phc, half) { if (ph < phc - half) return 0; if (ph > phc + half) return 1; return fl3Smooth((ph - (phc - half)) / (2 * half)); }
    function near(s, sc, half) { const d = ((s - (sc - half)) % FL3.P + FL3.P) % FL3.P; return d <= 2 * half ? d / (2 * half) : -1; }

    const N = 150, cell = 2 * rad;
    const ps = [];
    for (let i = 0; i < N; i++) { const s = Math.random() * FL3.P; ps.push({ s, u: (Math.random() * 2 - 1) * hWide * 0.55, e: Math.random() }); }
    const cache = new Array(N);
    const wisps = []; // heat leaving the pinch — emission rate ∝ power
    let raf;

    function frame() {
      const { v: V, r: R, kids: K } = propRef.current;
      const base = 0.46 * V / Math.max(1, R);
      const ph_p = ((pinchS - battS) % FL3.P + FL3.P) % FL3.P;

      for (let i = 0; i < N; i++) { const p = ps[i], a = FL3.at(p.s), nx = -a.ty, ny = a.tx; cache[i] = { x: a.x + nx * p.u, y: a.y + ny * p.u, tx: a.tx, ty: a.ty, nx, ny }; }
      const grid = new Map();
      for (let i = 0; i < N; i++) { const c = cache[i], k = Math.floor(c.x / cell) + "," + Math.floor(c.y / cell); let g = grid.get(k); if (!g) { g = []; grid.set(k, g); } g.push(i); }
      for (let i = 0; i < N; i++) { const ci = cache[i], gx = Math.floor(ci.x / cell), gy = Math.floor(ci.y / cell); for (let ox = -1; ox <= 1; ox++) for (let oy = -1; oy <= 1; oy++) { const g = grid.get((gx + ox) + "," + (gy + oy)); if (!g) continue; for (const j of g) { if (j <= i) continue; const cj = cache[j]; let dx = cj.x - ci.x, dy = cj.y - ci.y, d2 = dx * dx + dy * dy, mn = 2 * rad; if (d2 < mn * mn && d2 > 1e-4) { const d = Math.sqrt(d2), ov = (mn - d) / d * 0.5; dx *= ov; dy *= ov; const pi = ps[i], pj = ps[j]; pi.s -= dx * ci.tx + dy * ci.ty; pi.u -= dx * ci.nx + dy * ci.ny; pj.s += dx * cj.tx + dy * cj.ty; pj.u += dx * cj.nx + dy * cj.ny; } } } }

      for (let i = 0; i < N; i++) {
        const p = ps[i], h = halfW(p.s);
        p.s = (p.s + base * (hWide / h)) % FL3.P;
        const maxU = Math.max(rad * 0.5, h - rad);
        if (p.u > maxU) p.u = maxU; if (p.u < -maxU) p.u = -maxU;
        const ph = ((p.s - battS) % FL3.P + FL3.P) % FL3.P;
        let e = 1 - winFrac(ph, ph_p, pinchHalf);
        const nb = near(p.s, battS, battHalf); if (nb >= 0) e = nb;
        p.e = e;
      }

      // ---- draw ----
      ctx.clearRect(0, 0, VBW, VBH);
      const out = [], inn = [];
      for (let i = 0; i < FL3.pts.length; i++) { const a = FL3.pts[Math.max(0, i - 1)], c = FL3.pts[Math.min(FL3.pts.length - 1, i + 1)]; let tx = c[0] - a[0], ty = c[1] - a[1]; const tl = Math.hypot(tx, ty) || 1; tx /= tl; ty /= tl; const nx = -ty, ny = tx, h = halfW(FL3.cum[i]); out.push([FL3.pts[i][0] + nx * h, FL3.pts[i][1] + ny * h]); inn.push([FL3.pts[i][0] - nx * h, FL3.pts[i][1] - ny * h]); }
      ctx.beginPath(); out.forEach((p, i) => i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1])); for (let i = inn.length - 1; i >= 0; i--) ctx.lineTo(inn[i][0], inn[i][1]); ctx.closePath(); ctx.fillStyle = C.deep; ctx.fill();

      // heat glow at the pinch — scales with power P = V^2/R
      const P = (V * V) / R, pw = Math.min(1, P / 90);
      if (pw > 0.03) { const g = ctx.createRadialGradient(pinchPt.x, pinchPt.y, 3, pinchPt.x, pinchPt.y, 120); g.addColorStop(0, `rgba(192,57,43,${0.6 * pw})`); g.addColorStop(0.5, `rgba(224,120,46,${0.3 * pw})`); g.addColorStop(1, "rgba(0,0,0,0)"); ctx.fillStyle = g; ctx.fillRect(pinchPt.x - 130, pinchPt.y - 100, 260, 200); }

      // heat wisps — conservation made visible: every watt the battery pours in
      // leaves the loop right here, as heat. Spawn rate ∝ P.
      if (wisps.length < 80 && Math.random() < 0.10 + pw * 0.8) {
        const off = (Math.random() * 2 - 1) * pinchHalf * 0.8;
        const a = FL3.at(((pinchS + off) % FL3.P + FL3.P) % FL3.P);
        wisps.push({ x: a.x, y: a.y, age: 0, drift: (Math.random() * 2 - 1) * 0.5, ph: Math.random() * 6.28, big: 0.6 + pw });
      }
      for (let i = wisps.length - 1; i >= 0; i--) {
        const w = wisps[i]; w.age += 1 / 60;
        const t = w.age / 1.2;
        if (t >= 1) { wisps.splice(i, 1); continue; }
        const wx = w.x + w.drift * w.age * 32 + Math.sin(w.ph + w.age * 5) * 2.6;
        const wy = w.y - 30 * w.age - 16 * w.age * w.age;
        ctx.beginPath(); ctx.arc(wx, wy, (2 + 5 * t) * w.big, 0, 7);
        ctx.fillStyle = t < 0.4 ? `rgba(192,57,43,${0.5 * (1 - t)})` : `rgba(224,163,46,${0.5 * (1 - t)})`;
        ctx.fill();
      }

      // particles
      let spot = null;
      for (let i = 0; i < N; i++) { const c = cache[i], p = ps[i]; ctx.beginPath(); ctx.arc(c.x, c.y, rad, 0, 7); ctx.fillStyle = fl3Spectrum(p.e); ctx.fill(); if (i === 0) spot = { x: c.x, y: c.y, e: p.e }; }

      // channel outline
      ctx.lineWidth = 2; ctx.strokeStyle = C.ink; ctx.lineJoin = "round";
      ctx.beginPath(); out.forEach((p, i) => i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1])); ctx.stroke();
      ctx.beginPath(); inn.forEach((p, i) => i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1])); ctx.stroke();

      // resistor jaws that squeeze with R
      const SEG = 9, win = pinchHalf - 4, tO = [], tI = [], bO = [], bI = [];
      for (let i = 0; i <= SEG; i++) { const s = pinchS - win + (2 * win) * (i / SEG), a = FL3.at(s), h = halfW(s), nx = -a.ty, ny = a.tx, fr = hWide + 6; tO.push([a.x + nx * fr, a.y + ny * fr]); tI.push([a.x + nx * h, a.y + ny * h]); bO.push([a.x - nx * fr, a.y - ny * fr]); bI.push([a.x - nx * h, a.y - ny * h]); }
      const jaw = (O, I) => { ctx.beginPath(); O.forEach((p, i) => i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1])); for (let i = I.length - 1; i >= 0; i--) ctx.lineTo(I[i][0], I[i][1]); ctx.closePath(); ctx.fillStyle = "#d9c089"; ctx.fill(); ctx.lineWidth = 1.5; ctx.strokeStyle = C.ink; ctx.stroke(); };
      jaw(tO, tI); jaw(bO, bI);
      ctx.fillStyle = pw > 0.3 ? FL3_RED : C.ink; ctx.font = "600 13px 'IBM Plex Mono', monospace"; ctx.textAlign = "center";
      ctx.fillText(K ? "the squeeze" : "RESISTOR", pinchPt.x, pinchPt.y - hWide - 16);

      // battery
      ctx.strokeStyle = C.ink; ctx.lineCap = "round";
      ctx.lineWidth = 3.4; ctx.beginPath(); ctx.moveTo(battPt.x - 30, battPt.y - 9); ctx.lineTo(battPt.x + 30, battPt.y - 9); ctx.stroke();
      ctx.lineWidth = 3.4; ctx.beginPath(); ctx.moveTo(battPt.x - 16, battPt.y + 6); ctx.lineTo(battPt.x + 16, battPt.y + 6); ctx.stroke();
      ctx.fillStyle = FL3_BLUE; ctx.font = "600 15px 'IBM Plex Mono', monospace"; ctx.fillText("+", battPt.x - 44, battPt.y - 3);
      ctx.fillStyle = C.ink; ctx.fillText("\u2013", battPt.x - 44, battPt.y + 15);
      ctx.fillStyle = C.faint; ctx.font = "600 10px 'IBM Plex Mono', monospace"; ctx.fillText(K ? "fills the push back up" : "BATTERY \u00b7 restores push", battPt.x, battPt.y + 30);

      // spotlight + centre POWER readout (the Ch3 reveal)
      if (spot) {
        ctx.beginPath(); ctx.arc(spot.x, spot.y, rad + 3.4, 0, 7); ctx.lineWidth = 2.2; ctx.strokeStyle = C.ink; ctx.stroke();
        ctx.beginPath(); ctx.arc(spot.x, spot.y, rad, 0, 7); ctx.fillStyle = fl3Spectrum(spot.e); ctx.fill();
      }
      const mx = VBW / 2, my = VBH / 2;
      ctx.textAlign = "center";
      ctx.fillStyle = C.faint; ctx.font = "600 11px 'IBM Plex Mono', monospace";
      ctx.fillText(K ? "HEAT MADE AT THE SQUEEZE" : "POWER BURNED AT THE PINCH", mx, my - 20);
      ctx.fillStyle = fl3Spectrum(Math.max(0, 1 - pw));
      ctx.font = "600 38px 'Newsreader', serif";
      ctx.fillText(P.toFixed(1) + (K ? "" : " W"), mx, my + 18);
      ctx.fillStyle = C.faint; ctx.font = "500 11px 'IBM Plex Mono', monospace";
      ctx.fillText(K ? (pw > 0.6 ? "scalding!" : pw > 0.3 ? "hot" : pw > 0.1 ? "warm" : "barely warm") : "P = V \u00d7 I = " + V.toFixed(1) + " \u00d7 " + (V / R).toFixed(2) + " A", mx, my + 38);
      ctx.fillText(K ? "all the push the battery gives leaves as heat here" : "conservation: watts in at the battery = watts out here", mx, my + 56);

      raf = requestAnimationFrame(frame);
    }
    raf = requestAnimationFrame(frame);
    return () => { cancelAnimationFrame(raf); window.removeEventListener("resize", fit); };
  }, []);

  return <canvas ref={cv} className="loop3-canvas" style={{ width: "100%", display: "block" }}></canvas>;
}

Object.assign(window, { LoopScene3 });
