//! EAGLE `.lbr` (Fusion 360 Library) parser.
//!
//! XML format. Parts of interest:
//!  - `<deviceset name="...">` — the device's catalog name (closest to MPN).
//!  - `<symbol name="...">` containing `<pin name="..."/>` — symbol-side pin labels.
//!  - `<package name="...">` containing `<smd name="N"/>` and/or `<pad name="N"/>`.
//!  - `<connect gate="..." pin="VDD" pad="8"/>` — the bridge from symbol pin to
//!    footprint pad. Lets us reconstruct a per-pin {number, name} map.

use anyhow::{Context as _, Result};
use std::collections::BTreeMap;
use std::fs;
use std::path::Path;

use super::{Pad, PackageInfo, Part, Pin};

pub fn parse(path: &Path) -> Result<Part> {
    let raw = fs::read_to_string(path).with_context(|| format!("reading {}", path.display()))?;
    // Older EAGLE files (7.x and earlier) carry a `<!DOCTYPE eagle SYSTEM "eagle.dtd">`
    // declaration. roxmltree disallows DTDs by default, so opt in to parsing them.
    let opts = roxmltree::ParsingOptions { allow_dtd: true, ..Default::default() };
    let doc = roxmltree::Document::parse_with_options(&raw, opts).context("parsing EAGLE XML")?;

    let mut deviceset_name: Option<String> = None;
    // Per-symbol: map<pin_name, _>. We only need names for now.
    // Per-package: list<pad_name>.
    let mut package_pads: BTreeMap<String, Vec<String>> = BTreeMap::new();
    let mut package_dims: BTreeMap<String, (Option<f64>, Option<f64>)> = BTreeMap::new();
    // Connections: deviceset -> Vec<(pin_name, pad_number)>.
    let mut connections: Vec<(String, String)> = Vec::new();
    let mut chosen_package: Option<String> = None;

    // Walk every node (DFS, document order).
    for node in doc.descendants() {
        let tag = node.tag_name().name();
        match tag {
            "deviceset" => {
                if deviceset_name.is_none() {
                    if let Some(n) = node.attribute("name") {
                        deviceset_name = Some(n.to_string());
                    }
                }
            }
            "package" => {
                if let Some(name) = node.attribute("name") {
                    let mut pads: Vec<String> = Vec::new();
                    let mut min_x = f64::INFINITY;
                    let mut max_x = f64::NEG_INFINITY;
                    let mut min_y = f64::INFINITY;
                    let mut max_y = f64::NEG_INFINITY;
                    for child in node.descendants() {
                        let ct = child.tag_name().name();
                        if ct == "smd" || ct == "pad" {
                            if let Some(n) = child.attribute("name") {
                                pads.push(n.to_string());
                            }
                            if let (Some(xs), Some(ys)) =
                                (child.attribute("x"), child.attribute("y"))
                            {
                                if let (Ok(x), Ok(y)) = (xs.parse::<f64>(), ys.parse::<f64>()) {
                                    min_x = min_x.min(x);
                                    max_x = max_x.max(x);
                                    min_y = min_y.min(y);
                                    max_y = max_y.max(y);
                                }
                            }
                        }
                    }
                    let bx = if max_x.is_finite() { Some((max_x - min_x).abs()) } else { None };
                    let by = if max_y.is_finite() { Some((max_y - min_y).abs()) } else { None };
                    package_pads.insert(name.to_string(), pads);
                    package_dims.insert(name.to_string(), (bx, by));
                }
            }
            "connect" => {
                if let (Some(pin), Some(pad)) = (node.attribute("pin"), node.attribute("pad")) {
                    connections.push((pin.to_string(), pad.to_string()));
                }
            }
            "device" => {
                // <device name="..." package="..."> — pick the first package referenced.
                if chosen_package.is_none() {
                    if let Some(p) = node.attribute("package") {
                        chosen_package = Some(p.to_string());
                    }
                }
            }
            _ => {}
        }
    }

    // Reconstruct pins: prefer the connect[]'s name+pad mapping. Dedupe on (name, pad).
    let mut pins: Vec<Pin> = Vec::new();
    let mut seen: std::collections::HashSet<(String, String)> = std::collections::HashSet::new();
    for (pin_name, pad_num) in connections.iter() {
        let key = (pad_num.clone(), pin_name.clone());
        if seen.insert(key) {
            pins.push(Pin {
                number: pad_num.clone(),
                name: pin_name.clone(),
                is_stub_name: pin_name.is_empty() || pin_name == pad_num,
            });
        }
    }

    // Choose a package. Prefer the one referenced by the first <device package=...>, else
    // the first declared in document order.
    let pkg_key = chosen_package
        .clone()
        .or_else(|| package_pads.keys().next().cloned());

    let (pads, package) = if let Some(k) = pkg_key {
        let pads_vec = package_pads
            .get(&k)
            .cloned()
            .unwrap_or_default()
            .into_iter()
            .map(|n| Pad { number: n })
            .collect();
        let (bx, by) = package_dims.get(&k).cloned().unwrap_or((None, None));
        (
            pads_vec,
            Some(PackageInfo { raw_name: k, body_x_mm: bx, body_y_mm: by }),
        )
    } else {
        (Vec::new(), None)
    };

    Ok(Part {
        source: "eagle".to_string(),
        source_part_name: deviceset_name,
        pads,
        pins,
        package,
    })
}