//! KiCad `.kicad_sym` / `.kicad_mod` **reader** (the import side of the KiCad
//! adapter). Parses the s-expression into the neutral [`AdomLbrPart`].
//!
//! Units: KiCad is millimetres. Symbol pins convert mm → mils (the model's
//! schematic unit, 1 mil = 0.0254 mm); footprint pads stay in mm.
//!
//! v0 scope: pins (number/name/position/rotation) and pads (number/type/shape/
//! position/size/drill). Silk/courtyard graphics import lands with the graphics
//! work — they have nowhere to go in the Altium *writer* yet.

use crate::model::{AdomLbrPart, ConnectEntry, Footprint, FpGraphic, FpLayer, Pad, Pin, Symbol};
use anyhow::{Context, Result};

const MM_PER_MIL: f64 = 0.0254;

// The s-expression parser lives in crate::sexpr (shared, KiCad-parser-inspired:
// line-tracked errors, strict balance checking, version gate). Re-exported here
// for backward compatibility with existing use sites.
pub use crate::sexpr::{parse_root, Sx};


// ---------------------------------------------------------------------------
// Symbol import
// ---------------------------------------------------------------------------

fn import_symbol(text: &str, wanted: &str) -> Result<Symbol> {
    let root = parse_root(text)?;
    // The file's declared format version drives version-scoped semantics
    // (KiCad parser's m_requiredVersion gate).
    let doc_version = root.doc_version();
    // Top-level symbols only — a .kicad_sym is a LIBRARY and may hold many.
    // Everything below must be scoped to the selected one, or a multi-symbol
    // lib bleeds other symbols' properties/graphics into this part.
    let syms: Vec<&Sx> = root
        .items()
        .iter()
        .filter(|s| s.head() == Some("symbol"))
        .collect();
    let sym: &Sx = match syms.len() {
        0 => anyhow::bail!("no (symbol ...) in kicad_sym"),
        1 => syms[0],
        _ => syms
            .iter()
            .find(|s| s.atom_at(1).is_some_and(|n| n.eq_ignore_ascii_case(wanted)))
            .copied()
            .with_context(|| {
                let names: Vec<_> = syms.iter().filter_map(|s| s.atom_at(1)).collect();
                format!(
                    "library holds {} symbols and none is named {wanted:?} — available: {}",
                    syms.len(),
                    names.join(", ")
                )
            })?,
    };
    let name = sym.atom_at(1).unwrap_or("SYM").to_string();

    // designator prefix from the Reference property — of THIS symbol only
    let mut prefix = "U".to_string();
    let mut props = Vec::new();
    sym.collect("property", &mut props);
    for p in &props {
        if p.atom_at(1) == Some("Reference") {
            if let Some(r) = p.atom_at(2) {
                prefix = r.to_string();
            }
        }
    }

    // all pins anywhere under the symbol
    let mut pin_lists = Vec::new();
    sym.collect("pin", &mut pin_lists);
    let mut pins = Vec::new();
    for p in pin_lists {
        let at = p.child("at").map(|a| a.nums()).unwrap_or_default();
        let (xmm, ymm, rot) = (
            at.first().copied().unwrap_or(0.0),
            at.get(1).copied().unwrap_or(0.0),
            at.get(2).copied().unwrap_or(0.0),
        );
        let number = p
            .child("number")
            .and_then(|n| n.atom_at(1))
            .unwrap_or("?")
            .to_string();
        // "~" means "unnamed" only under the legacy (< 20250318) format —
        // gate on the file's declared version, exactly like KiCad's parser.
        let pname = p
            .child("name")
            .and_then(|n| n.atom_at(1))
            .map(|s| crate::sexpr::resolve_tilde(s, doc_version))
            .unwrap_or_default();
        // KiCad pin length is in mm → convert to mils (the model's unit).
        let len_mil = p
            .child("length")
            .and_then(|l| l.nums().first().copied())
            .map(|mm| (mm / MM_PER_MIL).round() as i32);
        pins.push(Pin {
            number,
            name: pname,
            x: (xmm / MM_PER_MIL).round() as i32,
            y: (ymm / MM_PER_MIL).round() as i32,
            length: len_mil,
            rotation: ((rot as i32 % 360 + 360) % 360) as u16,
        });
    }

    // Preserve the source's pin-number / pin-name visibility (KiCad
    // `(pin_numbers (hide yes))`). Absent → shown (KiCad default).
    let has_hide = |key: &str| -> bool {
        match sym.child(key) {
            None => false,
            Some(node) => node
                .child("hide")
                .and_then(|h| h.atom_at(1))
                .map(|v| v == "yes")
                .unwrap_or_else(|| node.items().iter().any(|i| matches!(i, Sx::Atom(a) if a == "hide"))),
        }
    };

    Ok(Symbol {
        name,
        pins,
        body: None,
        graphics: import_symbol_graphics(sym),
        designator_prefix: Some(prefix),
        footprint: None,
        parameters: Vec::new(),
        pin_numbers_hidden: Some(has_hide("pin_numbers")),
        pin_names_hidden: Some(has_hide("pin_names")),
    })
}

