//! Native Rust parser for Altium `.SchLib` (and `.IntLib`'s SchLib stream)
//! schematic-symbol libraries — the symbol counterpart to `altium-pcblib`.
//!
//! Altium `.SchLib` is a Microsoft OLE2 / Compound File Binary container. Each
//! component is a storage holding a `Data` stream of records:
//!   * **text records** — `<u16 len><u16 flags>` then `|KEY=VAL|KEY=VAL` ASCII.
//!     RECORD=1 is the component header, RECORD=14 a rectangle (body), RECORD=41
//!     a parameter.
//!   * **binary pin records** — `<u16 len><u16 flags>` then a payload whose first
//!     byte is `0x02`. Layout (offsets into the payload), reverse-engineered and
//!     validated pin-for-pin against KiCad 10's Altium importer:
//!         [14]      PinConglomerate  (u8) — orientation in bits 0..1
//!         [15..17]  PinLength        (i16)
//!         [17..19]  Location.X       (i16)
//!         [19..21]  Location.Y       (i16)
//!         [26..]    length-prefixed strings: Name, then Designator
//!
//! Output JSON mirrors `altium-pcblib`'s KiCad-shaped style so the rest of the
//! Adom EDA family consumes both the same way.

use anyhow::{anyhow, bail, Result};
use std::io::{Cursor, Read};
use std::path::Path;

/// Parse an Altium `.SchLib` from a file path → symbol JSON.
pub fn parse_altium_schlib_path(path: &Path) -> Result<serde_json::Value> {
    let bytes = std::fs::read(path)
        .map_err(|e| anyhow!("read .SchLib {}: {e}", path.display()))?;
    parse_altium_schlib_bytes(&bytes)
}

/// Parse an Altium `.SchLib` from raw bytes → symbol JSON.
pub fn parse_altium_schlib_bytes(bytes: &[u8]) -> Result<serde_json::Value> {
    if bytes.len() < 8 || bytes[..8] != [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1] {
        bail!("not an OLE2 compound document (bad magic)");
    }
    // Altium writes FATs that overshoot the file's real sector count; pad with
    // the CFBF free-sector sentinel (0xFF) and re-open. Same fix as altium-pcblib.
    let owned;
    let cursor = if bytes.len() < 1024 * 1024 {
        owned = {
            let mut v = bytes.to_vec();
            v.resize(1024 * 1024, 0xFF);
            v
        };
        Cursor::new(owned.as_slice())
    } else {
        Cursor::new(bytes)
    };
    let mut comp = cfb::CompoundFile::open(cursor).map_err(|e| anyhow!("open OLE2: {e}"))?;

    // Each non-housekeeping root storage is a component.
    let comps: Vec<String> = comp
        .read_root_storage()
        .filter_map(|e| {
            if !e.is_storage() {
                return None;
            }
            let n = e.name().to_string();
            if n.starts_with('\u{1}') || n == "Library" || n == "FileVersionInfo" {
                return None;
            }
            Some(n)
        })
        .collect();
    if comps.is_empty() {
        bail!("no component storages found in SchLib");
    }

    // Single-part library is the common chip-fetcher case: take the first.
    let name = &comps[0];
    let mut data = Vec::new();
    comp.open_stream(format!("/{name}/Data"))
        .map_err(|e| anyhow!("open /{name}/Data: {e}"))?
        .read_to_end(&mut data)?;
    parse_component_stream(name, &data)
}

