/* mna-sim.jsx — M2 of the free-build simulator.
   useMnaSim(parts, switchStates, opts) — owns the transient run loop that brings
   a placed board to life. Rebuilds the engine when topology/values change,
   advances it in wall-clock display-seconds on requestAnimationFrame, and exposes
   per-part current / voltage / LED brightness + node voltages for the UI.

   Requires mna-engine.jsx + mna-adapter.jsx loaded first. Hook name: useMnaSim.

   opts:
     active        run the loop? (default true) — pass an IntersectionObserver
                   flag to pause an offscreen board.
     dt            sim substep, display-seconds (default 1/240).
     maxStepsPerFrame  guard against spiral-of-death (default 120).
     fps           UI repaint throttle (default 30).
     Iref          LED current that maps to full brightness (default 0.018 A).

   returns {
     ready, reason, nodeV,
     current:{partIdx:A}, voltage:{partIdx:V}, brightness:{partIdx:0..1},
     netId(hole), probe(hole)->V, engine, link, reset()
   }
*/

const { useState: mnaUseState, useEffect: mnaUseEffect, useRef: mnaUseRef, useMemo: mnaUseMemo } = React;
const MNA_EMPTY = [];   // stable ref so `nodeV` doesn't change identity every render

function useMnaSim(parts, switchStates = {}, opts = {}) {
  const { active = true, dt = 1 / 240, maxStepsPerFrame = 120, fps = 30, Iref = 0.018 } = opts;

  const [, force] = mnaUseState(0);
  const engRef = mnaUseRef(null);
  const linkRef = mnaUseRef(null);
  const stRef = mnaUseRef({ v: null, lastWall: 0, acc: 0, lastPaint: 0 });

  // structural + value fingerprint; any change rebuilds the engine. MUST include
  // every value field the adapter reads — farads/henries are set directly by the
  // 555 bench's C knob (the cap is passed { C:0, farads }), so omitting them left
  // the C knob dead (engine never rebuilt → timing cap frozen at its first value).
  const topoKey = mnaUseMemo(
    () => JSON.stringify(parts.map(p => [p.type, p.a, p.b, p.c, p.R, p.C, p.farads, p.henries, p.volts, p.pos])) +
          "|" + JSON.stringify(switchStates),
    [parts, switchStates]
  );

  // (re)build on topology/value change, carrying capacitor charge across a
  // value-only rebuild (same element kinds in order → same node ids) so dragging
  // a pot or nudging an R doesn't reset a charging cap.
  mnaUseEffect(() => {
    const link = window.partsToNetlist(parts, switchStates);
    linkRef.current = link;
    if (!link.netlist) { engRef.current = null; stRef.current.v = null; force(n => (n + 1) & 0xffff); return; }
    const prevV = stRef.current.v;
    const prevEls = engRef.current ? engRef.current.els : null;
    const eng = window.mnaCreate(link.netlist);
    const shapeMatches = prevEls && prevV && prevEls.length === eng.els.length &&
                         prevEls.every((e, i) => e.k === eng.els[i].k);
    if (shapeMatches) {
      eng.els.forEach((e, i) => {
        if (e.k === "C") {
          const va = e.a === window.MNA_GND ? 0 : (prevV[e.a] || 0);
          const vb = e.b === window.MNA_GND ? 0 : (prevV[e.b] || 0);
          eng.setCapV(i, va - vb);
        }
      });
    } else {
      stRef.current.v = null;        // genuinely new circuit → fresh state
    }
    engRef.current = eng;
    stRef.current.acc = 0;
    force(n => (n + 1) & 0xffff);
  }, [topoKey]);

  // the run loop
  mnaUseEffect(() => {
    if (!active) return;
    const st = stRef.current;
    st.lastWall = performance.now();
    let raf;
    const tick = (now) => {
      const eng = engRef.current;
      if (eng) {
        let elapsed = (now - st.lastWall) / 1000;
        st.lastWall = now;
        elapsed = Math.min(elapsed, 0.05);     // cap after a tab-away so we don't spiral
        st.acc += elapsed;
        let steps = 0, v = st.v;
        while (st.acc >= dt && steps < maxStepsPerFrame) { v = eng.step(dt); st.acc -= dt; steps++; }
        if (!v) { v = eng.step(dt); }           // first paint: at least one DC solve
        st.v = v;
        if (now - st.lastPaint > 1000 / fps) { st.lastPaint = now; force(n => (n + 1) & 0xffff); }
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [active, topoKey, dt]);

  // derive outputs for this render
  const link = linkRef.current;
  const eng = engRef.current;
  const v = stRef.current.v;
  const out = {
    ready: !!eng,
    reason: link ? link.reason || null : null,
    nodeV: v || MNA_EMPTY,
    current: {}, voltage: {}, brightness: {},
    netId: link ? link.netId : () => -1,
    probe: (hole) => { if (!link || !v) return 0; const n = link.netId(hole); return n === window.MNA_GND ? 0 : (v[n] || 0); },
    engine: eng, link,
    reset: () => { if (engRef.current) engRef.current.reset(); stRef.current.v = null; stRef.current.acc = 0; force(n => (n + 1) & 0xffff); },
  };
  if (eng && v) {
    link.partOfEl.forEach((partIdx, elIdx) => {
      const e = eng.els[elIdx];
      out.current[partIdx] = eng.elementCurrent(elIdx, v);
      if (e.a != null && e.b != null) {
        const va = e.a === window.MNA_GND ? 0 : (v[e.a] || 0);
        const vb = e.b === window.MNA_GND ? 0 : (v[e.b] || 0);
        out.voltage[partIdx] = va - vb;
      }
      if (e.k === "LED") out.brightness[partIdx] = Math.max(0, Math.min(1, out.current[partIdx] / Iref));
    });
  }
  return out;
}

Object.assign(window, { useMnaSim });
