//! `.kicad_sym` generator — faithful Rust port of the Node `lib/sym-gen.js`
//! `generateICSym`. Category A (IC): rectangle body, pins on sides, group
//! labels. Output is byte-for-byte identical to the Node generator (verified
//! by the golden-file parity test in `tests/`).
//!
//! No gallia, no Node. (Category B / discrete baseline fetch — `symGet` — is
//! ported separately and routes through service-kicad.)

// ── Pin angle constants ──
const ANGLE_LEFT_SIDE: i32 = 0; // pin extends right from tip → left side of body
const ANGLE_RIGHT_SIDE: i32 = 180;
const ANGLE_BOTTOM_SIDE: i32 = 90;
const ANGLE_TOP_SIDE: i32 = 270;

const PIN_LENGTH: f64 = 2.54;
const PIN_PITCH: f64 = 2.54; // within a group
const GROUP_GAP: f64 = 3.81; // between groups
const LABEL_OFFSET: f64 = 2.0; // group label above first pin in group
const BODY_TOP_MARGIN: f64 = 2.54;
const BODY_BOTTOM_MARGIN: f64 = 2.54;
const MIN_BODY_WIDTH: f64 = 20.0;
const BODY_PADDING: f64 = 4.0;

const FS_PIN_NAME: f64 = 1.27;
const FS_GROUP_LABEL: f64 = 0.762;

#[derive(Clone)]
pub struct Pin {
    pub name: String,
    pub number: String,
    pub ptype: String, // input | output | power_in | bidirectional | tri_state | …
    pub group: Option<String>,
    pub side: Option<String>, // explicit override: left|right|top|bottom
}

pub struct IcInput {
    pub symbol_name: String,
    pub manufacturer: String,
    pub package: String,
    pub description: String,
    pub datasheet_url: String,
    pub reference_prefix: String,
    pub pins: Vec<Pin>,
    pub pin_layout: String, // "left-right" | "all-sides"
    pub chip_name_centered: bool,
    /// Reference designator placement (see `designator_at` for the position set).
    pub ref_pos: String,
    /// Value (chip name) placement (same position set as ref_pos).
    pub value_pos: String,
    /// distributor part numbers, insertion-ordered: (distributor, pn)
    pub distributor_pn: Vec<(String, String)>,
}

#[derive(Clone)]
struct SideGroup {
    group_name: String,
    pins: Vec<Pin>,
}

#[derive(Clone)]
struct PosPin {
    name: String,
    number: String,
    ptype: String,
    x: f64,
    y: f64,
}

#[derive(Clone)]
struct Label {
    name: String,
    x: f64,
    y: f64,
}

/// JS Number.toFixed(2) — round-half-away-from-zero, 2 decimals, "-0.00" → "0.00".
fn fx(v: f64) -> String {
    // JS toFixed rounds half away from zero; Rust's {:.2} rounds half-to-even.
    // Re-round explicitly to match JS for the rare exact-half case.
    let scaled = v * 100.0;
    let rounded = if scaled >= 0.0 { (scaled + 0.5).floor() } else { (scaled - 0.5).ceil() };
    let r = rounded / 100.0;
    let s = format!("{:.2}", r);
    if s == "-0.00" { "0.00".to_string() } else { s }
}

fn is_ground_name(name: &str) -> bool {
    let n = name.to_uppercase();
    n.starts_with("GND") || n.starts_with("VSS") || n.starts_with("AGND") || n.starts_with("DGND") || n.starts_with("EP")
}

fn layout_vertical(side_groups: &[SideGroup]) -> (Vec<PosPin>, Vec<Label>, f64) {
    let mut pins = Vec::new();
    let mut labels = Vec::new();
    let mut y = 0.0_f64;
    for (i, sg) in side_groups.iter().enumerate() {
        if i > 0 {
            y -= GROUP_GAP;
        }
        let label_y = y;
        y -= LABEL_OFFSET;
        for pin in &sg.pins {
            pins.push(PosPin { name: pin.name.clone(), number: pin.number.clone(), ptype: pin.ptype.clone(), x: 0.0, y });
            y -= PIN_PITCH;
        }
        if sg.group_name != "_ungrouped" {
            labels.push(Label { name: sg.group_name.clone(), x: 0.0, y: label_y });
        }
    }
    (pins, labels, y.abs())
}

