//! Canonical [`AdomLbrPart`] → KiCad `.kicad_sym` + `.kicad_mod` (the KiCad
//! WRITE side — inverse of `kicad.rs`). Symbol coords are stored in mils and
//! convert to mm here; footprint coords are already mm. Graphics arcs are
//! emitted in KiCad's 3-point (start/mid/end) form.

use crate::model::{Footprint, FpGraphic, FpLayer, Graphic, Symbol};

const MM_PER_MIL: f64 = 0.0254;

/// Selectable KiCad output format tier. Every KiCad reads all OLDER formats,
/// so only three tiers are meaningful (7 and 9 are redundant — their readers
/// accept the previous tier's files):
///   V6  — oldest stable s-expr grammar; for third-party tools stuck there
///         (Fusion 360's KiCad importer, kiutils-era parsers).
///   V8  — the "normalized bools" cleanup; the interop sweet spot, readable by
///         KiCad 8/9/10 and most modern third-party parsers. THE DEFAULT —
///         barrett's desktop runs KiCad 9.0.9, which REJECTS v10-format files
///         ("Unable to load library", verified 2026-07-06), and KiCad refuses
///         any file newer than itself.
///   V10 — current native format; opt in via --kicad-version 10.
/// Version values are what each KiCad release itself writes. Sources of truth
/// in the KiCad repo (branch `10.0`):
///   symbol lib → eeschema/sch_file_versions.h  SEXPR_SYMBOL_LIB_FILE_VERSION
///   footprint  → pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.h  SEXPR_BOARD_FILE_VERSION
/// ≥20250318 semantics: a bare "~" is a literal tilde, never "empty" — always
/// write real empty strings (valid in every tier).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum KicadVersion {
    V6,
    #[default]
    V8,
    V10,
}

impl KicadVersion {
    /// Parse a user-facing selector ("6" | "8" | "10").
    pub fn parse(s: &str) -> Option<Self> {
        match s.trim() {
            "6" | "v6" => Some(Self::V6),
            "8" | "v8" => Some(Self::V8),
            "10" | "v10" => Some(Self::V10),
            _ => None,
        }
    }
    pub fn sym_version(self) -> u32 {
        match self {
            Self::V6 => 20211014,
            Self::V8 => 20231120,
            Self::V10 => 20251024,
        }
    }
    pub fn fp_version(self) -> u32 {
        match self {
            Self::V6 => 20211014,
            Self::V8 => 20240108,
            Self::V10 => 20260206,
        }
    }
    /// `generator_version` token value; V6 predates the token entirely.
    pub fn generator_version(self) -> Option<&'static str> {
        match self {
            Self::V6 => None,
            Self::V8 => Some("8.0"),
            Self::V10 => Some("10.0"),
        }
    }
    /// V6 wrote bare boolean flags (`hide`); V8+ normalized to `(hide yes)`.
    fn hide_flag(self) -> &'static str {
        match self {
            Self::V6 => " hide",
            _ => " (hide yes)",
        }
    }
    /// `exclude_from_sim` didn't exist in the V6 grammar — real KiCad 6
    /// errors on unknown tokens, so omit it there.
    fn writes_exclude_from_sim(self) -> bool {
        !matches!(self, Self::V6)
    }
    /// Library header line(s) shared by the symbol emitters.
    fn sym_header(self, generator: &str) -> String {
        match self.generator_version() {
            Some(gv) => format!(
                "(kicad_symbol_lib\n\t(version {})\n\t(generator \"{generator}\")\n\t(generator_version \"{gv}\")\n",
                self.sym_version()
            ),
            None => format!("(kicad_symbol_lib\n\t(version {})\n\t(generator \"{generator}\")\n", self.sym_version()),
        }
    }
}

/// Convenience constants for callers that just want "current native".
pub const KICAD_SYM_VERSION: u32 = 20251024;
pub const KICAD_FP_VERSION: u32 = 20260206;
pub const KICAD_GENERATOR_VERSION: &str = "10.0";

/// Format a float compactly (KiCad tolerates plain decimals; trim trailing 0s).
fn n(v: f64) -> String {
    let s = format!("{v:.6}");
    let s = s.trim_end_matches('0').trim_end_matches('.');
    if s.is_empty() || s == "-0" { "0".into() } else { s.to_string() }
}

// ---------------------------------------------------------------------------
// Symbol → .kicad_sym
// ---------------------------------------------------------------------------

/// Export a symbol as a complete `.kicad_sym` library (one symbol), in the
/// default format tier (V8 — loads in KiCad 8/9/10; see [`KicadVersion`]).
pub fn export_kicad_symbol(sym: &Symbol) -> String {
    export_kicad_symbol_for(sym, KicadVersion::default())
}

