/* cl-flow.jsx — shared pieces for the Circuit Lab: a CALM charge-flow animator
   and a few schematic/breadboard symbol helpers.

   The animation rule is deliberately simple so it reads as orderly, not chaotic:
     • dots are EVENLY spaced along the path (one every `gap` px)
     • every dot on a path moves at the SAME speed
     • speed is strictly PROPORTIONAL to the branch current (px/s = |I| · SCALE)
     • fixed dot size, single colour — no pulsing, no per-dot recolour
   Two views (schematic + breadboard) each run their own ClFlow with the same
   current value, so the charges move at the same speed in lockstep.

   Names cl*. Exports: ClFlow, clPath, clResistor, clArrow, fmtI, fmtOhm, fmtV. */

const CL_NS = "http://www.w3.org/2000/svg";
const CL_SCALE = 18000;   // amps → px/sec base (11 mA ≈ 200 px/s → ~5 s per loop)
const CL_DOT = "#f5ff00"; // charges are ALWAYS this NEON yellow, any direction
let clUid = 0;            // unique ids for per-segment gradients

/* voltage → pathway colour: full +V = bright green, 0 = neutral grey, −V = bright
   red. This colours the WIRE/part the charges sit on (direction of drive). */
const CL_FWD = "#39ff14";    // full +V — NEON green
const CL_REV = "#ff0a3c";    // full −V — NEON red
const CL_STILL = "#9aa0a6";  // 0 V / no current — neutral grey
function clDirColor(I, eps = 0.0003) {
  if (!(Math.abs(I) > eps)) return CL_STILL;
  return I > 0 ? CL_FWD : CL_REV;
}

/* polyline helper: cumulative length + point-at-distance */
function clPath(pts) {
  const cum = [0];
  for (let i = 1; i < pts.length; i++) cum.push(cum[i - 1] + Math.hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y));
  const total = cum[pts.length - 1] || 1;
  const at = (s) => {
    s = ((s % total) + total) % total;
    let i = 1; while (i < pts.length && 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;
    return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f };
  };
  return { pts, cum, total, at };
}

/* formatting */
function fmtI(amps) {
  const mA = amps * 1000;
  if (Math.abs(mA) >= 1000) return (mA / 1000).toFixed(2) + " A";
  if (Math.abs(mA) >= 10) return mA.toFixed(0) + " mA";
  return mA.toFixed(1) + " mA";
}
function fmtOhm(r) {
  if (r >= 1000) return (r / 1000) % 1 === 0 ? (r / 1000) + " kΩ" : (r / 1000).toFixed(1) + " kΩ";
  return Math.round(r) + " Ω";
}
function fmtV(v) { return v.toFixed(v < 10 ? 2 : 1) + " V"; }
function fmtP(watts) {
  const mW = watts * 1000;
  if (Math.abs(mW) >= 1000) return (mW / 1000).toFixed(2) + " W";
  return mW.toFixed(1) + " mW";
}

/* voltage → colour ramp: high +V = bright green, 0 = neutral grey, −V = red.
   Magnitude scaled to vref (battery size), so charges fade as voltage drops
   across each resistor — a live picture of the potential around the loop. */
function clLerp(a, b, t) { return Math.round(a + (b - a) * t); }
function clVoltColor(v, vref) {
  const t = Math.max(-1, Math.min(1, v / (Math.abs(vref) || 1)));
  const grey = [154, 160, 166], green = [57, 255, 20], red = [255, 10, 60];
  const tgt = t >= 0 ? green : red, a = Math.abs(t);
  return `rgb(${clLerp(grey[0], tgt[0], a)},${clLerp(grey[1], tgt[1], a)},${clLerp(grey[2], tgt[2], a)})`;
}

/* CALM evenly-spaced charge animator. Dots are bright yellow (CL_DOT) moving
   forward; on pages that opt in via window.CL_DIRCOLOR they turn red (CL_REV)
   when the current is NEGATIVE — so reversed / AC flow reads as "going backwards"
   even when the node voltage (the pathway colour) stays positive. */
