//! The **adom-lbr** part model — the in-memory form of the adom-lbr json, the
//! canonical Adom library format. Every EDA adapter (Altium, EAGLE/Fusion,
//! OrCAD) converts to and from this one shape. It is Adom's own format; the
//! per-tool files are just generated views of it.
//!
//! Units: symbols use Altium schematic units (1 = 1/100 inch grid step, i.e.
//! "mils/10" as Altium stores Location.X/Y as i16 in 1/100-inch? — see schlib.rs
//! for the exact scale). Footprints use millimetres at the model layer; the
//! PcbLib encoder converts mm → Altium's 1/10000-mil integer units.

use serde::{Deserialize, Serialize};

/// A schematic symbol: a body rectangle plus pins. One SchLib holds many.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Symbol {
    /// LibReference — the component's library name.
    pub name: String,
    /// Pins, in declaration order.
    pub pins: Vec<Pin>,
    /// Body rectangle in mils (lo corner, hi corner). Defaults to a sane box.
    #[serde(default)]
    pub body: Option<BodyRect>,
    /// Generic body graphics (polylines/rects/circles/arcs). When present, these
    /// are drawn instead of the default body box. Format-neutral.
    #[serde(default)]
    pub graphics: Vec<Graphic>,
    /// Designator prefix without the '?' (e.g. "U", "R"). Default "U".
    #[serde(default)]
    pub designator_prefix: Option<String>,
    /// Optional footprint name to link (PCBLIB implementation).
    #[serde(default)]
    pub footprint: Option<String>,
    /// Component property fields (Altium RECORD=41 parameters) — e.g. Value,
    /// Manufacturer, MPN. Shown in the component's Properties panel.
    #[serde(default)]
    pub parameters: Vec<Parameter>,
    /// Whether pin numbers are hidden on the symbol (KiCad `(pin_numbers
    /// (hide yes))`). `None` → the exporter picks a passive-friendly default.
    #[serde(default)]
    pub pin_numbers_hidden: Option<bool>,
    /// Whether pin names are hidden on the symbol. `None` → default.
    #[serde(default)]
    pub pin_names_hidden: Option<bool>,
}

/// A component parameter / property field (Altium RECORD=41). Appears in the
/// component's Properties. `visible=false` → the field exists but isn't drawn
/// on the symbol (Altium `IsHidden=T`), which is the norm for metadata.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Parameter {
    /// Property name (e.g. "Value", "Manufacturer").
    pub name: String,
    /// Property value/text.
    pub value: String,
    /// Whether the field is drawn on the symbol. Metadata defaults to hidden.
    #[serde(default)]
    pub visible: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pin {
    /// Pin number / designator (e.g. "1", "A4").
    pub number: String,
    /// Pin name / label (e.g. "VDD", "GND").
    pub name: String,
    /// Location.X in mils.
    pub x: i32,
    /// Location.Y in mils.
    pub y: i32,
    /// Pin length in mils. Default 200.
    #[serde(default)]
    pub length: Option<i32>,
    /// Orientation in degrees: 0 / 90 / 180 / 270.
    #[serde(default)]
    pub rotation: u16,
}

/// A generic symbol graphic primitive (the symbol body / decorations). Units are
/// mils. This is format-agnostic: each EDA adapter maps it to its own primitive
/// (KiCad polyline/rectangle/circle/arc ↔ Altium RECORD=6/14/11/12). Works for
/// ANY component — nothing is cap-specific.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Graphic {
    /// Connected line segments through `points` (mils).
    Polyline { points: Vec<[f64; 2]>, #[serde(default)] width: f64 },
    /// Axis-aligned rectangle (mils).
    Rect { x1: f64, y1: f64, x2: f64, y2: f64, #[serde(default)] width: f64, #[serde(default)] filled: bool },
    /// Circle (mils).
    Circle { cx: f64, cy: f64, r: f64, #[serde(default)] width: f64 },
    /// Arc, angles in degrees (mils).
    Arc { cx: f64, cy: f64, r: f64, start: f64, end: f64, #[serde(default)] width: f64 },
}

impl Symbol {
    /// Shift the symbol so its bounding box (pins + graphics) is centered on the
    /// origin, snapped to the 50-mil grid so pins stay grid-aligned.
    pub fn center(&mut self) {
        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 &self.pins {
            upd(p.x as f64, p.y as f64);
        }
        for g in &self.graphics {
            match g {
                Graphic::Polyline { points, .. } => {
                    for pt in points {
                        upd(pt[0], pt[1]);
                    }
                }
                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);
                }
            }
        }
        if !minx.is_finite() {
            return;
        }
        let cx = (((minx + maxx) / 2.0) / 50.0).round() * 50.0;
        let cy = (((miny + maxy) / 2.0) / 50.0).round() * 50.0;
        for p in &mut self.pins {
            p.x -= cx as i32;
            p.y -= cy as i32;
        }
        for g in &mut self.graphics {
            match g {
                Graphic::Polyline { points, .. } => {
                    for pt in points.iter_mut() {
                        pt[0] -= cx;
                        pt[1] -= cy;
                    }
                }
                Graphic::Rect { x1, y1, x2, y2, .. } => {
                    *x1 -= cx;
                    *x2 -= cx;
                    *y1 -= cy;
                    *y2 -= cy;
                }
                Graphic::Circle { cx: gcx, cy: gcy, .. }
                | Graphic::Arc { cx: gcx, cy: gcy, .. } => {
                    *gcx -= cx;
                    *gcy -= cy;
                }
            }
        }
    }
}

#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct BodyRect {
    pub x1: i32,
    pub y1: i32,
    pub x2: i32,
    pub y2: i32,
}

