//! Self-render a whole `.kicad_sch` to a themed, INTERACTIVE SVG — the sibling of
//! pcb_render, for schematics. Same payoff: kicad-cli's `.svg` export is flat and
//! anonymous, so we parse the sheet ourselves and tag every wire with
//! `data-net` and every symbol instance with `data-ref`, which is what makes
//! net highlighting + per-component hover possible.
//!
//! Symbol bodies are drawn from their `lib_symbols` definition (rectangles /
//! polylines / circles / arcs / pins) transformed to each instance's placement.
//!
//! Net grouping (label-based / connectivity v1): union-find over wire endpoints
//! (+ junctions), then clusters that share a power-symbol or label NAME are
//! merged so e.g. every GND stub highlights together. Component pins whose tip
//! lands on a cluster point inherit that net.

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

// schematic grid is Y-DOWN already (page mm), same as SVG → no global flip.

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

#[derive(Serialize, Clone)]
pub struct Comp {
    pub reference: String,
    pub value: String,
    pub footprint: String,
    pub mpn: String,
    pub lcsc: String,
    pub x: f64,
    pub y: f64,
    pub bbox: [f64; 4],
    pub pin_count: usize,
}

struct Wire { x1: f64, y1: f64, x2: f64, y2: f64, net: usize }
struct Label { x: f64, y: f64, text: String, power: bool }

pub struct Sheet {
    libs: HashMap<String, LibSym>,
    comps: Vec<Comp>,
    comp_svgs: Vec<String>,   // pre-rendered <g data-ref> body per comp
    wires: Vec<Wire>,
    junctions: Vec<(f64, f64)>,
    labels: Vec<Label>,
    net_names: HashMap<usize, String>,
}

// ── union-find ──
struct Uf { p: Vec<usize> }
impl Uf {
    fn new(n: usize) -> Self { Uf { p: (0..n).collect() } }
    fn find(&mut self, a: usize) -> usize { let mut a = a; while self.p[a] != a { self.p[a] = self.p[self.p[a]]; a = self.p[a]; } a }
    fn union(&mut self, a: usize, b: usize) { let (ra, rb) = (self.find(a), self.find(b)); if ra != rb { self.p[ra] = rb; } }
}

fn key(x: f64, y: f64) -> (i64, i64) { ((x * 100.0).round() as i64, (y * 100.0).round() as i64) }

/// Rotate+mirror a lib point into sheet coords. Lib frame is Y-UP; the sheet is
/// Y-DOWN, so base transform negates Y, then rotation (CW on screen for KiCad's
/// `at` angle) and optional mirror compose, then translate to (px,py).
fn xf(lx: f64, ly: f64, px: f64, py: f64, rot: f64, mirror: u8) -> (f64, f64) {
    // lib Y-up → screen Y-down
    let (mut x, mut y) = (lx, -ly);
    // mirror: 1 = mirror across Y axis (x), 2 = across X axis (y)
    if mirror == 1 { x = -x; }
    if mirror == 2 { y = -y; }
    // rotation (KiCad rotates the symbol CCW by `rot`; on Y-down screen that is a
    // clockwise matrix). Use standard CCW matrix with -rot for screen.
    let a = (-rot).to_radians();
    let (c, s) = (a.cos(), a.sin());
    let (rx, ry) = (x * c - y * s, x * s + y * c);
    (px + rx, py + ry)
}