fn layout_horizontal(side_groups: &[SideGroup], start_x: f64) -> (Vec<PosPin>, Vec<Label>, f64) {
    let mut pins = Vec::new();
    let mut labels = Vec::new();
    let mut x = start_x;
    for (i, sg) in side_groups.iter().enumerate() {
        if i > 0 {
            x += GROUP_GAP;
        }
        let label_x = x;
        if sg.group_name != "_ungrouped" {
            x += LABEL_OFFSET;
        }
        for pin in &sg.pins {
            pins.push(PosPin { name: pin.name.clone(), number: pin.number.clone(), ptype: pin.ptype.clone(), x, y: 0.0 });
            x += PIN_PITCH;
        }
        if sg.group_name != "_ungrouped" {
            labels.push(Label { name: sg.group_name.clone(), x: label_x, y: 0.0 });
        }
    }
    (pins, labels, x - start_x)
}

/// Where the symbol's content actually extends past each body edge, so "out"
/// designators clear the pins/labels on that side (auto-overlap-avoidance).
struct Clear {
    top: f64,    // y for out-top (above any top pins)
    bot: f64,    // y for out-bot (below any bottom pins)
    left: f64,   // x for out-left (left of left pin numbers)
    right: f64,  // x for out-right (right of right pin numbers)
}

/// Position a designator (Reference or Value) relative to the body box, using
/// `clr` for the "out" positions so they don't collide with pins.
/// Positions: out-top / in-top / out-bot / in-bot / out-left / in-left /
/// out-right / in-right / center. Returns (x, y, justify-suffix).
fn designator_at(pos: &str, bl: f64, br: f64, bt: f64, bb: f64, clr: &Clear) -> (f64, f64, &'static str) {
    let cy = (bt + bb) / 2.0;
    match pos {
        "center" => (0.0, cy, ""),
        "in-top" => (0.0, bt - 2.2, ""),
        "out-bot" | "out-bottom" => (0.0, clr.bot, ""),
        "in-bot" | "in-bottom" => (0.0, bb + 2.2, ""),
        "out-left" => (clr.left, cy, " (justify right)"),
        "in-left" => (bl + 2.0, cy, " (justify left)"),
        "out-right" => (clr.right, cy, " (justify left)"),
        "in-right" => (br - 2.0, cy, " (justify right)"),
        // legacy corner positions
        "top-left" => (bl + 2.0, bt - 1.8, " (justify left)"),
        "bottom-left" => (bl + 2.0, bb + 1.8, " (justify left)"),
        _ => (0.0, clr.top, ""), // out-top default
    }
}