/// Import a KiCad symbol's body graphics (polyline / rectangle / circle / arc)
/// into neutral [`Graphic`]s, in mils. Generic — not part-specific.
fn import_symbol_graphics(root: &Sx) -> Vec<crate::model::Graphic> {
    use crate::model::Graphic;
    let to_mil = |mm: f64| mm / MM_PER_MIL;
    let mut out = Vec::new();
    let width_of = |g: &Sx| -> f64 {
        g.child("stroke")
            .and_then(|s| s.child("width"))
            .and_then(|w| w.nums().first().copied())
            .map(to_mil)
            .unwrap_or(6.0)
    };

    let mut polys = Vec::new();
    root.collect("polyline", &mut polys);
    for g in polys {
        if let Some(pts) = g.child("pts") {
            let mut xy = Vec::new();
            let mut pt_lists = Vec::new();
            pts.collect("xy", &mut pt_lists);
            for p in pt_lists {
                let n = p.nums();
                if n.len() >= 2 {
                    xy.push([to_mil(n[0]), to_mil(n[1])]);
                }
            }
            if xy.len() >= 2 {
                out.push(Graphic::Polyline { points: xy, width: width_of(g) });
            }
        }
    }

    // A shape is "filled" when its (fill (type ...)) is not `none` (KiCad fills
    // with `outline` or `background`). Cap plates are filled rectangles.
    let filled_of = |g: &Sx| -> bool {
        g.child("fill")
            .and_then(|f| f.child("type"))
            .and_then(|t| t.atom_at(1))
            .map(|t| t != "none")
            .unwrap_or(false)
    };

    let mut rects = Vec::new();
    root.collect("rectangle", &mut rects);
    for g in rects {
        let s = g.child("start").map(|c| c.nums()).unwrap_or_default();
        let e = g.child("end").map(|c| c.nums()).unwrap_or_default();
        if s.len() >= 2 && e.len() >= 2 {
            out.push(Graphic::Rect {
                x1: to_mil(s[0]), y1: to_mil(s[1]), x2: to_mil(e[0]), y2: to_mil(e[1]),
                width: width_of(g), filled: filled_of(g),
            });
        }
    }

    let mut circs = Vec::new();
    root.collect("circle", &mut circs);
    for g in circs {
        let c = g.child("center").map(|c| c.nums()).unwrap_or_default();
        let r = g.child("radius").and_then(|r| r.nums().first().copied());
        if c.len() >= 2 {
            if let Some(r) = r {
                out.push(Graphic::Circle { cx: to_mil(c[0]), cy: to_mil(c[1]), r: to_mil(r), width: width_of(g) });
            }
        }
    }

    // Arcs — KiCad gives 3 points (start / mid / end); convert to the neutral
    // center + radius + start/end-angle form. Direction is normalized so the
    // Altium CCW sweep from start→end passes through the mid point.
    let mut arcs = Vec::new();
    root.collect("arc", &mut arcs);
    for g in arcs {
        let s = g.child("start").map(|c| c.nums()).unwrap_or_default();
        let m = g.child("mid").map(|c| c.nums()).unwrap_or_default();
        let e = g.child("end").map(|c| c.nums()).unwrap_or_default();
        if s.len() >= 2 && m.len() >= 2 && e.len() >= 2 {
            if let Some((cx, cy, r, sa, ea)) =
                arc_from_3pts(s[0], s[1], m[0], m[1], e[0], e[1])
            {
                out.push(Graphic::Arc {
                    cx: to_mil(cx),
                    cy: to_mil(cy),
                    r: to_mil(r),
                    start: sa,
                    end: ea,
                    width: width_of(g),
                });
            }
        }
    }
    out
}

