/* osc-engine.jsx — a small TRANSIENT circuit simulator for the free-build
   oscillator bench. Unlike breadboard.jsx's static topology recognizers, this
   actually steps time: it solves the node voltages every dt with Modified Nodal
   Analysis (MNA), models capacitors with a backward-Euler companion, and drives
   an NE555 as a behavioral block (resistor divider + SR latch + two comparators
   + discharge switch). A correctly-wired astable therefore OSCILLATES on its
   own — and a miswired one does not, which is the whole pedagogical point.

   Pure JS (no React, no DOM) so it can be unit-tested in isolation. Works on an
   abstract NETLIST of integer node ids (node 0 = ground reference):

     netlist = {
       vsources: [{ pos, neg, V }],         // ideal voltage source (battery)
       resistors:[{ a, b, R }],             // ohms
       caps:     [{ a, b, C, id }],         // farads; id keys its stored voltage
       chips:    [{ id, pins:{ vcc,gnd,out,dis,thr,trig,rst,ctrl } }],  // 555
     }
     state = { capV:{id:V}, latch:{id:0|1}, t }   // persists across steps

   oscStep(netlist, state, dt) → { v:Float64Array, state, info } where v[n] is
   the solved voltage at node n (v[0]===0). Names osc- and OSC_ prefixed. */

const OSC_R_DRIVE = 20;      // 555 output drive resistance (Ω)
const OSC_R_DIS   = 15;      // discharge transistor on-resistance (Ω)
const OSC_R_DIV   = 5000;    // each leg of the internal 5k/5k/5k divider
const OSC_GMIN    = 1e-9;    // tiny leak to ground for numerical stability

/* ── dense linear solve Ax=b via Gaussian elimination w/ partial pivot ──── */
function oscSolve(A, b, n) {
  for (let col = 0; col < n; col++) {
    // pivot
    let piv = col, best = Math.abs(A[col][col]);
    for (let r = col + 1; r < n; r++) {
      const m = Math.abs(A[r][col]);
      if (m > best) { best = m; piv = r; }
    }
    if (best < 1e-18) continue;             // singular column — skip (left at 0)
    if (piv !== col) { const t = A[piv]; A[piv] = A[col]; A[col] = t; const tb = b[piv]; b[piv] = b[col]; b[col] = tb; }
    const d = A[col][col];
    for (let r = 0; r < n; r++) {
      if (r === col) continue;
      const f = A[r][col] / d;
      if (f === 0) continue;
      for (let c = col; c < n; c++) A[r][c] -= f * A[col][c];
      b[r] -= f * b[col];
    }
  }
  const x = new Float64Array(n);
  for (let i = 0; i < n; i++) x[i] = Math.abs(A[i][i]) < 1e-18 ? 0 : b[i] / A[i][i];
  return x;
}

/* ── count nodes ──────────────────────────────────────────────────────── */
function oscMaxNode(netlist) {
  let mx = 0;
  const bump = (n) => { if (n != null && n > mx) mx = n; };
  netlist.vsources.forEach(s => { bump(s.pos); bump(s.neg); });
  netlist.resistors.forEach(r => { bump(r.a); bump(r.b); });
  netlist.caps.forEach(c => { bump(c.a); bump(c.b); });
  netlist.chips.forEach(ch => Object.values(ch.pins).forEach(bump));
  return mx;
}

