//! Altium `.PcbLib` (footprint library) **encoder** + decoder.
//!
//! A PcbLib carries ~94 KB of `Library/*` boilerplate (master layer stack,
//! version info, pad/via library …) that is near-identical for every library,
//! plus per-component storages. So the encoder **rebuilds fresh from a real
//! Altium template**: every boilerplate stream is copied verbatim, the two
//! name-bearing library streams are patched, and one clean component is written
//! with the requested name + pad count. The template's own components are simply
//! not copied — so there's no stray default component.
//!
//! Pad record (type `0x02`) = six `[u32 len][bytes]` sub-blocks: designator
//! pstring, `00`, `|&|0` const, `00`, the 202-byte pad data block, empty. Pad
//! data offsets (from `altium-pcblib`): [0] layer, [13..17] i32 x,
//! [17..21] i32 y (Y-up), [21..25] u32 xsize, [25..29] u32 ysize,
//! [45..49] u32 hole, [49] shape, [52..60] f64 rotation.
//!
//! Component names + pad counts also live in `Library/ComponentParamsTOC/Data`
//! (one `Name=..|Pad Count=N|..` record), in a length-prefixed name list at the
//! **tail** of `Library/Data`, and in each `<comp>/Parameters`. All are rebuilt.

use crate::model::{Footprint, FpGraphic, FpLayer, Pad};
use anyhow::{bail, Context, Result};
use std::collections::BTreeMap;
use std::io::{Cursor, Read, Write};

const TEMPLATE: &[u8] = include_bytes!("../templates/pcblib_template.bin");
const TRACK_TPL: &[u8] = include_bytes!("../templates/track_tpl.bin");
const ARC_TPL: &[u8] = include_bytes!("../templates/arc_tpl.bin");
const ALTIUM_UNIT_TO_MM: f64 = 0.00000254;
const MM_TO_UNIT: f64 = 1.0 / ALTIUM_UNIT_TO_MM;

/// Component storage names in the template that we never copy (the real
/// components live in the model, not the skeleton).
const TEMPLATE_COMPONENTS: [&str; 2] = ["REFFP", "PCBComponent_1"];

/// Full layer encoding for a graphic record: `(layer_id [0], class_index [41],
/// class [43])`. Altium honours the tail pair `[41]/[43]` (not just `[0]`), so
/// both must be set or the primitive lands on whatever the template layer was.
///
/// Mapping (barrett's house convention, from his C0402): the part outline
/// lives on Mechanical 13 ("Assembly Top") and the courtyard on Mechanical 15
/// ("Courtyard Top") — nothing on Top Overlay.
///
/// ⚠ DELIBERATELY LOSSY: Silk and Fab both encode onto Mech13 (one layer byte
/// can't carry two roles), and the decoder ([`altium_decode::altium_layer`])
/// canonically maps 69 back to Fab. So a KiCad→Altium→KiCad round trip moves
/// F.SilkS art onto F.Fab. That is the house convention, not drift — the
/// `layer_roundtrip_house_convention` test pins it. Change both sites together.
fn layer_enc(l: &FpLayer) -> (u8, u8, u8) {
    match l {
        FpLayer::Silk => (69, 13, 2),       // Mechanical 13 (Assembly Top)
        FpLayer::Fab => (69, 13, 2),        // Mechanical 13 (Assembly Top)
        FpLayer::Courtyard => (71, 15, 2),  // Mechanical 15 (Courtyard Top)
        FpLayer::Copper => (1, 1, 1),       // Top Layer
        FpLayer::Other { altium } => {
            let a = *altium;
            match a {
                33 | 34 => (a, 6, 3),               // overlays
                57..=72 => (a, a - 56, 2),          // Mechanical 1..16
                _ => (a, 6, 3),
            }
        }
    }
}

fn ux(mm: f64) -> i32 {
    (mm * MM_TO_UNIT).round() as i32
}
/// Y flips: neutral Y-down → Altium Y-up.
fn uy(mm: f64) -> i32 {
    (-mm * MM_TO_UNIT).round() as i32
}

