/* charge-flow.jsx — "Watch the charges" overlay for the breadboard builder.
   Self-contained: it traces the conducting loop(s) through whatever parts are
   placed (its OWN union-find + DFS, so the verified sims are untouched), then
   animates spectrum charges (blue=full → amber → red=spent) along the real
   traces you laid. Handles every sandbox mode:
     • parallel branches  → every simple +→− path is enumerated & animated
     • LED / diode         → directed (anode→cathode only); backwards ⇒ no flow
     • switch              → open removes its edge (loop breaks or reroutes)
     • transistor          → collector→emitter loop + a thin base trickle (when on)
     • capacitor (RC)      → passes through; flow gated by the sim verdict
   Flow is GATED on the real sim result (state "lit" / I>0), so the overlay can
   never disagree with grading — if the sim says no current, charges freeze grey.
   Renders an SVG <g> in board coordinates. Uses window.bbHolePos / bbBaseNet.
   Names cf-/ChargeFlow prefixed. */

const CF_NS = "http://www.w3.org/2000/svg";
const CF_BLUE = "#2f6db0", CF_AMBER = "#e0a32e", CF_RED = "#c0392b";
function cfHexRgb(h) { h = h.replace("#", ""); return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)]; }
function cfMix(a, b, t) { const A = cfHexRgb(a), B = cfHexRgb(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 cfSpectrum(e) { e = Math.max(0, Math.min(1, e)); return e >= 0.5 ? cfMix(CF_AMBER, CF_BLUE, (e - 0.5) / 0.5) : cfMix(CF_RED, CF_AMBER, e / 0.5); }
// explicit "no current" states — if the sim reports one of these, freeze grey
// (a destroyed LED / hard fault). Every other state with a valid traced loop flows.
const OFF_STATES = { "pop": 1, "cooked": 1, "short": 1, "backwards": 1, "open": 1, "no-batt": 1, "switch-open": 1 };

function cfUF() {
  const p = {};
  const find = (x) => { while (p[x] !== x) { p[x] = p[p[x]]; x = p[x]; } return x; };
  const ens = (x) => { if (p[x] === undefined) p[x] = x; return x; };
  return { find: (x) => { ens(x); return find(x); }, union: (a, b) => { ens(a); ens(b); p[find(a)] = find(b); } };
}

/* Route from hole Hfrom to hole Hto THROUGH the jumpers that connect their
   nets, so the drawn path follows the wires instead of cutting straight across.
   BFS over baseNets (nodes) linked by wire parts (edges); returns the ordered
   list of jumper-endpoint holes to visit between Hfrom and Hto (exclusive). */
function cfRouteHoles(Hfrom, Hto, parts, baseNet) {
  const a = baseNet(Hfrom), b = baseNet(Hto);
  if (a === b) return [];                         // same column/rail → straight line is fine
  const adj = {};
  for (const p of parts) if (p.type === "wire") {
    const na = baseNet(p.a), nb = baseNet(p.b);
    (adj[na] = adj[na] || []).push({ to: nb, fromH: p.a, toH: p.b });
    (adj[nb] = adj[nb] || []).push({ to: na, fromH: p.b, toH: p.a });
  }
  const prev = {}, q = [a], seen = new Set([a]);
  while (q.length) { const cur = q.shift(); if (cur === b) break; for (const e of (adj[cur] || [])) if (!seen.has(e.to)) { seen.add(e.to); prev[e.to] = { from: cur, edge: e }; q.push(e.to); } }
  if (a !== b && !(b in prev)) return [];         // no jumper path (board-implicit only)
  const chain = []; let n = b;
  while (n !== a) { const pr = prev[n]; chain.unshift(pr.edge); n = pr.from; }
  const holes = [];
  for (const e of chain) { holes.push(e.fromH); holes.push(e.toH); }   // enter wire, cross to next net
  return holes;
}

/* build one renderable closed loop: battery+ → hops… → battery− → (body) → + */
function cfBuildLoop(battery, hops, holePos, parts, baseNet) {
  const pts = [];
  const push = (h) => { const q = holePos(h); if (!pts.length || pts[pts.length - 1].x !== q.x || pts[pts.length - 1].y !== q.y) pts.push({ x: q.x, y: q.y }); };
  const route = (from, to) => { for (const h of cfRouteHoles(from, to, parts, baseNet)) push(h); };
  const drops = [];
  push(battery.a);
  let last = battery.a;
  for (const h of hops) { route(last, h.entryH); push(h.entryH); const i0 = pts.length - 1; if (h.via) pts.push({ x: h.via.x, y: h.via.y }); push(h.exitH); drops.push({ i0, type: h.type }); last = h.exitH; }
  route(last, battery.b); push(battery.b);
  const battStart = pts.length - 1;
  push(battery.a);
  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 }; };
  const dropEls = drops.filter(d => d.type === "resistor" || d.type === "led").map(d => (cum[d.i0] + cum[d.i0 + 1]) / 2);
  const each = dropEls.length ? 1 / dropEls.length : 0;
  const battS0 = cum[battStart], battS1 = total;
  const energy = (s) => {
    if (s >= battS0) return (s - battS0) / ((battS1 - battS0) || 1);
    let e = 1, half = 11;
    for (const ds of dropEls) { if (s > ds + half) e -= each; else if (s > ds - half) e -= each * ((s - (ds - half)) / (2 * half)); }
    return Math.max(0, e);
  };
  const isBase = hops.some(h => h.type === "transistor-base");
  const isCap = hops.some(h => h.type === "capacitor");
  return { pts, cum, total, at, energy, isBase, isCap };
}

