//! Altium `.SchLib` / `.PcbLib` / `.IntLib` → canonical [`AdomLbrPart`] (the
//! DECODE side — the inverse of `schlib`/`pcblib`/`intlib` encoders).
//!
//! Walks the OLE2/CFBF component streams directly so we control unit conversion:
//! symbol pins + graphics come back in mils, footprint pads + graphics in mm
//! (Y-down neutral), with silk/courtyard/fab layers preserved as [`FpLayer`]s.

use crate::model::{
    AdomLbrPart, ConnectEntry, Footprint, FpGraphic, FpLayer, Graphic, Pad, Pin, Symbol,
};
use anyhow::{Context, Result};
use std::collections::BTreeMap;
use std::io::{Cursor, Read};

/// Altium base unit = 1/10000 mil = 0.00000254 mm (PCB coordinates).
const ALTIUM_UNIT_MM: f64 = 0.00000254;
/// Schematic binary coordinate units per mil (pin X).
const UNITS_PER_MIL: f64 = 25.6;

// ---------------------------------------------------------------------------
// Public entry points
// ---------------------------------------------------------------------------

/// Decode a `.SchLib` + `.PcbLib` pair into the canonical part. Either may be
/// `None` (e.g. footprint-only), but at least one is required.
pub fn decode_altium(schlib: Option<&[u8]>, pcblib: Option<&[u8]>, mpn: &str) -> Result<AdomLbrPart> {
    let symbol = match schlib {
        Some(b) => decode_schlib_symbol(b)?,
        None => Symbol {
            name: mpn.to_string(),
            pins: vec![],
            body: None,
            graphics: vec![],
            designator_prefix: Some("U".into()),
            footprint: None,
            parameters: vec![],
            pin_numbers_hidden: None,
            pin_names_hidden: None,
        },
    };
    let footprint = match pcblib {
        Some(b) => decode_pcblib_footprint(b)?,
        None => Footprint { name: mpn.to_string(), pads: vec![], graphics: vec![] },
    };
    build_part(symbol, footprint, mpn)
}

/// Decode a `.IntLib` (bundles both) into the canonical part.
pub fn decode_intlib_part(bytes: &[u8], mpn: &str) -> Result<AdomLbrPart> {
    let (sch, pcb) = crate::intlib::extract_intlib_libs(bytes)?;
    decode_altium(Some(&sch), Some(&pcb), mpn)
}

fn build_part(mut symbol: Symbol, footprint: Footprint, mpn: &str) -> Result<AdomLbrPart> {
    if symbol.footprint.is_none() && !footprint.name.is_empty() {
        symbol.footprint = Some(footprint.name.clone());
    }
    // identity connect over shared designators
    let pad_nums: std::collections::HashSet<&str> =
        footprint.pads.iter().map(|p| p.number.as_str()).collect();
    let connect = symbol
        .pins
        .iter()
        .filter(|p| pad_nums.contains(p.number.as_str()))
        .map(|p| ConnectEntry { pin: p.number.clone(), pad: p.number.clone() })
        .collect();
    // pull value/manufacturer/etc. out of the symbol's decoded parameters
    let param = |name: &str| {
        symbol
            .parameters
            .iter()
            .find(|p| p.name.eq_ignore_ascii_case(name))
            .map(|p| p.value.clone())
            .unwrap_or_default()
    };
    Ok(AdomLbrPart {
        schema_version: 2,
        mpn: if mpn.is_empty() { symbol.name.clone() } else { mpn.to_string() },
        manufacturer: param("Manufacturer"),
        package: param("Package"),
        value: param("Value"),
        category: String::new(),
        symbol,
        footprint,
        connect,
        model_3d: None,
        provenance: Some(serde_json::json!({"source": "altium_decode"})),
    })
}

// ---------------------------------------------------------------------------
// CFBF helpers
// ---------------------------------------------------------------------------

