//! altium-codec — encode + decode Altium `.SchLib` (symbols) and `.PcbLib`
//! (footprints) from a neutral part model.
//!
//! This is the Altium half of the universal EDA interchange writer: the
//! canonical JSON maps onto [`model`], the encoders emit native Altium binary
//! with **no Altium at runtime**, and the decoders (thin wrappers over the
//! validated `altium-schlib` / `altium-pcblib` readers) let every encode be
//! round-trip checked.

pub mod altium_decode;
pub mod intlib;
pub mod kicad;
pub mod kicad_export;
pub mod model;
pub mod pcblib;
pub mod schlib;
pub mod sexpr;

pub use altium_decode::{decode_altium, decode_intlib_part};
pub use intlib::{decode_intlib, encode_intlib};
pub use kicad::import_kicad;
pub use kicad_export::{export_kicad_footprint, export_kicad_symbol};
pub use model::{AdomLbrPart, BodyRect, ConnectEntry, Footprint, FpGraphic, FpLayer, Graphic, Pad, Parameter, Pin, Symbol};
pub use pcblib::{decode_pcblib, encode_pcblib};
pub use schlib::{add_symbols_to_schlib, decode_schlib_first, encode_schlib};

#[cfg(test)]
mod tests {
    use super::*;

    fn two_pin_symbol() -> Symbol {
        Symbol {
            name: "REF2".into(),
            designator_prefix: Some("U".into()),
            footprint: None,
            body: None,
            graphics: vec![],
            parameters: vec![],
            pin_numbers_hidden: None,
            pin_names_hidden: None,
            // pins in mils (connection point); length 100, angle 0 → Altium
            // stores the body end = -100 + 100 = 0.
            pins: vec![
                Pin { number: "1".into(), name: "A".into(), x: -100, y: 100, length: Some(100), rotation: 0 },
                Pin { number: "2".into(), name: "B".into(), x: -100, y: -100, length: Some(100), rotation: 0 },
            ],
        }
    }

    #[test]
    fn schlib_roundtrip_pins() {
        let sym = two_pin_symbol();
        let bytes = encode_schlib(&[sym.clone()]).expect("encode");
        let decoded = decode_schlib_first(&bytes).expect("decode our own output");

        assert_eq!(decoded["module_name"], "REF2");
        assert_eq!(decoded["pin_count"], 2);
        let pins = decoded["pins"].as_array().unwrap();
        assert_eq!(pins[0]["name"], "A");
        assert_eq!(pins[0]["number"], "1");
        assert_eq!(pins[1]["name"], "B");
        assert_eq!(pins[1]["number"], "2");
        // body-end X = (-100 + 100) mil * 25.6 = 0; Y = 100 mil / 10 = 10
        assert_eq!(pins[0]["x"], 0);
        assert_eq!(pins[0]["y"], 10);
    }

    /// Pins the house layer convention: Silk and Fab both encode onto Mech13
    /// (69) and decode back as Fab (deliberately lossy — see
    /// `pcblib::layer_enc`); Courtyard and Copper round-trip exactly.
    #[test]
    fn layer_roundtrip_house_convention() {
        use model::{FpGraphic, FpLayer};
        let line = |layer: FpLayer, y: f64| FpGraphic::Line { layer, x1: -1.0, y1: y, x2: 1.0, y2: y, width: 0.1 };
        let fp = Footprint {
            name: "LAYERFP".into(),
            pads: vec![Pad { number: "1".into(), x_mm: 0.0, y_mm: 0.0, w_mm: 1.0, h_mm: 1.0, drill_mm: 0.0, rotation: 0.0, shape: "rect".into(), plated: None }],
            graphics: vec![
                line(FpLayer::Silk, 1.0),
                line(FpLayer::Fab, 2.0),
                line(FpLayer::Courtyard, 3.0),
                line(FpLayer::Copper, 4.0),
            ],
        };
        let bytes = encode_pcblib(&fp).expect("encode");
        let part = decode_altium(None, Some(&bytes), "LAYERFP").expect("decode");
        let layer_of = |y_want: f64| {
            part.footprint.graphics.iter().find_map(|g| match g {
                model::FpGraphic::Line { layer, y1, .. } if (y1 - y_want).abs() < 1e-3 => Some(layer.clone()),
                _ => None,
            })
        };
        assert!(matches!(layer_of(1.0), Some(FpLayer::Fab)), "silk merges onto Mech13 → decodes as Fab (house convention)");
        assert!(matches!(layer_of(2.0), Some(FpLayer::Fab)), "fab round-trips");
        assert!(matches!(layer_of(3.0), Some(FpLayer::Courtyard)), "courtyard round-trips");
        assert!(matches!(layer_of(4.0), Some(FpLayer::Copper)), "copper round-trips");
    }

