//! Parse a `.kicad_sym` for the interactive viewer: pin list (name/number/type/
//! position/angle/length), group labels, body rectangle, and the KiCad→SVG
//! coordinate offset (matched against the rendered SVG's pin-wire paths so the
//! browser can overlay hit-zones exactly on the rendered symbol).
//!
//! Faithful port of gallia `viewer-gen.js` `parseKicadSym` +
//! `computeKicadToSvgOffset`. No gallia, no Node.

use regex::Regex;
use serde::Serialize;

#[derive(Serialize, Clone, Default)]
pub struct Pin {
    pub name: String,
    pub number: String,
    /// human label, e.g. "Power Input"
    pub r#type: String,
    /// raw enum, e.g. "power_in"
    pub etype: String,
    /// hex color for the type badge
    pub color: String,
    pub x: f64,
    pub y: f64,
    pub angle: i64,
    pub length: f64,
    pub desc: String,
}

#[derive(Serialize, Clone, Default)]
pub struct Group {
    pub name: String,
    pub desc: String,
}

#[derive(Serialize, Clone, Default)]
pub struct BodyRect {
    pub x1: f64,
    pub y1: f64,
    pub x2: f64,
    pub y2: f64,
}

#[derive(Serialize, Clone, Default)]
pub struct SymbolMeta {
    pub name: String,
    pub reference: String,
    pub value: String,
    pub footprint: String,
    pub datasheet: String,
    pub description: String,
    pub manufacturer: String,
    pub pins: Vec<Pin>,
    pub groups: Vec<Group>,
    pub hide_pin_names: bool,
    pub body_rect: Option<BodyRect>,
}

fn type_label(t: &str) -> String {
    match t {
        "power_in" => "Power Input",
        "power_out" => "Power Output",
        "input" => "Input",
        "output" => "Output",
        "bidirectional" => "Bidirectional",
        "passive" => "Passive",
        "tri_state" => "Tri-State",
        "unspecified" => "Unspecified",
        "open_collector" => "Open Collector",
        "open_emitter" => "Open Emitter",
        "unconnected" => "Unconnected",
        "free" => "Free",
        other => other,
    }
    .to_string()
}

fn type_color(t: &str) -> String {
    match t {
        "power_in" | "power_out" => "#ef5350",
        "input" => "#66bb6a",
        "output" => "#42a5f5",
        "bidirectional" => "#ab47bc",
        "passive" => "#78909c",
        _ => "#90a4ae",
    }
    .to_string()
}