fn read_streams(bytes: &[u8]) -> Result<BTreeMap<String, Vec<u8>>> {
    let mut comp = cfb::CompoundFile::open(Cursor::new(bytes.to_vec())).context("open CFBF")?;
    let paths: Vec<String> = comp
        .walk()
        .filter(|e| e.is_stream())
        .map(|e| e.path().to_string_lossy().to_string())
        .collect();
    let mut map = BTreeMap::new();
    for p in paths {
        let mut buf = Vec::new();
        comp.open_stream(&p)?.read_to_end(&mut buf)?;
        map.insert(p, buf);
    }
    Ok(map)
}

fn le_u16(b: &[u8], o: usize) -> u16 {
    u16::from_le_bytes([*b.get(o).unwrap_or(&0), *b.get(o + 1).unwrap_or(&0)])
}
fn le_u32(b: &[u8], o: usize) -> u32 {
    u32::from_le_bytes([
        *b.get(o).unwrap_or(&0),
        *b.get(o + 1).unwrap_or(&0),
        *b.get(o + 2).unwrap_or(&0),
        *b.get(o + 3).unwrap_or(&0),
    ])
}
fn le_i32(b: &[u8], o: usize) -> i32 {
    le_u32(b, o) as i32
}
fn le_i16(b: &[u8], o: usize) -> i16 {
    le_u16(b, o) as i16
}
fn le_i24(b: &[u8], o: usize) -> i32 {
    let v = (*b.get(o).unwrap_or(&0) as i32)
        | ((*b.get(o + 1).unwrap_or(&0) as i32) << 8)
        | ((*b.get(o + 2).unwrap_or(&0) as i32) << 16);
    (v << 8) >> 8 // sign-extend 24-bit
}
fn le_f64(b: &[u8], o: usize) -> f64 {
    let mut a = [0u8; 8];
    for i in 0..8 {
        a[i] = *b.get(o + i).unwrap_or(&0);
    }
    f64::from_le_bytes(a)
}

/// Read a `|KEY=VAL|...` field out of a text record.
fn field<'a>(txt: &'a str, key: &str) -> Option<&'a str> {
    let pat = format!("|{key}=");
    let i = txt.find(&pat)? + pat.len();
    let rest = &txt[i..];
    let end = rest.find('|').unwrap_or(rest.len());
    Some(&rest[..end])
}

// ---------------------------------------------------------------------------
// SchLib → Symbol
// ---------------------------------------------------------------------------

fn decode_schlib_symbol(bytes: &[u8]) -> Result<Symbol> {
    let streams = read_streams(bytes)?;
    // first component storage's Data (skip housekeeping streams)
    let (name, data) = streams
        .iter()
        .filter(|(p, _)| p.ends_with("/Data") && !p.contains("FileHeader") && !p.contains("Storage"))
        .map(|(p, d)| {
            let comp = p.trim_start_matches('/').split('/').next().unwrap_or("SYM").to_string();
            (comp, d.clone())
        })
        .next()
        .context("SchLib has no component Data stream")?;

    let mut pins = Vec::new();
    let mut graphics = Vec::new();
    let mut parameters = Vec::new();
    let mut designator_prefix = Some("U".to_string());
    let mut footprint = None;
    let mut lib_name = name.clone();

    let mut cur = 0usize;
    while cur + 4 <= data.len() {
        let len = le_u16(&data, cur) as usize;
        let flags = le_u16(&data, cur + 2);
        let payload = &data[cur + 4..(cur + 4 + len).min(data.len())];
        cur += 4 + len;
        if flags == 0x0100 {
            // binary pin record
            if let Some(p) = decode_pin(payload) {
                pins.push(p);
            }
            continue;
        }
        // text record
        if payload.first() != Some(&b'|') {
            continue;
        }
        let txt = String::from_utf8_lossy(payload);
        let txt = txt.split('\0').next().unwrap_or("");
        let rec = field(txt, "RECORD").unwrap_or("");
        match rec {
            "1" => {
                if let Some(n) = field(txt, "LibReference") {
                    lib_name = n.to_string();
                }
            }
            "6" | "11" | "12" | "14" => {
                if let Some(g) = decode_sym_graphic(txt, rec) {
                    graphics.push(g);
                }
            }
            "34" => {
                if let Some(t) = field(txt, "Text") {
                    designator_prefix = Some(t.trim_end_matches('?').to_string());
                }
            }
            "41" => {
                if let (Some(name), Some(text)) = (field(txt, "Name"), field(txt, "Text")) {
                    if name != "Comment" && !text.is_empty() && text != "*" {
                        parameters.push(crate::model::Parameter {
                            name: name.to_string(),
                            value: text.to_string(),
                            visible: !txt.contains("|IsHidden=T"),
                        });
                    }
                }
            }
            "45" => {
                if let Some(m) = field(txt, "ModelName") {
                    footprint = Some(m.to_string());
                }
            }
            _ => {}
        }
    }

    Ok(Symbol {
        name: lib_name,
        pins,
        body: None,
        graphics,
        designator_prefix,
        footprint,
        parameters,
        pin_numbers_hidden: None,
        pin_names_hidden: None,
    })
}