/* Enumerate every simple conducting loop from + to −. Returns { loops } or null. */
function cfTrace(parts, switchStates, result) {
  const holePos = window.bbHolePos, baseNet = window.bbBaseNet;
  if (!holePos || !baseNet) return null;
  const battery = parts.find(p => p.type === "battery");
  if (!battery) return null;

  const uf = cfUF();
  for (const p of parts) if (p.type === "wire") uf.union(baseNet(p.a), baseNet(p.b));
  const net = (h) => uf.find(baseNet(h));
  const SRC = net(battery.a), SNK = net(battery.b);
  if (SRC === SNK) return null;                         // dead short — don't animate

  const txOn = !!(result && result.baseDriven && result.state === "lit");

  // directed/undirected device edges
  const edges = [];
  parts.forEach((p, i) => {
    if (p === battery || p.type === "wire") return;
    if (p.type === "switch") { if (switchStates[i]) edges.push({ na: net(p.a), nb: net(p.b), aH: p.a, bH: p.b, type: "switch", idx: i, both: true }); return; }
    if (p.type === "resistor" || p.type === "capacitor") { edges.push({ na: net(p.a), nb: net(p.b), aH: p.a, bH: p.b, type: p.type, idx: i, both: true }); return; }
    if (p.type === "led" || p.type === "diode") { edges.push({ na: net(p.a), nb: net(p.b), aH: p.a, bH: p.b, type: p.type, idx: i, both: false }); return; }   // a(anode)→b only
    if (p.type === "transistor" && txOn) {
      const A = holePos(p.a), B = holePos(p.b), Cc = holePos(p.c);
      const via = { x: (A.x + B.x + Cc.x) / 3, y: (A.y + B.y + Cc.y) / 3 };   // body centroid
      edges.push({ na: net(p.b), nb: net(p.c), aH: p.b, bH: p.c, type: "transistor", idx: i, both: false, via });          // collector→emitter
      edges.push({ na: net(p.a), nb: net(p.c), aH: p.a, bH: p.c, type: "transistor-base", idx: i + 1000, both: false, via }); // base→emitter trickle
    }
  });

  const loops = [], MAXP = 6;
  const dfs = (node, visited, hops) => {
    if (loops.length >= MAXP) return;
    if (node === SNK) { loops.push(hops.slice()); return; }
    for (const e of edges) {
      let exitN, entryH, exitH;
      if (e.na === node) { exitN = e.nb; entryH = e.aH; exitH = e.bH; }
      else if (e.nb === node && e.both) { exitN = e.na; entryH = e.bH; exitH = e.aH; }
      else continue;
      if (visited.has(exitN)) continue;
      if (hops.some(h => h.idx === e.idx)) continue;
      visited.add(exitN); hops.push({ type: e.type, entryH, exitH, idx: e.idx, via: e.via });
      dfs(exitN, visited, hops);
      hops.pop(); visited.delete(exitN);
    }
  };
  dfs(SRC, new Set([SRC]), []);
  if (!loops.length) return null;

  return { loops: loops.map(h => cfBuildLoop(battery, h, holePos, parts, baseNet)), hasFlow: true };
}