/// Export a symbol targeting a specific KiCad format tier.
pub fn export_kicad_symbol_for(sym: &Symbol, ver: KicadVersion) -> String {
    let m = |mil: f64| n(mil * MM_PER_MIL);
    let name = &sym.name;
    let reference = sym.designator_prefix.clone().unwrap_or_else(|| "U".into());
    let footprint = sym.footprint.clone().unwrap_or_default();
    let value = sym
        .parameters
        .iter()
        .find(|p| p.name.eq_ignore_ascii_case("Value"))
        .map(|p| p.value.clone())
        .unwrap_or_else(|| name.clone());

    // Bounding box (mm) over pins + graphics, so the Reference/Value labels sit
    // just outside the symbol instead of overlapping it at the origin.
    let (top, bottom, cx) = symbol_bbox_mm(sym);

    let mut s = String::new();
    s.push_str(&ver.sym_header("adom-lbr"));
    s.push_str(&format!("\t(symbol \"{name}\"\n"));
    // Preserve the source's pin-number/name visibility; when unknown (e.g. built
    // from Altium or scratch), default passives (≤2 pins) to hidden like KiCad's
    // own R/C/L symbols.
    let passive = sym.pins.len() <= 2;
    let hide_num = sym.pin_numbers_hidden.unwrap_or(passive);
    let hide_name = sym.pin_names_hidden.unwrap_or(passive);
    let hide = ver.hide_flag();
    s.push_str(&if hide_num {
        format!("\t\t(pin_numbers{hide})\n")
    } else {
        "\t\t(pin_numbers)\n".to_string()
    });
    s.push_str(&if hide_name {
        format!("\t\t(pin_names (offset 0.254){hide})\n")
    } else {
        "\t\t(pin_names (offset 0.254))\n".to_string()
    });
    if ver.writes_exclude_from_sim() {
        s.push_str("\t\t(exclude_from_sim no)\n");
    }
    s.push_str("\t\t(in_bom yes)\n\t\t(on_board yes)\n");
    // properties (Reference above the symbol, Value below; metadata hidden at origin)
    let prop = |k: &str, v: &str, x: f64, y: f64, hidden: bool| {
        let h = if hidden { hide } else { "" };
        format!(
            "\t\t(property \"{k}\" \"{}\"\n\t\t\t(at {} {} 0)\n\t\t\t(effects (font (size 1.27 1.27)){h})\n\t\t)\n",
            esc(v), n(x), n(y)
        )
    };
    s.push_str(&prop("Reference", &reference, cx, top + 1.27, false));
    s.push_str(&prop("Value", &value, cx, bottom - 1.27, false));
    s.push_str(&prop("Footprint", &footprint, 0.0, 0.0, true));
    // "" not "~": since format 20250318 a tilde is a literal tilde.
    s.push_str(&prop("Datasheet", "", 0.0, 0.0, true));
    // any extra parameters become hidden properties (at origin)
    for p in &sym.parameters {
        if !p.name.eq_ignore_ascii_case("Value") {
            s.push_str(&prop(&p.name, &p.value, 0.0, 0.0, !p.visible));
        }
    }

    // body graphics → the _0_1 (shared) sub-symbol
    if !sym.graphics.is_empty() {
        s.push_str(&format!("\t\t(symbol \"{name}_0_1\"\n"));
        for g in &sym.graphics {
            s.push_str(&sym_graphic(g, &m));
        }
        s.push_str("\t\t)\n");
    }

    // pins → the _1_1 (unit 1) sub-symbol
    s.push_str(&format!("\t\t(symbol \"{name}_1_1\"\n"));
    for pin in &sym.pins {
        let len = pin.length.unwrap_or(200) as f64;
        // legacy sources used "~" for an unnamed pin; in KiCad 10 that's a
        // literal tilde, so write the real empty string.
        let pname = if pin.name == "~" { "" } else { pin.name.as_str() };
        s.push_str(&format!(
            "\t\t\t(pin unspecified line\n\t\t\t\t(at {} {} {})\n\t\t\t\t(length {})\n\
             \t\t\t\t(name \"{}\" (effects (font (size 1.27 1.27))))\n\
             \t\t\t\t(number \"{}\" (effects (font (size 1.27 1.27))))\n\t\t\t)\n",
            m(pin.x as f64), m(pin.y as f64), pin.rotation, m(len),
            esc(pname), esc(&pin.number)
        ));
    }
    s.push_str("\t\t)\n\t)\n)\n");
    s
}

