//! Self-render a `.kicad_sch` into the `.adom-sheet` inner SVG WITHOUT
//! service-kicad. Used as the fallback when the shared KiCad export can't render
//! a sheet — most importantly a HIERARCHICAL ROOT, which kicad-cli renders empty
//! when its child files aren't present. Draws every symbol from its `lib_symbols`
//! definition (rectangles / polylines / circles / arcs / pins) transformed to the
//! instance placement, plus wires, junctions, power symbols and labels, in
//! KiCad's own light-sheet colours. Each component is wrapped `<g data-cref>` so
//! the existing halo-on-hover works unchanged.
//!
//! Ported from adom-2dboard's sch_render.rs; here it only emits the inner
//! content (the caller wraps it in `<g class="adom-sheet">` + overlay).

use crate::sexpr::{blocks, esc, f};
use regex::Regex;
use std::collections::HashMap;
use std::fmt::Write as _;

// KiCad default schematic colours
const C_SYM: &str = "#840000";   // symbol bodies + pins (dark red)
const C_WIRE: &str = "#008484";  // wires + junctions (teal-green)
const C_NAME: &str = "#840000";  // reference designators
const C_VALUE: &str = "#008484"; // values
const C_LABEL: &str = "#840000"; // net / power labels

#[derive(Clone)]
struct Prim { kind: u8, pts: Vec<(f64, f64)>, r: f64 } // 0 poly/rect, 1 circle, 2 pin
struct LibSym { prims: Vec<Prim> }

/// lib Y-up -> screen Y-down, ROTATE CCW by `rot` (screen matrix uses -rot), then
/// MIRROR in the world/page frame, then translate to (px,py). KiCad applies the
/// mirror AFTER the rotation (in page space), not before — doing it before put a
/// rotated+mirrored symbol's body/pins on the wrong side while its axis-symmetric
/// features (e.g. a pin-1 socket arc) still matched, so a mirrored connector lit up
/// only by its text + pin-1 arc. `mirror==2` is KiCad `(mirror x)` = flip page Y;
/// `mirror==1` is `(mirror y)` = flip page X.
fn xf(lx: f64, ly: f64, px: f64, py: f64, rot: f64, mirror: u8) -> (f64, f64) {
    let (x, y) = (lx, -ly);
    let a = (-rot).to_radians();
    let (c, s) = (a.cos(), a.sin());
    let (mut rx, mut ry) = (x * c - y * s, x * s + y * c);
    if mirror == 1 { rx = -rx; } // (mirror y) → flip page X
    if mirror == 2 { ry = -ry; } // (mirror x) → flip page Y
    (px + rx, py + ry)
}

fn quoted(s: &str) -> Option<String> {
    let q1 = s.find('"')?; let rest = &s[q1 + 1..]; let q2 = rest.find('"')?; Some(rest[..q2].to_string())
}
fn capture1(s: &str, re: &str) -> Option<String> { Regex::new(re).ok()?.captures(s).map(|c| c[1].to_string()) }

/// Normalize a symbol name / lib_id to a bare key for FALLBACK lookup. KiCad
/// usually stores each `lib_symbols` entry under its full lib_id ("Device:LED"),
/// but on a name collision it renames the stored entry to a bare, dedup-suffixed
/// name ("LED_2") while INSTANCES still reference the original lib_id
/// ("Device:LED"). An exact lookup then misses and the symbol gets no geometry.
/// This drops the "Lib:" prefix and a trailing "_<digits>" so both sides collapse
/// to the same key ("LED"). Used ONLY as a fallback after the exact lookup fails.
pub fn lib_bare_key(name: &str) -> String {
    let base = name.rsplit(':').next().unwrap_or(name);
    if let Some(pos) = base.rfind('_') {
        if pos + 1 < base.len() && base[pos + 1..].chars().all(|c| c.is_ascii_digit()) {
            return base[..pos].to_string();
        }
    }
    base.to_string()
}
/// Resolve a lib def by exact lib_id, falling back to the bare-key alias (see
/// `lib_bare_key`) so dedup-renamed symbols like "LED_2" still resolve.
fn resolve_lib<'a, T>(libs: &'a HashMap<String, T>, bare: &HashMap<String, String>, lib_id: &str) -> Option<&'a T> {
    libs.get(lib_id).or_else(|| bare.get(&lib_bare_key(lib_id)).and_then(|n| libs.get(n)))
}
/// Build the bare-key → stored-name index for the fallback lookup.
fn bare_index<T>(libs: &HashMap<String, T>) -> HashMap<String, String> {
    let mut m = HashMap::new();
    for k in libs.keys() { m.entry(lib_bare_key(k)).or_insert_with(|| k.clone()); }
    m
}

