/* loop-scene4.jsx — Chapter 4 RC loop (capacitor). UNLIKE the steady loops of
   Ch1-3, a capacitor stops the flow once full and no charge crosses the gap.
   So this is an OPEN C-path: charges stream from the battery, through the
   resistor, and PILE onto the top plate; a mirror set leaves the bottom plate
   back to the battery. Flow speed/throughput ∝ I = |Vin−Vcap|/R, so the stream
   visibly tapers to a halt as the plates fill. Spectrum charges (blue→amber→
   red) as in Ch2/3. Discharge reverses and bypasses the battery.
   Driven by props (vIn, R, vCap, mode). Names fl4-/LoopScene4 prefixed. */

function fl4HexRgb(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 fl4MixHex(a, b, t) { const A = fl4HexRgb(a), B = fl4HexRgb(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)})`; }
const FL4_BLUE = "#2f6db0", FL4_AMBER = "#e0a32e", FL4_RED = "#c0392b";
function fl4Spectrum(e) { e = Math.max(0, Math.min(1, e)); return e >= 0.5 ? fl4MixHex(FL4_AMBER, FL4_BLUE, (e - 0.5) / 0.5) : fl4MixHex(FL4_RED, FL4_AMBER, e / 0.5); }

/* arc-length sampler over a polyline */
function fl4Path(pts) {
  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 L = cum[n - 1];
  function at(d) { d = Math.max(0, Math.min(L, d)); let i = 1; while (i < n && cum[i] < d) i++; const a = pts[i - 1], b = pts[i] || pts[0], seg = cum[i] - cum[i - 1] || 1, f = (d - cum[i - 1]) / seg; return { x: a[0] + (b[0] - a[0]) * f, y: a[1] + (b[1] - a[1]) * f }; }
  return { L, at, pts };
}

function LoopScene4({ vIn, R, vCap, mode, kids }) {
  const cv = React.useRef(null);
  const propRef = React.useRef({ vIn, R, vCap, mode, kids });
  propRef.current = { vIn, R, vCap, mode, 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" };

    // geometry: battery (left), resistor pinch (top), capacitor plates (right gap)
    const Lx = 150, Tx = 690, Ty = 120, By = 360;
    const batY = (Ty + By) / 2;
    const plateX = Tx, gapY = (Ty + By) / 2, gapH = 80;     // two plates centred on the right edge
    const topPlateY = gapY - gapH / 2, botPlateY = gapY + gapH / 2;
    const resX = (Lx + Tx) / 2;                              // resistor on top wire
    // TOP feed path: battery+ (top of battery) -> up -> across top (through R) -> down right edge -> top plate
    const feed = fl4Path([[Lx, batY - 24], [Lx, Ty], [Tx, Ty], [Tx, topPlateY]]);
    // BOTTOM return path: bottom plate -> down right edge -> across bottom -> up to battery- 
    const ret = fl4Path([[Tx, botPlateY], [Tx, By], [Lx, By], [Lx, batY + 24]]);
    const rad = 5.4, gap = 22;                              // single-file: one evenly-spaced line of charges
    const Nf = Math.max(2, Math.floor(feed.L / gap));
    const Nr = Math.max(2, Math.floor(ret.L / gap));

    // charges carry a position d along their path + an energy e (spectrum)
    const feedP = [], retP = [];
    for (let i = 0; i < Nf; i++) feedP.push({ d: (i / Nf) * feed.L });
    for (let i = 0; i < Nr; i++) retP.push({ d: (i / Nr) * ret.L });

    function eAtFeed(d) {                                    // energy: full until resistor, drained after
      const rD = feed.L * 0.5;                              // resistor ~ halfway along feed
      return d < rD ? 1 : Math.max(0, 1 - (d - rD) / (feed.L * 0.4));
    }

    let raf;
    function frame() {
      const { vIn: VIN, R: RR, vCap: VC, mode: M, kids: K } = propRef.current;
      const charging = M === "charge", discharging = M === "discharge";
      const dV = charging ? (VIN - VC) : discharging ? VC : 0;
      const I = Math.max(0, Math.abs(dV) / Math.max(0.4, RR));
      const fillFrac = VIN > 0 ? Math.max(0, Math.min(1, VC / VIN)) : 0;
      const moving = I > 0.03 && M !== "hold";
      const spd = moving ? (1.1 + I * 1.6) : 0;             // px/frame along path
      // Top wire carries POSITIVE charge to/from the top plate; bottom wire carries
      // ELECTRONS to/from the bottom plate. Both come from / return to the BATTERY,
      // so charging = both flow TOWARD their plate (piles grow) and discharging =
      // both flow AWAY (piles shrink). Neither carrier ever crosses the gap.
      const dirFeed = charging ? 1 : -1;   // feed: battery(d0) -> top plate(dL) when charging
      const dirRet  = charging ? -1 : 1;   // ret: toward bottom plate(d0) = d decreasing
      for (const p of feedP) { p.d = (p.d + dirFeed * spd + feed.L) % feed.L; }
      for (const p of retP) { p.d = (p.d + dirRet * spd + ret.L) % ret.L; }

      ctx.clearRect(0, 0, VBW, VBH);

      // wires
      ctx.strokeStyle = C.deep; ctx.lineWidth = 11; ctx.lineJoin = "round"; ctx.lineCap = "round";
      const drawWire = (pp) => { ctx.beginPath(); pp.pts.forEach((p, i) => i ? ctx.lineTo(p[0], p[1]) : ctx.moveTo(p[0], p[1])); ctx.stroke(); };
      drawWire(feed); drawWire(ret);

      // battery (dim during discharge — it's switched out)
      ctx.save(); ctx.globalAlpha = discharging ? 0.32 : 1;
      ctx.strokeStyle = C.ink; ctx.lineCap = "round";
      ctx.lineWidth = 3.4; ctx.beginPath(); ctx.moveTo(Lx - 11, batY - 16); ctx.lineTo(Lx + 11, batY - 16); ctx.stroke();
      ctx.lineWidth = 3.4; ctx.beginPath(); ctx.moveTo(Lx - 6, batY + 16); ctx.lineTo(Lx + 6, batY + 16); ctx.stroke();
      ctx.fillStyle = FL4_BLUE; ctx.font = "600 17px 'IBM Plex Mono', monospace"; ctx.textAlign = "center"; ctx.fillText("+", Lx - 20, batY - 12);
      ctx.fillStyle = C.ink; ctx.fillText("\u2013", Lx - 20, batY + 22);
      ctx.restore();

      // resistor on the top wire
      ctx.fillStyle = "#d9c089"; ctx.strokeStyle = C.ink; ctx.lineWidth = 1.6;
      ctx.beginPath(); ctx.rect(resX - 24, Ty - 9, 48, 18); ctx.fill(); ctx.stroke();
      ctx.fillStyle = C.ink; ctx.font = "600 15px 'IBM Plex Mono', monospace"; ctx.textAlign = "center"; ctx.fillText(K ? "squeeze" : "R", resX, Ty - 18);

      // charge carriers — an evenly-spaced FILE along each wire. The wire is
      // ALWAYS full of charge; "current" is just whether that file is MOVING.
      // So the dots are always drawn: they drift while charging/draining and
      // FREEZE IN PLACE the instant the flow stops (they never vanish — the
      // position only advances when `moving`, since spd = 0 otherwise). This is
      // the answer to "if charge is held, why do they keep moving?" — they only
      // move WHILE charging; once full, everything sits still.
      // While flowing, fade each dot IN at its battery end and OUT at its plate
      // end, so charge visibly emerges from the battery and is ABSORBED into the
      // plate — it never crosses the gap between plates, and never teleports.
      // Once stopped, the frozen dots are drawn solid so they stay clearly visible.
      // The wires carry current, but charge is ABSORBED at the plates — it never
      // crosses the gap. So each file fades to NOTHING well before its plate, with
      // a hard empty margin right at the cap; it fades in from its source. What
      // arrives is shown by the growing PILES on the plates, not dots crossing.
      // (Frozen/stopped = solid, so the resting charge stays clearly visible.)
      const NEAR = 100, RIN = 46, ROUT = 140;
      const feedAlpha = (d) => { if (!moving) return 1; const out = feed.L - d; if (out < NEAR) return 0; return Math.max(0, Math.min(1, Math.min(d / RIN, (out - NEAR) / ROUT))); };
      const retAlpha  = (d) => { if (!moving) return 1; if (d < NEAR) return 0; const out = ret.L - d; return Math.max(0, Math.min(1, Math.min((d - NEAR) / ROUT, out / RIN))); };
      for (const p of feedP) { const al = feedAlpha(p.d); if (al <= 0) continue; const a = feed.at(p.d); ctx.globalAlpha = al; ctx.beginPath(); ctx.arc(a.x, a.y, rad, 0, 7); ctx.fillStyle = FL4_BLUE; ctx.fill(); }
      for (const p of retP) { const al = retAlpha(p.d); if (al <= 0) continue; const a = ret.at(p.d); ctx.globalAlpha = al; ctx.beginPath(); ctx.arc(a.x, a.y, rad, 0, 7); ctx.fillStyle = FL4_RED; ctx.fill(); }
      ctx.globalAlpha = 1;

      if (moving) {
        // Two carriers, two wires: top = positive charge (blue), bottom = electrons
        // (red). Both head toward the cap while charging, away while draining.
        const drawArrow = (x, y, right, color) => {
          ctx.save(); ctx.translate(x, y); ctx.scale(right ? 1 : -1, 1);
          ctx.fillStyle = color; ctx.strokeStyle = color; ctx.lineWidth = 3; ctx.lineCap = "round";
          ctx.beginPath(); ctx.moveTo(-16, 0); ctx.lineTo(8, 0); ctx.stroke();
          ctx.beginPath(); ctx.moveTo(8, -6); ctx.lineTo(18, 0); ctx.lineTo(8, 6); ctx.closePath(); ctx.fill();
          ctx.restore();
        };
        drawArrow(resX + 96, Ty, charging, FL4_BLUE);
        drawArrow(resX + 96, By, charging, FL4_RED);
        ctx.textAlign = "center"; ctx.font = "600 13px 'IBM Plex Mono', monospace";
        ctx.fillStyle = FL4_BLUE; ctx.fillText(K ? "+ charge" : "+ charge", resX + 96, Ty - 14);
        ctx.fillStyle = FL4_RED;  ctx.fillText("electrons", resX + 96, By + 24);
      } else {
        // flow stopped: the carriers above are frozen in place — say so.
        ctx.fillStyle = C.faint; ctx.font = "600 13.5px 'IBM Plex Mono', monospace"; ctx.textAlign = "center";
        ctx.fillText(K ? "stopped" : "no current", resX + 96, Ty - 14);
      }

      // ── the capacitor: charge is HELD on the plates ──────────────────────
      // Top plate connects to + : it loses electrons → goes positive (blue +).
      // Bottom plate connects to − : electrons pile onto it → goes negative (red −).
      // The amount held grows with vCap; the gap between them stores the field.
      const plateW = 78, px0 = plateX - plateW / 2, px1 = plateX + plateW / 2;
      // stored field in the gap (faint lines, brighter as it fills)
      if (fillFrac > 0.04) {
        ctx.save(); ctx.globalAlpha = 0.18 + 0.5 * fillFrac; ctx.strokeStyle = FL4_BLUE; ctx.lineWidth = 1.3; ctx.setLineDash([3, 5]);
        for (let i = 0; i < 6; i++) { const fx = px0 + 9 + (i / 5) * (plateW - 18); ctx.beginPath(); ctx.moveTo(fx, topPlateY + 3); ctx.lineTo(fx, botPlateY - 3); ctx.stroke(); }
        ctx.restore();
      }
      // the two plates
      ctx.strokeStyle = C.ink; ctx.lineWidth = 5; ctx.lineCap = "round";
      ctx.beginPath(); ctx.moveTo(px0, topPlateY); ctx.lineTo(px1, topPlateY); ctx.stroke();
      ctx.beginPath(); ctx.moveTo(px0, botPlateY); ctx.lineTo(px1, botPlateY); ctx.stroke();
      // accumulated charge: two rows packed onto each inner face, count ∝ fill
      const cols = 8, rows = 2, maxDots = cols * rows, nDots = Math.round(maxDots * fillFrac);
      for (let i = 0; i < nDots; i++) {
        const col = i % cols, row = Math.floor(i / cols);
        const fx = px0 + 10 + (col / (cols - 1)) * (plateW - 20);
        ctx.beginPath(); ctx.arc(fx, topPlateY - 8 - row * 9, 3.7, 0, 7); ctx.fillStyle = FL4_BLUE; ctx.fill();   // + on top plate
        ctx.beginPath(); ctx.arc(fx, botPlateY + 8 + row * 9, 3.7, 0, 7); ctx.fillStyle = FL4_RED; ctx.fill();    // electrons on bottom plate
      }
      // signs + "held here" label
      ctx.fillStyle = FL4_BLUE; ctx.font = "600 19px 'IBM Plex Mono', monospace"; ctx.textAlign = "left";
      ctx.fillText("+", px1 + 12, topPlateY + 5);
      ctx.fillStyle = FL4_RED; ctx.fillText("\u2013", px1 + 12, botPlateY + 6);
      ctx.fillStyle = C.faint; ctx.font = "600 13.5px 'IBM Plex Mono', monospace"; ctx.textAlign = "center";
      ctx.fillText(K ? "charge is held HERE" : "held on the plates", plateX, botPlateY + 34);

      // centre readout: fill % + what the flow is doing
      const mx = (Lx + Tx) / 2 - 40, my = (Ty + By) / 2;
      ctx.textAlign = "center";
      ctx.fillStyle = C.faint; ctx.font = "600 13.5px 'IBM Plex Mono', monospace";
      ctx.fillText(K ? "HOW FULL" : "CAPACITOR CHARGE", mx, my - 24);
      ctx.fillStyle = fl4Spectrum(0.5 + 0.5 * fillFrac);
      ctx.font = "600 38px 'Newsreader', serif"; ctx.fillText(Math.round(fillFrac * 100) + "%", mx, my + 16);
      ctx.fillStyle = C.faint; ctx.font = "500 13.5px 'IBM Plex Mono', monospace";
      const status = M === "hold" ? "holding \u00b7 charge sits still" : !moving ? (charging ? "full \u00b7 charge held, no flow" : "empty \u00b7 flow has stopped") : charging ? "filling \u2014 flow tapering" : "draining \u2014 through the resistor";
      ctx.fillText(status, mx, my + 36);

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

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

Object.assign(window, { LoopScene4 });