/// One symbol body graphic → KiCad s-expr (mils→mm via `m`).
fn sym_graphic(g: &Graphic, m: &dyn Fn(f64) -> String) -> String {
    let stroke_fill = |w: f64, filled: bool| {
        let fill = if filled { "outline" } else { "none" };
        format!(
            "\t\t\t\t(stroke (width {}) (type default))\n\t\t\t\t(fill (type {fill}))\n",
            n((w * MM_PER_MIL).max(0.0))
        )
    };
    match g {
        Graphic::Rect { x1, y1, x2, y2, width, filled } => format!(
            "\t\t\t(rectangle\n\t\t\t\t(start {} {})\n\t\t\t\t(end {} {})\n{}\t\t\t)\n",
            m(*x1), m(*y1), m(*x2), m(*y2), stroke_fill(*width, *filled)
        ),
        Graphic::Circle { cx, cy, r, width } => format!(
            "\t\t\t(circle\n\t\t\t\t(center {} {})\n\t\t\t\t(radius {})\n{}\t\t\t)\n",
            m(*cx), m(*cy), m(*r), stroke_fill(*width, false)
        ),
        Graphic::Arc { cx, cy, r, start, end, width } => {
            let (s, mid, e) = arc_points(*cx, *cy, *r, *start, *end);
            format!(
                "\t\t\t(arc\n\t\t\t\t(start {} {})\n\t\t\t\t(mid {} {})\n\t\t\t\t(end {} {})\n{}\t\t\t)\n",
                m(s.0), m(s.1), m(mid.0), m(mid.1), m(e.0), m(e.1), stroke_fill(*width, false)
            )
        }
        Graphic::Polyline { points, width } => {
            let mut pts = String::new();
            for p in points {
                pts.push_str(&format!("\t\t\t\t\t(xy {} {})\n", m(p[0]), m(p[1])));
            }
            format!(
                "\t\t\t(polyline\n\t\t\t\t(pts\n{pts}\t\t\t\t)\n{}\t\t\t)\n",
                stroke_fill(*width, false)
            )
        }
    }
}

// ---------------------------------------------------------------------------
// Footprint → .kicad_mod
// ---------------------------------------------------------------------------

/// Export a footprint as a `.kicad_mod`, in the default format tier (V8 —
/// loads in KiCad 8/9/10; see [`KicadVersion`]).
pub fn export_kicad_footprint(fp: &Footprint) -> String {
    export_kicad_footprint_for(fp, KicadVersion::default())
}

/// Export a footprint targeting a specific KiCad format tier.
pub fn export_kicad_footprint_for(fp: &Footprint, ver: KicadVersion) -> String {
    let smd = fp.pads.iter().all(|p| p.drill_mm <= 0.0);
    let mut s = String::new();
    s.push_str(&format!("(footprint \"{}\"\n", esc(&fp.name)));
    match ver.generator_version() {
        Some(gv) => s.push_str(&format!(
            "\t(version {})\n\t(generator \"adom-lbr\")\n\t(generator_version \"{gv}\")\n\t(layer \"F.Cu\")\n",
            ver.fp_version()
        )),
        None => s.push_str(&format!(
            "\t(version {})\n\t(generator \"adom-lbr\")\n\t(layer \"F.Cu\")\n",
            ver.fp_version()
        )),
    }
    s.push_str(if smd { "\t(attr smd)\n" } else { "\t(attr through_hole)\n" });

    // silk / courtyard / fab graphics
    for g in &fp.graphics {
        s.push_str(&fp_graphic(g));
    }

    // pads
    for pad in &fp.pads {
        let thru = pad.drill_mm > 0.0;
        let npth = thru && pad.plated == Some(false);
        let (ptype, layers) = if npth {
            // NPTH (mounting hole): no copper ring, mask relief only.
            ("np_thru_hole", "\"*.Mask\"")
        } else if thru {
            ("thru_hole", "\"*.Cu\" \"*.Mask\"")
        } else {
            ("smd", "\"F.Cu\" \"F.Paste\" \"F.Mask\"")
        };
        let shape = match pad.shape.as_str() {
            "circle" => "circle",
            "oval" => "oval",
            "roundrect" => "roundrect",
            _ => "rect",
        };
        let at = if pad.rotation != 0.0 {
            format!("(at {} {} {})", n(pad.x_mm), n(pad.y_mm), n(pad.rotation))
        } else {
            format!("(at {} {})", n(pad.x_mm), n(pad.y_mm))
        };
        s.push_str(&format!(
            "\t(pad \"{}\" {ptype} {shape} {at} (size {} {})",
            esc(&pad.number), n(pad.w_mm), n(pad.h_mm)
        ));
        if thru {
            s.push_str(&format!(" (drill {})", n(pad.drill_mm)));
        }
        s.push_str(&format!(" (layers {layers})"));
        if shape == "roundrect" {
            s.push_str(" (roundrect_rratio 0.25)");
        }
        s.push_str(")\n");
    }
    s.push_str(")\n");
    s
}