/// Top-level `(symbol …)` blocks (not nested).
fn top_symbols(s: &str) -> Vec<&str> {
    let bytes = s.as_bytes();
    let mut out = vec![];
    let mut i = 0;
    while let Some(rel) = s[i..].find("(symbol") {
        let start = i + rel;
        let after = start + 7;
        if bytes.get(after).map(|c| !c.is_ascii_whitespace()).unwrap_or(true) { i = after; continue; }
        let mut depth = 0i32; let mut end = start;
        for j in start..s.len() { match bytes[j] { b'(' => depth += 1, b')' => { depth -= 1; if depth == 0 { end = j; break; } } _ => {} } }
        out.push(&s[start..=end.min(s.len() - 1)]);
        i = end + 1;
    }
    out
}
fn strip_first_block(s: &str, tag: &str) -> String {
    if let Some(b) = blocks(s, tag).into_iter().next() { return s.replacen(b, "", 1); }
    s.to_string()
}
fn poly_d(pts: &[(f64, f64)]) -> String {
    let mut d = String::new();
    for (i, &(x, y)) in pts.iter().enumerate() { let _ = write!(d, "{}{:.3} {:.3} ", if i == 0 { "M" } else { "L" }, x, y); }
    d
}

/// A quantized line-segment key (both endpoints to 0.05mm, order-independent) so
/// export strokes can be matched to the exact symbol geometry regardless of which
/// end KiCad drew first.
pub type SegKey = (i32, i32, i32, i32);
pub fn seg_key_pub(a: (f64, f64), b: (f64, f64)) -> SegKey { seg_key(a, b) }
fn seg_key(a: (f64, f64), b: (f64, f64)) -> SegKey {
    let q = |v: f64| (v * 20.0).round() as i32; // 0.05mm grid
    let (ax, ay, bx, by) = (q(a.0), q(a.1), q(b.0), q(b.1));
    if (ax, ay) <= (bx, by) { (ax, ay, bx, by) } else { (bx, by, ax, ay) }
}