pub fn parse(content: &str) -> Sheet {
    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();

    let mut sh = Sheet {
        libs: HashMap::new(), comps: vec![], comp_svgs: vec![], wires: vec![],
        junctions: vec![], labels: vec![], net_names: HashMap::new(),
    };

    // ── lib_symbols definitions ──
    if let Some(libblk) = blocks(content, "lib_symbols").into_iter().next() {
        // top-level defs: (symbol "Lib:Name" … nested (symbol "Name_x_y" graphics))
        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 });
                }
            }
            sh.libs.insert(name, LibSym { prims });
        }
    }

    // ── wires ──
    let content_wo_lib = strip_first_block(content, "lib_symbols");
    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 { sh.wires.push(Wire { x1: pts[0].0, y1: pts[0].1, x2: pts[1].0, y2: pts[1].1, net: 0 }); }
    }
    for j in blocks(&content_wo_lib, "junction") {
        if let Some(a) = at3.captures(j) { sh.junctions.push((f(a.get(1)), f(a.get(2)))); }
    }
    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() { sh.labels.push(Label { x: f(a.get(1)), y: f(a.get(2)), text, power: false }); }
        }
    }

    // ── symbol instances (top-level symbols outside lib_symbols) ──
    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 = String::new();
        let mut value = String::new();
        let mut footprint = String::new();
        let mut mpn = String::new();
        let mut lcsc = String::new();
        for c in prop.captures_iter(inst) {
            match &c[1] {
                "Reference" => reference = c[2].to_string(),
                "Value" => value = c[2].to_string(),
                "Footprint" => footprint = c[2].to_string(),
                k if k.eq_ignore_ascii_case("MPN") => mpn = c[2].to_string(),
                k if k.eq_ignore_ascii_case("LCSC") || k.eq_ignore_ascii_case("JLCPCB") => lcsc = c[2].to_string(),
                _ => {}
            }
        }
        let is_power = lib_id.starts_with("power:");
        // power symbols name their net (e.g. power:GND value "GND")
        if is_power && !value.is_empty() { sh.labels.push(Label { x: px, y: py, text: value.clone(), power: true }); }

        let lib = sh.libs.get(&lib_id);
        let mut body = String::new();
        let mut minx = f64::INFINITY; let mut miny = f64::INFINITY; let mut maxx = f64::NEG_INFINITY; let mut maxy = f64::NEG_INFINITY;
        let mut pin_n = 0usize;
        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="#d7c690" stroke-width="0.15"/>"##, tp[0].0, tp[0].1, pr.r); }
                    2 => { pin_n += 1; let _ = write!(body, r##"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="#7fd9b0" stroke-width="0.15"/>"##, tp[0].0, tp[0].1, tp[1].0, tp[1].1); }
                    _ => { let _ = write!(body, r##"<path d="{}" fill="none" stroke="#d7c690" stroke-width="0.15"/>"##, poly_d(&tp)); }
                }
            }
        }
        if !minx.is_finite() { minx = px - 1.27; miny = py - 1.27; maxx = px + 1.27; maxy = py + 1.27; }
        // reference + value labels
        if !is_power && !reference.is_empty() {
            let _ = write!(body, r##"<text x="{:.3}" y="{:.3}" font-size="1.3" fill="#f2d060">{}</text>"##, maxx + 0.4, miny + 1.2, esc(&reference));
            if !value.is_empty() { let _ = write!(body, r##"<text x="{:.3}" y="{:.3}" font-size="1.1" fill="#9fb0a8">{}</text>"##, maxx + 0.4, miny + 2.7, esc(&value)); }
        }
        let g = format!(r##"<g data-ref="{}">{}</g>"##, esc(&reference), body);
        sh.comp_svgs.push(g);
        if !is_power {
            sh.comps.push(Comp { reference, value, footprint, mpn, lcsc, x: px, y: py, bbox: [minx, miny, maxx, maxy], pin_count: pin_n });
        }
    }

    // ── net grouping: union-find over wire endpoints (+ junctions) ──
    let n = sh.wires.len();
    let mut uf = Uf::new(n.max(1));
    let mut endpoint_owner: HashMap<(i64, i64), usize> = HashMap::new();
    for (i, w) in sh.wires.iter().enumerate() {
        for k in [key(w.x1, w.y1), key(w.x2, w.y2)] {
            if let Some(&j) = endpoint_owner.get(&k) { uf.union(i, j); } else { endpoint_owner.insert(k, i); }
        }
    }
    // merge clusters that share a label/power NAME
    let mut name_to_cluster: HashMap<String, usize> = HashMap::new();
    // first assign each label to the nearest wire endpoint cluster
    let labels_snapshot: Vec<Label> = sh.labels.iter().map(|l| Label { x: l.x, y: l.y, text: l.text.clone(), power: l.power }).collect();
    for l in &labels_snapshot {
        if let Some(&wi) = endpoint_owner.get(&key(l.x, l.y)) {
            let c = uf.find(wi);
            match name_to_cluster.get(&l.text) { Some(&other) => uf.union(c, other), None => { name_to_cluster.insert(l.text.clone(), c); } }
        }
    }
    // assign a compact net id per root, remember names
    let mut root_id: HashMap<usize, usize> = HashMap::new();
    let mut next = 1usize;
    for i in 0..n {
        let r = uf.find(i);
        let id = *root_id.entry(r).or_insert_with(|| { let v = next; next += 1; v });
        sh.wires[i].net = id;
    }
    for l in &labels_snapshot {
        if let Some(&wi) = endpoint_owner.get(&key(l.x, l.y)) {
            let id = *root_id.get(&uf.find(wi)).unwrap_or(&0);
            if id != 0 { sh.net_names.entry(id).or_insert_with(|| l.text.clone()); }
        }
    }
    sh
}

pub fn render(sh: &Sheet) -> (String, RenderMeta) {
    let mut pts: Vec<(f64, f64)> = vec![];
    for w in &sh.wires { pts.push((w.x1, w.y1)); pts.push((w.x2, w.y2)); }
    for c in &sh.comps { pts.push((c.bbox[0], c.bbox[1])); pts.push((c.bbox[2], c.bbox[3])); }
    if pts.is_empty() { pts.push((0.0, 0.0)); pts.push((100.0, 100.0)); }
    let margin = 5.0;
    let min_x = pts.iter().map(|p| p.0).fold(f64::INFINITY, f64::min) - margin;
    let min_y = pts.iter().map(|p| p.1).fold(f64::INFINITY, f64::min) - margin;
    let max_x = pts.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max) + margin;
    let max_y = pts.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max) + margin;
    let (w, h) = (max_x - min_x, max_y - min_y);

    let mut s = String::new();
    let _ = write!(s, r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{:.3} {:.3} {:.3} {:.3}" preserveAspectRatio="xMidYMid meet" font-family="ui-sans-serif,system-ui,sans-serif">"##, min_x, min_y, w, h);
    let _ = write!(s, r##"<rect class="adom-board-sub" x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" fill="#0c130f"/>"##, min_x, min_y, w, h);

    // wires
    s.push_str(r##"<g class="adom-layer-copper">"##);
    for wi in &sh.wires {
        let _ = write!(s, r##"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" data-net="{}" stroke="#4f8f6f" stroke-width="0.18" stroke-linecap="round"/>"##, wi.x1, wi.y1, wi.x2, wi.y2, wi.net);
    }
    s.push_str("</g>");
    // junctions
    s.push_str(r##"<g class="adom-layer-via">"##);
    for &(x, y) in &sh.junctions { let _ = write!(s, r##"<circle cx="{:.3}" cy="{:.3}" r="0.4" fill="#7fd9b0"/>"##, x, y); }
    s.push_str("</g>");
    // symbol bodies
    s.push_str(r##"<g class="adom-layer-pad">"##);
    for g in &sh.comp_svgs { s.push_str(g); }
    s.push_str("</g>");
    // component hotspots
    s.push_str(r##"<g class="adom-layer-hotspot">"##);
    for c in &sh.comps {
        let [x0, y0, x1, y1] = c.bbox;
        let _ = write!(s, r##"<rect class="adom-hotspot" x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" data-ref="{}" fill="transparent"/>"##, x0 - 0.6, y0 - 0.6, (x1 - x0) + 1.2, (y1 - y0) + 1.2, esc(&c.reference));
    }
    s.push_str("</g>");
    s.push_str("</svg>");

    let net_names: HashMap<String, String> = sh.net_names.iter().map(|(k, v)| (k.to_string(), v.clone())).collect();
    let meta = RenderMeta {
        width_mm: w, height_mm: h, view_box: [min_x, min_y, w, h],
        net_names, components: sh.comps.clone(),
        track_count: sh.wires.len(), via_count: sh.junctions.len(), pad_count: 0,
    };
    (s, meta)
}

#[derive(Serialize)]
pub struct RenderMeta {
    pub width_mm: f64, pub height_mm: f64, pub view_box: [f64; 4],
    pub net_names: HashMap<String, String>, pub components: Vec<Comp>,
    pub track_count: usize, pub via_count: usize, pub pad_count: usize,
}

// ── helpers ──
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
}
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()) }

/// Top-level `(symbol …)` blocks (not nested). Used for both lib defs and
/// instances — callers pass the right slice.
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; }
        // slice this balanced block
        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
}

/// Remove the first `(<tag> …)` block so instance scanning doesn't see lib defs.
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()
}