//! Native Altium `.PcbLib` WRITER — pure Rust, no Altium. The inverse of the
//! reader. Strategy: take a real `.PcbLib` as a structural TEMPLATE (its
//! `/Library/*` board/layer/stackup blob is hard to synthesize and easy to keep
//! verbatim), then:
//!   * rebuild the footprint `/<fp>/Data` stream from the part's pads — each pad
//!     record cloned byte-for-byte from a template pad and patched at the
//!     documented geometry offsets (so pads are byte-conformant to Altium's own),
//!   * rename the footprint storage + its `Parameters` + the library's
//!     `ComponentParamsTOC`, and length-safe-patch `/Library/Data`.
//!
//! NB: not yet round-tripped through a real Altium install (none available);
//! validated by re-reading with this crate's parser + byte-conformance of pad
//! records against manufacturer `.PcbLib` files. Labeled experimental upstream.

use anyhow::{anyhow, Result};
use std::io::{Cursor, Read, Write};

/// A pad to emit, in Altium internal units (1 unit = 1/10000 mil = 2.54e-6 mm).
/// `x`/`y` are the pad centre; Altium Y is up. `hole` 0 = SMD. `shape`: 1 round,
/// 2 rect, 9 rounded-rect.
pub struct WritePad {
    pub designator: String,
    pub x: i32,
    pub y: i32,
    pub xsize: u32,
    pub ysize: u32,
    pub hole: u32,
    pub shape: u8,
    pub rotation: f64,
    pub layer: u8,
}

fn u32at(d: &[u8], o: usize) -> usize {
    u32::from_le_bytes([d[o], d[o + 1], d[o + 2], d[o + 3]]) as usize
}

/// Pull a footprint storage name + a template pad's 6 sub-blocks out of a
/// footprint `Data` stream.
fn template_pad(fdata: &[u8]) -> Result<Vec<Vec<u8>>> {
    let nbl = u32at(fdata, 0);
    let mut i = 4 + nbl;
    while i < fdata.len() {
        let t = fdata[i];
        let mut j = i + 1;
        let nsub = match t { 0x02 => 6, 0x05 => 2, _ => 1 };
        let mut blocks = Vec::with_capacity(nsub);
        for _ in 0..nsub {
            if j + 4 > fdata.len() { return Err(anyhow!("truncated record")); }
            let bl = u32at(fdata, j);
            j += 4;
            if j + bl > fdata.len() { return Err(anyhow!("truncated sub-block")); }
            blocks.push(fdata[j..j + bl].to_vec());
            j += bl;
        }
        if t == 0x02 { return Ok(blocks); }
        i = j;
    }
    Err(anyhow!("no pad record in template footprint"))
}

/// Emit one pad record: clone the template's 6 sub-blocks, patch sub-block 1
/// (designator) + sub-block 5 (geometry) per the documented byte map.
fn emit_pad(tmpl: &[Vec<u8>], p: &WritePad) -> Vec<u8> {
    let mut blocks: Vec<Vec<u8>> = tmpl.to_vec();
    // sub-block 1 = designator (Pascal string)
    let db = p.designator.as_bytes();
    let n = db.len().min(255);
    let mut des = vec![n as u8];
    des.extend_from_slice(&db[..n]);
    blocks[0] = des;
    // sub-block 5 = main pad data
    let b5 = &mut blocks[4];
    if b5.len() >= 49 {
        b5[0] = p.layer;
        b5[13..17].copy_from_slice(&p.x.to_le_bytes());
        b5[17..21].copy_from_slice(&p.y.to_le_bytes());
        for off in [21usize, 29, 37] { // top / mid / bottom x,y sizes (SMD: all equal)
            b5[off..off + 4].copy_from_slice(&p.xsize.to_le_bytes());
            b5[off + 4..off + 8].copy_from_slice(&p.ysize.to_le_bytes());
        }
        b5[45..49].copy_from_slice(&p.hole.to_le_bytes());
        if b5.len() > 51 { b5[49] = p.shape; b5[50] = p.shape; b5[51] = p.shape; }
        if b5.len() >= 60 { b5[52..60].copy_from_slice(&p.rotation.to_le_bytes()); }
    }
    let mut out = vec![0x02u8];
    for b in &blocks {
        out.extend_from_slice(&(b.len() as u32).to_le_bytes());
        out.extend_from_slice(b);
    }
    out
}

