//! Fusion 360 Electronics / EAGLE `.brd` board parser.
//!
//! Fusion Electronics is EAGLE underneath: `fusion_export_eagle_source` writes a
//! plain-XML `.brd`, so one parser covers both EDAs. We build the SAME neutral
//! `Board` model the KiCad parser produces, which means net highlighting, pad
//! hover, the overlay and the embed all work unchanged.
//!
//! Coordinates: EAGLE XML is already in **mm**, but its Y axis points UP while
//! ours (like KiCad's) points DOWN — so every Y is negated on the way in. Angles
//! are counter-clockwise in EAGLE, clockwise for us, hence the rotation negation.
//!
//! Unlike KiCad there is no native SVG export to sit under the overlay (Fusion
//! has no headless renderer), so these boards use our own self-render — the same
//! one that already backstops KiCad when service-kicad is down.

use crate::pcb_render::{Arc, Board, Comp, Gfx, Pad, Track, Via, ZonePoly};
use roxmltree::{Document, Node};
use std::collections::HashMap;
use std::fmt::Write as _;

pub fn looks_like_eagle(text: &str) -> bool {
    let head = &text[..text.len().min(4096)];
    head.contains("<eagle") && text.contains("<board")
}

fn at(n: Node, k: &str) -> String { n.attribute(k).unwrap_or("").to_string() }
fn num(n: Node, k: &str) -> f64 { n.attribute(k).and_then(|v| v.parse().ok()).unwrap_or(0.0) }

/// EAGLE layer number -> our layer bucket.
fn layer_of(n: i32) -> &'static str {
    match n {
        1 => "F.Cu",
        16 => "B.Cu",
        2..=15 => "In.Cu",
        17 | 18 => "F.Cu",         // Pads / Vias layers
        20 => "Edge",              // Dimension
        21 | 25 => "F.SilkS",      // tPlace / tNames
        22 | 26 => "B.SilkS",      // bPlace / bNames
        27 => "F.SilkS",           // tValues
        28 => "B.SilkS",
        39 | 41 => "F.CrtYd",      // tKeepout / tRestrict
        40 | 42 => "B.CrtYd",
        51 => "F.Fab",             // tDocu
        52 => "B.Fab",
        _ => "",
    }
}

/// EAGLE rotation strings: "R90", "MR90" (mirrored), "SR90" (spin).
fn rot_of(s: &str) -> (f64, bool) {
    let mirror = s.contains('M');
    let deg = s.trim_start_matches(['M', 'S', 'R'])
        .parse::<f64>()
        .unwrap_or_else(|_| s.chars().filter(|c| c.is_ascii_digit() || *c == '.' || *c == '-').collect::<String>().parse().unwrap_or(0.0));
    (deg, mirror)
}

/// Place a package-local point into board coordinates.
/// Mirroring flips X in the package frame (EAGLE mirrors about the Y axis).
fn place(px: f64, py: f64, ex: f64, ey: f64, deg: f64, mirror: bool) -> (f64, f64) {
    let x = if mirror { -px } else { px };
    let r = deg.to_radians();
    let (s, c) = (r.sin(), r.cos());
    let (rx, ry) = (x * c - py * s, x * s + py * c);
    // negate Y last: EAGLE is Y-up, we are Y-down
    (ex + rx, -(ey + ry))
}

/// An arc from (x1,y1) to (x2,y2) with EAGLE's `curve` (included angle, degrees).
fn curve_path(x1: f64, y1: f64, x2: f64, y2: f64, curve: f64) -> String {
    let (dx, dy) = (x2 - x1, y2 - y1);
    let chord = (dx * dx + dy * dy).sqrt();
    if chord < 1e-9 || curve.abs() < 1e-9 {
        return format!("M {:.4} {:.4} L {:.4} {:.4}", x1, y1, x2, y2);
    }
    let half = (curve.abs().to_radians()) / 2.0;
    let r = (chord / 2.0) / half.sin().max(1e-9);
    let large = if curve.abs() > 180.0 { 1 } else { 0 };
    // EAGLE curve is CCW-positive; our Y is already flipped, which reverses the
    // apparent sweep, so a positive curve draws with sweep=1 here.
    let sweep = if curve > 0.0 { 1 } else { 0 };
    format!("M {:.4} {:.4} A {:.4} {:.4} 0 {} {} {:.4} {:.4}", x1, y1, r.abs(), r.abs(), large, sweep, x2, y2)
}