/// Given three points on an arc (start, mid, end), return
/// `(center_x, center_y, radius, start_angle, end_angle)` with angles in degrees
/// (0..360). The angle order is chosen so a counter-clockwise sweep from the
/// start angle to the end angle (Altium's arc convention) passes through `mid`.
/// Returns `None` if the points are collinear.
fn arc_from_3pts(
    ax: f64, ay: f64, bx: f64, by: f64, cx: f64, cy: f64,
) -> Option<(f64, f64, f64, f64, f64)> {
    let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
    if d.abs() < 1e-9 {
        return None; // collinear → no unique circle
    }
    let a2 = ax * ax + ay * ay;
    let b2 = bx * bx + by * by;
    let c2 = cx * cx + cy * cy;
    let ux = (a2 * (by - cy) + b2 * (cy - ay) + c2 * (ay - by)) / d;
    let uy = (a2 * (cx - bx) + b2 * (ax - cx) + c2 * (bx - ax)) / d;
    let r = ((ax - ux).powi(2) + (ay - uy).powi(2)).sqrt();
    let ang = |x: f64, y: f64| {
        let mut a = (y - uy).atan2(x - ux).to_degrees();
        if a < 0.0 {
            a += 360.0;
        }
        a
    };
    let sa = ang(ax, ay);
    let ma = ang(bx, by);
    let ea = ang(cx, cy);
    // Relative CCW position from the start angle; if the mid isn't inside the
    // start→end CCW sweep, swap the endpoints so it is.
    let rel = |x: f64| {
        let mut v = (x - sa) % 360.0;
        if v < 0.0 {
            v += 360.0;
        }
        v
    };
    let (sa, ea) = if rel(ma) <= rel(ea) { (sa, ea) } else { (ea, sa) };
    Some((ux, uy, r, sa, ea))
}

// ---------------------------------------------------------------------------
// Footprint import
// ---------------------------------------------------------------------------

fn import_footprint(text: &str) -> Result<Footprint> {
    let root = parse_root(text)?;
    // (footprint "NAME" ...)  — name is the first atom after head
    let name = root.atom_at(1).unwrap_or("FP").to_string();

    let mut pad_lists = Vec::new();
    root.collect("pad", &mut pad_lists);
    let mut pads = Vec::new();
    for p in pad_lists {
        let number = p.atom_at(1).unwrap_or("?").to_string();
        let ptype = p.atom_at(2).unwrap_or("smd");
        let shape = p.atom_at(3).unwrap_or("rect").to_string();
        let at = p.child("at").map(|a| a.nums()).unwrap_or_default();
        let size = p.child("size").map(|a| a.nums()).unwrap_or_default();
        let rot = at.get(2).copied().unwrap_or(0.0);
        // (drill d) or (drill oval w h) — take the larger axis of a slot so
        // the hole isn't undersized (full slot support is a future field).
        let drill = p
            .child("drill")
            .map(|d| d.nums().into_iter().fold(0.0_f64, f64::max))
            .unwrap_or(0.0);
        let kshape = match shape.as_str() {
            "circle" => "circle",
            "oval" => "oval",
            "roundrect" => "roundrect",
            _ => "rect",
        };
        // thru_hole AND np_thru_hole both have a real hole; dropping the NPTH
        // drill would silently turn a mounting hole into an SMD copper pad.
        let is_th = ptype.ends_with("thru_hole");
        pads.push(Pad {
            number,
            x_mm: at.first().copied().unwrap_or(0.0),
            y_mm: at.get(1).copied().unwrap_or(0.0),
            w_mm: size.first().copied().unwrap_or(0.5),
            h_mm: size.get(1).copied().unwrap_or(0.5),
            drill_mm: if is_th { drill } else { 0.0 },
            rotation: rot,
            shape: kshape.to_string(),
            plated: if is_th { Some(ptype != "np_thru_hole") } else { None },
        });
    }

    Ok(Footprint { name, pads, graphics: import_footprint_graphics(&root) })
}

/// Map a KiCad footprint layer name to a neutral [`FpLayer`] role. Returns
/// `None` for layers we don't carry (paste/mask/etc.).
fn kicad_fp_layer(layer: &str) -> Option<FpLayer> {
    if layer.contains("SilkS") {
        Some(FpLayer::Silk)
    } else if layer.contains("CrtYd") {
        Some(FpLayer::Courtyard)
    } else if layer.contains("Fab") {
        Some(FpLayer::Fab)
    } else if layer.ends_with(".Cu") {
        Some(FpLayer::Copper)
    } else {
        None
    }
}