fn framed(payload: &[u8]) -> Vec<u8> {
    let mut out = Vec::with_capacity(payload.len() + 5);
    out.extend_from_slice(&(payload.len() as u16).to_le_bytes());
    out.extend_from_slice(&0u16.to_le_bytes());
    out.extend_from_slice(payload);
    out
}
fn text_payload(s: &str) -> Vec<u8> { let mut p = s.as_bytes().to_vec(); p.push(0); p }

fn build_params(fp: &str, height: &str, descr: &str) -> Vec<u8> {
    framed(&text_payload(&format!(
        "|PATTERN={fp}|HEIGHT={height}|DESCRIPTION={descr}|ITEMGUID=|REVISIONGUID=")))
}
fn build_toc(fp: &str, npads: usize, height: &str, descr: &str) -> Vec<u8> {
    framed(&text_payload(&format!(
        "Name={fp}|Pad Count={npads}|Height={height}|Description={descr}")))
}

/// Length-preserving byte replacement (only safe when from.len()==to.len()).
fn raw_replace(buf: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
    if from.len() != to.len() || from.is_empty() { return buf.to_vec(); }
    let mut out = buf.to_vec();
    let mut i = 0;
    while i + from.len() <= out.len() {
        if &out[i..i + from.len()] == from {
            out[i..i + from.len()].copy_from_slice(to);
            i += from.len();
        } else { i += 1; }
    }
    out
}

/// Emit a native `.PcbLib` for one footprint, using `template` (a real `.PcbLib`)
/// for the library/board/layer housekeeping. The footprint storage is rebuilt
/// from `pads`; the rest is cloned + renamed.
pub fn to_pcblib_bytes(
    template: &[u8],
    fpname: &str,
    description: &str,
    height: &str,
    pads: &[WritePad],
) -> Result<Vec<u8>> {
    let mut v = template.to_vec();
    if v.len() < 1024 * 1024 { v.resize(1024 * 1024, 0xFF); }
    let mut c = cfb::CompoundFile::open(Cursor::new(v))?;
    let tfp = c.read_root_storage()
        .find(|e| e.is_storage() && !e.name().starts_with('\u{1}') && e.name() != "Library" && e.name() != "FileVersionInfo")
        .map(|e| e.name().to_string())
        .ok_or_else(|| anyhow!("no footprint storage in template"))?;

    let mut tdata = Vec::new();
    c.open_stream(format!("/{tfp}/Data"))?.read_to_end(&mut tdata)?;
    let tmplpad = template_pad(&tdata)?;

    // new footprint Data: name block + pad records
    let mut nd = Vec::new();
    let nb_name = fpname.as_bytes();
    let nn = nb_name.len().min(255);
    let mut nb = vec![nn as u8];
    nb.extend_from_slice(&nb_name[..nn]);
    nd.extend_from_slice(&(nb.len() as u32).to_le_bytes());
    nd.extend_from_slice(&nb);
    for p in pads { nd.extend_from_slice(&emit_pad(&tmplpad, p)); }

    // rewrite container, renaming the footprint storage + patching name refs
    let entries: Vec<_> = c.walk().map(|e| (e.path().to_path_buf(), e.is_storage())).collect();
    let mut out = cfb::CompoundFile::create(Cursor::new(Vec::new()))?;
    let old_root = format!("/{tfp}");
    let new_root = format!("/{fpname}");
    for (p, is_stg) in &entries {
        let ps = p.to_string_lossy().to_string();
        if ps == "/" { continue; }
        let nps = if ps == old_root || ps.starts_with(&format!("{old_root}/")) {
            ps.replacen(&old_root, &new_root, 1)
        } else { ps.clone() };
        if *is_stg {
            let _ = out.create_storage(&nps);
        } else {
            let mut buf = Vec::new();
            c.open_stream(&ps)?.read_to_end(&mut buf)?;
            if ps == format!("/{tfp}/Data") {
                buf = nd.clone();
            } else if ps == format!("/{tfp}/Parameters") {
                buf = build_params(fpname, height, description);
            } else if ps == "/Library/ComponentParamsTOC/Data" {
                buf = build_toc(fpname, pads.len(), height, description);
            } else if ps == "/Library/Data" {
                // only length-safe; otherwise leave the template's refs intact
                buf = raw_replace(&buf, tfp.as_bytes(), fpname.as_bytes());
            }
            out.create_stream(&nps)?.write_all(&buf)?;
        }
    }
    out.flush()?;
    Ok(out.into_inner().into_inner())
}