struct Pkg<'a, 'i> { nodes: Vec<Node<'a, 'i>> }

pub fn parse_brd(xml: &str) -> Result<Board, String> {
    let opts = roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() };
    let doc = Document::parse_with_options(xml, opts).map_err(|e| format!("eagle .brd: {e}"))?;
    let root = doc.root_element();
    let board = root.descendants().find(|n| n.has_tag_name("board")).ok_or("eagle: no <board>")?;
    let mut b = Board::default();

    // ── packages, keyed "library:package" ────────────────────────────────────
    let mut pkgs: HashMap<String, Pkg> = HashMap::new();
    for lib in board.descendants().filter(|n| n.has_tag_name("library")) {
        let lname = at(lib, "name");
        for p in lib.descendants().filter(|n| n.has_tag_name("package")) {
            pkgs.insert(format!("{}:{}", lname, at(p, "name")), Pkg { nodes: p.children().filter(|c| c.is_element()).collect() });
        }
    }

    // ── nets: one id per <signal>, plus (element,pad) -> net ─────────────────
    let mut pad_net: HashMap<(String, String), u32> = HashMap::new();
    let signals: Vec<Node> = board.descendants().filter(|n| n.has_tag_name("signal")).collect();
    for (i, sig) in signals.iter().enumerate() {
        let id = (i + 1) as u32;
        b.nets.insert(id, at(*sig, "name"));
        for cr in sig.children().filter(|c| c.has_tag_name("contactref")) {
            pad_net.insert((at(cr, "element"), at(cr, "pad")), id);
        }
    }

    // ── routing inside signals: wires (tracks/arcs), vias, copper polygons ───
    for (i, sig) in signals.iter().enumerate() {
        let net = (i + 1) as u32;
        for w in sig.children().filter(|c| c.has_tag_name("wire")) {
            let layer = layer_of(num(w, "layer") as i32);
            if !layer.ends_with(".Cu") { continue; }
            let (x1, y1) = (num(w, "x1"), -num(w, "y1"));
            let (x2, y2) = (num(w, "x2"), -num(w, "y2"));
            let width = num(w, "width");
            let curve = num(w, "curve");
            if curve.abs() > 1e-9 {
                b.arcs.push(Arc { d: curve_path(x1, y1, x2, y2, -curve), layer: layer.into(), width, net });
            } else {
                b.tracks.push(Track { x1, y1, x2, y2, width, layer: layer.into(), net });
            }
        }
        for v in sig.children().filter(|c| c.has_tag_name("via")) {
            let drill = num(v, "drill");
            let size = if num(v, "diameter") > 0.0 { num(v, "diameter") } else { drill + 0.5 };
            b.vias.push(Via { x: num(v, "x"), y: -num(v, "y"), size, drill, net });
        }
        for poly in sig.children().filter(|c| c.has_tag_name("polygon")) {
            let layer = layer_of(num(poly, "layer") as i32);
            if !layer.ends_with(".Cu") { continue; }
            let pts: Vec<(f64, f64)> = poly.children().filter(|c| c.has_tag_name("vertex"))
                .map(|v| (num(v, "x"), -num(v, "y"))).collect();
            if pts.len() < 3 { continue; }
            let mut d = String::new();
            for (i, (x, y)) in pts.iter().enumerate() {
                let _ = write!(d, "{} {:.4} {:.4} ", if i == 0 { "M" } else { "L" }, x, y);
            }
            d.push('Z');
            b.zones.push(ZonePoly { d, layer: layer.into(), net });
        }
    }

    // ── board outline + free graphics from <plain> ───────────────────────────
    for n in board.children().filter(|c| c.has_tag_name("plain")).flat_map(|p| p.children()) {
        if !n.is_element() { continue; }
        push_plain(&mut b, n);
    }

    // ── elements: place package geometry, resolve pads ───────────────────────
    for el in board.descendants().filter(|n| n.has_tag_name("element")) {
        let name = at(el, "name");
        let (ex, ey) = (num(el, "x"), num(el, "y"));
        let (deg, mirror) = rot_of(&at(el, "rot"));
        let key = format!("{}:{}", at(el, "library"), at(el, "package"));
        let side = if mirror { 'B' } else { 'F' };
        let mut pad_count = 0usize;
        // bbox is accumulated from PLACED points as we go — never by re-parsing
        // numbers back out of a path `d` (arc radii would be read as coordinates)
        let mut ext: Vec<(f64, f64)> = Vec::new();

        if let Some(pkg) = pkgs.get(&key) {
            for n in &pkg.nodes {
                let n = *n;
                match n.tag_name().name() {
                    // THT pad
                    "pad" => {
                        let (x, y) = place(num(n, "x"), num(n, "y"), ex, ey, deg, mirror);
                        let dia = if num(n, "diameter") > 0.0 { num(n, "diameter") } else { num(n, "drill") * 1.8 };
                        let shape = match at(n, "shape").as_str() {
                            "square" => "rect", "octagon" => "roundrect", "long" => "oval", _ => "circle",
                        };
                        let (w, h) = if shape == "oval" { (dia * 2.0, dia) } else { (dia, dia) };
                        let numname = at(n, "name");
                        let net = *pad_net.get(&(name.clone(), numname.clone())).unwrap_or(&0);
                        ext.push((x - w / 2.0, y - h / 2.0)); ext.push((x + w / 2.0, y + h / 2.0));
                        pad_count += 1;
                        b.pads.push(Pad {
                            reference: name.clone(), num: numname, net, x, y,
                            rot: -(deg + num(n, "rot")), w, h,
                            shape: shape.into(), drill: num(n, "drill"), mount: "thru_hole".into(), side: 'F',
                        });
                    }
                    // SMD pad
                    "smd" => {
                        let (x, y) = place(num(n, "x"), num(n, "y"), ex, ey, deg, mirror);
                        let numname = at(n, "name");
                        let net = *pad_net.get(&(name.clone(), numname.clone())).unwrap_or(&0);
                        let lay = num(n, "layer") as i32;
                        let pside = if (lay == 16) != mirror { 'B' } else { 'F' };
                        let shape = if num(n, "roundness") > 0.0 { "roundrect" } else { "rect" };
                        let (pw, ph) = (num(n, "dx"), num(n, "dy"));
                        ext.push((x - pw / 2.0, y - ph / 2.0)); ext.push((x + pw / 2.0, y + ph / 2.0));
                        pad_count += 1;
                        b.pads.push(Pad {
                            reference: name.clone(), num: numname, net, x, y,
                            rot: -(deg + num(n, "rot")), w: pw, h: ph,
                            shape: shape.into(), drill: 0.0, mount: "smd".into(), side: pside,
                        });
                    }
                    _ => {
                        if let Some((g, layer, pts)) = gfx_of(n, ex, ey, deg, mirror) {
                            ext.extend(pts);
                            push_layer(&mut b, layer, g, side);
                        }
                    }
                }
            }
        }

        let bbox = if ext.is_empty() {
            [ex - 0.5, -ey - 0.5, ex + 0.5, -ey + 0.5]
        } else {
            let (mut mnx, mut mny, mut mxx, mut mxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
            for (x, y) in &ext { mnx = mnx.min(*x); mxx = mxx.max(*x); mny = mny.min(*y); mxy = mxy.max(*y); }
            [mnx, mny, mxx, mxy]
        };
        b.comps.push(Comp {
            reference: name,
            value: at(el, "value"),
            footprint: at(el, "package"),
            side: if side == 'B' { "bottom".into() } else { "top".into() },
            mpn: String::new(),
            lcsc: String::new(),
            x: ex, y: -ey,
            bbox,
            pad_count,
        });
    }

    Ok(b)
}