pub fn generate_ic_sym(input: &IcInput) -> String {
    let pin_layout = if input.pin_layout.is_empty() { "left-right" } else { input.pin_layout.as_str() };

    // Group pins by group name, preserving first-seen order.
    let mut groups: Vec<(String, Vec<Pin>)> = Vec::new();
    for pin in &input.pins {
        let g = pin.group.clone().unwrap_or_else(|| "_ungrouped".to_string());
        if let Some(entry) = groups.iter_mut().find(|(n, _)| *n == g) {
            entry.1.push(pin.clone());
        } else {
            groups.push((g, vec![pin.clone()]));
        }
    }

    let mut left: Vec<SideGroup> = Vec::new();
    let mut right: Vec<SideGroup> = Vec::new();
    let mut top: Vec<SideGroup> = Vec::new();
    let mut bottom: Vec<SideGroup> = Vec::new();

    for (group_name, group_pins) in &groups {
        // Partition the group's pins by their EXPLICIT side first, so a group
        // (incl. the flat "_ungrouped" group) can span sides — e.g. flat layout
        // splitting pins half-left / half-right. Pins with no explicit side fall
        // through to the type-based auto-placement below.
        for side in ["left", "right", "top", "bottom"] {
            let pins: Vec<Pin> = group_pins.iter().filter(|p| p.side.as_deref() == Some(side)).cloned().collect();
            if pins.is_empty() {
                continue;
            }
            let sg = SideGroup { group_name: group_name.clone(), pins };
            match side {
                "left" => left.push(sg),
                "right" => right.push(sg),
                "top" => top.push(sg),
                "bottom" => bottom.push(sg),
                _ => {}
            }
        }
        let group_pins: Vec<Pin> = group_pins.iter().filter(|p| p.side.is_none()).cloned().collect();
        if !group_pins.is_empty() {
            let sg = SideGroup { group_name: group_name.clone(), pins: group_pins.clone() };
            let has_inputs = group_pins.iter().any(|p| p.ptype == "input" || p.ptype == "bidirectional");
            let has_power = group_pins.iter().any(|p| p.ptype == "power_in" || p.ptype == "power_out");
            let has_outputs = group_pins.iter().any(|p| p.ptype == "output" || p.ptype == "tri_state");
            let is_ground = group_pins.iter().all(|p| p.ptype == "power_in" && is_ground_name(&p.name));

            if pin_layout == "all-sides" {
                if is_ground {
                    bottom.push(sg);
                } else if has_power && !has_inputs && !has_outputs {
                    top.push(sg);
                } else if has_outputs && !has_inputs {
                    right.push(sg);
                } else {
                    left.push(sg);
                }
            } else {
                if is_ground {
                    left.push(sg);
                } else if has_outputs && !has_inputs {
                    right.push(sg);
                } else if has_power && !has_inputs {
                    right.push(sg);
                } else {
                    left.push(sg);
                }
            }
        }
    }

    let (left_pins, left_labels, _lh) = layout_vertical(&left);
    let (right_pins, right_labels, _rh) = layout_vertical(&right);

    // Body width from longest pin names (left-right) OR top/bottom pin count.
    let longest_left = left_pins.iter().map(|p| p.name.chars().count()).max().unwrap_or(0) as f64;
    let longest_right = right_pins.iter().map(|p| p.name.chars().count()).max().unwrap_or(0) as f64;
    let name_width = (longest_left + longest_right) * 1.0 + BODY_PADDING * 2.0;

    let top_pin_count: usize = top.iter().map(|g| g.pins.len()).sum();
    let bottom_pin_count: usize = bottom.iter().map(|g| g.pins.len()).sum();
    let top_group_gaps = (top.len().saturating_sub(1)) as f64 * GROUP_GAP;
    let bottom_group_gaps = (bottom.len().saturating_sub(1)) as f64 * GROUP_GAP;
    let top_label_offsets = top.iter().filter(|g| g.group_name != "_ungrouped").count() as f64 * LABEL_OFFSET;
    let bottom_label_offsets = bottom.iter().filter(|g| g.group_name != "_ungrouped").count() as f64 * LABEL_OFFSET;
    let top_width = top_pin_count as f64 * PIN_PITCH + top_group_gaps + top_label_offsets + BODY_PADDING * 2.0;
    let bottom_width = bottom_pin_count as f64 * PIN_PITCH + bottom_group_gaps + bottom_label_offsets + BODY_PADDING * 2.0;

    let body_width = name_width.max(MIN_BODY_WIDTH).max(top_width).max(bottom_width);
    let half_width = body_width / 2.0;

    let body_left = -(half_width / 2.54).ceil() * 2.54;
    let body_right = (half_width / 2.54).ceil() * 2.54;

    let (_td_pins, _td_labels, top_total) = layout_horizontal(&top, 0.0);
    let (_bd_pins, _bd_labels, bottom_total) = layout_horizontal(&bottom, 0.0);
    let top_start_x = -top_total / 2.0;
    let bottom_start_x = -bottom_total / 2.0;
    let (top_pins, top_labels, _tw) = layout_horizontal(&top, top_start_x);
    let (bottom_pins, bottom_labels, _bw) = layout_horizontal(&bottom, bottom_start_x);

    let mut all_label_ys: Vec<f64> = Vec::new();
    all_label_ys.extend(left_labels.iter().map(|l| l.y));
    all_label_ys.extend(right_labels.iter().map(|l| l.y));
    let top_label_y = all_label_ys.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
    let top_label_y = if all_label_ys.is_empty() { 0.0 } else { top_label_y };

    let mut top_pin_clearance = 0.0_f64;
    let mut bottom_pin_clearance = 0.0_f64;
    if !top_pins.is_empty() {
        let longest_top_name = top_pins.iter().map(|p| p.name.chars().count()).max().unwrap_or(0) as f64;
        let longest_top_label = top_labels.iter().map(|l| l.name.chars().count()).max().unwrap_or(0) as f64;
        top_pin_clearance = (longest_top_name * FS_PIN_NAME * 0.7).max(longest_top_label * FS_GROUP_LABEL * 0.7) + 2.0;
    }
    if !bottom_pins.is_empty() {
        let longest_bottom_name = bottom_pins.iter().map(|p| p.name.chars().count()).max().unwrap_or(0) as f64;
        let longest_bottom_label = bottom_labels.iter().map(|l| l.name.chars().count()).max().unwrap_or(0) as f64;
        bottom_pin_clearance = (longest_bottom_name * FS_PIN_NAME * 0.7).max(longest_bottom_label * FS_GROUP_LABEL * 0.7) + 2.5;
    }

    let body_top = top_label_y + BODY_TOP_MARGIN + top_pin_clearance;

    let mut all_pin_ys: Vec<f64> = Vec::new();
    all_pin_ys.extend(left_pins.iter().map(|p| p.y));
    all_pin_ys.extend(right_pins.iter().map(|p| p.y));
    let lowest_pin_y = if all_pin_ys.is_empty() { -10.0 } else { all_pin_ys.iter().cloned().fold(f64::INFINITY, f64::min) };
    let body_bottom = lowest_pin_y - BODY_BOTTOM_MARGIN - bottom_pin_clearance;

    let mut lines: Vec<String> = Vec::new();
    lines.push("(kicad_symbol_lib".to_string());
    lines.push("  (version 20231120)".to_string());
    lines.push("  (generator \"adom\")".to_string());
    lines.push(format!("  (symbol \"{}\"", input.symbol_name));
    lines.push("    (pin_names (offset 1.016))".to_string());
    lines.push("    (exclude_from_sim no)".to_string());
    lines.push("    (in_bom yes)".to_string());
    lines.push("    (on_board yes)".to_string());

    // Auto-avoidance: push "out" designators beyond whatever's actually on each
    // side (top/bottom pins in square layouts; left/right pin numbers).
    let longest_left_num = left_pins.iter().map(|p| p.number.chars().count()).max().unwrap_or(0) as f64;
    let longest_right_num = right_pins.iter().map(|p| p.number.chars().count()).max().unwrap_or(0) as f64;
    let clr = Clear {
        top: if top_pins.is_empty() { body_top + 2.54 } else { body_top + PIN_LENGTH + 6.0 },
        bot: if bottom_pins.is_empty() { body_bottom - 2.54 } else { body_bottom - PIN_LENGTH - 6.0 },
        left: body_left - PIN_LENGTH - longest_left_num * 0.8 - 2.5,
        right: body_right + PIN_LENGTH + longest_right_num * 0.8 + 2.5,
    };

    lines.push(format!("    (property \"Reference\" \"{}\"", input.reference_prefix));
    {
        let pos = if input.ref_pos.is_empty() { "out-top" } else { input.ref_pos.as_str() };
        let (rx, ry, just) = designator_at(pos, body_left, body_right, body_top, body_bottom, &clr);
        lines.push(format!("      (at {} {} 0)", fx(rx), fx(ry)));
        lines.push(format!("      (effects (font (size 1.27 1.27)){just})"));
    }
    lines.push("    )".to_string());

    lines.push(format!("    (property \"Value\" \"{}\"", input.symbol_name));
    {
        // "none" hides the Value (e.g. the chip name is laser-marked on the 3D
        // chip instead). KiCad requires the property to exist, so emit it hidden.
        if input.value_pos == "none" {
            let (vx, vy, _) = designator_at("out-bot", body_left, body_right, body_top, body_bottom, &clr);
            lines.push(format!("      (at {} {} 0)", fx(vx), fx(vy)));
            lines.push("      (effects (font (size 1.27 1.27)) hide)".to_string());
        } else {
            let pos = if input.value_pos.is_empty() { "out-bot" } else { input.value_pos.as_str() };
            let (vx, vy, just) = designator_at(pos, body_left, body_right, body_top, body_bottom, &clr);
            lines.push(format!("      (at {} {} 0)", fx(vx), fx(vy)));
            lines.push(format!("      (effects (font (size 1.27 1.27)){just})"));
        }
    }
    lines.push("    )".to_string());

    for (prop, val) in [
        ("Footprint", input.package.as_str()),
        ("Datasheet", input.datasheet_url.as_str()),
        ("Description", input.description.as_str()),
        ("Manufacturer", input.manufacturer.as_str()),
    ] {
        lines.push(format!("    (property \"{prop}\" \"{val}\""));
        lines.push("      (at 0 0 0)".to_string());
        lines.push("      (effects (font (size 1.27 1.27)) (hide yes))".to_string());
        lines.push("    )".to_string());
    }

    for (dist, pn) in &input.distributor_pn {
        let mut label = String::new();
        let mut chars = dist.chars();
        if let Some(c) = chars.next() {
            label.extend(c.to_uppercase());
            label.push_str(chars.as_str());
        }
        lines.push(format!("    (property \"{label}\" \"{pn}\""));
        lines.push("      (at 0 0 0)".to_string());
        lines.push("      (effects (font (size 1.27 1.27)) (hide yes))".to_string());
        lines.push("    )".to_string());
    }

    // Graphics sub-symbol (_0_1)
    lines.push(format!("    (symbol \"{}_0_1\"", input.symbol_name));
    lines.push(format!("      (rectangle (start {} {}) (end {} {})", fx(body_left), fx(body_top), fx(body_right), fx(body_bottom)));
    lines.push("        (stroke (width 0.254) (type default))".to_string());
    lines.push("        (fill (type background))".to_string());
    lines.push("      )".to_string());

    for label in &left_labels {
        lines.push(format!("      (text \"{}\" (at {} {} 0)", label.name, fx(body_left + 1.02), fx(label.y)));
        lines.push("        (effects (font (size 0.762 0.762) (color 132 0 0 1)) (justify left))".to_string());
        lines.push("      )".to_string());
    }
    for label in &right_labels {
        lines.push(format!("      (text \"{}\" (at {} {} 0)", label.name, fx(body_right - 1.02), fx(label.y)));
        lines.push("        (effects (font (size 0.762 0.762) (color 132 0 0 1)) (justify right))".to_string());
        lines.push("      )".to_string());
    }
    for label in &top_labels {
        lines.push(format!("      (text \"{}\" (at {} {} 900)", label.name, fx(label.x), fx(body_top - 1.02)));
        lines.push("        (effects (font (size 0.762 0.762) (color 132 0 0 1)) (justify right))".to_string());
        lines.push("      )".to_string());
    }
    for label in &bottom_labels {
        lines.push(format!("      (text \"{}\" (at {} {} 900)", label.name, fx(label.x), fx(body_bottom + 1.02)));
        lines.push("        (effects (font (size 0.762 0.762) (color 132 0 0 1)) (justify left))".to_string());
        lines.push("      )".to_string());
    }
    if input.chip_name_centered {
        let center_y = fx((body_top + body_bottom) / 2.0);
        lines.push(format!("      (text \"{}\" (at 0 {} 0)", input.symbol_name, center_y));
        lines.push("        (effects (font (size 1.27 1.27) (color 100 100 100 1)) (justify center))".to_string());
        lines.push("      )".to_string());
    }
    lines.push("    )".to_string());

    // Pins sub-symbol (_1_1)
    lines.push(format!("    (symbol \"{}_1_1\"", input.symbol_name));
    for pin in &left_pins {
        let tip_x = body_left - PIN_LENGTH;
        lines.push(format!("      (pin {} line (at {} {} {}) (length 2.54) (name \"{}\") (number \"{}\"))", pin.ptype, fx(tip_x), fx(pin.y), ANGLE_LEFT_SIDE, pin.name, pin.number));
    }
    for pin in &right_pins {
        let tip_x = body_right + PIN_LENGTH;
        lines.push(format!("      (pin {} line (at {} {} {}) (length 2.54) (name \"{}\") (number \"{}\"))", pin.ptype, fx(tip_x), fx(pin.y), ANGLE_RIGHT_SIDE, pin.name, pin.number));
    }
    if !top_pins.is_empty() {
        let top_pin_entry = body_top - 0.1;
        for pin in &top_pins {
            let tip_y = top_pin_entry + PIN_LENGTH;
            lines.push(format!("      (pin {} line (at {} {} {}) (length 2.54) (name \"{}\") (number \"{}\"))", pin.ptype, fx(pin.x), fx(tip_y), ANGLE_TOP_SIDE, pin.name, pin.number));
        }
    }
    if !bottom_pins.is_empty() {
        let bottom_pin_entry = body_bottom + 0.1;
        for pin in &bottom_pins {
            let tip_y = bottom_pin_entry - PIN_LENGTH;
            lines.push(format!("      (pin {} line (at {} {} {}) (length 2.54) (name \"{}\") (number \"{}\"))", pin.ptype, fx(pin.x), fx(tip_y), ANGLE_BOTTOM_SIDE, pin.name, pin.number));
        }
    }

    lines.push("    )".to_string());
    lines.push("  )".to_string());
    lines.push(")".to_string());
    lines.push(String::new());

    lines.join("\n")
}

