//! Parse a `.kicad_sch` for the INTERACTION overlay: each placed symbol's
//! identity (ref / value / footprint / LCSC PN / description) and its bounding
//! box in schematic PAGE coordinates (mm), so we can drop a transparent hover
//! hotspot over it aligned to the KiCad-native SVG. We do NOT render the sheet —
//! service-kicad provides the authentic visual.

use crate::sexpr::{blocks, esc, f};
use regex::Regex;
use serde::Serialize;

#[derive(Serialize, Clone)]
pub struct Comp {
    pub reference: String,
    pub value: String,
    pub footprint: String,
    pub lcsc: String,
    pub description: String,
    pub datasheet: String,
    pub x: f64,
    pub y: f64,
    pub bbox: [f64; 4], // page mm: [minx,miny,maxx,maxy]
    pub pin_count: usize,
    /// Hit regions that belong to this symbol: the body bbox PLUS a box around
    /// each visible property text (reference/value), because KiCad's selection
    /// haloes that text too. Used only for tagging, not shipped in the meta.
    #[serde(skip)]
    pub regions: Vec<[f64; 4]>,
}

/// A hierarchical-sheet symbol on this sheet: a box that links to a child
/// `.kicad_sch`. Clicking it in the viewer navigates into that child.
#[derive(Serialize, Clone)]
pub struct SubSheet {
    pub name: String,
    pub file: String,
    pub bbox: [f64; 4], // page mm
}

pub struct Sheet {
    pub comps: Vec<Comp>,
    pub sub_sheets: Vec<SubSheet>,
    pub content_bbox: (f64, f64, f64, f64), // minx,miny,w,h (mm) with margin
}

struct LibSym { pts: Vec<(f64, f64)> } // all graphic/pin points (lib coords)

/// Transform a lib point to page coords (lib frame is Y-up; sheet is Y-down).
// KiCad applies the mirror in page space AFTER rotation (see kicad_self::xf) — do
// it in the same order here so a rotated+mirrored symbol's hit region is correct.
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; }
    if mirror == 2 { ry = -ry; }
    (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())
}
/// The n-th (0-based) double-quoted string in `s` — property name = 0, value = 1.
fn nth_quoted(s: &str, n: usize) -> Option<String> {
    let mut rest = s;
    for i in 0.. {
        let q1 = rest.find('"')?;
        let after = &rest[q1 + 1..];
        let q2 = after.find('"')?;
        if i == n { return Some(after[..q2].to_string()); }
        rest = &after[q2 + 1..];
    }
    None
}
fn cap1(s: &str, re: &str) -> Option<String> { Regex::new(re).ok()?.captures(s).map(|c| c[1].to_string()) }

