/* fl-model.jsx — free-placement circuit model + geometry-based node extraction
   for the Falstad-style sandbox. A part is:
     { id, type, ends:[{x,y},...], value?, volts? }
   types: "wire" | "resistor" | "battery" | "ground" | "led" | "cap"
   Electrical nodes are derived from GEOMETRY: endpoints sharing a grid point are
   the same node; a wire merges its two endpoints; every "ground" forces its node
   to 0. Multiple grounds & multiple sources fall out for free.

   flBuildNetlist(parts) → {
     netlist: { nNodes, elements },   // ready for window.mnaCreate
     nodeOf(point) -> nodeId,         // 0 = ground/reference
     partElem: Map(partId -> elementIndex),
   }
   Names fl*. */

function flKey(p) { return Math.round(p.x) + "," + Math.round(p.y); }

function flDSU() {
  const par = {};
  const add = (x) => { if (par[x] === undefined) par[x] = x; };
  const find = (x) => { add(x); while (par[x] !== x) { par[x] = par[par[x]]; x = par[x]; } return x; };
  const union = (a, b) => { par[find(a)] = find(b); };
  return { add, find, union };
}

function flBuildNetlist(parts) {
  const dsu = flDSU();
  parts.forEach(pt => pt.ends.forEach(e => dsu.add(flKey(e))));
  // wires (and CLOSED switches) merge their two endpoints
  parts.forEach(pt => {
    if (pt.type === "wire" || (pt.type === "switch" && pt.closed))
      dsu.union(flKey(pt.ends[0]), flKey(pt.ends[1]));
  });

  // ground class → reference node 0
  const grounds = parts.filter(pt => pt.type === "ground");
  let groundRoot = null;
  if (grounds.length) {
    const gk = grounds.map(g => flKey(g.ends[0]));
    gk.forEach(k => dsu.union(k, gk[0]));
    groundRoot = dsu.find(gk[0]);
  } else {
    const bat = parts.find(pt => pt.type === "battery");
    if (bat) groundRoot = dsu.find(flKey(bat.ends[1]));     // battery − becomes reference
    else if (parts.length) groundRoot = dsu.find(flKey(parts[0].ends[0]));
  }

  const idOf = new Map();
  let next = 1;
  if (groundRoot != null) idOf.set(groundRoot, 0);
  const nodeOfKey = (k) => {
    const r = dsu.find(k);
    if (!idOf.has(r)) idOf.set(r, next++);
    return idOf.get(r);
  };

  // build elements (skip wires & grounds — they only define connectivity)
  const elements = [];
  const partElem = new Map();
  const partElem2 = new Map();   // second element for 3-terminal parts (pot bottom half)
  const push = (pt, el) => { partElem.set(pt.id, elements.length); elements.push(el); };
  parts.forEach(pt => {
    const nid = (i) => nodeOfKey(flKey(pt.ends[i]));
    switch (pt.type) {
      case "resistor": push(pt, { k: "R", a: nid(0), b: nid(1), ohms: pt.value != null ? pt.value : 330 }); break;
      case "battery":  push(pt, { k: "V", a: nid(0), b: nid(1), volts: pt.volts != null ? pt.volts : 9, r: 0.05 }); break;
      case "led":      push(pt, { k: "LED", a: nid(0), b: nid(1), vf: 1.9, ron: 18 }); break;
      case "diode":    push(pt, { k: "D", a: nid(0), b: nid(1), vf: 0.7, ron: 8 }); break;
      case "cap":      push(pt, { k: "C", a: nid(0), b: nid(1), farads: (pt.value != null ? pt.value : 2) / 330 }); break;
      case "ind":      push(pt, { k: "L", a: nid(0), b: nid(1), henries: pt.value != null ? pt.value : 5 }); break;
      case "pot": {
        const total = pt.value != null ? pt.value : 10000;
        const pos = pt.pos != null ? pt.pos : 0.5;
        const rt = Math.max(1, total * (1 - pos)), rb = Math.max(1, total * pos);   // pos=1 → wiper at end0 (+)
        partElem.set(pt.id, elements.length);  elements.push({ k: "R", a: nid(0), b: nid(1), ohms: rt });   // a → wiper
        partElem2.set(pt.id, elements.length); elements.push({ k: "R", a: nid(1), b: nid(2), ohms: rb });   // wiper → b
        break;
      }
      case "npn":      push(pt, { k: "NPN", b: nid(0), c: nid(1), e: nid(2) }); break;   // base/collector/emitter
      default: break;  // wire, ground
    }
  });
  // assign ids to any remaining endpoints (so nodeOf works for wires/grounds in rendering)
  parts.forEach(pt => pt.ends.forEach(e => nodeOfKey(flKey(e))));

  const nNodes = Math.max(1, next);
  const nodeOf = (p) => {
    const r = dsu.find(flKey(p));
    return idOf.has(r) ? idOf.get(r) : 0;
  };
  return { netlist: { nNodes, elements }, nodeOf, partElem, partElem2 };
}

