/* loop-scene2.jsx — Chapter 2 closed-loop charge sim (series & parallel).
   Replaces the schematic in the sticky panel; driven by props (v, r1, r2,
   topology) so it stays synced with the lesson sliders.
   - 3-stop energy spectrum: blue (full) -> amber (half) -> red (spent/heat).
   - Tracks one spotlight charge with a centred energy readout (like Ch1).
   - SERIES: one racetrack, two pinches in a row.
   - PARALLEL: rectangular ladder, battery + return on the LEFT, charge splits
     across the two rungs and rejoins.
   - Resistors visibly squeeze the channel as R rises.
   All names fl2-/LoopScene2 prefixed to avoid cross-file collisions. */

function fl2HexRgb(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 fl2MixHex(a, b, t) { const A = fl2HexRgb(a), B = fl2HexRgb(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 fl2Smooth(t) { t = Math.max(0, Math.min(1, t)); return t * t * (3 - 2 * t); }
function fl2OpenFromR(R) { return Math.max(0.14, Math.min(1, 1.04 - (R - 1) / 9 * 0.9)); }
function fl2WinFrac(ph, phc, half) { if (ph < phc - half) return 0; if (ph > phc + half) return 1; return fl2Smooth((ph - (phc - half)) / (2 * half)); }
function fl2Densify(pts, step) {
  const out = [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 / step)); for (let j = 1; j <= k; j++) { const t = j / k; out.push([a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t]); } }
  return out;
}

/* 3-stop spectrum: 1.0 blue -> 0.5 amber -> 0.0 red. */
const FL2_BLUE = "#2f6db0", FL2_AMBER = "#e0a32e", FL2_RED = "#c0392b";
function fl2Spectrum(e) {
  e = Math.max(0, Math.min(1, e));
  return e >= 0.5 ? fl2MixHex(FL2_AMBER, FL2_BLUE, (e - 0.5) / 0.5)
                  : fl2MixHex(FL2_RED, FL2_AMBER, e / 0.5);
}

function fl2Build(points) {
  const n = points.length;
  const cum = [0];
  for (let i = 1; i < n; i++) { const dx = points[i][0] - points[i - 1][0], dy = points[i][1] - points[i - 1][1]; cum.push(cum[i - 1] + Math.hypot(dx, dy)); }
  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 = points[i - 1], b = points[i] || points[0];
    const 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 sAtPoint(px, py) { let best = 0, bd = Infinity; for (let i = 0; i < n; i++) { const d = (points[i][0] - px) ** 2 + (points[i][1] - py) ** 2; if (d < bd) { bd = d; best = cum[i]; } } return best; }
  return { P, at, sAtPoint, points, cum };
}

/* rounded rectangle racetrack (flow order clockwise from bottom-centre) */
function fl2Rect(X0, Y0, X1, Y1, rc, 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;
}

/* corner-rounded polyline from rectangular waypoints */
function fl2Round(way, r, closed) {
  const out = [];
  const n = way.length;
  for (let i = 0; i < n; i++) {
    const p = way[i];
    if ((i === 0 || i === n - 1) && !closed) { out.push(p.slice()); continue; }
    const a = way[(i - 1 + n) % n], b = way[(i + 1) % n];
    const va = [a[0] - p[0], a[1] - p[1]], vb = [b[0] - p[0], b[1] - p[1]];
    const la = Math.hypot(...va) || 1, lb = Math.hypot(...vb) || 1;
    const ra = Math.min(r, la / 2), rb = Math.min(r, lb / 2);
    const pA = [p[0] + va[0] / la * ra, p[1] + va[1] / la * ra];
    const pB = [p[0] + vb[0] / lb * rb, p[1] + vb[1] / lb * rb];
    out.push(pA);
    for (let k = 1; k < 5; k++) { const t = k / 5, u = 1 - t; out.push([u * u * pA[0] + 2 * u * t * p[0] + t * t * pB[0], u * u * pA[1] + 2 * u * t * p[1] + t * t * pB[1]]); }
    out.push(pB);
  }
  if (closed) out.push(out[0].slice());
  return out;
}

function LoopScene2({ v, r1, r2, topology, kids, height }) {
  const cv = React.useRef(null);
  const propRef = React.useRef({ v, r1, r2, topology, kids });
  propRef.current = { v, r1, r2, topology, kids };

  React.useEffect(() => {
    const canvas = cv.current, ctx = canvas.getContext("2d");
    const VBW = 820, VBH = 440;
    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 = 16, rad = 4.4;

    // ---------- geometry ----------
    // SERIES: racetrack, two pinches on top straight, battery bottom-centre.
    const sLoop = fl2Build(fl2Densify(fl2Rect(150, 110, 670, 340, 64, 12), 6));
    const sPinch1 = sLoop.sAtPoint(300, 110), sPinch2 = sLoop.sAtPoint(520, 110);
    const sBatt = sLoop.sAtPoint(410, 340);
    const sPinchHalf = 38, sBattHalf = 44;

    // PARALLEL: rectangular ladder. Node A (left bus x=250), node B (right bus
    // PARALLEL: rectangular ladder, battery in the MIDDLE return line. Node A
    // (left, x=180) splits; node B (right, x=640) rejoins; the return trunk runs
    // straight back through the centre (y=220) with the battery at x=400.
    const Ax = 180, Bx = 640, yMid = 220, yTop = 110, yBot = 330, battX = 400;
    const trunkWay = [[Bx, yMid], [Ax, yMid]];                          // straight middle return B -> A
    const br1Way = [[Ax, yMid], [Ax, yTop], [Bx, yTop], [Bx, yMid]];    // A up -> across top (R1) -> down to B
    const br2Way = [[Ax, yMid], [Ax, yBot], [Bx, yBot], [Bx, yMid]];    // A down -> across bottom (R2) -> up to B
    const trunkR = fl2Round(trunkWay, 16, false);
    const loop1 = fl2Build(fl2Densify([...fl2Round(br1Way, 16, false), ...trunkR.slice(1)], 6));
    const loop2 = fl2Build(fl2Densify([...fl2Round(br2Way, 16, false), ...trunkR.slice(1)], 6));
    const p1Pinch = loop1.sAtPoint(410, yTop), p1End = loop1.sAtPoint(Bx, yMid), p1Batt = loop1.sAtPoint(battX, yMid);
    const p2Pinch = loop2.sAtPoint(410, yBot), p2End = loop2.sAtPoint(Bx, yMid), p2Batt = loop2.sAtPoint(battX, yMid);
    const pPinchHalf = 30, pBattHalf = 40;

    function pinchFactor(m, half, R) { return m < half ? 1 - (1 - fl2OpenFromR(R)) * fl2Smooth(1 - m / half) : 1; }
    function halfWSeries(s) {
      const d = (sc) => Math.abs(((s - sc + sLoop.P / 2 + sLoop.P) % sLoop.P) - sLoop.P / 2);
      return hWide * Math.min(pinchFactor(d(sPinch1), sPinchHalf, propRef.current.r1), pinchFactor(d(sPinch2), sPinchHalf, propRef.current.r2));
    }
    function halfWBranch(lp, pinchS, R, s) { const m = Math.abs(((s - pinchS + lp.P / 2 + lp.P) % lp.P) - lp.P / 2); return hWide * pinchFactor(m, pPinchHalf, R); }

    // ---------- particles ----------
    const N = 140;
    let parts = [];
    function seed() {
      const { topology: topo, r1: R1, r2: R2 } = propRef.current;
      parts = [];
      if (topo === "series") { for (let i = 0; i < N; i++) parts.push({ loop: 0, s: Math.random() * sLoop.P, u: (Math.random() * 2 - 1) * hWide * 0.5, e: Math.random(), rank: Math.random() }); }
      else { const thr = (1 / R1) / (1 / R1 + 1 / R2); for (let i = 0; i < N; i++) { const rank = Math.random(), b1 = rank < thr, lp = b1 ? loop1 : loop2; parts.push({ loop: b1 ? 1 : 2, rank, s: Math.random() * lp.P, u: (Math.random() * 2 - 1) * hWide * 0.5, e: Math.random() }); } parts[0].spot = true; }
    }
    let lastTopo = null;

    function near(s, sc, half, P) { const d = ((s - (sc - half)) % P + P) % P; return d <= 2 * half ? d / (2 * half) : -1; }

    let raf;
    function frame() {
      const { v: V, r1: R1, r2: R2, topology: topo, kids: K } = propRef.current;
      if (topo !== lastTopo) { seed(); lastTopo = topo; }

      // ---- update ----
      if (topo === "series") {
        const base = 0.44 * V / Math.max(1, R1 + R2), sh1 = R1 / (R1 + R2), P = sLoop.P;
        const ph1 = ((sPinch1 - sBatt) % P + P) % P, ph2 = ((sPinch2 - sBatt) % P + P) % P;
        for (const p of parts) {
          const h = halfWSeries(p.s);
          p.s = (p.s + base * (hWide / h)) % P; p.u += (-p.u) * 0.04;
          const ph = ((p.s - sBatt) % P + P) % P;
          let e = 1 - sh1 * fl2WinFrac(ph, ph1, sPinchHalf) - (1 - sh1) * fl2WinFrac(ph, ph2, sPinchHalf);
          const nb = near(p.s, sBatt, sBattHalf, P);
          if (nb >= 0) e = nb;
          p.e = e;
        }
      } else {
        const Req = (R1 * R2) / (R1 + R2), base = 0.44 * V / Math.max(1, Req);
        const thr = (1 / R1) / (1 / R1 + 1 / R2);
        for (const p of parts) {
          const lp = p.loop === 1 ? loop1 : loop2, pinchS = p.loop === 1 ? p1Pinch : p2Pinch, end = p.loop === 1 ? p1End : p2End, battS = p.loop === 1 ? p1Batt : p2Batt, R = p.loop === 1 ? R1 : R2;
          const h = halfWBranch(lp, pinchS, R, p.s);
          const ns = p.s + base * (hWide / h);
          if (ns >= lp.P) { if (p.spot) p.rank = Math.random(); p.loop = p.rank < thr ? 1 : 2; }   // re-choose branch at node A; spotlight re-rolls so the weighting is visible
          p.s = ns % lp.P; p.u += (-p.u) * 0.04;
          const P = lp.P, ph = ((p.s - battS) % P + P) % P, php = ((pinchS - battS) % P + P) % P;
          let e = 1 - fl2WinFrac(ph, php, pPinchHalf);
          const nb = near(p.s, battS, pBattHalf, P);
          if (nb >= 0) e = nb;
          p.e = e;
        }
      }

      // ---- draw ----
      ctx.clearRect(0, 0, VBW, VBH);
      const loops = topo === "series" ? [{ b: sLoop, hw: halfWSeries }]
        : [{ b: loop1, hw: (s) => halfWBranch(loop1, p1Pinch, R1, s) }, { b: loop2, hw: (s) => halfWBranch(loop2, p2Pinch, R2, s) }];

      const offset = (L) => {
        const pts = L.b.points, out = [], inn = [];
        for (let i = 0; i < pts.length; i++) {
          const a = pts[Math.max(0, i - 1)], c = pts[Math.min(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 = L.hw(L.b.cum[i]);
          out.push([pts[i][0] + nx * h, pts[i][1] + ny * h]); inn.push([pts[i][0] - nx * h, pts[i][1] - ny * h]);
        }
        return { out, inn };
      };

      // channel fill
      for (const L of loops) { const { out, inn } = offset(L); 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 pinches (redder/brighter the more power)
      const glow = (cx, cy, R) => { const pw = Math.min(1, (V * V / R) / 90); if (pw < 0.05) return; const g = ctx.createRadialGradient(cx, cy, 3, cx, cy, 78); g.addColorStop(0, `rgba(192,57,43,${0.46 * pw})`); g.addColorStop(1, "rgba(0,0,0,0)"); ctx.fillStyle = g; ctx.fillRect(cx - 84, cy - 64, 168, 128); };
      if (topo === "series") { const a = sLoop.at(sPinch1), b = sLoop.at(sPinch2); glow(a.x, a.y, R1); glow(b.x, b.y, R2); }
      else { glow(415, yTop, R1); glow(415, yBot, R2); }

      // particles (spectrum colour)
      let spot = null;
      for (let idx = 0; idx < parts.length; idx++) {
        const p = parts[idx], lp = p.loop === 1 ? loop1 : (p.loop === 2 ? loop2 : sLoop);
        const a = lp.at(p.s), nx = -a.ty, ny = a.tx, x = a.x + nx * p.u, y = a.y + ny * p.u;
        ctx.beginPath(); ctx.arc(x, y, rad, 0, 7); ctx.fillStyle = fl2Spectrum(p.e); ctx.fill();
        if (idx === 0) spot = { x, y, e: p.e };
      }

      // channel outline
      for (const L of loops) { const { out, inn } = offset(L); ctx.lineWidth = 1.8; 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 that SQUEEZES: tan jaws framing the narrowed opening (grow with R)
      const drawResistor = (loop, pinchS, hwFn, R, label) => {
        const c = loop.at(pinchS), horiz = Math.abs(c.tx) > Math.abs(c.ty);
        const K = 9, win = pPinchHalf - 2, topO = [], topI = [], botO = [], botI = [];
        for (let i = 0; i <= K; i++) {
          const s = pinchS - win + (2 * win) * (i / K), a = loop.at(s), h = hwFn(s), nx = -a.ty, ny = a.tx, frame2 = hWide + 6;
          topO.push([a.x + nx * frame2, a.y + ny * frame2]); topI.push([a.x + nx * h, a.y + ny * h]);
          botO.push([a.x - nx * frame2, a.y - ny * frame2]); botI.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(topO, topI); jaw(botO, botI);
        ctx.fillStyle = C.ink; ctx.font = "600 13px 'IBM Plex Mono', monospace"; ctx.textAlign = "center";
        const lp = loop.at(pinchS); ctx.fillText(label, lp.x + (horiz ? 0 : 30), lp.y - (horiz ? hWide + 16 : 0));
      };
      const battery = (cx, cy, vert) => { ctx.strokeStyle = C.ink; ctx.lineCap = "round"; if (vert) { ctx.lineWidth = 3.4; ctx.beginPath(); ctx.moveTo(cx - 12, cy - 7); ctx.lineTo(cx + 12, cy - 7); ctx.stroke(); ctx.lineWidth = 3.4; ctx.beginPath(); ctx.moveTo(cx - 7, cy + 7); ctx.lineTo(cx + 7, cy + 7); ctx.stroke(); } else { ctx.lineWidth = 3.4; ctx.beginPath(); ctx.moveTo(cx - 7, cy - 13); ctx.lineTo(cx - 7, cy + 13); ctx.stroke(); ctx.lineWidth = 3.4; ctx.beginPath(); ctx.moveTo(cx + 7, cy - 8); ctx.lineTo(cx + 7, cy + 8); ctx.stroke(); } };

      if (topo === "series") {
        drawResistor(sLoop, sPinch1, halfWSeries, R1, "R\u2081"); drawResistor(sLoop, sPinch2, halfWSeries, R2, "R\u2082");
        const bt = sLoop.at(sBatt); battery(bt.x, bt.y, false);
        ctx.fillStyle = C.faint; ctx.font = "600 10px 'IBM Plex Mono', monospace"; ctx.textAlign = "center";
        ctx.fillText(K ? "same flow all the way round" : "one loop \u00b7 same current everywhere", VBW / 2, VBH - 12);
      } else {
        drawResistor(loop1, p1Pinch, (s) => halfWBranch(loop1, p1Pinch, R1, s), R1, "R\u2081");
        drawResistor(loop2, p2Pinch, (s) => halfWBranch(loop2, p2Pinch, R2, s), R2, "R\u2082");
        const bc = loop1.at(p1Batt); battery(battX, yMid, false);
        ctx.fillStyle = C.ink; [[Ax, yMid], [Bx, yMid]].forEach(p => { ctx.beginPath(); ctx.arc(p[0], p[1], 5, 0, 7); ctx.fill(); });
        ctx.fillStyle = FL2_BLUE; ctx.font = "600 13px 'IBM Plex Mono', monospace"; ctx.textAlign = "center"; ctx.fillText("+", battX - 22, yMid + 4);
        ctx.fillStyle = C.ink; ctx.fillText("\u2013", battX + 22, yMid + 4);
        ctx.fillStyle = C.faint; ctx.font = "600 10px 'IBM Plex Mono', monospace";
        ctx.fillText(K ? "flow splits \u2014 more down the looser branch" : "same push on each branch \u00b7 flow splits, then adds", VBW / 2, VBH - 12);
      }

      // spotlight charge + centred energy readout
      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 = fl2Spectrum(spot.e); ctx.fill();
        ctx.fillStyle = C.ink; ctx.font = "600 10px 'IBM Plex Mono', monospace"; ctx.textAlign = "center"; ctx.fillText("this one", spot.x, spot.y - 12);
        const mx = VBW / 2, my = topo === "series" ? VBH / 2 : 278;
        ctx.fillStyle = C.faint; ctx.font = "600 11px 'IBM Plex Mono', monospace";
        ctx.fillText(K ? "THIS DROP\u2019S PUSH" : "THIS CHARGE\u2019S PUSH", mx, my - 26);
        const bw = 150, bh = 15;
        ctx.fillStyle = C.deep; if (ctx.roundRect) { ctx.beginPath(); ctx.roundRect(mx - bw / 2, my - 11, bw, bh, 7.5); ctx.fill(); } else ctx.fillRect(mx - bw / 2, my - 11, bw, bh);
        ctx.fillStyle = fl2Spectrum(spot.e); const fw = Math.max(bh, bw * spot.e); if (ctx.roundRect) { ctx.beginPath(); ctx.roundRect(mx - bw / 2, my - 11, fw, bh, 7.5); ctx.fill(); } else ctx.fillRect(mx - bw / 2, my - 11, fw, bh);
        ctx.fillStyle = C.ink; ctx.font = "600 27px 'Newsreader', serif"; ctx.fillText(Math.round(spot.e * 100) + "%", mx, my + 36);
        // the PUSH it carries drops across the pinches — but the CHARGE itself
        // (measured in coulombs) is conserved all the way round.
        ctx.fillStyle = C.faint; ctx.font = "500 9.5px 'IBM Plex Mono', monospace";
        ctx.fillText(K ? "the charge itself never runs out" : "charge carried: 1 C — constant", mx, my + 52);
      }

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

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

Object.assign(window, { LoopScene2 });