function ClFlow({ path, current, getCurrent, gap = 30, color = CL_DOT, r = 4, speedMul = 1 }) {
  const gRef = React.useRef(null);
  const phaseRef = React.useRef(0);
  const lastRef = React.useRef(performance.now());
  const curRef = React.useRef(current); curRef.current = current;
  const mulRef = React.useRef(speedMul); mulRef.current = speedMul;

  const n = Math.max(2, Math.round(path.total / gap));

  // build the fixed dot pool
  React.useEffect(() => {
    const g = gRef.current; if (!g) return;
    while (g.firstChild) g.removeChild(g.firstChild);
    for (let k = 0; k < n; k++) {
      const c = document.createElementNS(CL_NS, "circle");
      c.setAttribute("r", r);
      c.setAttribute("stroke", "rgba(0,0,0,0.25)");
      c.setAttribute("stroke-width", "0.5");
      g.appendChild(c);
    }
  }, [n, r]);

  React.useEffect(() => {
    let raf;
    const frame = (now) => {
      const g = gRef.current;
      const dt = Math.min(0.05, (now - lastRef.current) / 1000);
      lastRef.current = now;
      const I = getCurrent ? getCurrent() : curRef.current;
      const dir = I >= 0 ? 1 : -1;
      const speed = Math.abs(I) * CL_SCALE * mulRef.current;   // px/sec, proportional × view multiplier
      phaseRef.current += dir * speed * dt;
      const moving = speed > 0.4;
      if (g) {
        const kids = g.childNodes, len = path.total, step = len / kids.length;
        for (let k = 0; k < kids.length; k++) {
          const s = (phaseRef.current + k * step) % len;
          const p = path.at(s);
          kids[k].setAttribute("cx", p.x.toFixed(1));
          kids[k].setAttribute("cy", p.y.toFixed(1));
          kids[k].setAttribute("opacity", moving ? "1" : "0");   // hidden where no current flows
          kids[k].setAttribute("fill", CL_DOT);                  // charges always yellow (direction shows on the PATH)
        }
      }
      raf = requestAnimationFrame(frame);
    };
    raf = requestAnimationFrame(frame);
    return () => cancelAnimationFrame(raf);
  }, [path]);

  return <g ref={gRef}></g>;
}

/* a static direction arrowhead at point p, pointing along unit vector (ux,uy) */
function clArrow(p, ux, uy, key, color = "var(--ink-faint)") {
  const a = 6, b = 3.4;
  const tipx = p.x + ux * a, tipy = p.y + uy * a;
  const lx = p.x - uy * b, ly = p.y + ux * b;
  const rx = p.x + uy * b, ry = p.y - ux * b;
  return <polygon key={key} points={`${tipx},${tipy} ${lx},${ly} ${rx},${ry}`} fill={color} />;
}

/* horizontal/vertical resistor zig between two endpoints — FIXED zig size with
   straight leads, so the resistor keeps its shape no matter how far it's spanned. */
function clResistor(x0, y0, x1, y1, zigLen = 60, w = 8) {
  const dx = x1 - x0, dy = y1 - y0, len = Math.hypot(dx, dy) || 1;
  const ux = dx / len, uy = dy / len;        // along
  const px = -uy, py = ux;                    // perpendicular
  const zl = Math.min(zigLen, Math.max(20, len - 12));
  const s0 = (len - zl) / 2, s1 = s0 + zl;    // zig start/end distances
  const P = (s) => ({ x: x0 + ux * s, y: y0 + uy * s });
  const a = P(s0), b = P(s1);
  let d = `M ${x0} ${y0} L ${a.x.toFixed(1)} ${a.y.toFixed(1)}`;
  const zigs = 6;
  for (let i = 0; i < zigs; i++) {
    const t = (i + 0.5) / zigs, side = i % 2 ? -1 : 1;
    const cx = a.x + (b.x - a.x) * t + px * w * side, cy = a.y + (b.y - a.y) * t + py * w * side;
    d += ` L ${cx.toFixed(1)} ${cy.toFixed(1)}`;
  }
  d += ` L ${b.x.toFixed(1)} ${b.y.toFixed(1)} L ${x1} ${y1}`;
  return d;
}

/* a small "NN mA" current tag at (x,y), optionally with a leading arrow */
function clCurrentTag(x, y, amps, key, anchor = "middle") {
  return (
    <text key={key} x={x} y={y} textAnchor={anchor} fontFamily="IBM Plex Mono, monospace"
          fontSize="14" fill="var(--current)" fontWeight="600">{window.fmtI(amps)}</text>
  );
}

/* a wire/pathway coloured BY VOLTAGE (green +V, grey 0, red −V), with yellow
   charges riding it. Pass vStart/vEnd (node potentials) for the colour; a single
   equipotential wire has vStart==vEnd (solid), a part spanning two nodes ramps.
   stroke overrides (e.g. "none" = dots only, or an explicit colour like black). */