impl Default for BodyRect {
    fn default() -> Self {
        BodyRect { x1: 0, y1: -50, x2: 200, y2: 50 }
    }
}

/// A PCB footprint: pads plus silk / courtyard / fab graphics. One PcbLib holds
/// many.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Footprint {
    /// Pattern / footprint name.
    pub name: String,
    pub pads: Vec<Pad>,
    /// Non-copper graphics (silkscreen, courtyard, fab/assembly outlines). Kept
    /// layer-tagged so every EDA target lands them on the right layer.
    #[serde(default)]
    pub graphics: Vec<FpGraphic>,
}

/// Footprint layer role — format-neutral. Each EDA adapter maps it to its own
/// layer (KiCad `F.SilkS`/`F.CrtYd`/`F.Fab` ↔ Altium TopOverlay(33)/Mech15(71)/
/// Mech13(69)). `Other` carries the raw Altium layer byte so unknown layers
/// survive a round trip.
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(tag = "role", rename_all = "lowercase")]
pub enum FpLayer {
    Silk,
    Courtyard,
    Fab,
    Copper,
    Other {
        /// Raw Altium layer id (for round-trip fidelity).
        #[serde(default)]
        altium: u8,
    },
}

/// A footprint graphic primitive on a non-copper layer. Units: mm, Y-down
/// (neutral convention). Format-neutral; each adapter maps it to its own record
/// (KiCad fp_line/fp_rect/fp_circle/fp_arc ↔ Altium Track/Arc).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum FpGraphic {
    /// Straight segment.
    Line { layer: FpLayer, x1: f64, y1: f64, x2: f64, y2: f64, #[serde(default)] width: f64 },
    /// Axis-aligned rectangle (emitted as 4 lines in most targets).
    Rect { layer: FpLayer, x1: f64, y1: f64, x2: f64, y2: f64, #[serde(default)] width: f64 },
    /// Circle.
    Circle { layer: FpLayer, cx: f64, cy: f64, r: f64, #[serde(default)] width: f64 },
    /// Arc, angles in degrees (CCW from start to end).
    Arc { layer: FpLayer, cx: f64, cy: f64, r: f64, start: f64, end: f64, #[serde(default)] width: f64 },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Pad {
    /// Pad designator (e.g. "1", "2", "GND").
    pub number: String,
    /// Centre X in mm (KiCad/neutral convention, Y-down).
    pub x_mm: f64,
    /// Centre Y in mm (Y-down — the encoder flips to Altium's Y-up).
    pub y_mm: f64,
    /// Pad width / height in mm.
    pub w_mm: f64,
    pub h_mm: f64,
    /// Drill diameter in mm. 0 → SMD; > 0 → through-hole.
    #[serde(default)]
    pub drill_mm: f64,
    /// Rotation in degrees.
    #[serde(default)]
    pub rotation: f64,
    /// "rect" | "circle" | "oval" | "roundrect".
    #[serde(default = "default_shape")]
    pub shape: String,
    /// Through-hole plating. `None`/`Some(true)` → plated (normal TH pad);
    /// `Some(false)` → NPTH (KiCad `np_thru_hole`, e.g. a mounting hole).
    /// Only meaningful when `drill_mm > 0`. NOTE: the Altium binary encoder
    /// currently emits NPTH as a plated TH pad (the plated flag byte in the
    /// pad record is not yet calibrated) — the hole survives, the plating
    /// distinction doesn't. KiCad export round-trips it faithfully.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub plated: Option<bool>,
}

fn default_shape() -> String {
    "rect".to_string()
}

/// The unified "Adom Library" part — one document carrying identity + symbol +
/// footprint + the pin↔pad link. This is the adom-lbr json the wiki stores and
/// every EDA adapter converts to/from.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdomLbrPart {
    pub schema_version: u32,
    pub mpn: String,
    #[serde(default)]
    pub manufacturer: String,
    #[serde(default)]
    pub package: String,
    #[serde(default)]
    pub value: String,
    #[serde(default)]
    pub category: String,
    pub symbol: Symbol,
    pub footprint: Footprint,
    #[serde(default)]
    pub connect: Vec<ConnectEntry>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub model_3d: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub provenance: Option<serde_json::Value>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConnectEntry {
    pub pin: String,
    pub pad: String,
}