/// 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
}

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 arc_sme = Regex::new(r"\(start\s+(-?[\d.]+)\s+(-?[\d.]+)\)\s*\(mid\s+(-?[\d.]+)\s+(-?[\d.]+)\)\s*\(end\s+(-?[\d.]+)\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: collect each definition's graphic + pin points ──
    let mut libs: std::collections::HashMap<String, LibSym> = std::collections::HashMap::new();
    if let Some(libblk) = blocks(content, "lib_symbols").into_iter().next() {
        for def in top_symbols(libblk) {
            let name = match quoted(def) { Some(n) if !n.is_empty() => n, _ => continue };
            let mut pts: Vec<(f64, f64)> = vec![];
            for r in blocks(def, "rectangle") {
                if let Some(c) = se.captures(r) {
                    pts.push((f(c.get(1)), f(c.get(2))));
                    pts.push((f(c.get(3)), f(c.get(4))));
                }
            }
            for p in blocks(def, "polyline") { for c in xy.captures_iter(p) { pts.push((f(c.get(1)), f(c.get(2)))); } }
            for c in blocks(def, "circle") {
                if let (Some(ce), Some(ra)) = (center.captures(c), radius.captures(c)) {
                    let (cx, cy, r) = (f(ce.get(1)), f(ce.get(2)), f(ra.get(1)));
                    pts.push((cx - r, cy - r)); pts.push((cx + r, cy + r));
                }
            }
            // arcs (inductor coils, curved plates): include start/mid/end so the
            // symbol's hit region covers the ARC BULGES, not just the axis line —
            // otherwise hovering the curve of an inductor didn't select it.
            for a in blocks(def, "arc") {
                if let Some(c) = arc_sme.captures(a) {
                    pts.push((f(c.get(1)), f(c.get(2))));
                    pts.push((f(c.get(3)), f(c.get(4))));
                    pts.push((f(c.get(5)), f(c.get(6))));
                }
            }
            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);
                    pts.push((lx, ly));
                    pts.push((lx + ang.to_radians().cos() * len, ly + ang.to_radians().sin() * len));
                }
            }
            libs.insert(name, LibSym { pts });
        }
    }
    // bare-key alias index so dedup-renamed lib entries (e.g. "LED_2" for an
    // instance's "Device:LED") still resolve — see kicad_self::lib_bare_key.
    let libs_bare: std::collections::HashMap<String, String> = {
        let mut m = std::collections::HashMap::new();
        for k in libs.keys() { m.entry(crate::kicad_self::lib_bare_key(k)).or_insert_with(|| k.clone()); }
        m
    };

    let content_wo_lib = strip_first(content, "lib_symbols");
    let mut comps: Vec<Comp> = vec![];
    let (mut gminx, mut gminy, mut gmaxx, mut gmaxy) = (f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);

    for inst in top_symbols(&content_wo_lib) {
        let lib_id = match cap1(inst, r##"\(lib_id\s+"([^"]+)"\)"##) { Some(s) => s, None => continue };
        if lib_id.starts_with("power:") { continue; } // skip power/ground symbols
        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, mut footprint) = (String::new(), String::new(), String::new());
        let (mut lcsc, mut description, mut datasheet) = (String::new(), 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(),
                "Footprint" => footprint = c[2].to_string(),
                "Description" => description = c[2].to_string(),
                "Datasheet" => datasheet = c[2].to_string(),
                k if k.eq_ignore_ascii_case("LCSC PN") || k.eq_ignore_ascii_case("LCSC") || k.eq_ignore_ascii_case("JLCPCB") => lcsc = c[2].to_string(),
                _ => {}
            }
        }
        // bbox from the lib def's points transformed to page coords
        let (mut minx, mut miny, mut maxx, mut maxy) = (f64::INFINITY, f64::INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
        let mut pin_n = 0usize;
        let lib_resolved = libs.get(&lib_id)
            .or_else(|| libs_bare.get(&crate::kicad_self::lib_bare_key(&lib_id)).and_then(|n| libs.get(n)));
        if let Some(lib) = lib_resolved {
            for &(lx, ly) in &lib.pts {
                let (x, y) = xf(lx, ly, px, py, rot, mirror);
                minx = minx.min(x); miny = miny.min(y); maxx = maxx.max(x); maxy = maxy.max(y);
            }
            pin_n = blocks(libs_def(content, &lib_id).as_deref().unwrap_or(""), "pin").len();
        }
        if !minx.is_finite() { minx = px - 2.54; miny = py - 2.54; maxx = px + 2.54; maxy = py + 2.54; }
        gminx = gminx.min(minx); gminy = gminy.min(miny); gmaxx = gmaxx.max(maxx); gmaxy = gmaxy.max(maxy);

        // regions = symbol body + a box around each VISIBLE property text
        let mut regions = vec![[minx, miny, maxx, maxy]];
        for pb in blocks(inst, "property") {
            if pb.contains("(hide yes)") { continue; }
            let txt = nth_quoted(pb, 1).unwrap_or_default();
            if txt.is_empty() || txt == "~" { continue; }
            let a = match at3.captures(pb) { Some(a) => a, None => continue };
            let (tx, ty, trot) = (f(a.get(1)), f(a.get(2)), f(a.get(3)));
            let size = Regex::new(r"\(size\s+([\d.]+)").ok().and_then(|r| r.captures(pb).map(|c| f(c.get(1)))).unwrap_or(1.27);
            let w = txt.chars().count() as f64 * size * 0.9 + 1.2;
            let hh = size * 0.95;
            // justify decides which way the text runs from its anchor
            let (a0, a1) = if pb.contains("(justify right") { (-w, 0.0) }
                else if pb.contains("(justify left") { (0.0, w) }
                else { (-w / 2.0, w / 2.0) };
            let r = if (trot - 90.0).abs() < 1.0 || (trot - 270.0).abs() < 1.0 {
                [tx - hh, ty + a0, tx + hh, ty + a1] // vertical text
            } else {
                [tx + a0, ty - hh, tx + a1, ty + hh]
            };
            gminx = gminx.min(r[0]); gminy = gminy.min(r[1]); gmaxx = gmaxx.max(r[2]); gmaxy = gmaxy.max(r[3]);
            regions.push(r);
        }
        comps.push(Comp { reference, value, footprint, lcsc, description, datasheet, x: px, y: py, bbox: [minx, miny, maxx, maxy], pin_count: pin_n, regions });
    }

    // include wires in the content bounds so the crop isn't too tight
    for w in blocks(&content_wo_lib, "wire") {
        for c in xy.captures_iter(w) { let (x, y) = (f(c.get(1)), f(c.get(2))); gminx = gminx.min(x); gminy = gminy.min(y); gmaxx = gmaxx.max(x); gmaxy = gmaxy.max(y); }
    }
    // hierarchical-sheet symbols: `(sheet (at x y) (size w h) … (property "Sheetname" ..)
    // (property "Sheetfile" "child.kicad_sch"))`. Each is a clickable box.
    let mut sub_sheets = Vec::new();
    let sheet_at = Regex::new(r"\(at\s+([\d.-]+)\s+([\d.-]+)").unwrap();
    let sheet_sz = Regex::new(r"\(size\s+([\d.-]+)\s+([\d.-]+)").unwrap();
    let prop = |blk: &str, key: &str| -> String {
        Regex::new(&format!(r#"\(property\s+"{key}"\s+"([^"]*)""#)).ok()
            .and_then(|r| r.captures(blk).map(|c| c[1].to_string())).unwrap_or_default()
    };
    for blk in blocks(content, "sheet") {
        let file = prop(blk, "Sheetfile");
        if file.is_empty() { continue; } // not a hierarchical sheet symbol
        let (x, y) = match sheet_at.captures(blk) { Some(c) => (f(c.get(1)), f(c.get(2))), None => continue };
        let (w, h) = sheet_sz.captures(blk).map(|c| (f(c.get(1)), f(c.get(2)))).unwrap_or((25.4, 25.4));
        let bbox = [x, y, x + w, y + h];
        gminx = gminx.min(x); gminy = gminy.min(y); gmaxx = gmaxx.max(x + w); gmaxy = gmaxy.max(y + h);
        sub_sheets.push(SubSheet { name: prop(blk, "Sheetname"), file, bbox });
    }

    if !gminx.is_finite() { gminx = 0.0; gminy = 0.0; gmaxx = 100.0; gmaxy = 100.0; }
    let m = 5.0;
    let content_bbox = (gminx - m, gminy - m, (gmaxx - gminx) + 2.0 * m, (gmaxy - gminy) + 2.0 * m);
    Sheet { comps, sub_sheets, content_bbox }
}

/// Number of pins for a lib — pulled from the raw lib def (rough, for the card).
fn libs_def(content: &str, lib_id: &str) -> Option<String> {
    let libblk = blocks(content, "lib_symbols").into_iter().next()?;
    for def in top_symbols(libblk) {
        if quoted(def).as_deref() == Some(lib_id) { return Some(def.to_string()); }
    }
    None
}

fn strip_first(s: &str, tag: &str) -> String {
    if let Some(b) = blocks(s, tag).into_iter().next() { return s.replacen(b, "", 1); }
    s.to_string()
}

/// Build the transparent hover-hotspot overlay (one rect per component).
pub fn build_overlay(sheet: &Sheet) -> String {
    use std::fmt::Write as _;
    let mut s = String::from(r##"<g class="adom-overlay">"##);
    for c in &sheet.comps {
        let [x0, y0, x1, y1] = c.bbox;
        let _ = write!(s, r##"<rect class="adom-comp" x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" data-ref="{}" fill="transparent"/>"##,
            x0, y0, (x1 - x0).max(0.5), (y1 - y0).max(0.5), esc(&c.reference));
    }
    // hierarchical sheet symbols: a clickable box that navigates into the child
    for sh in &sheet.sub_sheets {
        let [x0, y0, x1, y1] = sh.bbox;
        let _ = write!(s, r##"<rect class="adom-sheet-link" x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" data-sheet="{}" data-sheet-name="{}" fill="transparent"/>"##,
            x0, y0, (x1 - x0).max(0.5), (y1 - y0).max(0.5), esc(&sh.file), esc(&sh.name));
    }
    s.push_str("</g>");
    s
}
