//! KiCad `.kicad_sym` and `.kicad_mod` parser.
//!
//! Both files are KiCad-flavored S-expressions. We don't need a full parser —
//! we extract specific tokens with a tiny tokenizer that handles parens +
//! double-quoted strings (with escapes) + bare atoms. Good enough for what we
//! compare and noticeably smaller than pulling in a kicad-specific crate.

use anyhow::{Context as _, Result};
use std::fs;
use std::path::Path;

use super::{Pad, PackageInfo, Part, Pin};

pub fn parse(sym_path: Option<&Path>, mod_path: Option<&Path>) -> Result<Part> {
    let mut source_part_name = None;
    let mut pins: Vec<Pin> = Vec::new();
    let mut pads: Vec<Pad> = Vec::new();
    let mut package: Option<PackageInfo> = None;

    if let Some(p) = sym_path {
        let raw = fs::read_to_string(p).with_context(|| format!("reading {}", p.display()))?;
        let toks = tokenize(&raw);
        let (name, parsed_pins) = scan_kicad_sym(&toks);
        source_part_name = name;
        pins = parsed_pins;
    }
    if let Some(p) = mod_path {
        let raw = fs::read_to_string(p).with_context(|| format!("reading {}", p.display()))?;
        let toks = tokenize(&raw);
        let (fp_name, parsed_pads) = scan_kicad_mod(&toks);
        pads = parsed_pads;
        if let Some(name) = fp_name {
            package = Some(PackageInfo { raw_name: name, body_x_mm: None, body_y_mm: None });
        }
    }

    Ok(Part {
        source: "kicad".to_string(),
        source_part_name,
        pads,
        pins,
        package,
    })
}

#[derive(Debug, Clone, PartialEq)]
enum Tok {
    LParen,
    RParen,
    /// A double-quoted string with escapes resolved.
    Str(String),
    /// A bare atom (identifier or number).
    Atom(String),
}

fn tokenize(s: &str) -> Vec<Tok> {
    let mut toks = Vec::with_capacity(s.len() / 8);
    let bytes = s.as_bytes();
    let mut i = 0;
    while i < bytes.len() {
        let c = bytes[i];
        if c.is_ascii_whitespace() {
            i += 1;
            continue;
        }
        if c == b'(' {
            toks.push(Tok::LParen);
            i += 1;
            continue;
        }
        if c == b')' {
            toks.push(Tok::RParen);
            i += 1;
            continue;
        }
        if c == b'"' {
            // Quoted string with backslash escapes.
            i += 1;
            let mut buf = String::new();
            while i < bytes.len() && bytes[i] != b'"' {
                if bytes[i] == b'\\' && i + 1 < bytes.len() {
                    buf.push(bytes[i + 1] as char);
                    i += 2;
                } else {
                    buf.push(bytes[i] as char);
                    i += 1;
                }
            }
            if i < bytes.len() {
                i += 1; // closing "
            }
            toks.push(Tok::Str(buf));
            continue;
        }
        // Bare atom: read until whitespace or paren.
        let start = i;
        while i < bytes.len() && !bytes[i].is_ascii_whitespace() && bytes[i] != b'(' && bytes[i] != b')' {
            i += 1;
        }
        toks.push(Tok::Atom(s[start..i].to_string()));
    }
    toks
}

/// Scan the kicad_sym tokens for the first symbol name and every (pin ...)'s
/// (number "X") + (name "Y") pairs.
fn scan_kicad_sym(toks: &[Tok]) -> (Option<String>, Vec<Pin>) {
    let mut name = None;
    let mut pins: Vec<Pin> = Vec::new();

    let mut i = 0;
    while i < toks.len() {
        // Find first `( symbol "..."` form for the part name.
        if name.is_none()
            && toks[i] == Tok::LParen
            && matches!(toks.get(i + 1), Some(Tok::Atom(a)) if a == "symbol")
        {
            if let Some(Tok::Str(s)) = toks.get(i + 2) {
                name = Some(s.clone());
            }
        }
        // Find `( pin <type> <shape> ... ) and read the (name ...) + (number ...).
        if toks[i] == Tok::LParen
            && matches!(toks.get(i + 1), Some(Tok::Atom(a)) if a == "pin")
        {
            // Scan until matching RParen, collecting first "(name ...)" + "(number ...)".
            let mut depth = 1;
            let mut j = i + 2;
            let mut pin_name: Option<String> = None;
            let mut pin_number: Option<String> = None;
            while j < toks.len() && depth > 0 {
                match &toks[j] {
                    Tok::LParen => {
                        depth += 1;
                        if pin_name.is_none()
                            && matches!(toks.get(j + 1), Some(Tok::Atom(a)) if a == "name")
                        {
                            if let Some(Tok::Str(s)) = toks.get(j + 2) {
                                pin_name = Some(s.clone());
                            }
                        }
                        if pin_number.is_none()
                            && matches!(toks.get(j + 1), Some(Tok::Atom(a)) if a == "number")
                        {
                            if let Some(Tok::Str(s)) = toks.get(j + 2) {
                                pin_number = Some(s.clone());
                            }
                        }
                    }
                    Tok::RParen => {
                        depth -= 1;
                    }
                    _ => {}
                }
                j += 1;
            }
            if let (Some(num), nm) = (pin_number.clone(), pin_name.clone()) {
                let nm = nm.unwrap_or_default();
                let is_stub = nm.is_empty() || nm == num;
                pins.push(Pin { number: num, name: nm, is_stub_name: is_stub });
            }
            i = j;
            continue;
        }
        i += 1;
    }

    (name, pins)
}

/// Scan the kicad_mod tokens for the (footprint "name" ...) and every (pad "X" ...).
fn scan_kicad_mod(toks: &[Tok]) -> (Option<String>, Vec<Pad>) {
    let mut name = None;
    let mut pads: Vec<Pad> = Vec::new();

    let mut i = 0;
    while i < toks.len() {
        // Either `(footprint "name"` (modern KiCad ≥ 6) or `(module "name"`
        // (older KiCad / many UL exports) carries the package name.
        if name.is_none()
            && toks[i] == Tok::LParen
            && matches!(toks.get(i + 1), Some(Tok::Atom(a)) if a == "footprint" || a == "module")
        {
            if let Some(Tok::Str(s)) = toks.get(i + 2) {
                name = Some(s.clone());
            }
        }
        if toks[i] == Tok::LParen
            && matches!(toks.get(i + 1), Some(Tok::Atom(a)) if a == "pad")
        {
            // (pad "1" smd rect ...). The pad number is always the first arg.
            if let Some(Tok::Str(num)) = toks.get(i + 2) {
                pads.push(Pad { number: num.clone() });
            } else if let Some(Tok::Atom(num)) = toks.get(i + 2) {
                // Some KiCad libs emit pad numbers as bare atoms (e.g. "1" without quotes).
                pads.push(Pad { number: num.clone() });
            }
        }
        i += 1;
    }

    (name, pads)
}