/// Free graphics on the board itself (outline on layer 20, silk, etc).
fn push_plain(b: &mut Board, n: Node) {
    if let Some((g, layer, _)) = gfx_of(n, 0.0, 0.0, 0.0, false) {
        push_layer(b, layer, g, 'F');
    }
}

fn push_layer(b: &mut Board, layer: &str, g: Gfx, side: char) {
    match layer {
        "Edge" => b.edges.push(g),
        "F.SilkS" => { if side == 'B' { b.silk_b.push(g) } else { b.silk_f.push(g) } }
        "B.SilkS" => { if side == 'B' { b.silk_f.push(g) } else { b.silk_b.push(g) } }
        "F.Fab" => { if side == 'B' { b.fab_b.push(g) } else { b.fab_f.push(g) } }
        "B.Fab" => { if side == 'B' { b.fab_f.push(g) } else { b.fab_b.push(g) } }
        "F.CrtYd" => { if side == 'B' { b.crtyd_b.push(g) } else { b.crtyd_f.push(g) } }
        "B.CrtYd" => { if side == 'B' { b.crtyd_f.push(g) } else { b.crtyd_b.push(g) } }
        _ => {}
    }
}

/// Turn one EAGLE graphic element into an SVG path in board coordinates.
fn gfx_of(n: Node, ex: f64, ey: f64, deg: f64, mirror: bool) -> Option<(Gfx, &'static str, Vec<(f64, f64)>)> {
    let layer = layer_of(num(n, "layer") as i32);
    if layer.is_empty() || layer.ends_with(".Cu") { return None; }
    let p = |x: f64, y: f64| place(x, y, ex, ey, deg, mirror);
    match n.tag_name().name() {
        "wire" => {
            let (x1, y1) = p(num(n, "x1"), num(n, "y1"));
            let (x2, y2) = p(num(n, "x2"), num(n, "y2"));
            let curve = num(n, "curve");
            let d = if curve.abs() > 1e-9 { curve_path(x1, y1, x2, y2, -curve) }
                    else { format!("M {:.4} {:.4} L {:.4} {:.4}", x1, y1, x2, y2) };
            Some((Gfx { d, width: num(n, "width").max(0.05), closed_fill: false }, layer, vec![(x1, y1), (x2, y2)]))
        }
        "circle" => {
            let (cx, cy) = p(num(n, "x"), num(n, "y"));
            let r = num(n, "radius");
            let d = format!("M {:.4} {:.4} A {r:.4} {r:.4} 0 1 0 {:.4} {:.4} A {r:.4} {r:.4} 0 1 0 {:.4} {:.4} Z",
                cx - r, cy, cx + r, cy, cx - r, cy);
            Some((Gfx { d, width: num(n, "width").max(0.05), closed_fill: false }, layer, vec![(cx - r, cy - r), (cx + r, cy + r)]))
        }
        "rectangle" => {
            let (x1, y1) = (num(n, "x1"), num(n, "y1"));
            let (x2, y2) = (num(n, "x2"), num(n, "y2"));
            let corners = [(x1, y1), (x2, y1), (x2, y2), (x1, y2)];
            let mut d = String::new();
            let mut pts = Vec::new();
            for (i, (x, y)) in corners.iter().enumerate() {
                let (px, py) = p(*x, *y);
                pts.push((px, py));
                let _ = write!(d, "{} {:.4} {:.4} ", if i == 0 { "M" } else { "L" }, px, py);
            }
            d.push('Z');
            Some((Gfx { d, width: 0.0, closed_fill: true }, layer, pts))
        }
        "polygon" => {
            let pts: Vec<(f64, f64)> = n.children().filter(|c| c.has_tag_name("vertex"))
                .map(|v| p(num(v, "x"), num(v, "y"))).collect();
            if pts.len() < 3 { return None; }
            let mut d = String::new();
            for (i, (x, y)) in pts.iter().enumerate() {
                let _ = write!(d, "{} {:.4} {:.4} ", if i == 0 { "M" } else { "L" }, x, y);
            }
            d.push('Z');
            Some((Gfx { d, width: num(n, "width"), closed_fill: true }, layer, pts))
        }
        _ => None, // <text>, <hole>, <dimension>, <attribute> — not stroke geometry
    }
}