/* loop-scene5.jsx — Chapter 5 closed-loop charge sim (the switch).
   One loop, battery + resistor + a SWITCH on the top wire. The whole point:
   an OPEN switch breaks the loop and the current stops EVERYWHERE at once (not
   just at the switch) — there's no "downstream" puddle, the entire file freezes.
   Closed switch -> spectrum charges circulate (blue->amber->red as in Ch2-4).
   Driven by props (v, r, on). Names fl5-/LoopScene5 prefixed. */

function fl5HexRgb(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 fl5MixHex(a, b, t) { const A = fl5HexRgb(a), B = fl5HexRgb(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 fl5Smooth(t) { t = Math.max(0, Math.min(1, t)); return t * t * (3 - 2 * t); }
function fl5OpenFromR(R) { return Math.max(0.14, Math.min(1, 1.04 - (R - 1) / 9 * 0.9)); }
const FL5_BLUE = "#2f6db0", FL5_AMBER = "#e0a32e", FL5_RED = "#c0392b";
function fl5Spectrum(e) { e = Math.max(0, Math.min(1, e)); return e >= 0.5 ? fl5MixHex(FL5_AMBER, FL5_BLUE, (e - 0.5) / 0.5) : fl5MixHex(FL5_RED, FL5_AMBER, e / 0.5); }

const FL5 = (() => {
  const X0 = 130, X1 = 690, Y0 = 110, Y1 = 350, rc = 64;
  const arc = (cx, cy, a0, a1, k) => { const o = []; for (let i = 0; i <= k; i++) { const a = a0 + (a1 - a0) * (i / k); o.push([cx + rc * Math.cos(a), cy + rc * Math.sin(a)]); } return o; };
  let pts = [[(X0 + X1) / 2, Y1], [X0 + rc, Y1]];
  pts.push(...arc(X0 + rc, Y1 - rc, Math.PI / 2, Math.PI, 12), [X0, Y0 + rc]);
  pts.push(...arc(X0 + rc, Y0 + rc, Math.PI, 1.5 * Math.PI, 12), [X1 - rc, Y0]);
  pts.push(...arc(X1 - rc, Y0 + rc, -Math.PI / 2, 0, 12), [X1, Y1 - rc]);
  pts.push(...arc(X1 - rc, Y1 - rc, 0, Math.PI / 2, 12), [(X0 + X1) / 2, Y1]);
  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 LoopScene5({ v, r, on, kids }) {
  const cv = React.useRef(null);
  const propRef = React.useRef({ v, r, on, kids });
  propRef.current = { v, r, on, 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;

    // switch sits on the top wire (left of centre); resistor right of centre; battery bottom
    const swS = FL5.sAt(300, FL5.Y0);
    const pinchS = FL5.sAt(500, FL5.Y0);
    const battS = FL5.sAt((FL5.X0 + FL5.X1) / 2, FL5.Y1);
    const pinchHalf = 40, battHalf = 46;
    const swPt = FL5.at(swS), pinchPt = FL5.at(pinchS), battPt = FL5.at(battS);

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

    const N = 150, cell = 2 * rad;
    const ps = [];
    for (let i = 0; i < N; i++) ps.push({ s: Math.random() * FL5.P, u: (Math.random() * 2 - 1) * hWide * 0.55, e: Math.random() });
    const cache = new Array(N);
    let openAmt = propRef.current.on ? 0 : 1;   // animated 0..1 switch-open fraction
    let raf;

    function frame() {
      const { v: V, r: R, on: ON, kids: K } = propRef.current;
      openAmt += ((ON ? 0 : 1) - openAmt) * 0.18;
      const flowing = ON && openAmt < 0.5;
      const base = flowing ? 0.46 * V / Math.max(1, R) : 0;
      const ph_p = ((pinchS - battS) % FL5.P + FL5.P) % FL5.P;

      for (let i = 0; i < N; i++) { const p = ps[i], a = FL5.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 }; }
      if (flowing) {
        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)) % FL5.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) % FL5.P + FL5.P) % FL5.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 < FL5.pts.length; i++) { const a = FL5.pts[Math.max(0, i - 1)], c = FL5.pts[Math.min(FL5.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(FL5.cum[i]); out.push([FL5.pts[i][0] + nx * h, FL5.pts[i][1] + ny * h]); inn.push([FL5.pts[i][0] - nx * h, FL5.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();

      // particles — frozen in place when not flowing (the whole loop stops at once)
      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 = flowing ? fl5Spectrum(p.e) : C.faint; ctx.fill(); if (i === 0) spot = { x: c.x, y: c.y, e: p.e }; }

      // channel outline (skip a window at the switch so the break reads clearly)
      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
      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 = FL5.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 = C.ink; ctx.font = "600 13px 'IBM Plex Mono', monospace"; ctx.textAlign = "center"; ctx.fillText(K ? "squeeze" : "R", pinchPt.x, pinchPt.y - hWide - 14);

      // ── the switch: a lever that lifts off its contact when open ──
      const lever = 34, liftAng = openAmt * 0.62;     // radians up from horizontal
      const baseX = swPt.x - lever / 2, baseY = swPt.y;
      const hingeX = baseX, contactX = swPt.x + lever / 2;
      // contact pads + wire stubs
      ctx.fillStyle = C.ink;
      ctx.beginPath(); ctx.arc(hingeX, baseY, 4.2, 0, 7); ctx.fill();
      ctx.beginPath(); ctx.arc(contactX, baseY, 4.2, 0, 7); ctx.fill();
      // the lever arm (pivots at hinge, rises by liftAng)
      const tipX = hingeX + Math.cos(-liftAng) * lever, tipY = baseY + Math.sin(-liftAng) * lever;
      ctx.strokeStyle = openAmt > 0.5 ? FL5_RED : C.ink; ctx.lineWidth = 4; ctx.lineCap = "round";
      ctx.beginPath(); ctx.moveTo(hingeX, baseY); ctx.lineTo(tipX, tipY); ctx.stroke();
      ctx.fillStyle = openAmt > 0.5 ? FL5_RED : FL5_BLUE; ctx.font = "600 11px 'IBM Plex Mono', monospace"; ctx.textAlign = "center";
      ctx.fillText(ON ? (K ? "ON" : "CLOSED") : (K ? "OFF" : "OPEN"), swPt.x, baseY - 30);

      // 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 = FL5_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);

      // centre readout
      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 ? "IS IT FLOWING?" : "CIRCUIT STATE", mx, my - 20);
      ctx.fillStyle = flowing ? FL5_BLUE : FL5_RED; ctx.font = "600 34px 'Newsreader', serif";
      ctx.fillText(flowing ? (K ? "GO" : "CLOSED") : (K ? "STOP" : "OPEN"), mx, my + 14);
      ctx.fillStyle = C.faint; ctx.font = "500 11px 'IBM Plex Mono', monospace";
      ctx.fillText(flowing ? (K ? "the whole ring moves" : "current flows everywhere at once") : (K ? "nothing moves \u2014 anywhere" : "one break stops the entire loop"), mx, my + 34);

      // spotlight only while flowing
      if (flowing && 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 = fl5Spectrum(spot.e); ctx.fill(); }

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

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

Object.assign(window, { LoopScene5 });