function ChargeFlow({ parts, switchStates, live, result, kids }) {
  const gRef = React.useRef(null);
  const traceRef = React.useRef(null);
  const resultRef = React.useRef(result);
  resultRef.current = result;

  const trace = React.useMemo(
    () => (live ? cfTrace(parts || [], switchStates || {}, result) : null),
    [live, parts, switchStates, result]
  );
  traceRef.current = trace;

  // (re)build the circle pools whenever the loop structure changes
  React.useEffect(() => {
    const g = gRef.current; if (!g) return;
    while (g.firstChild) g.removeChild(g.firstChild);
    const pools = [];
    const loops = trace ? trace.loops : [];
    loops.forEach(lp => {
      const per = lp.isBase ? 9 : 16, arr = [];
      for (let k = 0; k < per; k++) { const c = document.createElementNS(CF_NS, "circle"); c.setAttribute("r", lp.isBase ? "3.3" : "5"); c.setAttribute("stroke", "rgba(0,0,0,0.22)"); c.setAttribute("stroke-width", "0.6"); g.appendChild(c); arr.push(c); }
      pools.push(arr);
    });
    g.__pools = pools;
  }, [trace]);

  React.useEffect(() => {
    let raf, t = 0;
    const phases = [];                                   // per-loop accumulated phase (monotonic)
    function frame() {
      const g = gRef.current, tr = traceRef.current, res = resultRef.current;
      if (g && g.__pools && tr) {
        const flowing = !!tr && !(res && OFF_STATES[res.state]);
        // current drives flow SPEED. Single-loop sims expose res.I; the multi-LED
        // engine (Lights bench) only has per-branch leds[], so sum those — without
        // this, dragging the pot wouldn't visibly speed up / slow the charges.
        let I = (res && res.I) || 0;
        if (!I && res && res.leds) I = Object.values(res.leds).reduce((a, L) => a + (L && L.on ? (L.I || 0) : 0), 0);
        if (!I) I = 8;
        const baseStep = flowing ? (0.4 + Math.min(2.6, I / 14)) : 0;
        t += 0.045;
        const pulse = 0.5 + 0.5 * Math.sin(t);            // 0..1 breathing wave
        for (let li = 0; li < tr.loops.length; li++) {
          const lp = tr.loops[li], pool = g.__pools[li]; if (!pool) continue;
          if (phases[li] == null) phases[li] = 0;
          // speed factor stays >= 0 so motion never reverses; the cap branch
          // gently surges (0.5x..1.6x) and eases instead of flinging back.
          const speedScale = lp.isBase ? 0.7 : (lp.isCap ? (0.5 + 1.1 * pulse) : 1);
          phases[li] += baseStep * speedScale;            // accumulate, never multiply the running total
          for (let k = 0; k < pool.length; k++) {
            const s = (phases[li] + (k / pool.length) * lp.total) % lp.total;
            const p = lp.at(s), e = lp.energy(s), c = pool[k];
            c.setAttribute("cx", p.x.toFixed(1)); c.setAttribute("cy", p.y.toFixed(1));
            c.setAttribute("fill", flowing ? cfSpectrum(e) : "#b9b1a1");
            if (lp.isCap) c.setAttribute("r", (5 + 2.2 * pulse).toFixed(1));   // gentle breathing, not wild
          }
        }
      }
      raf = requestAnimationFrame(frame);
    }
    raf = requestAnimationFrame(frame);
    return () => cancelAnimationFrame(raf);
  }, []);

  const flowing = !(result && OFF_STATES[result.state]) && !!trace;
  return (
    <g className="cf-layer" style={{ pointerEvents: "none", transition: "opacity .25s", opacity: trace ? 1 : 0 }}>
      {trace && trace.loops.map((lp, i) => (
        <polyline key={i} points={lp.pts.map(p => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(" ")}
                  fill="none" stroke={lp.isCap ? "var(--water)" : (flowing ? "var(--current)" : "var(--ink-faint)")}
                  strokeOpacity={lp.isBase ? 0.1 : (lp.isCap ? 0.22 : 0.16)} strokeWidth={lp.isBase ? 5 : 9}
                  strokeLinejoin="round" strokeLinecap="round" />
      ))}
      <g ref={gRef}></g>
    </g>
  );
}

Object.assign(window, { ChargeFlow, cfTrace });