#[cfg(test)]
mod tests {
    use super::*;
    fn pin(name: &str, number: &str, ptype: &str, group: &str) -> Pin {
        Pin { name: name.into(), number: number.into(), ptype: ptype.into(), group: Some(group.into()), side: None }
    }
    #[test]
    fn tst_ic8_matches_golden() {
        let input = IcInput {
            symbol_name: "TST-IC8".into(),
            manufacturer: "Adom Test".into(),
            package: "SOIC-8".into(),
            description: "Parity-harness test IC".into(),
            datasheet_url: "https://example.com/ds.pdf".into(),
            reference_prefix: "U".into(),
            pins: vec![
                pin("VCC", "8", "power_in", "Power"),
                pin("GND", "4", "power_in", "Power"),
                pin("IN+", "1", "input", "Inputs"),
                pin("IN-", "2", "input", "Inputs"),
                pin("OUT", "6", "output", "Outputs"),
                pin("EN", "3", "input", "Ctrl"),
            ],
            pin_layout: "left-right".into(),
            chip_name_centered: false,
            ref_pos: "top".into(),
            value_pos: "out-bot".into(),
            distributor_pn: vec![],
        };
        bless_eq(generate_ic_sym(&input), "tests/golden/TST-IC8.kicad_sym");
    }
    /// Regression check: compare to the golden file, or rewrite it when BLESS=1.
    fn bless_eq(got: String, path: &str) {
        if std::env::var("BLESS").is_ok() {
            std::fs::write(path, &got).unwrap();
            return;
        }
        let golden = std::fs::read_to_string(path).unwrap_or_default();
        assert_eq!(got, golden, "generator output drifted from {path} (run BLESS=1 cargo test to update)");
    }
    #[test]
    fn tst_all_matches_golden() {
        let p = |name:&str,number:&str,ptype:&str,group:&str| Pin{name:name.into(),number:number.into(),ptype:ptype.into(),group:Some(group.into()),side:None};
        let input = IcInput {
            symbol_name: "TST-ALL".into(), manufacturer: "Adom Test".into(), package: "QFN-16".into(),
            description: "all-sides coverage".into(), datasheet_url: "https://x/y.pdf".into(),
            reference_prefix: "U".into(),
            pins: vec![
                p("V3V3","16","power_in","Power"),
                p("GND","8","power_in","Gnd"),
                p("SDA","1","bidirectional","IO"),
                p("SCL","2","bidirectional","IO"),
                p("TX","5","output","Out"),
            ],
            pin_layout: "all-sides".into(), chip_name_centered: false,
            ref_pos: "out-top".into(), value_pos: "out-bot".into(),
            distributor_pn: vec![("mouser".into(),"MOU-123".into())],
        };
        bless_eq(generate_ic_sym(&input), "tests/golden/TST-ALL.kicad_sym");
    }
}