//! EAGLE / Fusion 360 `.lbr` (XML) → KiCad `.kicad_sym` + `.kicad_mod`.
//!
//! Fusion 360 inherited EAGLE's XML library format, so one parser covers both.
//! Coordinates in the EAGLE XML are millimetres (KiCad's unit too); EAGLE's Y
//! axis points UP while KiCad footprints point DOWN, so we negate Y.
//!
//! This is a faithful native conversion — no KiCad install, no service round-trip
//! — so each format renders its OWN geometry (export drift between formats shows).

use anyhow::{anyhow, Result};
use regex::Regex;
use std::collections::HashMap;

fn attr(tag: &str, name: &str) -> Option<String> {
    let re = Regex::new(&format!(r#"{name}="([^"]*)""#)).ok()?;
    re.captures(tag).map(|c| c[1].to_string())
}
fn fattr(tag: &str, name: &str) -> Option<f64> {
    attr(tag, name).and_then(|v| v.trim().parse().ok())
}
/// EAGLE rotation string "R90" / "MR180" → degrees (mirror flag ignored here).
fn rot_deg(tag: &str) -> f64 {
    attr(tag, "rot")
        .and_then(|r| r.trim_start_matches(['R', 'M', 'S']).parse::<f64>().ok())
        .unwrap_or(0.0)
}

/// Extract the first `<TAG ...>...</TAG>` (or the whole tag) block by name.
fn block<'a>(xml: &'a str, tag: &str) -> Option<&'a str> {
    let open = format!("<{tag}");
    let start = xml.find(&open)?;
    let close = format!("</{tag}>");
    let end = xml[start..].find(&close).map(|e| start + e + close.len())?;
    Some(&xml[start..end])
}

/// pin-name → pad-number, from the deviceset `<connect .../>` rows.
fn connect_map(xml: &str) -> HashMap<String, String> {
    let mut m = HashMap::new();
    for c in Regex::new(r"<connect\b[^>]*/?>").unwrap().find_iter(xml) {
        let t = c.as_str();
        if let (Some(pin), Some(pad)) = (attr(t, "pin"), attr(t, "pad")) {
            m.insert(pin, pad);
        }
    }
    m
}

/// Convert a `.lbr` to (kicad_sym, kicad_mod). Either may be empty if the
/// library has no symbol / no package.
pub fn convert(lbr: &str, name: &str) -> Result<(String, String)> {
    if !lbr.contains("<eagle") {
        return Err(anyhow!("not an EAGLE/Fusion .lbr (no <eagle> root)"));
    }
    let kicad_mod = block(lbr, "package").map(|p| package_to_mod(p, name)).unwrap_or_default();
    let kicad_sym = block(lbr, "symbol").map(|s| symbol_to_sym(s, name, &connect_map(lbr))).unwrap_or_default();
    Ok((kicad_sym, kicad_mod))
}

// ------------------------------------------------------------------------
// Native SVG renderers — draw straight from the EAGLE geometry, the same way
// altium-pcblib/altium-schlib do, so the .lbr renders its OWN geometry with no
// KiCad round-trip. (EAGLE coords are mm, Y-up → negate Y to screen space.)
// ------------------------------------------------------------------------