fn parse_component_stream(comp_name: &str, d: &[u8]) -> Result<serde_json::Value> {
    let mut pins: Vec<serde_json::Value> = Vec::new();
    let mut description = String::new();
    let mut part_count = 1i64;
    let mut body: Option<serde_json::Value> = None;

    let mut i = 0usize;
    while i + 4 <= d.len() {
        let plen = u16::from_le_bytes([d[i], d[i + 1]]) as usize;
        let is_text = i + 4 < d.len() && d[i + 4] == b'|';
        i += 4;
        if plen == 0 || i + plen > d.len() {
            break;
        }
        let payload = &d[i..i + plen];
        i += plen;

        if is_text {
            let txt = String::from_utf8_lossy(payload);
            let rec = field(&txt, "RECORD").unwrap_or_default();
            match rec.as_str() {
                "1" => {
                    description = field(&txt, "ComponentDescription").unwrap_or_default();
                    part_count = field(&txt, "PartCount").and_then(|v| v.parse().ok()).unwrap_or(1);
                }
                "14" => {
                    // Rectangle = the symbol body. Coords are |Location.X|.Y|Corner.X|.Y.
                    let lx = field(&txt, "Location.X").and_then(|v| v.parse::<f64>().ok());
                    let ly = field(&txt, "Location.Y").and_then(|v| v.parse::<f64>().ok());
                    let cx = field(&txt, "Corner.X").and_then(|v| v.parse::<f64>().ok());
                    let cy = field(&txt, "Corner.Y").and_then(|v| v.parse::<f64>().ok());
                    if let (Some(lx), Some(ly), Some(cx), Some(cy)) = (lx, ly, cx, cy) {
                        body = Some(serde_json::json!({
                            "x1": lx, "y1": ly, "x2": cx, "y2": cy
                        }));
                    }
                }
                _ => {}
            }
            continue;
        }

        if payload.first() == Some(&0x02) {
            pins.push(parse_pin(payload));
        }
    }

    if pins.is_empty() {
        bail!("no pins parsed from component {comp_name}");
    }

    Ok(serde_json::json!({
        "module_name": comp_name,
        "description": description,
        "part_count": part_count,
        "pin_count": pins.len(),
        "pins": pins,
        "body": body,
        "_source": "altium_schlib",
    }))
}

/// Decode one binary pin record payload → pin JSON.
fn parse_pin(p: &[u8]) -> serde_json::Value {
    let i16at = |o: usize| p.get(o..o + 2).map(|b| i16::from_le_bytes([b[0], b[1]]) as i64).unwrap_or(0);
    let conglom = p.get(14).copied().unwrap_or(0);
    let length = i16at(15);
    let x = i16at(17);
    let y = i16at(19);
    // strings: Name then Designator, starting at offset 26
    let mut j = 26usize;
    let name = read_pstr(p, &mut j);
    let designator = read_pstr(p, &mut j);
    // Orientation: PinConglomerate bits 0..1 → 0/90/180/270°.
    let rotation = (conglom & 0x03) as i64 * 90;
    serde_json::json!({
        "number": designator,
        "name": name,
        "x": x,
        "y": y,
        "length": length,
        "rotation": rotation,
        "conglomerate": conglom,
    })
}

/// `<u8 len><bytes>` length-prefixed string.
fn read_pstr(d: &[u8], i: &mut usize) -> String {
    if *i >= d.len() {
        return String::new();
    }
    let l = d[*i] as usize;
    *i += 1;
    if *i + l > d.len() {
        return String::new();
    }
    let s = String::from_utf8_lossy(&d[*i..*i + l]).to_string();
    *i += l;
    s
}

/// Pull `KEY=VALUE` out of a `|KEY=VAL|KEY=VAL` Altium text record.
fn field(rec: &str, key: &str) -> Option<String> {
    let needle = format!("{key}=");
    for part in rec.split('|') {
        if let Some(v) = part.strip_prefix(&needle) {
            return Some(v.trim().to_string());
        }
    }
    None
}

// ------------------------------------------------------------------------
// Native SVG renderer — draws the symbol directly from the parsed pins, the
// same way altium-pcblib renders footprints. No KiCad, no service round-trip.
// ------------------------------------------------------------------------

/// Altium SCH coordinate → mm. Pin pitch is 2560 internal units = 100 mil =
/// 2.54 mm (the schematic-grid standard), so one unit = 2.54/2560 mm.
const MM_PER_UNIT: f64 = 2.54 / 2560.0;