function ClWire({ pts, current = 0, getCurrent, flowMul = 1, gap = 28, width = 3, dots = true, stroke = null, vStart = null, vEnd = null, vref = 9 }) {
  const path = clPath(pts);
  const a = pts[0], b = pts[pts.length - 1];
  // Lock onto this wire's ESTABLISHED forward direction (the first steady sign it
  // settles into). A DC return path drawn "backwards" is still forward for itself
  // → never red. Only a current that later crosses to the OPPOSITE sign (a genuine
  // reversal — a reversed battery, or the back half of an LC/RLC oscillation) reads
  // as reversed and turns the pathway red.
  const fwdRef = React.useRef(0);
  if (fwdRef.current === 0 && Math.abs(current) > 0.001) fwdRef.current = current > 0 ? 1 : -1;
  const reversed = fwdRef.current !== 0 && current * fwdRef.current < 0 && Math.abs(current) > 0.0008;
  let strokeVal = null, grad = null;
  if (stroke === "none") { strokeVal = null; }
  else if (stroke) { strokeVal = stroke; }
  else if (vStart != null) {
    const c0 = clVoltColor(vStart, vref), c1 = clVoltColor(vEnd, vref);
    if (c0 === c1) { strokeVal = c0; }
    else {
      const id = "clg" + (clUid++);
      grad = (
        <defs><linearGradient id={id} gradientUnits="userSpaceOnUse" x1={a.x} y1={a.y} x2={b.x} y2={b.y}>
          <stop offset="0%" stopColor={c0} /><stop offset="100%" stopColor={c1} />
        </linearGradient></defs>
      );
      strokeVal = `url(#${id})`;
    }
  } else { strokeVal = "var(--ink-soft)"; }
  // Direction cue on the PATH (sandbox opt-in): when the current is meaningfully
  // NEGATIVE (reversed — e.g. the back half of an LC/RLC oscillation), tint the
  // pathway red instead of its voltage colour. Charges themselves stay yellow.
  if (window.CL_DIRCOLOR && strokeVal && reversed) { strokeVal = CL_REV; grad = null; }
  return (
    <g>
      {grad}
      {strokeVal && <polyline points={pts.map(p => `${p.x},${p.y}`).join(" ")} fill="none"
                stroke={strokeVal} strokeWidth={width} strokeLinejoin="round" strokeLinecap="round" />}
      {dots && <ClFlow path={path} current={current} getCurrent={getCurrent} gap={gap} speedMul={flowMul} />}
    </g>
  );
}

/* a resistor zig coloured by voltage (gradient vStart→vEnd along its axis).
   Use in the SCHEMATIC so the part shows its voltage drop. */
function ClResistorV({ x0, y0, x1, y1, vStart, vEnd, vref = 9, width = 3 }) {
  const c0 = clVoltColor(vStart, vref), c1 = clVoltColor(vEnd, vref);
  if (c0 === c1) {
    return <path d={clResistor(x0, y0, x1, y1)} fill="none" stroke={c0} strokeWidth={width} strokeLinejoin="round" />;
  }
  const id = "clr" + (clUid++);
  return (
    <g>
      <defs><linearGradient id={id} gradientUnits="userSpaceOnUse" x1={x0} y1={y0} x2={x1} y2={y1}>
        <stop offset="0%" stopColor={c0} /><stop offset="100%" stopColor={c1} />
      </linearGradient></defs>
      <path d={clResistor(x0, y0, x1, y1)} fill="none" stroke={`url(#${id})`} strokeWidth={width} strokeLinejoin="round" />
    </g>
  );
}

/* schematic battery symbol on a vertical edge at x, between topY and botY.
   Leads carry voltage-coloured charges (0 at bottom, V at top). */
function clSchemBattery(x, topY, botY, V, Isig, flowMul, vref) {
  const my = (topY + botY) / 2 + 7;
  return (
    <g key="schembat">
      <window.ClWire pts={[{ x, y: botY }, { x, y: my + 18 }]} current={Isig} flowMul={flowMul} gap={30} width={2.8} vStart={0} vEnd={0} vref={vref} />
      <window.ClWire pts={[{ x, y: my - 18 }, { x, y: topY }]} current={Isig} flowMul={flowMul} gap={30} width={2.8} vStart={V} vEnd={V} vref={vref} />
      <line x1={x - 17} y1={my - 18} x2={x + 17} y2={my - 18} stroke="var(--ink)" strokeWidth="3.4" />
      <line x1={x - 9} y1={my - 7} x2={x + 9} y2={my - 7} stroke="var(--ink)" strokeWidth="2.2" />
      <line x1={x - 17} y1={my + 4} x2={x + 17} y2={my + 4} stroke="var(--ink)" strokeWidth="3.4" />
      <line x1={x - 9} y1={my + 15} x2={x + 9} y2={my + 15} stroke="var(--ink)" strokeWidth="2.2" />
      <text x={x - 26} y={my - 12} textAnchor="end" fontFamily="IBM Plex Mono, monospace" fontSize="15" fill="var(--current)">+</text>
      <text x={x - 26} y={my + 24} textAnchor="end" fontFamily="IBM Plex Mono, monospace" fontSize="15" fill="var(--ink-soft)">−</text>
      <text x={x - 40} y={my + 6} textAnchor="end" fontFamily="IBM Plex Mono, monospace" fontSize="16" fill="var(--ink)" fontWeight="600">{window.fmtV(V)}</text>
    </g>
  );
}