/* ── one timestep ─────────────────────────────────────────────────────── */
function oscStep(netlist, state, dt) {
  const maxNode = oscMaxNode(netlist);
  const nV = maxNode;                       // unknown node voltages: 1..maxNode
  const nSrc = netlist.vsources.length;
  const N = nV + nSrc;                       // total unknowns
  const A = [], b = new Float64Array(N);
  for (let i = 0; i < N; i++) { A.push(new Float64Array(N)); }

  // node index → matrix row (node k → row k-1; ground node 0 is the reference)
  const ri = (node) => node - 1;
  const stampG = (a, c, g) => {             // conductance g between nodes a,c
    if (a > 0) A[ri(a)][ri(a)] += g;
    if (c > 0) A[ri(c)][ri(c)] += g;
    if (a > 0 && c > 0) { A[ri(a)][ri(c)] -= g; A[ri(c)][ri(a)] -= g; }
  };
  const stampI = (a, c, cur) => {           // current source cur from a→c
    if (a > 0) b[ri(a)] -= cur;
    if (c > 0) b[ri(c)] += cur;
  };

  // tiny leak so every node is referenced (avoids singular float nodes)
  for (let n = 1; n <= nV; n++) A[ri(n)][ri(n)] += OSC_GMIN;

  // resistors
  for (const r of netlist.resistors) {
    const g = 1 / Math.max(1e-3, r.R);
    stampG(r.a, r.b, g);
  }

  // capacitors — backward-Euler companion: Geq in parallel with Ieq
  const capState = state.capV || {};
  for (const c of netlist.caps) {
    const Geq = c.C / dt;
    const vPrev = capState[c.id] || 0;       // voltage across cap last step
    stampG(c.a, c.b, Geq);
    stampI(c.a, c.b, -Geq * vPrev);          // companion source: injects +Geq·Vprev into node a
  }

  // 555 chips — behavioral, driven by the latch decided from PREVIOUS voltages
  const latchState = state.latch || {};
  const vPrev = state.v || new Float64Array(maxNode + 1);
  const Vof = (node) => (node == null ? 0 : (node === 0 ? 0 : (vPrev[node] || 0)));
  const chipPlan = [];
  for (const ch of netlist.chips) {
    const p = ch.pins;
    const vcc = Vof(p.vcc), gnd = Vof(p.gnd);
    const span = vcc - gnd;
    const thr = Vof(p.thr) - gnd;
    const trig = Vof(p.trig) - gnd;
    const rstV = p.rst == null ? span : (Vof(p.rst) - gnd);
    let q = latchState[ch.id] || 0;
    // reset pin dominant when pulled low
    if (rstV < 0.7) q = 0;
    else {
      if (thr > (2 / 3) * span) q = 0;        // threshold comparator → reset
      if (trig < (1 / 3) * span) q = 1;       // trigger comparator → set
    }
    chipPlan.push({ ch, q });

    // internal 5k/5k/5k divider vcc→(2/3)→ctrl(1/3 above gnd is trig ref)…
    // model two legs so CTRL node (pin5) reads ~2/3·VCC if the user probes it.
    if (p.ctrl != null) {
      stampG(p.vcc, p.ctrl, 1 / OSC_R_DIV);            // 5k from VCC to CTRL
      stampG(p.ctrl, p.gnd, 1 / (2 * OSC_R_DIV));      // 10k from CTRL to GND → 2/3
    }
    // OUTPUT: push-pull through OSC_R_DRIVE toward VCC (high) or GND (low)
    if (p.out != null) {
      if (q) stampG(p.out, p.vcc, 1 / OSC_R_DRIVE);
      else   stampG(p.out, p.gnd, 1 / OSC_R_DRIVE);
    }
    // DISCHARGE: closed to GND when output LOW, open when HIGH
    if (p.dis != null && !q) stampG(p.dis, p.gnd, 1 / OSC_R_DIS);
  }

  // voltage sources (battery) — MNA augmented rows
  netlist.vsources.forEach((s, k) => {
    const row = nV + k;
    if (s.pos > 0) { A[ri(s.pos)][row] += 1; A[row][ri(s.pos)] += 1; }
    if (s.neg > 0) { A[ri(s.neg)][row] -= 1; A[row][ri(s.neg)] -= 1; }
    b[row] += s.V;
  });

  const x = oscSolve(A, b, N);

  // unpack node voltages (node 0 = 0)
  const v = new Float64Array(maxNode + 1);
  for (let n = 1; n <= maxNode; n++) v[n] = x[ri(n)] || 0;

  // update capacitor voltages + latch for next step
  const capVNext = {};
  for (const c of netlist.caps) capVNext[c.id] = (c.a ? v[c.a] : 0) - (c.b ? v[c.b] : 0);
  const latchNext = {};
  for (const pl of chipPlan) latchNext[pl.ch.id] = pl.q;

  return {
    v,
    state: { capV: capVNext, latch: latchNext, v, t: (state.t || 0) + dt },
    info: { chips: chipPlan.map(pl => ({ id: pl.ch.id, out: pl.q })) },
  };
}

/* ── convenience: run N steps, return the final state (for warm-up) ─────── */
function oscRun(netlist, state, dt, steps) {
  let s = state;
  for (let i = 0; i < steps; i++) s = oscStep(netlist, s, dt).state;
  return s;
}

if (typeof window !== "undefined") Object.assign(window, { oscStep, oscRun, oscSolve, OSC_R_DRIVE, OSC_R_DIS });