    /// The `|KEY=VALUE` text records have no escape mechanism — encoding must
    /// REFUSE values that would desync the stream, not silently corrupt it.
    #[test]
    fn schlib_rejects_corrupting_inputs() {
        // '|' in a parameter value
        let mut s = two_pin_symbol();
        s.parameters.push(Parameter { name: "Value".into(), value: "10k|1%".into(), visible: true });
        assert!(encode_schlib(&[s]).is_err(), "pipe in parameter value must be rejected");

        // '|' in the symbol name
        let mut s = two_pin_symbol();
        s.name = "BAD|NAME".into();
        assert!(encode_schlib(&[s]).is_err(), "pipe in symbol name must be rejected");

        // pin longer than the u16 length field can hold (max 2559 mil)
        let mut s = two_pin_symbol();
        s.pins[0].length = Some(3000);
        assert!(encode_schlib(&[s]).is_err(), "oversize pin length must be rejected");
    }

    /// Storage name, FileHeader LibRef, and RECORD=1 LibReference must agree —
    /// and a name like "LM358.A" must keep its dot (CFBF allows it).
    #[test]
    fn schlib_dotted_name_stays_resolvable() {
        let mut s = two_pin_symbol();
        s.name = "LM358.A".into();
        let bytes = encode_schlib(&[s]).expect("encode");
        let decoded = decode_schlib_first(&bytes).expect("decode");
        assert_eq!(decoded["module_name"], "LM358.A");
        // merging the same symbol again is a no-op (dedup on the storage name)
        let mut again = two_pin_symbol();
        again.name = "LM358.A".into();
        let merged = add_symbols_to_schlib(&bytes, &[again]).expect("merge");
        assert_eq!(merged, bytes, "re-adding an existing symbol must not change the library");
    }

    /// NPTH mounting holes must survive KiCad import → export: keep the drill
    /// (not become an SMD copper pad) and stay `np_thru_hole`.
    #[test]
    fn npth_pad_roundtrips_through_kicad() {
        let sym = "(kicad_symbol_lib (symbol \"M3\" (pin passive line (at 0 0 0) (length 2.54) (name \"1\") (number \"1\"))))";
        let fpt = concat!(
            "(footprint \"MountM3\" (layer \"F.Cu\")\n",
            "  (pad \"\" np_thru_hole circle (at 0 0) (size 3.2 3.2) (drill 3.2) (layers \"*.Cu\" \"*.Mask\"))\n",
            "  (pad \"1\" thru_hole circle (at 5 0) (size 1.7 1.7) (drill oval 1.0 1.6) (layers \"*.Cu\" \"*.Mask\"))\n",
            ")"
        );
        let part = import_kicad(sym, fpt, "M3").expect("import");
        let npth = &part.footprint.pads[0];
        assert!(npth.drill_mm > 3.1, "NPTH drill preserved, got {}", npth.drill_mm);
        assert_eq!(npth.plated, Some(false));
        let th = &part.footprint.pads[1];
        assert_eq!(th.plated, Some(true));
        assert!((th.drill_mm - 1.6).abs() < 1e-9, "slot drill takes the larger axis, got {}", th.drill_mm);

        let out = export_kicad_footprint(&part.footprint);
        assert!(out.contains("np_thru_hole"), "export keeps np_thru_hole:\n{out}");
        assert!(out.contains("(drill 3.2)"), "export keeps the NPTH drill:\n{out}");
    }

    #[test]
    fn pcblib_roundtrip_pads() {
        let fp = Footprint {
            name: "REFFP".into(),
            pads: vec![
                Pad { number: "1".into(), x_mm: -0.762, y_mm: 0.0, w_mm: 1.016, h_mm: 1.27, drill_mm: 0.0, rotation: 0.0, shape: "rect".into(), plated: None },
                Pad { number: "2".into(), x_mm: 0.762, y_mm: 0.0, w_mm: 1.016, h_mm: 1.27, drill_mm: 0.0, rotation: 0.0, shape: "rect".into(), plated: None },
            ],
            graphics: vec![],
        };
        let bytes = encode_pcblib(&fp).expect("encode");
        let decoded = decode_pcblib(&bytes).expect("decode our own output");

        assert_eq!(decoded["pad_count"], 2);
        let pads = decoded["pads"].as_array().unwrap();
        assert_eq!(pads[0]["number"], "1");
        // -0.762 mm in, -0.762 mm back (within rounding)
        let x = pads[0]["at"][0].as_f64().unwrap();
        assert!((x - (-0.762)).abs() < 1e-3, "x was {x}");
        let w = pads[0]["size"][0].as_f64().unwrap();
        assert!((w - 1.016).abs() < 1e-3, "w was {w}");
    }
}