fn xesc(s: &str) -> String { s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;") }

/// Render the EAGLE `<package>` (footprint) to a standalone SVG: SMD + THT pads
/// plus silk/fab wires. Returns "" if the library has no package.
pub fn render_footprint_svg(lbr: &str, name: &str) -> String {
    let Some(pkg) = block(lbr, "package") else { return String::new() };
    // Collect pad rects (x,y,w,h) and silk lines, computing the bbox as we go.
    let mut rects: Vec<(f64, f64, f64, f64, String)> = Vec::new();
    let mut lines: Vec<(f64, f64, f64, f64, f64, String)> = Vec::new();
    let (mut minx, mut miny, mut maxx, mut maxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
    let mut bump = |x: f64, y: f64, hw: f64, hh: f64, mnx: &mut f64, mny: &mut f64, mxx: &mut f64, mxy: &mut f64| {
        *mnx = mnx.min(x - hw); *mny = mny.min(y - hh); *mxx = mxx.max(x + hw); *mxy = mxy.max(y + hh);
    };
    for m in Regex::new(r"<smd\b[^>]*/?>").unwrap().find_iter(pkg) {
        let t = m.as_str();
        let (Some(x), Some(y), Some(dx), Some(dy)) = (fattr(t, "x"), fattr(t, "y"), fattr(t, "dx"), fattr(t, "dy")) else { continue };
        let n = attr(t, "name").unwrap_or_default();
        let (sy, hw, hh) = (-y, dx / 2.0, dy / 2.0);
        bump(x, sy, hw, hh, &mut minx, &mut miny, &mut maxx, &mut maxy);
        rects.push((x - hw, sy - hh, dx, dy, n));
    }
    for m in Regex::new(r"<pad\b[^>]*/?>").unwrap().find_iter(pkg) {
        let t = m.as_str();
        let (Some(x), Some(y)) = (fattr(t, "x"), fattr(t, "y")) else { continue };
        let n = attr(t, "name").unwrap_or_default();
        let d = fattr(t, "diameter").unwrap_or_else(|| fattr(t, "drill").map(|dr| dr + 0.5).unwrap_or(1.5));
        let (sy, hw) = (-y, d / 2.0);
        bump(x, sy, hw, hw, &mut minx, &mut miny, &mut maxx, &mut maxy);
        rects.push((x - hw, sy - hw, d, d, n));
    }
    for m in Regex::new(r"<wire\b[^>]*/?>").unwrap().find_iter(pkg) {
        let t = m.as_str();
        let (Some(x1), Some(y1), Some(x2), Some(y2)) = (fattr(t, "x1"), fattr(t, "y1"), fattr(t, "x2"), fattr(t, "y2")) else { continue };
        let layer = attr(t, "layer").unwrap_or_default();
        let col = match layer.as_str() { "21" | "22" => "#cfd8dc", "51" | "52" => "#6b7785", _ => continue };
        lines.push((x1, -y1, x2, -y2, fattr(t, "width").filter(|w| *w > 0.0).unwrap_or(0.12), col.into()));
    }
    if !minx.is_finite() { return String::new(); }
    let pad = 1.0;
    let (vx, vy, vw, vh) = (minx - pad, miny - pad, (maxx - minx) + 2.0 * pad, (maxy - miny) + 2.0 * pad);
    let mut s = format!(
        r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{vx:.3} {vy:.3} {vw:.3} {vh:.3}" width="{:.0}" height="{:.0}" font-family="monospace">"#,
        vw * 50.0, vh * 50.0
    );
    s += &format!(r##"<rect x="{vx:.3}" y="{vy:.3}" width="{vw:.3}" height="{vh:.3}" fill="#0d1117"/>"##);
    for (x1, y1, x2, y2, w, col) in &lines {
        s += &format!(r#"<line x1="{x1:.3}" y1="{y1:.3}" x2="{x2:.3}" y2="{y2:.3}" stroke="{col}" stroke-width="{w:.3}"/>"#);
    }
    for (x, y, w, h, n) in &rects {
        s += &format!(r##"<rect x="{x:.3}" y="{y:.3}" width="{w:.3}" height="{h:.3}" rx="0.05" fill="#ff9a4a" stroke="#7a3c10" stroke-width="0.03"/>"##);
        s += &format!(r##"<text x="{:.3}" y="{:.3}" font-size="{:.2}" text-anchor="middle" dominant-baseline="middle" fill="#111">{}</text>"##,
            x + w / 2.0, y + h / 2.0, (w.min(*h) * 0.55).clamp(0.2, 0.6), xesc(n));
    }
    s += &format!(r##"<text x="{:.3}" y="{:.3}" font-size="0.6" fill="#00e6dc">{} (EAGLE)</text>"##, vx + 0.3, vy + 0.9, xesc(name));
    s += "</svg>\n";
    s
}

/// Render the EAGLE `<symbol>` to a standalone SVG: pins (with names + pad
/// numbers from `<connect>`) plus the body wires. Returns "" if no symbol.
pub fn render_symbol_svg(lbr: &str, name: &str) -> String {
    let Some(sym) = block(lbr, "symbol") else { return String::new() };
    let connects = connect_map(lbr);
    let (mut minx, mut miny, mut maxx, mut maxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
    struct P { x: f64, y: f64, len: f64, rot: f64, name: String, num: String }
    let mut pins: Vec<P> = Vec::new();
    for m in Regex::new(r"<pin\b[^>]*/?>").unwrap().find_iter(sym) {
        let t = m.as_str();
        let (Some(pname), Some(x), Some(y)) = (attr(t, "name"), fattr(t, "x"), fattr(t, "y")) else { continue };
        let len = match attr(t, "length").as_deref() { Some("point") => 0.0, Some("short") => 2.54, Some("long") => 7.62, _ => 5.08 };
        let num = connects.get(&pname).cloned().unwrap_or_else(|| pname.clone());
        let (sx, sy) = (x, -y);
        minx = minx.min(sx); maxx = maxx.max(sx); miny = miny.min(sy); maxy = maxy.max(sy);
        pins.push(P { x: sx, y: sy, len, rot: rot_deg(t), name: pname, num });
    }
    let mut lines: Vec<(f64, f64, f64, f64, f64)> = Vec::new();
    for m in Regex::new(r"<wire\b[^>]*/?>").unwrap().find_iter(sym) {
        let t = m.as_str();
        let (Some(x1), Some(y1), Some(x2), Some(y2)) = (fattr(t, "x1"), fattr(t, "y1"), fattr(t, "x2"), fattr(t, "y2")) else { continue };
        for (vx, vy) in [(x1, -y1), (x2, -y2)] { minx = minx.min(vx); maxx = maxx.max(vx); miny = miny.min(vy); maxy = maxy.max(vy); }
        lines.push((x1, -y1, x2, -y2, fattr(t, "width").filter(|w| *w > 0.0).unwrap_or(0.15)));
    }
    if pins.is_empty() && lines.is_empty() { return String::new(); }
    let pad = 4.0;
    let (vx, vy, vw, vh) = (minx - pad, miny - pad, (maxx - minx) + 2.0 * pad, (maxy - miny) + 2.0 * pad);
    let mut s = format!(
        r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{vx:.3} {vy:.3} {vw:.3} {vh:.3}" width="{:.0}" height="{:.0}" font-family="monospace">"#,
        vw * 14.0, vh * 14.0
    );
    s += &format!(r##"<rect x="{vx:.3}" y="{vy:.3}" width="{vw:.3}" height="{vh:.3}" fill="#0d1117"/>"##);
    for (x1, y1, x2, y2, w) in &lines {
        s += &format!(r##"<line x1="{x1:.3}" y1="{y1:.3}" x2="{x2:.3}" y2="{y2:.3}" stroke="#00b8b1" stroke-width="{w:.3}"/>"##);
    }
    for p in &pins {
        // pin extends from (x,y) outward along its rotation; R0 points right.
        let (dx, dy) = match ((p.rot / 90.0).round() as i64).rem_euclid(4) {
            0 => (p.len, 0.0), 1 => (0.0, -p.len), 2 => (-p.len, 0.0), _ => (0.0, p.len),
        };
        let (ex, ey) = (p.x + dx, p.y + dy);
        s += &format!(r##"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="#7cb6ff" stroke-width="0.15"/>"##, p.x, p.y, ex, ey);
        s += &format!(r##"<text x="{:.3}" y="{:.3}" font-size="1.2" fill="#e6edf3" text-anchor="middle">{}</text>"##, ex, ey - 0.6, xesc(&p.name));
        s += &format!(r##"<text x="{:.3}" y="{:.3}" font-size="0.9" fill="#8b949e" text-anchor="middle">{}</text>"##, ex, ey + 1.4, xesc(&p.num));
    }
    s += &format!(r##"<text x="{:.3}" y="{:.3}" font-size="1.6" fill="#00e6dc">{} (EAGLE, {} pins)</text>"##, vx + 0.5, vy + 2.0, xesc(name), pins.len());
    s += "</svg>\n";
    s
}

/// EAGLE `<package>` → KiCad `.kicad_mod` (SMD + THT pads + silk lines).
fn package_to_mod(pkg: &str, name: &str) -> String {
    // Collect pads + silk/fab wires FIRST, tracking the footprint's Y extent in
    // KiCad space, so RefDes/Value land CLEAR of the whole footprint — matching
    // adom-footprint's fp_gen rule (RefDes ~1 mm above the top, Value ~1 mm below
    // the bottom). The old hardcoded `(at 0 ±3)` sat inside the pad ring of larger
    // parts (e.g. an LQFP-64 whose pads reach ±5.9 mm), smearing text over copper.
    let mut geom = String::new();
    let (mut miny, mut maxy) = (f64::MAX, f64::MIN);
    let mut track = |ya: f64, yb: f64| {
        miny = miny.min(ya).min(yb);
        maxy = maxy.max(ya).max(yb);
    };
    // SMD pads
    for m in Regex::new(r"<smd\b[^>]*/?>").unwrap().find_iter(pkg) {
        let t = m.as_str();
        let (Some(n), Some(x), Some(y), Some(dx), Some(dy)) =
            (attr(t, "name"), fattr(t, "x"), fattr(t, "y"), fattr(t, "dx"), fattr(t, "dy")) else { continue };
        let r = rot_deg(t);
        let round = fattr(t, "roundness").unwrap_or(0.0) / 100.0;
        let (shape, extra) = if round > 0.0 {
            ("roundrect".to_string(), format!(" (roundrect_rratio {:.3})", (round / 2.0).min(0.5)))
        } else { ("rect".to_string(), String::new()) };
        let ky = -y;
        // Rotation-agnostic conservative half-extent (a 90°-rotated pad swaps dx/dy).
        let half = dx.max(dy) / 2.0;
        track(ky - half, ky + half);
        let at = if r != 0.0 { format!("{x:.4} {ky:.4} {r}") } else { format!("{x:.4} {ky:.4}") };
        geom += &format!(
            "  (pad \"{n}\" smd {shape} (at {at}) (size {dx:.4} {dy:.4}) (layers \"F.Cu\" \"F.Paste\" \"F.Mask\"){extra})\n"
        );
    }
    // THT pads
    for m in Regex::new(r"<pad\b[^>]*/?>").unwrap().find_iter(pkg) {
        let t = m.as_str();
        let (Some(n), Some(x), Some(y)) = (attr(t, "name"), fattr(t, "x"), fattr(t, "y")) else { continue };
        let drill = fattr(t, "drill").unwrap_or(0.6);
        let dia = fattr(t, "diameter").unwrap_or(drill + 0.5);
        let ky = -y;
        track(ky - dia / 2.0, ky + dia / 2.0);
        geom += &format!(
            "  (pad \"{n}\" thru_hole circle (at {x:.4} {ky:.4}) (size {dia:.4} {dia:.4}) (drill {drill:.4}) (layers \"*.Cu\" \"*.Mask\"))\n"
        );
    }
    // Silk / fab wires (layer 21 = tPlace silk, 51 = tDocu fab).
    for m in Regex::new(r"<wire\b[^>]*/?>").unwrap().find_iter(pkg) {
        let t = m.as_str();
        let (Some(x1), Some(y1), Some(x2), Some(y2)) =
            (fattr(t, "x1"), fattr(t, "y1"), fattr(t, "x2"), fattr(t, "y2")) else { continue };
        let layer = attr(t, "layer").unwrap_or_default();
        let kl = match layer.as_str() { "21" | "22" => "F.SilkS", "51" | "52" => "F.Fab", _ => continue };
        let w = fattr(t, "width").filter(|w| *w > 0.0).unwrap_or(0.12);
        let (ky1, ky2) = (-y1, -y2);
        track(ky1, ky2);
        geom += &format!(
            "  (fp_line (start {x1:.4} {ky1:.4}) (end {x2:.4} {ky2:.4}) (stroke (width {w:.3}) (type solid)) (layer \"{kl}\"))\n"
        );
    }
    // RefDes/Value clear of the full Y extent (+1 mm); fall back to ±3 if empty.
    let r2 = |v: f64| (v * 100.0).round() / 100.0;
    let (ref_y, val_y) = if miny <= maxy { (r2(miny - 1.0), r2(maxy + 1.0)) } else { (-3.0, 3.0) };
    let mut out = format!(
        "(footprint \"{name}\" (version 20221018) (generator adom-sfconvert) (layer \"F.Cu\")\n  \
         (attr smd)\n  \
         (fp_text reference \"REF**\" (at 0 {ref_y}) (layer \"F.SilkS\") (effects (font (size 1 1) (thickness 0.15))))\n  \
         (fp_text value \"{name}\" (at 0 {val_y}) (layer \"F.Fab\") (effects (font (size 1 1) (thickness 0.15))))\n"
    );
    out += &geom;
    out += ")\n";
    out
}

/// Re-place a `.kicad_mod`'s RefDes/Value text CLEAR of the whole footprint —
/// applies the SAME one placement rule as adom-footprint's fp_gen (RefDes ~1 mm
/// above the top extent, Value ~1 mm below) to a `.kicad_mod` we didn't generate
/// ourselves, e.g. the `altium_pcblib` crate's output, which hardcodes `(at 0 ±3)`
/// (smeared over the pad ring of larger parts). Rewrites only the FIRST
/// `(fp_text reference …)` / `(fp_text value …)` `(at …)`; any other text is left
/// alone. No-op when the mod has no pad/line geometry to measure.
pub fn reposition_fp_text(km: &str) -> String {
    let pad = Regex::new(r"\(pad\b[^()]*\(at -?[\d.]+ (-?[\d.]+)(?: -?[\d.]+)?\) \(size -?[\d.]+ (-?[\d.]+)\)").unwrap();
    let line = Regex::new(r"\(start -?[\d.]+ (-?[\d.]+)\) \(end -?[\d.]+ (-?[\d.]+)\)").unwrap();
    let (mut miny, mut maxy) = (f64::MAX, f64::MIN);
    for c in pad.captures_iter(km) {
        let y: f64 = c[1].parse().unwrap_or(0.0);
        let dy: f64 = c[2].parse().unwrap_or(0.0);
        miny = miny.min(y - dy / 2.0);
        maxy = maxy.max(y + dy / 2.0);
    }
    for c in line.captures_iter(km) {
        let y1: f64 = c[1].parse().unwrap_or(0.0);
        let y2: f64 = c[2].parse().unwrap_or(0.0);
        miny = miny.min(y1).min(y2);
        maxy = maxy.max(y1).max(y2);
    }
    if miny > maxy {
        return km.to_string();
    }
    let r2 = |v: f64| (v * 100.0).round() / 100.0;
    let (ref_y, val_y) = (r2(miny - 1.0), r2(maxy + 1.0));
    let mut out = km.to_string();
    for (kind, y) in [("reference", ref_y), ("value", val_y)] {
        let re = Regex::new(&format!(
            r"(\(fp_text {kind}\b[^()]*\(at )-?[\d.]+ -?[\d.]+(?: -?[\d.]+)?(\))"
        )).unwrap();
        let rep = format!("${{1}}0 {y}${{2}}");
        out = re.replace(&out, rep.as_str()).into_owned();
    }
    out
}

/// EAGLE `<symbol>` → KiCad `.kicad_sym` (pins + the body rectangle).
fn symbol_to_sym(sym: &str, name: &str, connects: &HashMap<String, String>) -> String {
    let mut pins = String::new();
    let (mut minx, mut miny, mut maxx, mut maxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
    for m in Regex::new(r"<pin\b[^>]*/?>").unwrap().find_iter(sym) {
        let t = m.as_str();
        let (Some(pname), Some(x), Some(y)) = (attr(t, "name"), fattr(t, "x"), fattr(t, "y")) else { continue };
        // EAGLE pin length keyword → mm; the pin's (x,y) is its CONNECTION end.
        let len = match attr(t, "length").as_deref() {
            Some("point") => 0.0, Some("short") => 2.54, Some("long") => 7.62, _ => 5.08,
        };
        let rot = rot_deg(t); // R0 pin body is to the LEFT, pin points right (angle 0 in KiCad means pin extends to the right from its body anchor)
        let num = connects.get(&pname).cloned().unwrap_or_else(|| pname.clone());
        minx = minx.min(x); maxx = maxx.max(x); miny = miny.min(y); maxy = maxy.max(y);
        pins += &format!(
            "      (pin bidirectional line (at {x:.3} {y:.3} {rot}) (length {len:.2})\n        \
             (name \"{}\" (effects (font (size 1.27 1.27)))) (number \"{}\" (effects (font (size 1.27 1.27)))))\n",
            esc(&pname), esc(&num)
        );
    }
    if minx > maxx { return String::new(); }
    let pad = 2.54;
    format!(
        "(kicad_symbol_lib (version 20211014) (generator adom-sfconvert)\n  \
         (symbol \"{name}\" (in_bom yes) (on_board yes)\n    \
         (property \"Reference\" \"U\" (at 0 {:.2} 0) (effects (font (size 1.27 1.27))))\n    \
         (property \"Value\" \"{name}\" (at 0 {:.2} 0) (effects (font (size 1.27 1.27))))\n    \
         (symbol \"{name}_1_1\"\n      \
         (rectangle (start {:.2} {:.2}) (end {:.2} {:.2}) (stroke (width 0.254) (type default)) (fill (type background)))\n{pins}    )\n  )\n)\n",
        maxy + pad + 1.27, miny - pad - 1.27,
        minx + pad, maxy + pad, maxx - pad, miny - pad,
    )
}

fn esc(s: &str) -> String { s.replace('"', "'") }