/// Parse the `.kicad_sym`. `symbol_name` is the symbol block to read (must exist).
pub fn parse_kicad_sym(content: &str, symbol_name: &str) -> SymbolMeta {
    let mut meta = SymbolMeta { name: symbol_name.to_string(), ..Default::default() };

    let hide_names = Regex::new(r"(?m)\(pin_names[^\n]*\(hide\s+yes\)").unwrap();
    if hide_names.is_match(content) {
        meta.hide_pin_names = true;
    }

    // The requested symbol's block to end of file (single-symbol files).
    let esc = regex::escape(symbol_name);
    let sym_re = Regex::new(&format!(r#"(?s)\(symbol "{esc}".*"#)).unwrap();
    let mut sym_block = match sym_re.find(content) {
        Some(m) => m.as_str().to_string(),
        None => return meta,
    };

    // Properties from the requested block.
    let prop_re = Regex::new(r#"\(property "(\w+)" "([^"]*)""#).unwrap();
    for c in prop_re.captures_iter(&sym_block) {
        let key = c[1].to_lowercase();
        let val = c[2].to_string();
        match key.as_str() {
            "reference" => meta.reference = val,
            "value" => meta.value = val,
            "footprint" => meta.footprint = val,
            "datasheet" => meta.datasheet = val,
            "description" => meta.description = val,
            "manufacturer" => meta.manufacturer = val,
            _ => {}
        }
    }
    if meta.name.is_empty() {
        meta.name = if !meta.value.is_empty() { meta.value.clone() } else { symbol_name.to_string() };
    }

    // Follow extends for pin/geometry (child only overrides properties).
    let ext_re = Regex::new(r#"\(extends\s+"([^"]+)"\)"#).unwrap();
    if let Some(c) = ext_re.captures(&sym_block) {
        let parent = regex::escape(&c[1]);
        let parent_re = Regex::new(&format!(r#"(?s)\(symbol "{parent}".*"#)).unwrap();
        if let Some(pm) = parent_re.find(content) {
            sym_block = pm.as_str().to_string();
            if hide_names.is_match(&sym_block) {
                meta.hide_pin_names = true;
            }
        }
    }

    // Pins — compact and multi-line.
    // Angle may be integer (gallia-generated) or decimal "180.0"
    // (manufacturer/SnapEDA files). `\)\s+\(name` requires only whitespace
    // before (name, which naturally skips `hide` pins (not drawn in the SVG,
    // so they'd get floating hit-zones).
    let pin_re = Regex::new(
        r#"(?s)\(pin\s+(\w+)\s+(\w+)\s+\(at\s+([-\d.]+)\s+([-\d.]+)\s+([-\d.]+)\)\s+\(length\s+([-\d.]+)\)\s+\(name\s+"([^"]*)".*?\(number\s+"([^"]*)""#,
    )
    .unwrap();
    for c in pin_re.captures_iter(&sym_block) {
        let etype = c[1].to_string();
        let name_raw = c[7].to_string();
        let number = c[8].to_string();
        let name = if name_raw == "~" { number.clone() } else { name_raw };
        meta.pins.push(Pin {
            color: type_color(&etype),
            r#type: type_label(&etype),
            etype,
            x: c[3].parse().unwrap_or(0.0),
            y: c[4].parse().unwrap_or(0.0),
            angle: c[5].parse::<f64>().unwrap_or(0.0).round() as i64,
            length: c[6].parse().unwrap_or(2.54),
            name,
            number,
            desc: String::new(),
        });
    }

    // Group labels — (text "LABEL" (at ..) \n (effects (font (size 0.762 0.762) (color 132 0 0 1))
    let grp_re = Regex::new(
        r#"(?s)\(text "([^"]+)"\s+\(at\s+[-\d.]+\s+[-\d.]+\s+\d+\)\s*\(effects\s+\(font\s+\(size\s+0\.762\s+0\.762\)\s+\(color\s+132\s+0\s+0\s+1\)\)"#,
    )
    .unwrap();
    for c in grp_re.captures_iter(&sym_block) {
        meta.groups.push(Group { name: c[1].to_string(), desc: String::new() });
    }

    // Body rectangle.
    let rect_re = Regex::new(
        r#"\(rectangle\s+\(start\s+([-\d.]+)\s+([-\d.]+)\)\s+\(end\s+([-\d.]+)\s+([-\d.]+)\)"#,
    )
    .unwrap();
    if let Some(c) = rect_re.captures(&sym_block) {
        meta.body_rect = Some(BodyRect {
            x1: c[1].parse().unwrap_or(0.0),
            y1: c[2].parse().unwrap_or(0.0),
            x2: c[3].parse().unwrap_or(0.0),
            y2: c[4].parse().unwrap_or(0.0),
        });
    }

    meta
}

/// Recover the KiCad→SVG offset by matching pin-wire paths in the rendered SVG
/// against pin coords. Mapping: svgX = ox + kicadX, svgY = oy - kicadY.
///
/// Every pin wire is the same length, so a single pin×wire match is ambiguous.
/// Instead we compute a candidate offset for EVERY plausible (pin, wire) pair
/// and take the **consensus** — the offset the most pins agree on. Correctly
/// matched pins all vote for the true global translation; mismatches scatter.
pub fn compute_offset(svg: &str, pins: &[Pin]) -> Option<(f64, f64)> {
    if pins.is_empty() {
        return None;
    }
    let line_re = Regex::new(r#"d="M(-?[\d.]+)\s+(-?[\d.]+)\s+L(-?[\d.]+)\s+(-?[\d.]+)\s*""#).unwrap();
    let lines: Vec<(f64, f64, f64, f64)> = line_re
        .captures_iter(svg)
        .filter_map(|c| Some((c[1].parse().ok()?, c[2].parse().ok()?, c[3].parse().ok()?, c[4].parse().ok()?)))
        .collect();
    if lines.is_empty() {
        return None;
    }

    use std::collections::HashMap;
    // Bucket candidate offsets to 0.01mm; vote.
    let mut votes: HashMap<(i64, i64), (u32, f64, f64)> = HashMap::new();
    for pin in pins {
        let expected = if pin.length > 0.0 { pin.length } else { 2.54 };
        let is_horiz = pin.angle == 0 || pin.angle == 180;
        for &(x1, y1, x2, y2) in &lines {
            let dx = (x2 - x1).abs();
            let dy = (y2 - y1).abs();
            let len = (dx * dx + dy * dy).sqrt();
            if (len - expected).abs() > 0.05 {
                continue;
            }
            if is_horiz && dy > 0.01 {
                continue;
            }
            if !is_horiz && dx > 0.01 {
                continue;
            }
            let (tip_x, tip_y) = match pin.angle {
                0 => (x1.min(x2), (y1 + y2) / 2.0),
                180 => (x1.max(x2), (y1 + y2) / 2.0),
                90 => ((x1 + x2) / 2.0, y1.max(y2)),
                270 => ((x1 + x2) / 2.0, y1.min(y2)),
                _ => continue,
            };
            let ox = tip_x - pin.x;
            let oy = tip_y + pin.y;
            let key = ((ox * 100.0).round() as i64, (oy * 100.0).round() as i64);
            let e = votes.entry(key).or_insert((0, 0.0, 0.0));
            e.0 += 1;
            e.1 += ox;
            e.2 += oy;
        }
    }
    // Most-voted offset wins; average the exact values in that bucket.
    votes
        .into_values()
        .max_by_key(|&(n, _, _)| n)
        .map(|(n, sx, sy)| (sx / n as f64, sy / n as f64))
}