/// Decode a binary pin record → neutral [`Pin`] (connection-point mils).
fn decode_pin(p: &[u8]) -> Option<Pin> {
    if p.len() < 26 {
        return None;
    }
    // orientation packed in low 2 bits of the length field [15:17]
    let field15 = le_u16(p, 15);
    let aorient = (field15 & 0x3) as u32;
    let length_units = (field15 & 0xFFFC) as f64;
    let len_mil = (length_units / UNITS_PER_MIL).round() as i32;
    // body-end location: X = i24 @[17:20] (÷25.6/mil), Y = i16 @[20:22] (×10 mil)
    let body_x = (le_i24(p, 17) as f64 / UNITS_PER_MIL).round() as i32;
    let body_y = (le_i16(p, 20) as i32) * 10;
    // kicad angle q from aorient = (q + 2) % 4
    let q = ((aorient + 2) % 4) as i32;
    let (dx, dy) = match q % 4 {
        0 => (1, 0),
        1 => (0, 1),
        2 => (-1, 0),
        _ => (0, -1),
    };
    // connection = body_end - length * dir
    let x = body_x - len_mil * dx;
    let y = body_y - len_mil * dy;
    // strings: name pstr then designator pstr @ 26
    let mut j = 26usize;
    let name = read_pstr(p, &mut j);
    let number = read_pstr(p, &mut j);
    Some(Pin {
        number: if number.is_empty() { "?".into() } else { number },
        name,
        x,
        y,
        length: Some(len_mil),
        rotation: (q * 90) as u16,
    })
}

fn read_pstr(p: &[u8], j: &mut usize) -> String {
    if *j >= p.len() {
        return String::new();
    }
    let n = p[*j] as usize;
    let start = *j + 1;
    let end = (start + n).min(p.len());
    *j = end;
    String::from_utf8_lossy(&p[start..end]).to_string()
}