/// Patch the layer id `[0]` and the tail class pair — Track: `[41]/[43]`,
/// Arc: `[len-8]/[len-6]` — from the neutral layer role.
fn patch_layer(p: &mut [u8], l: &FpLayer, tail_b41: usize) {
    let (b0, b41, b43) = layer_enc(l);
    p[0] = b0;
    p[tail_b41] = b41;
    p[tail_b41 + 2] = b43;
}

/// A Track record (type 0x04, 1 sub-block): layer + endpoints + width.
fn emit_track(layer: &FpLayer, x1: f64, y1: f64, x2: f64, y2: f64, w: f64) -> Vec<u8> {
    let mut p = TRACK_TPL.to_vec();
    let tail = p.len() - 8; // class pair sits 8 bytes before the end
    patch_layer(&mut p, layer, tail);
    p[13..17].copy_from_slice(&ux(x1).to_le_bytes());
    p[17..21].copy_from_slice(&uy(y1).to_le_bytes());
    p[21..25].copy_from_slice(&ux(x2).to_le_bytes());
    p[25..29].copy_from_slice(&uy(y2).to_le_bytes());
    p[29..33].copy_from_slice(&((w * MM_TO_UNIT).round() as u32).to_le_bytes());
    frame_pcb_record(0x04, &p)
}

/// An Arc record (type 0x01, 1 sub-block): layer + centre + radius + angles + width.
fn emit_arc(layer: &FpLayer, cx: f64, cy: f64, r: f64, start: f64, end: f64, w: f64) -> Vec<u8> {
    let mut p = ARC_TPL.to_vec();
    let tail = p.len() - 8;
    patch_layer(&mut p, layer, tail);
    p[13..17].copy_from_slice(&ux(cx).to_le_bytes());
    p[17..21].copy_from_slice(&uy(cy).to_le_bytes());
    p[21..25].copy_from_slice(&((r * MM_TO_UNIT).round() as i32).to_le_bytes());
    p[25..33].copy_from_slice(&start.to_le_bytes());
    p[33..41].copy_from_slice(&end.to_le_bytes());
    p[41..45].copy_from_slice(&((w * MM_TO_UNIT).round() as u32).to_le_bytes());
    frame_pcb_record(0x01, &p)
}

/// `<u8 type><u32 len><payload>` — one single-block PCB record.
fn frame_pcb_record(rt: u8, payload: &[u8]) -> Vec<u8> {
    let mut rec = vec![rt];
    rec.extend_from_slice(&(payload.len() as u32).to_le_bytes());
    rec.extend_from_slice(payload);
    rec
}

/// Expand a footprint's graphics into Altium primitive records, each tagged with
/// its `(objtype, objid_name)` for the GUID/UID sidecars (Track=4, Arc=1). A Rect
/// becomes 4 tracks; a Circle becomes a full 0–360 arc; an Arc's angles get the
/// Y-flip (neutral Y-down → Altium Y-up) applied as `-end .. -start`.
fn expand_fp_graphics(fp: &Footprint) -> Vec<(u32, &'static str, Vec<u8>)> {
    let mut out = Vec::new();
    for g in &fp.graphics {
        match g {
            FpGraphic::Line { layer, x1, y1, x2, y2, width } => {
                out.push((4, "Track", emit_track(layer, *x1, *y1, *x2, *y2, *width)));
            }
            FpGraphic::Rect { layer, x1, y1, x2, y2, width } => {
                for (a, b, c, d) in [
                    (*x1, *y1, *x2, *y1),
                    (*x2, *y1, *x2, *y2),
                    (*x2, *y2, *x1, *y2),
                    (*x1, *y2, *x1, *y1),
                ] {
                    out.push((4, "Track", emit_track(layer, a, b, c, d, *width)));
                }
            }
            FpGraphic::Circle { layer, cx, cy, r, width } => {
                out.push((1, "Arc", emit_arc(layer, *cx, *cy, *r, 0.0, 360.0, *width)));
            }
            FpGraphic::Arc { layer, cx, cy, r, start, end, width } => {
                out.push((1, "Arc", emit_arc(layer, *cx, *cy, *r, -end, -start, *width)));
            }
        }
    }
    out
}