/* a part's electrical 2-terminal sub-branches with their engine element index.
   Most parts = one branch (ends[0]→ends[1]); a pot = two (a→wiper, wiper→b). */
function flPartBranches(part, link) {
  if (part.type === "pot") {
    const e0 = link.partElem.get(part.id), e1 = link.partElem2.get(part.id);
    const out = [];
    if (e0 != null) out.push({ ei: e0, a: part.ends[0], b: part.ends[1], kind: "R" });
    if (e1 != null) out.push({ ei: e1, a: part.ends[1], b: part.ends[2], kind: "R" });
    return out;
  }
  if (part.type === "npn") {
    const ei = link.partElem.get(part.id);
    if (ei == null) return [];
    return [{ ei, a: part.ends[1], b: part.ends[2], kind: "npn" }];   // collector → emitter carries Ic
  }
  const ei = link.partElem.get(part.id);
  if (ei == null) return [];
  return [{ ei, a: part.ends[0], b: part.ends[1], kind: part.type }];
}

/* Signed current each wire carries, by current-conservation (KCL) flow propagation.
   Every COMPONENT current is known from the engine; wires route it. We solve the
   wire flows by repeatedly finding a grid-point where exactly one wire is still
   unknown and fixing it from KCL. Works for series/parallel (no pure-wire loops).
   Convention for "current flowing FROM a node INTO an element" at endpoint i:
     • voltage source V: along-ends current = −elementCurrent (cur>0 exits + = end0)
     • all others (R/L/C/LED): along-ends current = +elementCurrent (enters end0)
   into-element at end0 = +Ialong, at end1 = −Ialong.
   Returns Map(wireId → current), +ve = flows ends[0] → ends[1]. */
function flWireCurrents(parts, link, eng, v) {
  const isConductor = (p) => p.type === "wire" || (p.type === "switch" && p.closed);
  const wires = parts.filter(isConductor);
  const result = new Map();
  // rhs(node) = −Σ(component into-element at that node) = Σ wire into-wire terms
  const compInto = {};   // gridKey → net current flowing from node into components
  for (const pt of parts) {
    if (isConductor(pt) || pt.type === "ground" || pt.type === "switch") continue;
    for (const br of flPartBranches(pt, link)) {
      if (br.ei == null) continue;
      const cur = eng.elementCurrent(br.ei, v);
      const along = pt.type === "battery" ? -cur : cur;   // current a→b
      const ka = flKey(br.a), kb = flKey(br.b);
      compInto[ka] = (compInto[ka] || 0) + along;
      compInto[kb] = (compInto[kb] || 0) - along;
    }
  }
  // unknown wires per node
  const unknown = new Map(wires.map(w => [w.id, w]));
  const nodeWires = {};   // gridKey → [{w, sign}] where sign: +1 if this is ends[0], −1 if ends[1]
  wires.forEach(w => {
    const k0 = flKey(w.ends[0]), k1 = flKey(w.ends[1]);
    (nodeWires[k0] = nodeWires[k0] || []).push({ id: w.id, sign: +1 });
    (nodeWires[k1] = nodeWires[k1] || []).push({ id: w.id, sign: -1 });
  });
  let guard = wires.length * 4 + 8;
  while (unknown.size && guard-- > 0) {
    let progressed = false;
    for (const k of Object.keys(nodeWires)) {
      const inc = nodeWires[k];
      const open = inc.filter(e => unknown.has(e.id));
      if (open.length === 1) {
        // KCL: Σ(into-wire) + compInto = 0 ; into-wire for a wire at this node = sign*f
        // known wires contribute sign*f_known; solve the one open wire.
        let known = compInto[k] || 0;
        for (const e of inc) if (!unknown.has(e.id)) known += e.sign * result.get(e.id);
        const o = open[0];
        result.set(o.id, -known / o.sign);
        unknown.delete(o.id);
        progressed = true;
      }
    }
    if (!progressed) break;
  }
  // any wire we couldn't resolve (pure-wire loop) → 0
  wires.forEach(w => { if (!result.has(w.id)) result.set(w.id, 0); });
  return result;
}

Object.assign(window, { flKey, flBuildNetlist, flWireCurrents, flPartBranches });