/// Decode a symbol graphic text record (RECORD=6/11/12/14) → [`Graphic`] (mils).
fn decode_sym_graphic(txt: &str, rec: &str) -> Option<Graphic> {
    // coordinate = base*10 + frac/10000 (mils), from `KEY` + optional `KEY_Frac`
    let coord = |key: &str| -> f64 {
        let base: f64 = field(txt, key).and_then(|v| v.parse().ok()).unwrap_or(0.0);
        let frac: f64 = field(txt, &format!("{key}_Frac"))
            .and_then(|v| v.parse().ok())
            .unwrap_or(0.0);
        base * 10.0 + frac / 10000.0
    };
    let width = field(txt, "LineWidth").and_then(|v| v.parse::<f64>().ok()).unwrap_or(1.0) * 10.0;
    match rec {
        "14" => Some(Graphic::Rect {
            x1: coord("Location.X"),
            y1: coord("Location.Y"),
            x2: coord("Corner.X"),
            y2: coord("Corner.Y"),
            width,
            filled: txt.contains("|IsSolid=T"),
        }),
        "11" => Some(Graphic::Circle {
            cx: coord("Location.X"),
            cy: coord("Location.Y"),
            r: coord("Radius"),
            width,
        }),
        "12" => Some(Graphic::Arc {
            cx: coord("Location.X"),
            cy: coord("Location.Y"),
            r: coord("Radius"),
            start: field(txt, "StartAngle").and_then(|v| v.parse().ok()).unwrap_or(0.0),
            end: field(txt, "EndAngle").and_then(|v| v.parse().ok()).unwrap_or(360.0),
            width,
        }),
        "6" => {
            let n: usize = field(txt, "LocationCount").and_then(|v| v.parse().ok()).unwrap_or(0);
            let mut points = Vec::new();
            for i in 1..=n {
                points.push([coord(&format!("X{i}")), coord(&format!("Y{i}"))]);
            }
            if points.len() >= 2 {
                Some(Graphic::Polyline { points, width })
            } else {
                None
            }
        }
        _ => None,
    }
}

// ---------------------------------------------------------------------------
// PcbLib → Footprint (pads + silk/courtyard/fab graphics)
// ---------------------------------------------------------------------------

fn decode_pcblib_footprint(bytes: &[u8]) -> Result<Footprint> {
    let streams = read_streams(bytes)?;
    let (name, data) = streams
        .iter()
        .filter(|(p, _)| {
            p.ends_with("/Data")
                && !p.contains("/Library")
                && !p.contains("FileVersionInfo")
                && !p.contains("FileHeader")
        })
        .map(|(p, d)| {
            let comp = p.trim_start_matches('/').split('/').next().unwrap_or("FP").to_string();
            (comp, d.clone())
        })
        .next()
        .context("PcbLib has no component Data stream")?;

    // component name block: <u32 len><name>
    let name_len = le_u32(&data, 0) as usize;
    let mut cur = 4 + name_len;
    let fp_name = {
        // the block holds a pstring: <u8 len><name>
        let blk = &data[4..(4 + name_len).min(data.len())];
        if !blk.is_empty() {
            let n = blk[0] as usize;
            String::from_utf8_lossy(&blk[1..(1 + n).min(blk.len())]).to_string()
        } else {
            name
        }
    };

    let mut pads = Vec::new();
    let mut graphics = Vec::new();

    while cur < data.len() {
        let rt = data[cur];
        cur += 1;
        let nblocks = match rt {
            0x02 => 6,             // Pad
            0x05 => 2,             // Text
            0x01 | 0x03 | 0x04 | 0x06 | 0x09 | 0x0B | 0x0C => 1,
            _ => 1,
        };
        let mut blocks: Vec<&[u8]> = Vec::new();
        let mut ok = true;
        for _ in 0..nblocks {
            if cur + 4 > data.len() {
                ok = false;
                break;
            }
            let blen = le_u32(&data, cur) as usize;
            cur += 4;
            if cur + blen > data.len() {
                ok = false;
                break;
            }
            blocks.push(&data[cur..cur + blen]);
            cur += blen;
        }
        if !ok {
            break;
        }
        match rt {
            0x02 => {
                if let Some(pad) = decode_pad(&blocks) {
                    pads.push(pad);
                }
            }
            0x04 => {
                if let Some(g) = decode_track(blocks[0]) {
                    graphics.push(g);
                }
            }
            0x01 => {
                if let Some(g) = decode_arc(blocks[0]) {
                    graphics.push(g);
                }
            }
            _ => {}
        }
    }

    Ok(Footprint { name: fp_name, pads, graphics })
}