/// The EXACT set of line segments that make up each symbol on the sheet, keyed by
/// reference. Built from `lib_symbols` (rectangle / polyline / pin edges)
/// transformed to each instance's placement. This is the ground truth for hover
/// highlighting: a stroke in the KiCad export belongs to a component IFF it
/// coincides with one of that component's segments — a wire or another part's line
/// never will. (Circles/arcs are matched separately by centre+radius.)
pub fn symbol_segments(content: &str) -> (HashMap<SegKey, String>, HashMap<(i32, i32, i32), String>) {
    let at3 = Regex::new(r"\(at\s+(-?[\d.]+)\s+(-?[\d.]+)(?:\s+(-?[\d.]+))?\)").unwrap();
    let se = Regex::new(r"\(start\s+(-?[\d.]+)\s+(-?[\d.]+)\)\s*\(end\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
    // arc: (start x y) (mid x y) (end x y) — capture start (1,2), mid (3,4), end (5,6)
    let arc_se = Regex::new(r"\(start\s+(-?[\d.]+)\s+(-?[\d.]+)\)\s*\(mid\s+(-?[\d.]+)\s+(-?[\d.]+)\)\s*\(end\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
    let xy = Regex::new(r"\(xy\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
    let center = Regex::new(r"\(center\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
    let radius = Regex::new(r"\(radius\s+(-?[\d.]+)\)").unwrap();
    let length = Regex::new(r"\(length\s+(-?[\d.]+)\)").unwrap();
    let mirror_re = Regex::new(r"\(mirror\s+(x|y)\)").unwrap();

    let mut libs: HashMap<String, LibSym> = HashMap::new();
    if let Some(libblk) = blocks(content, "lib_symbols").into_iter().next() {
        for def in top_symbols(libblk) {
            let name = quoted(def).unwrap_or_default();
            if name.is_empty() { continue; }
            let mut prims = vec![];
            for r in blocks(def, "rectangle") {
                if let Some(c) = se.captures(r) {
                    let (x1, y1, x2, y2) = (f(c.get(1)), f(c.get(2)), f(c.get(3)), f(c.get(4)));
                    prims.push(Prim { kind: 0, pts: vec![(x1, y1), (x2, y1), (x2, y2), (x1, y2), (x1, y1)], r: 0.0 });
                }
            }
            for p in blocks(def, "polyline") {
                let pts: Vec<(f64, f64)> = xy.captures_iter(p).map(|c| (f(c.get(1)), f(c.get(2)))).collect();
                if pts.len() >= 2 { prims.push(Prim { kind: 0, pts, r: 0.0 }); }
            }
            for c in blocks(def, "circle") {
                if let (Some(ce), Some(ra)) = (center.captures(c), radius.captures(c)) {
                    prims.push(Prim { kind: 1, pts: vec![(f(ce.get(1)), f(ce.get(2)))], r: f(ra.get(1)) });
                }
            }
            // arcs (inductor coils, curved cap plates, crystal): KiCad's SVG export
            // draws each as an `A` command giving only its start+end, so match on the
            // CHORD (start→end). Without this, arc-bodied parts (L, ferrite, crystal)
            // never highlighted their body.
            for a in blocks(def, "arc") {
                if let Some(c) = arc_se.captures(a) {
                    let (sx, sy, ex, ey) = (f(c.get(1)), f(c.get(2)), f(c.get(5)), f(c.get(6)));
                    prims.push(Prim { kind: 0, pts: vec![(sx, sy), (ex, ey)], r: 0.0 });
                }
            }
            for pin in blocks(def, "pin") {
                if let Some(a) = at3.captures(pin) {
                    let (lx, ly, ang) = (f(a.get(1)), f(a.get(2)), f(a.get(3)));
                    let len = length.captures(pin).map(|c| f(c.get(1))).unwrap_or(2.54);
                    let (dx, dy) = (ang.to_radians().cos() * len, ang.to_radians().sin() * len);
                    prims.push(Prim { kind: 2, pts: vec![(lx, ly), (lx + dx, ly + dy)], r: 0.0 });
                }
            }
            libs.insert(name, LibSym { prims });
        }
    }

    let bare = bare_index(&libs);
    let content_wo_lib = strip_first_block(content, "lib_symbols");
    let mut segs: HashMap<SegKey, String> = HashMap::new();
    let mut circles: HashMap<(i32, i32, i32), String> = HashMap::new();
    for inst in top_symbols(&content_wo_lib) {
        let lib_id = match capture1(inst, r##"\(lib_id\s+"([^"]+)"\)"##) { Some(s) => s, None => continue };
        let a = match at3.captures(inst) { Some(a) => a, None => continue };
        let (px, py, rot) = (f(a.get(1)), f(a.get(2)), f(a.get(3)));
        let mirror = mirror_re.captures(inst).map(|c| if &c[1] == "x" { 2u8 } else { 1u8 }).unwrap_or(0);
        let reference = capture1(inst, r##"\(property\s+"Reference"\s+"([^"]*)""##).unwrap_or_default();
        if reference.is_empty() || lib_id.starts_with("power:") { continue; }
        if let Some(lib) = resolve_lib(&libs, &bare, &lib_id) {
            for pr in &lib.prims {
                match pr.kind {
                    1 => { // circle: centre+radius key
                        let (cx, cy) = xf(pr.pts[0].0, pr.pts[0].1, px, py, rot, mirror);
                        let q = |v: f64| (v * 20.0).round() as i32;
                        circles.insert((q(cx), q(cy), q(pr.r)), reference.clone());
                    }
                    _ => { // polyline/rect/pin: consecutive segments
                        let tp: Vec<(f64, f64)> = pr.pts.iter().map(|&(x, y)| xf(x, y, px, py, rot, mirror)).collect();
                        for w in tp.windows(2) { segs.insert(seg_key(w[0], w[1]), reference.clone()); }
                    }
                }
            }
        }
    }
    (segs, circles)
}

/// Anchor points of things that are NOT a component's own text: power-symbol
/// labels (power:GND, power:+3V3 …) and net / global / hierarchical labels. Text
/// in the export sitting on one of these is a NET/POWER label, never a symbol's
/// pin-name or field, so it must not be tagged to a component.
pub fn label_anchors(content: &str) -> Vec<(f64, f64)> {
    let at = Regex::new(r"\(at\s+(-?[\d.]+)\s+(-?[\d.]+)").unwrap();
    let mut out = Vec::new();
    let cw = strip_first_block(content, "lib_symbols");
    // schematic `(at)` is Y-DOWN, same frame as the export/page — do NOT negate Y.
    for tag in ["label", "global_label", "hierarchical_label"] {
        for l in blocks(&cw, tag) {
            if let Some(c) = at.captures(l) { out.push((f(c.get(1)), f(c.get(2)))); }
        }
    }
    // Power symbols: the VISIBLE label is the Value property's text (e.g. "GND"),
    // which sits at its OWN `(at)`, NOT the symbol origin (the origin can be closer
    // to a neighbouring component's pin number than to its own label — using it made
    // the exclusion grab pin numbers). The Value `(at)` is exactly where the text is.
    let val_at = Regex::new(r##"\(property\s+"Value"\s+"[^"]*"\s*\(at\s+(-?[\d.]+)\s+(-?[\d.]+)"##).unwrap();
    for inst in top_symbols(&cw) {
        if capture1(inst, r##"\(lib_id\s+"(power:[^"]+)"\)"##).is_some() {
            if let Some(c) = val_at.captures(inst) { out.push((f(c.get(1)), f(c.get(2)))); }
            else if let Some(c) = at.captures(inst) { out.push((f(c.get(1)), f(c.get(2)))); }
        }
    }
    out
}

/// Page-space anchor points for each symbol's PIN TEXT (names inside the body,
/// numbers along the stubs). A pin is `(at lx ly ang) (length L)`: `(lx,ly)` is
/// the CONNECTION point (where wires / net labels attach — deliberately excluded),
/// the pin runs inward to the BODY point `B = (lx+L·cosang, ly+L·sinang)` where the
/// pin NAME sits, and the pin NUMBER sits at the stub MIDPOINT `M`. We return B and
/// M (per pin) so a text group landing on one is tagged to that symbol — while a
/// net/power label at the connection point never is. Skips power symbols.
pub fn pin_anchors(content: &str) -> Vec<(String, f64, f64)> {
    let at3 = Regex::new(r"\(at\s+(-?[\d.]+)\s+(-?[\d.]+)(?:\s+(-?[\d.]+))?\)").unwrap();
    let length = Regex::new(r"\(length\s+(-?[\d.]+)\)").unwrap();
    let mirror_re = Regex::new(r"\(mirror\s+(x|y)\)").unwrap();

    // lib_name -> Vec<(lx, ly, ang, len)>
    let mut libpins: HashMap<String, Vec<(f64, f64, f64, f64)>> = HashMap::new();
    if let Some(libblk) = blocks(content, "lib_symbols").into_iter().next() {
        for def in top_symbols(libblk) {
            let name = quoted(def).unwrap_or_default();
            if name.is_empty() { continue; }
            let mut pins = vec![];
            for pin in blocks(def, "pin") {
                if let Some(a) = at3.captures(pin) {
                    let (lx, ly, ang) = (f(a.get(1)), f(a.get(2)), f(a.get(3)));
                    let len = length.captures(pin).map(|c| f(c.get(1))).unwrap_or(2.54);
                    pins.push((lx, ly, ang, len));
                }
            }
            libpins.insert(name, pins);
        }
    }
    let bare = bare_index(&libpins);

    let cw = strip_first_block(content, "lib_symbols");
    let mut out = Vec::new();
    for inst in top_symbols(&cw) {
        let lib_id = match capture1(inst, r##"\(lib_id\s+"([^"]+)"\)"##) { Some(s) => s, None => continue };
        if lib_id.starts_with("power:") { continue; }
        let a = match at3.captures(inst) { Some(a) => a, None => continue };
        let (px, py, rot) = (f(a.get(1)), f(a.get(2)), f(a.get(3)));
        let mirror = mirror_re.captures(inst).map(|c| if &c[1] == "x" { 2u8 } else { 1u8 }).unwrap_or(0);
        let reference = capture1(inst, r##"\(property\s+"Reference"\s+"([^"]*)""##).unwrap_or_default();
        if reference.is_empty() { continue; }
        if let Some(pins) = resolve_lib(&libpins, &bare, &lib_id) {
            for &(lx, ly, ang, len) in pins {
                let (dx, dy) = (ang.to_radians().cos() * len, ang.to_radians().sin() * len);
                // body point (pin name) and stub midpoint (pin number), in page space
                let b = xf(lx + dx, ly + dy, px, py, rot, mirror);
                let m = xf(lx + dx / 2.0, ly + dy / 2.0, px, py, rot, mirror);
                out.push((reference.clone(), b.0, b.1));
                out.push((reference.clone(), m.0, m.1));
            }
        }
    }
    out
}

/// The ON-SHEET TEXT that belongs to each symbol: its Reference and Value fields.
/// KiCad stores each as `(property "Reference"|"Value" "<text>" (at x y …))` with
/// an ABSOLUTE page position; that's the ONLY text that is the symbol's own (pin
/// names/numbers are the symbol GRAPHIC, and net/power labels are standalone).
/// Returns (ref-designator, field text, x, y) for each VISIBLE Reference/Value.
pub fn field_anchors(content: &str) -> Vec<(String, String, f64, f64)> {
    let at = Regex::new(r"\(at\s+(-?[\d.]+)\s+(-?[\d.]+)").unwrap();
    let cw = strip_first_block(content, "lib_symbols");
    let mut out = Vec::new();
    for inst in top_symbols(&cw) {
        if capture1(inst, r##"\(lib_id\s+"(power:[^"]+)"\)"##).is_some() { continue; }
        let reference = capture1(inst, r##"\(property\s+"Reference"\s+"([^"]*)""##).unwrap_or_default();
        if reference.is_empty() { continue; }
        // each `(property "Name" "Value" (at x y ..) (effects .. [ (hide yes) ]))`
        for pb in blocks(inst, "property") {
            let name = quoted(pb).unwrap_or_default();
            if name != "Reference" && name != "Value" { continue; }
            // the field text is the SECOND quoted string
            let text = nth_quoted(pb, 1).unwrap_or_default();
            // skip hidden fields (not rendered on the sheet)
            if pb.contains("(hide yes)") || pb.contains("hide)") { continue; }
            if let Some(c) = at.captures(pb) {
                out.push((reference.clone(), text, f(c.get(1)), f(c.get(2))));
            }
        }
    }
    out
}
fn nth_quoted(s: &str, n: usize) -> Option<String> {
    let mut rest = s; let mut i = 0;
    loop {
        let q1 = rest.find('"')? + 1;
        let after = &rest[q1..];
        let q2 = after.find('"')?;
        let val = after[..q2].to_string();
        if i == n { return Some(val); }
        rest = &after[q2 + 1..]; i += 1;
    }
}

/// Render the sheet's drawable content (symbols + wires + power + labels) as the
/// inner SVG for `<g class="adom-sheet">`. Returns "" if there's nothing to draw.
pub fn render_inner(content: &str) -> String {
    let at3 = Regex::new(r"\(at\s+(-?[\d.]+)\s+(-?[\d.]+)(?:\s+(-?[\d.]+))?\)").unwrap();
    let se = Regex::new(r"\(start\s+(-?[\d.]+)\s+(-?[\d.]+)\)\s*\(end\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
    let xy = Regex::new(r"\(xy\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
    let center = Regex::new(r"\(center\s+(-?[\d.]+)\s+(-?[\d.]+)\)").unwrap();
    let radius = Regex::new(r"\(radius\s+(-?[\d.]+)\)").unwrap();
    let length = Regex::new(r"\(length\s+(-?[\d.]+)\)").unwrap();
    let prop = Regex::new(r##"\(property\s+"([^"]+)"\s+"([^"]*)""##).unwrap();
    let mirror_re = Regex::new(r"\(mirror\s+(x|y)\)").unwrap();

    // ── lib_symbols ──
    let mut libs: HashMap<String, LibSym> = HashMap::new();
    if let Some(libblk) = blocks(content, "lib_symbols").into_iter().next() {
        for def in top_symbols(libblk) {
            let name = quoted(def).unwrap_or_default();
            if name.is_empty() { continue; }
            let mut prims = vec![];
            for r in blocks(def, "rectangle") {
                if let Some(c) = se.captures(r) {
                    let (x1, y1, x2, y2) = (f(c.get(1)), f(c.get(2)), f(c.get(3)), f(c.get(4)));
                    prims.push(Prim { kind: 0, pts: vec![(x1, y1), (x2, y1), (x2, y2), (x1, y2), (x1, y1)], r: 0.0 });
                }
            }
            for p in blocks(def, "polyline") {
                let pts: Vec<(f64, f64)> = xy.captures_iter(p).map(|c| (f(c.get(1)), f(c.get(2)))).collect();
                if pts.len() >= 2 { prims.push(Prim { kind: 0, pts, r: 0.0 }); }
            }
            for c in blocks(def, "circle") {
                if let (Some(ce), Some(ra)) = (center.captures(c), radius.captures(c)) {
                    prims.push(Prim { kind: 1, pts: vec![(f(ce.get(1)), f(ce.get(2)))], r: f(ra.get(1)) });
                }
            }
            for pin in blocks(def, "pin") {
                if let Some(a) = at3.captures(pin) {
                    let (lx, ly, ang) = (f(a.get(1)), f(a.get(2)), f(a.get(3)));
                    let len = length.captures(pin).map(|c| f(c.get(1))).unwrap_or(2.54);
                    let (dx, dy) = (ang.to_radians().cos() * len, ang.to_radians().sin() * len);
                    prims.push(Prim { kind: 2, pts: vec![(lx, ly), (lx + dx, ly + dy)], r: 0.0 });
                }
            }
            libs.insert(name, LibSym { prims });
        }
    }
    let bare = bare_index(&libs);

    let content_wo_lib = strip_first_block(content, "lib_symbols");
    let mut out = String::new();

    // ── wires + junctions ──
    for w in blocks(&content_wo_lib, "wire") {
        let pts: Vec<(f64, f64)> = xy.captures_iter(w).map(|c| (f(c.get(1)), f(c.get(2)))).collect();
        if pts.len() >= 2 {
            let _ = write!(out, r##"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="{}" stroke-width="0.15" stroke-linecap="round"/>"##,
                pts[0].0, pts[0].1, pts[1].0, pts[1].1, C_WIRE);
        }
    }
    for j in blocks(&content_wo_lib, "junction") {
        if let Some(a) = at3.captures(j) {
            let _ = write!(out, r##"<circle cx="{:.3}" cy="{:.3}" r="0.4" fill="{}"/>"##, f(a.get(1)), f(a.get(2)), C_WIRE);
        }
    }
    // net / global / hierarchical labels
    for l in blocks(&content_wo_lib, "label").into_iter()
        .chain(blocks(&content_wo_lib, "global_label"))
        .chain(blocks(&content_wo_lib, "hierarchical_label")) {
        if let Some(a) = at3.captures(l) {
            let text = quoted(l).unwrap_or_default();
            if !text.is_empty() {
                let _ = write!(out, r##"<text x="{:.3}" y="{:.3}" font-size="1.4" fill="{}" font-family="monospace" dominant-baseline="middle">{}</text>"##,
                    f(a.get(1)) + 0.5, f(a.get(2)), C_LABEL, esc(&text));
            }
        }
    }

    // ── symbol instances ──
    for inst in top_symbols(&content_wo_lib) {
        let lib_id = match capture1(inst, r##"\(lib_id\s+"([^"]+)"\)"##) { Some(s) => s, None => continue };
        let a = match at3.captures(inst) { Some(a) => a, None => continue };
        let (px, py, rot) = (f(a.get(1)), f(a.get(2)), f(a.get(3)));
        let mirror = mirror_re.captures(inst).map(|c| if &c[1] == "x" { 2u8 } else { 1u8 }).unwrap_or(0);

        let (mut reference, mut value) = (String::new(), String::new());
        for c in prop.captures_iter(inst) {
            match &c[1] { "Reference" => reference = c[2].to_string(), "Value" => value = c[2].to_string(), _ => {} }
        }
        let is_power = lib_id.starts_with("power:");
        let lib = resolve_lib(&libs, &bare, &lib_id);
        let mut body = String::new();
        let (mut minx, mut miny, mut maxx, mut maxy) = (f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
        if let Some(lib) = lib {
            for pr in &lib.prims {
                let tp: Vec<(f64, f64)> = pr.pts.iter().map(|&(x, y)| xf(x, y, px, py, rot, mirror)).collect();
                for &(x, y) in &tp { minx = minx.min(x); miny = miny.min(y); maxx = maxx.max(x); maxy = maxy.max(y); }
                match pr.kind {
                    1 => { let _ = write!(body, r##"<circle cx="{:.3}" cy="{:.3}" r="{:.3}" fill="none" stroke="{}" stroke-width="0.15"/>"##, tp[0].0, tp[0].1, pr.r, C_SYM); }
                    2 => { let _ = write!(body, r##"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="{}" stroke-width="0.15"/>"##, tp[0].0, tp[0].1, tp[1].0, tp[1].1, C_SYM); }
                    _ => { let _ = write!(body, r##"<path d="{}" fill="none" stroke="{}" stroke-width="0.15"/>"##, poly_d(&tp), C_SYM); }
                }
            }
        }
        if !minx.is_finite() { minx = px - 1.27; miny = py - 1.27; maxx = px + 1.27; maxy = py + 1.27; }
        // power symbols show their net name; regular parts show ref + value
        if is_power {
            if !value.is_empty() {
                let _ = write!(body, r##"<text x="{:.3}" y="{:.3}" font-size="1.4" fill="{}" font-family="monospace" text-anchor="middle">{}</text>"##, px, maxy + 1.6, C_LABEL, esc(&value));
            }
        } else if !reference.is_empty() {
            let _ = write!(body, r##"<text x="{:.3}" y="{:.3}" font-size="1.3" fill="{}" font-family="monospace">{}</text>"##, maxx + 0.4, miny + 1.2, C_NAME, esc(&reference));
            if !value.is_empty() { let _ = write!(body, r##"<text x="{:.3}" y="{:.3}" font-size="1.1" fill="{}" font-family="monospace">{}</text>"##, maxx + 0.4, miny + 2.7, C_VALUE, esc(&value)); }
        }
        let _ = write!(out, r##"<g data-cref="{}">{}</g>"##, esc(&reference), body);
    }

    out
}