fn kicad_fp_layer(l: &FpLayer) -> &'static str {
    match l {
        FpLayer::Silk => "F.SilkS",
        FpLayer::Courtyard => "F.CrtYd",
        FpLayer::Fab => "F.Fab",
        FpLayer::Copper => "F.Cu",
        FpLayer::Other { .. } => "F.Fab",
    }
}

fn fp_graphic(g: &FpGraphic) -> String {
    match g {
        FpGraphic::Line { layer, x1, y1, x2, y2, width } => format!(
            "\t(fp_line (start {} {}) (end {} {})\n\t\t(stroke (width {}) (type default)) (layer \"{}\"))\n",
            n(*x1), n(*y1), n(*x2), n(*y2), n(*width), kicad_fp_layer(layer)
        ),
        FpGraphic::Rect { layer, x1, y1, x2, y2, width } => format!(
            "\t(fp_rect (start {} {}) (end {} {})\n\t\t(stroke (width {}) (type default)) (fill none) (layer \"{}\"))\n",
            n(*x1), n(*y1), n(*x2), n(*y2), n(*width), kicad_fp_layer(layer)
        ),
        FpGraphic::Circle { layer, cx, cy, r, width } => format!(
            "\t(fp_circle (center {} {}) (end {} {})\n\t\t(stroke (width {}) (type default)) (fill none) (layer \"{}\"))\n",
            n(*cx), n(*cy), n(cx + r), n(*cy), n(*width), kicad_fp_layer(layer)
        ),
        FpGraphic::Arc { layer, cx, cy, r, start, end, width } => {
            let (s, mid, e) = arc_points(*cx, *cy, *r, *start, *end);
            format!(
                "\t(fp_arc (start {} {}) (mid {} {}) (end {} {})\n\t\t(stroke (width {}) (type default)) (layer \"{}\"))\n",
                n(s.0), n(s.1), n(mid.0), n(mid.1), n(e.0), n(e.1), n(*width), kicad_fp_layer(layer)
            )
        }
    }
}

// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------

/// center/radius/start-end angles (deg, CCW) → the (start, mid, end) points
/// KiCad uses. Same units in and out.
fn arc_points(cx: f64, cy: f64, r: f64, start: f64, end: f64) -> ((f64, f64), (f64, f64), (f64, f64)) {
    // Angles can come straight out of a decoded (untrusted) binary — normalize
    // arithmetically. A `while sweep <= 0 { += 360 }` loop hangs on huge
    // negatives and never terminates on NaN.
    let (start, end) = (
        if start.is_finite() { start } else { 0.0 },
        if end.is_finite() { end } else { 0.0 },
    );
    let mut sweep = (end - start).rem_euclid(360.0);
    if sweep == 0.0 {
        sweep = 360.0; // same start/end angle = full circle, as before
    }
    let mid_ang = start + sweep / 2.0;
    let pt = |deg: f64| {
        let a = deg.to_radians();
        (cx + r * a.cos(), cy + r * a.sin())
    };
    (pt(start), pt(mid_ang), pt(end))
}

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

/// Symbol bounding box in mm over pins + graphics → `(top_y, bottom_y, center_x)`.
/// Used to place the Reference (above) and Value (below) labels clear of the art.
fn symbol_bbox_mm(sym: &Symbol) -> (f64, f64, f64) {
    let (mut minx, mut maxx) = (f64::INFINITY, f64::NEG_INFINITY);
    let (mut miny, mut maxy) = (f64::INFINITY, f64::NEG_INFINITY);
    let mut upd = |x: f64, y: f64| {
        minx = minx.min(x);
        maxx = maxx.max(x);
        miny = miny.min(y);
        maxy = maxy.max(y);
    };
    for p in &sym.pins {
        upd(p.x as f64, p.y as f64);
    }
    for g in &sym.graphics {
        match g {
            Graphic::Rect { x1, y1, x2, y2, .. } => {
                upd(*x1, *y1);
                upd(*x2, *y2);
            }
            Graphic::Circle { cx, cy, r, .. } | Graphic::Arc { cx, cy, r, .. } => {
                upd(cx - r, cy - r);
                upd(cx + r, cy + r);
            }
            Graphic::Polyline { points, .. } => {
                for p in points {
                    upd(p[0], p[1]);
                }
            }
        }
    }
    if !minx.is_finite() {
        return (2.54, -2.54, 0.0);
    }
    (
        maxy * MM_PER_MIL,
        miny * MM_PER_MIL,
        ((minx + maxx) / 2.0) * MM_PER_MIL,
    )
}