// ---------------------------------------------------------------------------
// Encode
// ---------------------------------------------------------------------------

/// Encode one footprint into a clean `.PcbLib` byte image.
pub fn encode_pcblib(fp: &Footprint) -> Result<Vec<u8>> {
    encode_pcblib_many(std::slice::from_ref(fp))
}

/// Encode several footprints into one `.PcbLib`.
pub fn encode_pcblib_many(fps: &[Footprint]) -> Result<Vec<u8>> {
    if fps.is_empty() {
        bail!("no footprints to encode");
    }
    let tpl = read_all_streams(TEMPLATE)?;
    let pad_data_tpl = harvest_pad_data_block(&tpl)?;

    // Fresh CFBF.
    let cursor = Cursor::new(Vec::<u8>::new());
    let mut comp = cfb::CompoundFile::create(cursor).context("create CFBF")?;
    let mut made_storages: std::collections::BTreeSet<String> = Default::default();

    // 1) Copy every boilerplate stream verbatim, except the template's own
    //    component storages. Patch the two name-bearing library streams.
    for (path, bytes) in &tpl {
        if is_template_component_stream(path) {
            continue;
        }
        let patched;
        let data: &[u8] = if path == "/Library/Data" {
            patched = patch_library_data_tail(bytes, fps);
            &patched
        } else if path == "/Library/ComponentParamsTOC/Data" {
            patched = build_component_toc(fps);
            &patched
        } else if path == "/Library/ComponentParamsTOC/Header" {
            patched = 1u32.to_le_bytes().to_vec();
            &patched
        } else {
            bytes
        };
        write_stream(&mut comp, path, data, &mut made_storages)?;
    }

    // 2) Write each component's storages.
    for fp in fps {
        write_component(&mut comp, fp, &pad_data_tpl, &mut made_storages)?;
    }

    comp.flush()?;
    Ok(comp.into_inner().into_inner())
}