/// Altium layer byte → neutral role. Counterpart of `pcblib::layer_enc`.
/// NOTE: the house convention encodes BOTH Silk and Fab onto Mech13 (69), so
/// 69 canonically decodes to Fab — silk art merged there on encode stays Fab
/// after a round trip (deliberate; see the doc on `layer_enc`). Vendor libs
/// (UL/SamacSys) put silk on TopOverlay (33), which decodes to Silk correctly.
fn altium_layer(byte: u8) -> FpLayer {
    match byte {
        33 => FpLayer::Silk,
        69 => FpLayer::Fab,
        71 => FpLayer::Courtyard,
        1 | 32 => FpLayer::Copper,
        b => FpLayer::Other { altium: b },
    }
}

fn decode_pad(blocks: &[&[u8]]) -> Option<Pad> {
    let designator = blocks.first()?;
    let n = *designator.first()? as usize;
    let number = String::from_utf8_lossy(&designator[1..(1 + n).min(designator.len())]).to_string();
    let pd = blocks.get(4)?; // pad data block
    if pd.len() < 60 {
        return None;
    }
    let x = le_i32(pd, 13) as f64 * ALTIUM_UNIT_MM;
    let y = -(le_i32(pd, 17) as f64) * ALTIUM_UNIT_MM; // Altium Y-up → neutral Y-down
    let w = le_u32(pd, 21) as f64 * ALTIUM_UNIT_MM;
    let h = le_u32(pd, 25) as f64 * ALTIUM_UNIT_MM;
    let hole = le_u32(pd, 45) as f64 * ALTIUM_UNIT_MM;
    let shape = match pd[49] {
        1 => "circle",
        9 => "roundrect",
        _ => "rect",
    };
    let rotation = if pd.len() >= 60 { le_f64(pd, 52) } else { 0.0 };
    Some(Pad {
        number,
        x_mm: x,
        y_mm: y,
        w_mm: w,
        h_mm: h,
        drill_mm: hole,
        rotation,
        shape: shape.to_string(),
        // The plated flag byte in the Altium pad record isn't calibrated yet —
        // unknown, so leave None (treated as plated downstream).
        plated: None,
    })
}

/// Track (0x04): [0]layer, [13:17]x1, [17:21]y1, [21:25]x2, [25:29]y2, [29:33]width.
fn decode_track(p: &[u8]) -> Option<FpGraphic> {
    if p.len() < 33 {
        return None;
    }
    let layer = altium_layer(p[0]);
    let c = |o: usize| le_i32(p, o) as f64 * ALTIUM_UNIT_MM;
    Some(FpGraphic::Line {
        layer,
        x1: c(13),
        y1: -c(17),
        x2: c(21),
        y2: -c(25),
        width: le_u32(p, 29) as f64 * ALTIUM_UNIT_MM,
    })
}

/// Arc (0x01): [0]layer, [13:17]cx, [17:21]cy, [21:25]r, [25:33]f64 start,
/// [33:41]f64 end, [41:45]width. A full 0..360 arc becomes a Circle.
fn decode_arc(p: &[u8]) -> Option<FpGraphic> {
    if p.len() < 45 {
        return None;
    }
    let layer = altium_layer(p[0]);
    let c = |o: usize| le_i32(p, o) as f64 * ALTIUM_UNIT_MM;
    let cx = c(13);
    let cy = -c(17);
    let r = c(21);
    let start = le_f64(p, 25);
    let end = le_f64(p, 33);
    let width = le_u32(p, 41) as f64 * ALTIUM_UNIT_MM;
    if (end - start).abs() >= 359.9 || (start == 0.0 && end == 360.0) {
        Some(FpGraphic::Circle { layer, cx, cy, r, width })
    } else {
        // Y-flip mirrors angles: negate + swap so the visible sweep matches.
        Some(FpGraphic::Arc { layer, cx, cy, r, start: -end, end: -start, width })
    }
}