//! Altium `.SchDoc` schematic parser + renderer.
//!
//! A SchDoc is an OLE2 compound file whose `FileHeader` stream is a flat list of
//! `[u32 len][ASCII |KEY=VAL|…]` records — no binary decoding needed at all.
//! Every record has a numeric `RECORD` type; the ones we draw:
//!
//!   1 Component   2 Pin        4 Label     6 Polyline   7 Polygon
//!   8 Ellipse    12 Arc       13 Line     14 Rectangle 17 Power port
//!  25 Net label  27 Wire      29 Junction 34 Designator 41 Parameter
//!
//! Child primitives (pins, graphics, designators) point at their component with
//! `OwnerIndex`, and — crucially — their coordinates are already ABSOLUTE sheet
//! coordinates, so no per-symbol transform is needed.
//!
//! Units: Altium schematic coordinates are 1/100 inch (10 mil) with an optional
//! `_Frac` field out of 100000. Y is up, ours is down, so Y is negated.
//!
//! Altium has no headless renderer (no CLI at all), so we draw the sheet
//! ourselves in Altium's own colour convention and tag every primitive with its
//! component — the frontend's halo highlight then works unchanged.

use crate::sch_parse::{Comp, Sheet};
use std::collections::HashMap;
use std::fmt::Write as _;
use std::io::{Cursor, Read};

/// 10 mil -> mm
const U: f64 = 0.254;

pub fn looks_like_altium(bytes: &[u8]) -> bool {
    bytes.len() > 8 && bytes[..8] == [0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1]
}

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

type Rec = HashMap<String, String>;

fn kv(rec: &str) -> Rec {
    rec.trim_matches('|')
        .split('|')
        .filter_map(|p| p.split_once('='))
        .map(|(k, v)| (k.to_uppercase(), v.to_string()))
        .collect()
}

fn s<'a>(r: &'a Rec, k: &str) -> &'a str { r.get(k).map(|v| v.as_str()).unwrap_or("") }
fn i(r: &Rec, k: &str) -> i64 { s(r, k).parse().unwrap_or(0) }

/// A coordinate: `<KEY>` in 10-mil units plus optional `<KEY>_FRAC` out of 100000.
fn coord(r: &Rec, k: &str) -> f64 {
    let base = i(r, k) as f64;
    let frac = i(r, &format!("{k}_FRAC")) as f64 / 100_000.0;
    (base + frac) * U
}
fn cx(r: &Rec, k: &str) -> f64 { coord(r, k) }
fn cy(r: &Rec, k: &str) -> f64 { -coord(r, k) }

// Altium's default schematic colours (BGR ints in the file; we just use its palette)
const C_WIRE: &str = "#0000c8";   // wires: blue
const C_SYM: &str = "#800000";    // symbol graphics: dark red
const C_PIN: &str = "#800000";
const C_DESIG: &str = "#800000";
const C_PARAM: &str = "#0000c8";
const C_NETLBL: &str = "#0000c8";