/// Write the full set of streams for one footprint component.
fn write_component<F: Read + Write + std::io::Seek>(
    comp: &mut cfb::CompoundFile<F>,
    fp: &Footprint,
    pad_data_tpl: &[u8],
    made: &mut std::collections::BTreeSet<String>,
) -> Result<()> {
    let n = fp.pads.len();
    let base = format!("/{}", &fp.name);

    // <comp>/Data — name block + pad records.
    let mut data = Vec::new();
    let mut name_block = vec![fp.name.len() as u8];
    name_block.extend_from_slice(fp.name.as_bytes());
    data.extend_from_slice(&(name_block.len() as u32).to_le_bytes());
    data.extend_from_slice(&name_block);
    for pad in &fp.pads {
        data.extend(encode_pad_record(pad, pad_data_tpl));
    }
    // Silk / courtyard / fab graphics as Track/Arc records after the pads.
    let graphics = expand_fp_graphics(fp);
    for (_, _, rec) in &graphics {
        data.extend_from_slice(rec);
    }
    let total = n + graphics.len(); // every record is one primitive
    write_stream(comp, &format!("{base}/Data"), &data, made)?;
    write_stream(comp, &format!("{base}/Header"), &(total as u32).to_le_bytes(), made)?;

    // Parameters.
    let params = format!(
        "|PATTERN={}|HEIGHT=0mil|DESCRIPTION=|ITEMGUID=|REVISIONGUID=",
        fp.name
    );
    write_stream(comp, &format!("{base}/Parameters"), &u32_text(&params), made)?;

    // WideStrings — minimal constant.
    write_stream(comp, &format!("{base}/WideStrings"), &[0x01, 0, 0, 0, 0], made)?;

    // PrimitiveGuids/Data — [u32 hdr=0x55][objtype 0 + comp GUID][objtype 2 + idx + pad GUID]*
    let mut pg = 0x55u32.to_le_bytes().to_vec();
    pg.extend_from_slice(&0u32.to_le_bytes()); // component entry: objtype 0
    pg.extend_from_slice(&guid16(&fp.name, 0));
    for (i, _) in fp.pads.iter().enumerate() {
        pg.extend_from_slice(&2u32.to_le_bytes()); // objtype 2 = pad
        pg.extend_from_slice(&(i as u32).to_le_bytes());
        pg.extend_from_slice(&guid16(&fp.name, (i as u32) + 1));
    }
    for (gi, (objtype, _, _)) in graphics.iter().enumerate() {
        let idx = (n + gi) as u32;
        pg.extend_from_slice(&objtype.to_le_bytes());
        pg.extend_from_slice(&idx.to_le_bytes());
        pg.extend_from_slice(&guid16(&fp.name, idx + 1));
    }
    write_stream(comp, &format!("{base}/PrimitiveGuids/Data"), &pg, made)?;
    write_stream(
        comp,
        &format!("{base}/PrimitiveGuids/Header"),
        &((total as u32) + 1).to_le_bytes(),
        made,
    )?;

    // UniqueIDPrimitiveInformation/Data — one text record per primitive.
    let mut uid = Vec::new();
    for i in 0..n {
        let rec = format!("|PRIMITIVEINDEX={i}|PRIMITIVEOBJECTID=Pad|UNIQUEID=");
        uid.extend(u32_text(&rec));
    }
    for (gi, (_, objid, _)) in graphics.iter().enumerate() {
        let rec = format!("|PRIMITIVEINDEX={}|PRIMITIVEOBJECTID={objid}|UNIQUEID=", n + gi);
        uid.extend(u32_text(&rec));
    }
    write_stream(
        comp,
        &format!("{base}/UniqueIDPrimitiveInformation/Data"),
        &uid,
        made,
    )?;
    write_stream(
        comp,
        &format!("{base}/UniqueIDPrimitiveInformation/Header"),
        &(total as u32).to_le_bytes(),
        made,
    )?;
    Ok(())
}

/// `<u32 len>` + text + `\0`.
fn u32_text(text: &str) -> Vec<u8> {
    let mut payload = text.as_bytes().to_vec();
    payload.push(0);
    let mut out = (payload.len() as u32).to_le_bytes().to_vec();
    out.extend_from_slice(&payload);
    out
}

/// Build `Library/ComponentParamsTOC/Data`: one length-prefixed text record
/// holding a `Name=..|Pad Count=N|Height=0|Description=\r\n` line per component.
fn build_component_toc(fps: &[Footprint]) -> Vec<u8> {
    let mut text = String::new();
    for fp in fps {
        text.push_str(&format!(
            "Name={}|Pad Count={}|Height=0|Description=\r\n",
            fp.name,
            fp.pads.len()
        ));
    }
    u32_text(&text)
}

/// Rebuild the trailing component-name list of `Library/Data`:
/// `<u32 count>` then per component `<u32 namelen+1><u8 namelen><name>`.
/// The list sits at the very end after the last `...GUID=\0`; we locate it by
/// reading the existing trailing `<u32 count>` and slicing it off.
fn patch_library_data_tail(orig: &[u8], fps: &[Footprint]) -> Vec<u8> {
    // The original tail = [u32 count] + count*(name block). Walk backwards: the
    // template has exactly 2 names (REFFP, PCBComponent_1). Find the count u32 by
    // scanning from the known structure — simplest: reconstruct the prefix by
    // dropping the original list and appending ours.
    //
    // The list start = position of the u32 count. We find it by parsing from a
    // candidate: the byte right after the final "...GUID=\0" key. Robust approach:
    // try every position from the end that parses as a valid [count][names...]
    // consuming exactly to end-of-stream.
    if let Some(list_start) = find_name_list_start(orig) {
        let mut out = orig[..list_start].to_vec();
        out.extend_from_slice(&(fps.len() as u32).to_le_bytes());
        for fp in fps {
            let nb_len = fp.name.len() as u32 + 1;
            out.extend_from_slice(&nb_len.to_le_bytes());
            out.push(fp.name.len() as u8);
            out.extend_from_slice(fp.name.as_bytes());
        }
        out
    } else {
        // Couldn't locate — leave Library/Data unchanged (component still works
        // via storage enumeration; TOC + storages carry the truth).
        orig.to_vec()
    }
}