/* sample a quadratic arc P0→(ctrl)→P1 into points (for a curved jumper wire) */
function clArc(p0, ctrl, p1, n = 12) {
  const pts = [];
  for (let i = 0; i <= n; i++) {
    const t = i / n, u = 1 - t;
    pts.push({ x: u * u * p0.x + 2 * u * t * ctrl.x + t * t * p1.x, y: u * u * p0.y + 2 * u * t * ctrl.y + t * t * p1.y });
  }
  return pts;
}

/* a fixed-shape banded resistor body centred on the span yTop..yBot, with plain
   leads filling the rest. Body never stretches — only the leads grow. */
function clResBody(x, yTop, yBot, key, bodyH = 58) {
  const bh = Math.min(bodyH, Math.max(20, (yBot - yTop) - 8));
  const by = (yTop + yBot) / 2 - bh / 2;
  const bands = ["#8a5a2b", "#111", "#b8731f", "#caa64a"];
  return (
    <g key={key}>
      <line x1={x} y1={yTop} x2={x} y2={by} stroke="var(--ink-soft)" strokeWidth="2.4" />
      <line x1={x} y1={by + bh} x2={x} y2={yBot} stroke="var(--ink-soft)" strokeWidth="2.4" />
      <rect x={x - 12} y={by} width="24" height={bh} rx="7" fill="#d8c6a0" stroke="#b9a279" strokeWidth="1" />
      {bands.map((c, i) => <rect key={i} x={x - 12} y={by + 9 + i * 9} width="24" height="4.5" fill={c} />)}
    </g>
  );
}

/* a recognisable AA-style battery CELL (not the schematic symbol), FIXED size,
   centred on the span yTop..yBot at x — keeps its shape no matter the span. */
function clBatteryCell(x, railTopY, railBotY, V, key, cellH = 132) {
  const mid = (railTopY + railBotY) / 2;
  const bodyTop = mid - cellH / 2, bodyBot = mid + cellH / 2;
  const w = 17;
  return (
    <g key={key}>
      {/* leads to the rails */}
      <line x1={x} y1={railTopY} x2={x} y2={bodyTop - 7} stroke="var(--ink-soft)" strokeWidth="2.5" />
      <line x1={x} y1={bodyBot} x2={x} y2={railBotY} stroke="var(--ink-soft)" strokeWidth="2.5" />
      {/* positive nub */}
      <rect x={x - 6} y={bodyTop - 7} width="12" height="7" rx="2" fill="#7d6320" />
      {/* cell body */}
      <rect x={x - w} y={bodyTop} width={2 * w} height={bodyBot - bodyTop} rx="6" fill="#d8a83a" stroke="#9c7820" strokeWidth="1.5" />
      {/* wrapper band + shine */}
      <rect x={x - w} y={bodyTop + 12} width={2 * w} height="12" fill="#b6862a" opacity="0.85" />
      <rect x={x - w + 4} y={bodyTop + 4} width="4" height={bodyBot - bodyTop - 8} rx="2" fill="#f0d27a" opacity="0.7" />
      {/* terminals */}
      <text x={x} y={bodyTop + 10} textAnchor="middle" fontFamily="IBM Plex Mono, monospace" fontSize="13" fill="#3a2d08" fontWeight="700">+</text>
      <text x={x} y={bodyBot - 4} textAnchor="middle" fontFamily="IBM Plex Mono, monospace" fontSize="13" fill="#3a2d08" fontWeight="700">−</text>
      {/* value, rotated up the cell */}
      <text x={x} y={(bodyTop + bodyBot) / 2} textAnchor="middle" fontFamily="IBM Plex Mono, monospace" fontSize="13" fill="#3a2d08" fontWeight="700" transform={`rotate(-90 ${x} ${(bodyTop + bodyBot) / 2})`}>{window.fmtV(V)}</text>
    </g>
  );
}

Object.assign(window, { ClFlow, ClWire, ClResistorV, clPath, clArc, clResistor, clResBody, clBatteryCell, clSchemBattery, clArrow, clCurrentTag, clDirColor, clVoltColor, fmtI, fmtOhm, fmtV, fmtP, CL_DOT, CL_FWD, CL_REV, CL_STILL });