/// Import a KiCad footprint's silk/courtyard/fab graphics into neutral
/// [`FpGraphic`]s (mm, Y-down). Covers fp_line / fp_rect / fp_circle / fp_arc.
fn import_footprint_graphics(root: &Sx) -> Vec<FpGraphic> {
    let mut out = Vec::new();
    let layer_of = |g: &Sx| -> Option<FpLayer> {
        g.child("layer")
            .and_then(|l| l.atom_at(1))
            .and_then(kicad_fp_layer)
    };
    let width_of = |g: &Sx| -> f64 {
        g.child("stroke")
            .and_then(|s| s.child("width"))
            .and_then(|w| w.nums().first().copied())
            .or_else(|| g.child("width").and_then(|w| w.nums().first().copied()))
            .unwrap_or(0.12)
    };

    let mut lines = Vec::new();
    root.collect("fp_line", &mut lines);
    for g in lines {
        if let Some(layer) = layer_of(g) {
            let s = g.child("start").map(|c| c.nums()).unwrap_or_default();
            let e = g.child("end").map(|c| c.nums()).unwrap_or_default();
            if s.len() >= 2 && e.len() >= 2 {
                out.push(FpGraphic::Line { layer, x1: s[0], y1: s[1], x2: e[0], y2: e[1], width: width_of(g) });
            }
        }
    }

    let mut rects = Vec::new();
    root.collect("fp_rect", &mut rects);
    for g in rects {
        if let Some(layer) = layer_of(g) {
            let s = g.child("start").map(|c| c.nums()).unwrap_or_default();
            let e = g.child("end").map(|c| c.nums()).unwrap_or_default();
            if s.len() >= 2 && e.len() >= 2 {
                out.push(FpGraphic::Rect { layer, x1: s[0], y1: s[1], x2: e[0], y2: e[1], width: width_of(g) });
            }
        }
    }

    let mut circs = Vec::new();
    root.collect("fp_circle", &mut circs);
    for g in circs {
        if let Some(layer) = layer_of(g) {
            let c = g.child("center").map(|c| c.nums()).unwrap_or_default();
            let e = g.child("end").map(|c| c.nums()).unwrap_or_default();
            if c.len() >= 2 && e.len() >= 2 {
                let r = ((e[0] - c[0]).powi(2) + (e[1] - c[1]).powi(2)).sqrt();
                out.push(FpGraphic::Circle { layer, cx: c[0], cy: c[1], r, width: width_of(g) });
            }
        }
    }

    let mut arcs = Vec::new();
    root.collect("fp_arc", &mut arcs);
    for g in arcs {
        if let Some(layer) = layer_of(g) {
            let s = g.child("start").map(|c| c.nums()).unwrap_or_default();
            let m = g.child("mid").map(|c| c.nums()).unwrap_or_default();
            let e = g.child("end").map(|c| c.nums()).unwrap_or_default();
            if s.len() >= 2 && m.len() >= 2 && e.len() >= 2 {
                if let Some((cx, cy, r, sa, ea)) = arc_from_3pts(s[0], s[1], m[0], m[1], e[0], e[1]) {
                    out.push(FpGraphic::Arc { layer, cx, cy, r, start: sa, end: ea, width: width_of(g) });
                }
            }
        }
    }
    out
}

// ---------------------------------------------------------------------------
// Public: KiCad symbol + footprint → AdomLbrPart
// ---------------------------------------------------------------------------

/// Import a KiCad symbol + footprint pair into the neutral part. The pin↔pad
/// link is identity-mapped (pin number == pad number) where they coincide, and
/// the symbol is given the footprint name so the Altium writer links them.
pub fn import_kicad(sym_text: &str, mod_text: &str, mpn: &str) -> Result<AdomLbrPart> {
    let mut symbol = import_symbol(sym_text, mpn)?;
    let footprint = import_footprint(mod_text)?;
    symbol.footprint = Some(footprint.name.clone());
    // Center the symbol on the origin (KiCad sources are often drawn off-origin).
    symbol.center();

    // identity connect map over shared designators
    let pad_nums: std::collections::HashSet<&str> =
        footprint.pads.iter().map(|p| p.number.as_str()).collect();
    let connect = symbol
        .pins
        .iter()
        .filter(|p| pad_nums.contains(p.number.as_str()))
        .map(|p| ConnectEntry {
            pin: p.number.clone(),
            pad: p.number.clone(),
        })
        .collect();

    Ok(AdomLbrPart {
        schema_version: 2,
        mpn: mpn.to_string(),
        manufacturer: String::new(),
        package: String::new(),
        value: String::new(),
        category: String::new(),
        symbol,
        footprint,
        connect,
        model_3d: None,
        provenance: Some(serde_json::json!({"source":"kicad_import"})),
    })
}