pub fn render_sheet(bytes: &[u8]) -> Result<(String, Sheet), String> {
    let mut cf = cfb::CompoundFile::open(Cursor::new(bytes.to_vec()))
        .map_err(|e| format!("altium .SchDoc: not a compound file ({e})"))?;
    let mut raw = Vec::new();
    cf.open_stream("/FileHeader")
        .map_err(|e| format!("altium .SchDoc: no FileHeader ({e})"))?
        .read_to_end(&mut raw)
        .map_err(|e| format!("altium .SchDoc: {e}"))?;

    // [u32 len][ascii] repeated
    let mut recs: Vec<Rec> = Vec::new();
    let mut off = 0usize;
    while off + 4 <= raw.len() {
        let ln = u32::from_le_bytes([raw[off], raw[off + 1], raw[off + 2], raw[off + 3]]) as usize;
        off += 4;
        if ln == 0 || off + ln > raw.len() { break; }
        let txt = String::from_utf8_lossy(&raw[off..off + ln]).trim_end_matches('\0').to_string();
        recs.push(kv(&txt));
        off += ln;
    }
    if recs.is_empty() { return Err("altium .SchDoc: no records".into()); }

    // ── components, by record ordinal (that's what OwnerIndex references) ────
    // value comes from the "Comment" parameter, footprint from the implementation
    // OwnerIndex is 0-based over records EXCLUDING the leading HEADER record,
    // so a component sitting at recs[29] is referenced as OwnerIndex=28.
    let mut comp_of: HashMap<usize, usize> = HashMap::new(); // owner-index -> comps idx
    let mut comps: Vec<Comp> = Vec::new();
    for (idx, r) in recs.iter().enumerate() {
        if s(r, "RECORD") != "1" { continue; }
        comp_of.insert(idx.saturating_sub(1), comps.len());
        comps.push(Comp {
            reference: String::new(), // from the RECORD=34 designator
            value: String::new(),
            footprint: String::new(),
            lcsc: String::new(),
            description: s(r, "COMPONENTDESCRIPTION").to_string(),
            datasheet: String::new(),
            x: cx(r, "LOCATION.X"), y: cy(r, "LOCATION.Y"),
            bbox: [f64::MAX, f64::MAX, f64::MIN, f64::MIN],
            pin_count: 0,
            regions: Vec::new(),
        });
    }

    // owner lookup for any child record
    let owner = |r: &Rec| -> Option<usize> {
        let oi = s(r, "OWNERINDEX");
        if oi.is_empty() { return None; }
        oi.parse::<usize>().ok().and_then(|k| comp_of.get(&k).copied())
    };

    // ── designator / parameters attach identity to components ───────────────
    for r in &recs {
        match s(r, "RECORD") {
            "34" => {
                if let Some(c) = owner(r) { comps[c].reference = s(r, "TEXT").to_string(); }
            }
            "41" => {
                if let Some(c) = owner(r) {
                    let name = s(r, "NAME").to_uppercase();
                    let text = s(r, "TEXT").to_string();
                    match name.as_str() {
                        "COMMENT" | "VALUE" => if comps[c].value.is_empty() { comps[c].value = text },
                        "HELPURL" | "DATASHEET" | "DATASHEETURL" => comps[c].datasheet = text,
                        n if n.contains("LCSC") || n.contains("JLC") => comps[c].lcsc = text,
                        _ => {}
                    }
                }
            }
            "45" => {
                // implementation: footprint model
                if let Some(c) = owner(r) {
                    if comps[c].footprint.is_empty() { comps[c].footprint = s(r, "MODELNAME").to_string(); }
                }
            }
            _ => {}
        }
    }

    let mut body = String::new();
    let mut ext: Vec<(f64, f64)> = Vec::new();
    // per-component extents, so the halo covers exactly that symbol
    let mut cext: Vec<Vec<(f64, f64)>> = vec![Vec::new(); comps.len()];

    let mut fit = |pts: &[(f64, f64)], c: Option<usize>, ext: &mut Vec<(f64, f64)>, cext: &mut Vec<Vec<(f64, f64)>>| {
        ext.extend_from_slice(pts);
        if let Some(ci) = c { cext[ci].extend_from_slice(pts); }
    };

    for r in &recs {
        let rt = s(r, "RECORD");
        let c = owner(r);
        let tag = match c {
            Some(ci) if !comps[ci].reference.is_empty() => format!(r##" data-cref="{}""##, esc(&comps[ci].reference)),
            _ => String::new(),
        };
        match rt {
            // pin: a stub from its root, direction from PinConglomerate bits 0-1
            "2" => {
                let (x, y) = (cx(r, "LOCATION.X"), cy(r, "LOCATION.Y"));
                let len = i(r, "PINLENGTH") as f64 * U;
                let dir = (i(r, "PINCONGLOMERATE") & 0x03) as u8;
                let (x2, y2) = match dir {
                    0 => (x + len, y),
                    1 => (x, y - len),
                    2 => (x - len, y),
                    _ => (x, y + len),
                };
                if let Some(ci) = c { comps[ci].pin_count += 1; }
                fit(&[(x, y), (x2, y2)], c, &mut ext, &mut cext);
                let _ = write!(body, r##"<line{} x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="{}" stroke-width="0.15"/>"##,
                    tag, x, y, x2, y2, C_PIN);

                // Pin NAME sits inside the body (opposite the stub) and the pin
                // DESIGNATOR just outside it — same convention as KiCad/Altium.
                // PinConglomerate bit 3 = designator visible, bit 4 = name visible.
                let cong = i(r, "PINCONGLOMERATE");
                let (name, desig) = (s(r, "NAME"), s(r, "DESIGNATOR"));
                let gap = 0.5;
                // unit vector along the stub, so we can step inward / outward
                let (ux, uy) = match dir { 0 => (1.0, 0.0), 1 => (0.0, -1.0), 2 => (-1.0, 0.0), _ => (0.0, 1.0) };
                if cong & 0x10 != 0 && !name.is_empty() {
                    // inward from the root = into the symbol body
                    let (nx, ny) = (x - ux * gap, y - uy * gap);
                    let anchor = if ux > 0.5 { "end" } else if ux < -0.5 { "start" } else { "middle" };
                    let _ = write!(body, r##"<text{} x="{:.3}" y="{:.3}" font-size="1.8" fill="{}" text-anchor="{}" dominant-baseline="middle">{}</text>"##,
                        tag, nx, ny, C_PIN, anchor, esc(name));
                    if let Some(ci) = c { cext[ci].push((nx, ny)); }
                }
                if cong & 0x08 != 0 && !desig.is_empty() {
                    // just above the outer end of the stub
                    let (dx2, dy2) = (x2 - ux * gap, y2 - uy * gap - 0.4);
                    let anchor = if ux > 0.5 { "end" } else if ux < -0.5 { "start" } else { "middle" };
                    let _ = write!(body, r##"<text{} x="{:.3}" y="{:.3}" font-size="2.1" fill="{}" text-anchor="{}">{}</text>"##,
                        tag, dx2, dy2, C_DESIG, anchor, esc(desig));
                    if let Some(ci) = c { cext[ci].push((dx2, dy2)); }
                }
            }
            // polyline / polygon: X1..Xn, Y1..Yn
            "6" | "7" => {
                let n = i(r, "LOCATIONCOUNT") as usize;
                let mut pts = Vec::new();
                for k in 1..=n {
                    let xk = format!("X{k}");
                    let yk = format!("Y{k}");
                    if !r.contains_key(&xk) { break; }
                    pts.push((coord(r, &xk), -coord(r, &yk)));
                }
                if pts.len() < 2 { continue; }
                let mut d = String::new();
                for (j, (x, y)) in pts.iter().enumerate() {
                    let _ = write!(d, "{} {:.3} {:.3} ", if j == 0 { "M" } else { "L" }, x, y);
                }
                if rt == "7" { d.push('Z'); }
                fit(&pts, c, &mut ext, &mut cext);
                let fill = if rt == "7" { C_SYM } else { "none" };
                let _ = write!(body, r##"<path{} d="{}" fill="{}" fill-opacity="0.2" stroke="{}" stroke-width="0.15"/>"##, tag, d, fill, C_SYM);
            }
            // line
            "13" => {
                let (x1, y1) = (cx(r, "LOCATION.X"), cy(r, "LOCATION.Y"));
                let (x2, y2) = (cx(r, "CORNER.X"), cy(r, "CORNER.Y"));
                fit(&[(x1, y1), (x2, y2)], c, &mut ext, &mut cext);
                let _ = write!(body, r##"<line{} x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="{}" stroke-width="0.15"/>"##,
                    tag, x1, y1, x2, y2, C_SYM);
            }
            // rectangle (10 = rounded)
            "14" | "10" => {
                let (x1, y1) = (cx(r, "LOCATION.X"), cy(r, "LOCATION.Y"));
                let (x2, y2) = (cx(r, "CORNER.X"), cy(r, "CORNER.Y"));
                fit(&[(x1, y1), (x2, y2)], c, &mut ext, &mut cext);
                let _ = write!(body, r##"<rect{} x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" fill="#fffbc8" fill-opacity="0.7" stroke="{}" stroke-width="0.15"/>"##,
                    tag, x1.min(x2), y1.min(y2), (x2 - x1).abs(), (y2 - y1).abs(), C_SYM);
            }
            // ellipse / arc
            "8" | "12" | "11" => {
                let (x, y) = (cx(r, "LOCATION.X"), cy(r, "LOCATION.Y"));
                let rr = coord(r, "RADIUS").max(coord(r, "SECONDARYRADIUS")).max(0.2);
                fit(&[(x - rr, y - rr), (x + rr, y + rr)], c, &mut ext, &mut cext);
                let _ = write!(body, r##"<circle{} cx="{:.3}" cy="{:.3}" r="{:.3}" fill="none" stroke="{}" stroke-width="0.15"/>"##,
                    tag, x, y, rr, C_SYM);
            }
            // wire (net) — the sheet's connectivity
            "27" => {
                let n = i(r, "LOCATIONCOUNT") as usize;
                let mut pts = Vec::new();
                for k in 1..=n {
                    let xk = format!("X{k}");
                    if !r.contains_key(&xk) { break; }
                    pts.push((coord(r, &xk), -coord(r, &format!("Y{k}"))));
                }
                if pts.len() < 2 { continue; }
                let mut d = String::new();
                for (j, (x, y)) in pts.iter().enumerate() {
                    let _ = write!(d, "{} {:.3} {:.3} ", if j == 0 { "M" } else { "L" }, x, y);
                }
                ext.extend_from_slice(&pts);
                let _ = write!(body, r##"<path class="adom-wire" d="{}" fill="none" stroke="{}" stroke-width="0.2" stroke-linecap="round"/>"##, d, C_WIRE);
            }
            // junction
            "29" => {
                let (x, y) = (cx(r, "LOCATION.X"), cy(r, "LOCATION.Y"));
                ext.push((x, y));
                let _ = write!(body, r##"<circle cx="{:.3}" cy="{:.3}" r="0.4" fill="{}"/>"##, x, y, C_WIRE);
            }
            // power port: stem + the actual GND / earth / bar glyph, then its text
            "17" => {
                let (x, y) = (cx(r, "LOCATION.X"), cy(r, "LOCATION.Y"));
                // Orientation: 0 right, 1 up, 2 left, 3 down (our Y points down)
                let (dx, dy) = match i(r, "ORIENTATION") { 0 => (1.0, 0.0), 1 => (0.0, -1.0), 2 => (-1.0, 0.0), _ => (0.0, 1.0) };
                let (px, py) = (-dy, dx); // perpendicular, for the bars
                let stem = 2.0;
                let (ex2, ey2) = (x + dx * stem, y + dy * stem);
                ext.push((x, y)); ext.push((ex2 + px * 2.0, ey2 + py * 2.0)); ext.push((ex2 - px * 2.0, ey2 - py * 2.0));
                let _ = write!(body, r##"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="{}" stroke-width="0.15"/>"##,
                    x, y, ex2, ey2, C_NETLBL);
                let bar = |out: &mut String, along: f64, half: f64| {
                    let (bx, by) = (x + dx * along, y + dy * along);
                    let _ = write!(out, r##"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="{}" stroke-width="0.18"/>"##,
                        bx + px * half, by + py * half, bx - px * half, by - py * half, C_NETLBL);
                };
                match i(r, "STYLE") {
                    // 4 = Power Ground: three shortening bars
                    4 => { bar(&mut body, stem, 1.6); bar(&mut body, stem + 0.55, 1.0); bar(&mut body, stem + 1.1, 0.45); }
                    // 5 = Signal Ground: solid triangle
                    5 => {
                        let (ax, ay) = (x + dx * (stem + 1.6), y + dy * (stem + 1.6));
                        let _ = write!(body, r##"<path d="M {:.3} {:.3} L {:.3} {:.3} L {:.3} {:.3} Z" fill="{}"/>"##,
                            ex2 + px * 1.5, ey2 + py * 1.5, ex2 - px * 1.5, ey2 - py * 1.5, ax, ay, C_NETLBL);
                    }
                    // 6 = Earth: one bar plus hatch strokes
                    6 => {
                        bar(&mut body, stem, 1.6);
                        for k in 0..3 {
                            let o = -1.0 + k as f64;
                            let (sx, sy) = (x + dx * stem + px * o, y + dy * stem + py * o);
                            let _ = write!(body, r##"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="{}" stroke-width="0.15"/>"##,
                                sx, sy, sx + dx * 0.9 - px * 0.6, sy + dy * 0.9 - py * 0.6, C_NETLBL);
                        }
                    }
                    // 1 = Arrow
                    1 => {
                        let (ax, ay) = (x + dx * (stem + 1.0), y + dy * (stem + 1.0));
                        let _ = write!(body, r##"<path d="M {:.3} {:.3} L {:.3} {:.3} L {:.3} {:.3} Z" fill="{}"/>"##,
                            ex2 + px * 0.8, ey2 + py * 0.8, ex2 - px * 0.8, ey2 - py * 0.8, ax, ay, C_NETLBL);
                    }
                    // 0 = Circle
                    0 => { let _ = write!(body, r##"<circle cx="{:.3}" cy="{:.3}" r="0.6" fill="none" stroke="{}" stroke-width="0.15"/>"##, ex2, ey2, C_NETLBL); }
                    // 2 = Bar (and anything else)
                    _ => bar(&mut body, stem, 1.6),
                }
                if s(r, "SHOWNETNAME") == "T" && !s(r, "TEXT").is_empty() {
                    let t = s(r, "TEXT");
                    let (tx, ty) = (x + dx * (stem + 2.4), y + dy * (stem + 2.4));
                    ext.push((tx, ty));
                    let _ = write!(body, r##"<text x="{:.3}" y="{:.3}" font-size="2.1" fill="{}" text-anchor="middle" dominant-baseline="middle">{}</text>"##,
                        tx, ty, C_NETLBL, esc(t));
                }
            }
            // designator / net label / free label
            "34" | "25" | "4" => {
                let text = if rt == "17" { s(r, "TEXT") } else { s(r, "TEXT") };
                if text.is_empty() { continue; }
                let (x, y) = (cx(r, "LOCATION.X"), cy(r, "LOCATION.Y"));
                let color = match rt { "34" => C_DESIG, "25" | "17" => C_NETLBL, _ => C_PARAM };
                ext.push((x, y)); ext.push((x + text.chars().count() as f64 * 0.9, y - 1.6));
                if let Some(ci) = c { cext[ci].push((x, y)); cext[ci].push((x + text.chars().count() as f64 * 0.9, y - 1.6)); }
                let _ = write!(body, r##"<text{} x="{:.3}" y="{:.3}" font-size="2.2" fill="{}">{}</text>"##, tag, x, y, color, esc(text));
            }
            // visible parameter (e.g. the value under a part)
            "41" => {
                if s(r, "ISHIDDEN") == "T" { continue; }
                let text = s(r, "TEXT");
                if text.is_empty() || text.starts_with('=') { continue; }
                let (x, y) = (cx(r, "LOCATION.X"), cy(r, "LOCATION.Y"));
                ext.push((x, y));
                if let Some(ci) = c { cext[ci].push((x, y)); cext[ci].push((x + text.chars().count() as f64 * 0.9, y - 1.6)); }
                let _ = write!(body, r##"<text{} x="{:.3}" y="{:.3}" font-size="2.1" fill="{}">{}</text>"##, tag, x, y, C_PARAM, esc(text));
            }
            _ => {}
        }
    }

    // component bboxes / halo regions from what we actually drew
    for (ci, pts) in cext.iter().enumerate() {
        if pts.is_empty() {
            let (x, y) = (comps[ci].x, comps[ci].y);
            comps[ci].bbox = [x - 2.0, y - 2.0, x + 2.0, y + 2.0];
        } else {
            let (mut mnx, mut mny, mut mxx, mut mxy) = (f64::MAX, f64::MAX, f64::MIN, f64::MIN);
            for (x, y) in pts { mnx = mnx.min(*x); mxx = mxx.max(*x); mny = mny.min(*y); mxy = mxy.max(*y); }
            comps[ci].bbox = [mnx, mny, mxx, mxy];
        }
        comps[ci].regions = vec![comps[ci].bbox];
    }
    // components without a designator can't be hovered meaningfully
    comps.retain(|c| !c.reference.is_empty());

    if ext.is_empty() { ext.push((0.0, 0.0)); ext.push((100.0, 100.0)); }
    let m = 5.0;
    let minx = ext.iter().map(|p| p.0).fold(f64::INFINITY, f64::min) - m;
    let miny = ext.iter().map(|p| p.1).fold(f64::INFINITY, f64::min) - m;
    let maxx = ext.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max) + m;
    let maxy = ext.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max) + m;
    let sheet = Sheet { comps, sub_sheets: Vec::new(), content_bbox: (minx, miny, maxx - minx, maxy - miny) };

    let mut out = String::new();
    let _ = write!(out, r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{:.3} {:.3} {:.3} {:.3}" preserveAspectRatio="xMidYMid meet" font-family="Times New Roman, Times, serif">"##,
        minx, miny, maxx - minx, maxy - miny);
    let _ = write!(out, r##"<rect class="adom-sheet-bg" x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" fill="#ffffff"/>"##,
        minx, miny, maxx - minx, maxy - miny);
    let _ = write!(out, r##"<g class="adom-sheet">{}</g>"##, body);
    out.push_str(&crate::sch_parse::build_overlay(&sheet));
    out.push_str("</svg>");
    Ok((out, sheet))
}