/* mna-adapter.jsx — M1 of the free-build simulator.
   Bridges placed breadboard parts → the netlist mna-engine.jsx wants.

   Reuses the SAME hole→net collapse the verdict sims use (column banks via
   baseNet, wires + closed switches via union-find), then maps each part to an
   MNA element. Pure functions, no React, exported on window (mna* prefixed).

   partsToNetlist(parts, switchStates) →
     {
       netlist:  { nNodes, elements } | null,   // null when unsimulatable
       partOfEl: number[],                       // elements[i] came from parts[partOfEl[i]]
       elOfPart: (partIndex) => elIndex | -1,
       netId:    (holeId) => nodeId,             // resolve any hole to its node id
       reason?:  "no-batt",
     }

   Node id 0 is always the battery − net (= MNA_GND). */

/* baseNet: a hole id → its base electrical net, BEFORE wires.
   Mirror of breadboard.jsx so the adapter is testable in isolation; prefers the
   exported copy when breadboard.jsx is present. */
function mnaBaseNet(id) {
  if (window.bbBaseNet) return window.bbBaseNet(id);
  if (id[0] === "P") return "RAILP";           // + rail
  if (id[0] === "N") return "RAILN";           // − rail
  if (id[0] === "T") return "T" + id[1];        // top bank, joined down a column
  if (id[0] === "B") return "B" + id[1];        // bottom bank
  return id;
}

/* tiny union-find (same shape as breadboard.jsx's makeUF) */
function mnaUF() {
  const p = {};
  const find = (x) => { while (p[x] !== undefined && p[x] !== x) { p[x] = p[p[x]] ?? p[x]; x = p[x]; } return x; };
  const ensure = (x) => { if (p[x] === undefined) p[x] = x; return x; };
  const union = (a, b) => { ensure(a); ensure(b); p[find(a)] = find(b); };
  return { find: (x) => { ensure(x); return find(x); }, union };
}

/* potentiometer resistance from knob pos (mirror of bbPotR) — pots keep p.R
   live, so this is only a fallback when p.R is absent. */
function mnaPotR(pos, p) {
  const Rmax = (p && p.Rmax) || 10000;
  const t = pos == null ? 0.5 : pos;
  if (p && p.taper === "lin") return Math.max(1, Math.round(t * Rmax));
  const B = 200;
  return Math.max(1, Math.round(Rmax * (Math.pow(B, t) - 1) / (B - 1)));
}

/* component defaults — tuned to the course, not datasheet-exact */
const MNA_DEFAULTS = {
  Vbatt: 9, Rsrc: 0.05,
  Rdefault: 330,
  ledVf: 1.9, ledRon: 18,
  diodeVf: 0.7, diodeRon: 8,
  /* Cap "units" → farads. The verdict RC sim uses τ_display = (R/330)·C_units
     seconds; the engine integrates τ = R·farads. Setting farads = C_units/330
     makes the live sim charge at the SAME watchable rate the course already
     teaches — one shared timescale, in display-seconds. */
  capUnitToFarad: 1 / 330,
  /* Inductor "units" → henries. Dual of the cap scale; with a unit value the
     RL time constant τ = L/R lands in watchable display-seconds. */
  indUnitToHenry: 100,
};

function partsToNetlist(parts, switchStates = {}) {
  const noop = { netlist: null, partOfEl: [], elOfPart: () => -1, netId: () => -1 };
  const battery = parts.find(p => p.type === "battery");
  if (!battery) return { ...noop, reason: "no-batt" };

  /* 1) collapse nets: wires and CLOSED switches act as ideal joins.
        (open switch = omitted entirely → its two nets stay separate) */
  const uf = mnaUF();
  parts.forEach((p, i) => {
    if (p.type === "wire") uf.union(mnaBaseNet(p.a), mnaBaseNet(p.b));
    else if (p.type === "switch" && switchStates[i]) uf.union(mnaBaseNet(p.a), mnaBaseNet(p.b));
  });
  const rootOf = (hole) => uf.find(mnaBaseNet(hole));

  /* 2) integer node ids; force battery − net → 0 (ground) */
  const idOf = new Map([[rootOf(battery.b), 0]]);
  let next = 1;
  const netId = (hole) => {
    const r = rootOf(hole);
    if (!idOf.has(r)) idOf.set(r, next++);
    return idOf.get(r);
  };
  netId(battery.a); // make sure + rail exists even on an otherwise bare board

  /* 3) parts → elements */
  const elements = [];
  const partOfEl = [];
  const elIndexOfPart = new Map();
  const push = (el, partIdx) => { elIndexOfPart.set(partIdx, elements.length); elements.push(el); partOfEl.push(partIdx); };

  parts.forEach((p, i) => {
    switch (p.type) {
      case "battery":
        push({ k: "V", a: netId(p.a), b: netId(p.b), volts: p.volts ?? MNA_DEFAULTS.Vbatt, r: MNA_DEFAULTS.Rsrc }, i);
        break;
      case "resistor": {
        const R = p.R != null ? p.R : (p.pot ? mnaPotR(p.pos, p) : MNA_DEFAULTS.Rdefault);
        push({ k: "R", a: netId(p.a), b: netId(p.b), ohms: R }, i);
        break;
      }
      case "led":
        push({ k: "LED", a: netId(p.a), b: netId(p.b), vf: p.vf ?? MNA_DEFAULTS.ledVf, ron: p.ron ?? MNA_DEFAULTS.ledRon }, i);
        break;
      case "diode":
        push({ k: "D", a: netId(p.a), b: netId(p.b), vf: p.vf ?? MNA_DEFAULTS.diodeVf, ron: p.ron ?? MNA_DEFAULTS.diodeRon }, i);
        break;
      case "capacitor":
        push({ k: "C", a: netId(p.a), b: netId(p.b), farads: p.farads != null ? p.farads : (p.C != null ? p.C : 2) * MNA_DEFAULTS.capUnitToFarad }, i);
        break;
      case "inductor":
        push({ k: "L", a: netId(p.a), b: netId(p.b), henries: (p.L != null ? p.L : 1) * MNA_DEFAULTS.indUnitToHenry }, i);
        break;
      case "transistor": // a=base, b=collector, c=emitter (matches simulateTransistor)
        push({ k: "NPN", b: netId(p.a), c: netId(p.b), e: netId(p.c) }, i);
        break;
      case "opamp": // a=in+, b=in−, c=out; rails default 0..9
        push({ k: "OPAMP", np: netId(p.a), nn: netId(p.b), out: netId(p.c),
               vpos: p.vpos != null ? p.vpos : MNA_DEFAULTS.Vbatt, vneg: p.vneg != null ? p.vneg : 0 }, i);
        break;
      case "ic555":
        if (p.pins) {
          const n = (h) => (h == null ? null : netId(h));
          push({ k: "IC555", vcc: n(p.pins.vcc), gnd: n(p.pins.gnd), out: n(p.pins.out),
                 dis: n(p.pins.dis), thr: n(p.pins.thr), trig: n(p.pins.trig),
                 rst: n(p.pins.rst), ctrl: n(p.pins.ctrl) }, i);
        }
        break;
      // switch (handled in step 1) · wire (handled in step 1) → no element
    }
  });

  return {
    netlist: { nNodes: next, elements },
    partOfEl,
    elOfPart: (partIdx) => (elIndexOfPart.has(partIdx) ? elIndexOfPart.get(partIdx) : -1),
    netId,
  };
}

Object.assign(window, { partsToNetlist, mnaPotR, mnaBaseNet, MNA_DEFAULTS });