/// Scan from the end for a position P such that `orig[P..]` parses exactly as
/// `<u32 count>` followed by `count` blocks of `<u32 len><len bytes>` ending at
/// end-of-stream. Returns P.
fn find_name_list_start(orig: &[u8]) -> Option<usize> {
    let n = orig.len();
    // The list is short; only scan the last 256 bytes.
    let lo = n.saturating_sub(256);
    for p in (lo..=n.saturating_sub(4)).rev() {
        if parses_as_name_list(&orig[p..]) {
            return Some(p);
        }
    }
    None
}

fn parses_as_name_list(b: &[u8]) -> bool {
    if b.len() < 4 {
        return false;
    }
    let count = u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize;
    if count == 0 || count > 64 {
        return false;
    }
    let mut cur = 4;
    for _ in 0..count {
        if cur + 4 > b.len() {
            return false;
        }
        let blen = u32::from_le_bytes([b[cur], b[cur + 1], b[cur + 2], b[cur + 3]]) as usize;
        cur += 4;
        if blen == 0 || blen > 256 || cur + blen > b.len() {
            return false;
        }
        // first byte is the pstring length and must equal blen-1
        if b[cur] as usize != blen - 1 {
            return false;
        }
        cur += blen;
    }
    cur == b.len()
}

/// Build one pad record (type 0x02 + 6 sub-blocks), patching the data block.
fn encode_pad_record(pad: &Pad, pad_tpl: &[u8]) -> Vec<u8> {
    let mut designator = vec![pad.number.len() as u8];
    designator.extend_from_slice(pad.number.as_bytes());

    let mut pd = pad_tpl.to_vec();
    let x_units = (pad.x_mm * MM_TO_UNIT).round() as i32;
    let y_units = (-pad.y_mm * MM_TO_UNIT).round() as i32; // Y-down → Altium Y-up
    let xs = (pad.w_mm * MM_TO_UNIT).round() as u32;
    let ys = (pad.h_mm * MM_TO_UNIT).round() as u32;
    let hole = (pad.drill_mm * MM_TO_UNIT).round() as u32;
    let shape = shape_byte(&pad.shape);
    pd[0] = if hole > 0 { 0x21 } else { 1 }; // multilayer (thru) vs top
    pd[13..17].copy_from_slice(&x_units.to_le_bytes());
    pd[17..21].copy_from_slice(&y_units.to_le_bytes());
    pd[21..25].copy_from_slice(&xs.to_le_bytes());
    pd[25..29].copy_from_slice(&ys.to_le_bytes());
    pd[29..33].copy_from_slice(&xs.to_le_bytes());
    pd[33..37].copy_from_slice(&ys.to_le_bytes());
    pd[37..41].copy_from_slice(&xs.to_le_bytes());
    pd[41..45].copy_from_slice(&ys.to_le_bytes());
    pd[45..49].copy_from_slice(&hole.to_le_bytes());
    pd[49] = shape;
    pd[50] = shape;
    pd[51] = shape;
    if pd.len() >= 60 {
        pd[52..60].copy_from_slice(&pad.rotation.to_le_bytes());
    }

    let blocks: [&[u8]; 6] = [
        &designator,
        &[0x00],
        &[0x04, 0x7c, 0x26, 0x7c, 0x30],
        &[0x00],
        &pd,
        &[],
    ];
    let mut rec = vec![0x02u8];
    for b in blocks {
        rec.extend_from_slice(&(b.len() as u32).to_le_bytes());
        rec.extend_from_slice(b);
    }
    rec
}