/// Render the parsed symbol to a standalone SVG. Pins are grouped into a left
/// and right bank by X sign and stacked in designator order at a uniform pitch
/// — a clean, readable schematic symbol that preserves which side each pin is
/// on and its name/number (the data that actually matters for comparison).
pub fn render_symbol_svg(sym: &serde_json::Value) -> String {
    let module = sym.get("module_name").and_then(|v| v.as_str()).unwrap_or("?");
    let empty = vec![];
    let pins = sym.get("pins").and_then(|v| v.as_array()).unwrap_or(&empty);

    // Split into banks by X sign; preserve native vertical order (top→bottom).
    let mut left: Vec<&serde_json::Value> = Vec::new();
    let mut right: Vec<&serde_json::Value> = Vec::new();
    for p in pins {
        let x = p.get("x").and_then(|v| v.as_i64()).unwrap_or(0);
        if x < 0 { left.push(p); } else { right.push(p); }
    }
    let key = |p: &&serde_json::Value| -> i64 { -p.get("y").and_then(|v| v.as_i64()).unwrap_or(0) };
    left.sort_by_key(key);
    right.sort_by_key(key);

    let pitch = 2.54_f64; // mm between pins
    let rows = left.len().max(right.len()).max(1) as f64;
    let body_w = 38.0_f64;
    let body_h = rows * pitch + pitch; // one row of padding top+bottom
    let pin_len = 7.62_f64;
    let name_pad = 1.4_f64;

    let total_w = body_w + 2.0 * pin_len + 44.0; // room for pin names outside? names go inside
    let total_h = body_h + 8.0;
    let ox = total_w / 2.0; // body centered horizontally
    let bx1 = ox - body_w / 2.0;
    let bx2 = ox + body_w / 2.0;
    let by1 = 4.0;
    let by2 = by1 + body_h;

    let mut s = String::new();
    s.push_str(&format!(
        r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {total_w:.1} {total_h:.1}" width="{:.0}" height="{:.0}" font-family="monospace">"#,
        total_w * 14.0, total_h * 14.0
    ));
    s.push('\n');
    s.push_str(&format!(r##"<rect x="0" y="0" width="{total_w:.1}" height="{total_h:.1}" fill="#0d1117"/>"##));
    // body
    s.push_str(&format!(
        r##"<rect x="{bx1:.2}" y="{by1:.2}" width="{:.2}" height="{:.2}" fill="#10212a" stroke="#00b8b1" stroke-width="0.25"/>"##,
        bx2 - bx1, by2 - by1
    ));
    s.push('\n');

    let draw = |bank: &Vec<&serde_json::Value>, is_left: bool, out: &mut String| {
        for (idx, p) in bank.iter().enumerate() {
            let num = p.get("number").and_then(|v| v.as_str()).unwrap_or("");
            let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");
            let cy = by1 + pitch + idx as f64 * pitch;
            let (x_body, x_tip, anchor, nx) = if is_left {
                (bx1, bx1 - pin_len, "start", bx1 + name_pad)
            } else {
                (bx2, bx2 + pin_len, "end", bx2 - name_pad)
            };
            // pin line
            out.push_str(&format!(
                r##"<line x1="{x_body:.2}" y1="{cy:.2}" x2="{x_tip:.2}" y2="{cy:.2}" stroke="#7cb6ff" stroke-width="0.18"/>"##
            ));
            // pin number above the line, near the tip
            let numx = if is_left { x_tip + 0.6 } else { x_tip - 0.6 };
            let numanchor = if is_left { "start" } else { "end" };
            out.push_str(&format!(
                r##"<text x="{numx:.2}" y="{:.2}" font-size="1.0" fill="#8b949e" text-anchor="{numanchor}">{}</text>"##,
                cy - 0.5, esc(num)
            ));
            // pin name inside the body
            let display = altium_name_to_plain(name);
            out.push_str(&format!(
                r##"<text x="{nx:.2}" y="{:.2}" font-size="1.25" fill="#e6edf3" text-anchor="{anchor}">{}</text>"##,
                cy + 0.45, esc(&display)
            ));
            out.push('\n');
        }
    };
    draw(&left, true, &mut s);
    draw(&right, false, &mut s);

    // header
    s.push_str(&format!(
        r##"<text x="{ox:.2}" y="{:.2}" font-size="1.5" fill="#00e6dc" text-anchor="middle">{} ({} pins)</text>"##,
        by1 - 1.2, esc(module), pins.len()
    ));
    let _ = MM_PER_UNIT;
    s.push_str("\n</svg>\n");
    s
}

/// Altium overbar/special markup → plain readable text. Altium marks an active-
/// low signal by following each char with a backslash (`C\S\` = C̄S̄); collapse
/// those so a name reads as "CS".
fn altium_name_to_plain(s: &str) -> String {
    s.replace('\\', "")
}

fn esc(s: &str) -> String {
    s.replace('&', "&amp;").replace('<', "&lt;").replace('>', "&gt;")
}

// ------------------------------------------------------------------------
// .kicad_sym emitter — for EXPORT only (when the user wants KiCad output).
// Rendering uses the native SVG above; this is the convert-on-export path.
// ------------------------------------------------------------------------

/// Emit a `.kicad_sym` from the parsed symbol. Banks pins left/right like the
/// SVG so the exported KiCad symbol is laid out sensibly.
pub fn to_kicad_sym(sym: &serde_json::Value, lib_name: &str) -> String {
    let empty = vec![];
    let pins = sym.get("pins").and_then(|v| v.as_array()).unwrap_or(&empty);
    let mut left: Vec<&serde_json::Value> = Vec::new();
    let mut right: Vec<&serde_json::Value> = Vec::new();
    for p in pins {
        let x = p.get("x").and_then(|v| v.as_i64()).unwrap_or(0);
        if x < 0 { left.push(p); } else { right.push(p); }
    }
    let key = |p: &&serde_json::Value| -> i64 { -p.get("y").and_then(|v| v.as_i64()).unwrap_or(0) };
    left.sort_by_key(key);
    right.sort_by_key(key);

    let pitch = 2.54_f64;
    let rows = left.len().max(right.len()).max(1) as f64;
    let half_h = (rows * pitch) / 2.0 + pitch;
    let half_w = 19.05_f64;
    let pin_len = 5.08_f64;

    let mut body = String::new();
    let mut emit = |bank: &Vec<&serde_json::Value>, is_left: bool, out: &mut String| {
        for (idx, p) in bank.iter().enumerate() {
            let num = p.get("number").and_then(|v| v.as_str()).unwrap_or("");
            let name = p.get("name").and_then(|v| v.as_str()).unwrap_or("");
            let cy = half_h - pitch - idx as f64 * pitch;
            let (px, rot) = if is_left { (-half_w - pin_len, 0) } else { (half_w + pin_len, 180) };
            out.push_str(&format!(
                "      (pin bidirectional line (at {px:.2} {cy:.2} {rot}) (length {pin_len:.2})\n        (name \"{}\" (effects (font (size 1.27 1.27)))) (number \"{}\" (effects (font (size 1.27 1.27)))))\n",
                kicad_name(name), kesc(num)
            ));
        }
    };
    emit(&left, true, &mut body);
    emit(&right, false, &mut body);

    format!(
        "(kicad_symbol_lib (version 20211014) (generator altium-schlib)\n  (symbol \"{lib_name}\" (in_bom yes) (on_board yes)\n    (property \"Reference\" \"U\" (at 0 {:.2} 0) (effects (font (size 1.27 1.27))))\n    (property \"Value\" \"{lib_name}\" (at 0 {:.2} 0) (effects (font (size 1.27 1.27))))\n    (symbol \"{lib_name}_1_1\"\n      (rectangle (start {:.2} {:.2}) (end {:.2} {:.2}) (stroke (width 0.254) (type default)) (fill (type background)))\n{body}    )\n  )\n)\n",
        half_h + 1.27, -half_h - 1.27, -half_w, half_h, half_w, -half_h,
    )
}

/// KiCad uses `~{X}` for an overbar; Altium's `X\` per-char → wrap the whole
/// name in one overbar group when it's marked active-low.
fn kicad_name(s: &str) -> String {
    if s.contains('\\') {
        format!("~{{{}}}", s.replace('\\', ""))
    } else {
        kesc(s)
    }
}
fn kesc(s: &str) -> String {
    s.replace('"', "'")
}