fn shape_byte(shape: &str) -> u8 {
    match shape {
        "circle" | "oval" => 1,
        "roundrect" => 9,
        _ => 2,
    }
}

/// Deterministic 16-byte GUID from a name + salt (no RNG; reproducible builds).
fn guid16(name: &str, salt: u32) -> [u8; 16] {
    let mut out = [0u8; 16];
    let mut h: u64 = 0x9e3779b97f4a7c15 ^ salt as u64;
    for (i, b) in name.bytes().chain(salt.to_le_bytes()).enumerate() {
        h ^= (b as u64).wrapping_shl(((i % 8) * 8) as u32);
        h = h.wrapping_mul(0xff51afd7ed558ccd);
        h ^= h >> 33;
    }
    for i in 0..16 {
        h = h.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
        out[i] = (h >> 56) as u8;
    }
    out
}

// ---------------------------------------------------------------------------
// Template I/O
// ---------------------------------------------------------------------------

fn read_all_streams(bytes: &[u8]) -> Result<BTreeMap<String, Vec<u8>>> {
    let cursor = Cursor::new(bytes.to_vec());
    let mut comp = cfb::CompoundFile::open(cursor).context("open template")?;
    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 is_template_component_stream(path: &str) -> bool {
    TEMPLATE_COMPONENTS
        .iter()
        .any(|c| path == format!("/{c}") || path.starts_with(&format!("/{c}/")))
}

fn harvest_pad_data_block(tpl: &BTreeMap<String, Vec<u8>>) -> Result<Vec<u8>> {
    let data = tpl
        .get("/REFFP/Data")
        .context("template missing /REFFP/Data")?;
    let name_block_len = u32::from_le_bytes([data[0], data[1], data[2], data[3]]) as usize;
    let mut cur = 4 + name_block_len;
    if data.get(cur) != Some(&0x02) {
        bail!("template first record is not a pad");
    }
    cur += 1;
    let mut blocks = Vec::new();
    for _ in 0..6 {
        let blen =
            u32::from_le_bytes([data[cur], data[cur + 1], data[cur + 2], data[cur + 3]]) as usize;
        cur += 4;
        blocks.push(data[cur..cur + blen].to_vec());
        cur += blen;
    }
    Ok(blocks[4].clone())
}

/// Create any missing parent storages, then write `path` as a stream.
fn write_stream<F: Read + Write + std::io::Seek>(
    comp: &mut cfb::CompoundFile<F>,
    path: &str,
    bytes: &[u8],
    made: &mut std::collections::BTreeSet<String>,
) -> Result<()> {
    // ensure parent storages
    let parts: Vec<&str> = path.trim_start_matches('/').split('/').collect();
    let mut acc = String::new();
    for seg in &parts[..parts.len().saturating_sub(1)] {
        acc.push('/');
        acc.push_str(seg);
        if made.insert(acc.clone()) {
            comp.create_storage(&acc)
                .with_context(|| format!("create storage {acc}"))?;
        }
    }
    let mut s = comp
        .create_stream(path)
        .with_context(|| format!("create stream {path}"))?;
    s.write_all(bytes)?;
    s.flush()?;
    Ok(())
}

// ---------------------------------------------------------------------------
// Decode
// ---------------------------------------------------------------------------

/// Decode a `.PcbLib` byte image to footprint JSON via `altium-pcblib`.
pub fn decode_pcblib(bytes: &[u8]) -> Result<serde_json::Value> {
    altium_pcblib::parse_altium_pcblib_bytes(bytes).context("altium-pcblib decode")
}