//! Altium `.IntLib` (Integrated Library) **encoder** + decoder.
//!
//! An IntLib is a CFBF container that *bundles* a symbol library and a footprint
//! library so the symbol's footprint model always resolves (no external library
//! search path needed). Structure:
//! ```text
//!   /SchLib/0.schlib        0x02 marker + zlib(a complete standard .SchLib)
//!   /PCBLib/0.pcblib        0x02 marker + zlib(a complete standard .PcbLib)
//!   /LibCrossRef.Txt        symbol↔footprint cross-reference (framed strings)
//!   /Parameters   .bin      |KEY=VAL parameters (note: 3 spaces in the name)
//!   /Version.Txt            00 02 00 00 00
//! ```
//! Because we already encode the inner `.SchLib`/`.PcbLib`, the IntLib writer is
//! just: encode both, zlib-wrap, and assemble the container + crossref.

use crate::model::{Footprint, Symbol};
use crate::{encode_pcblib, encode_schlib};
use anyhow::{Context, Result};
use flate2::write::ZlibEncoder;
use flate2::Compression;
use std::io::{Cursor, Read, Write};

/// `0x02` marker byte + zlib-DEFLATE of the library bytes (Altium's scheme).
fn wrap(bytes: &[u8]) -> Result<Vec<u8>> {
    let mut enc = ZlibEncoder::new(Vec::new(), Compression::default());
    enc.write_all(bytes)?;
    let z = enc.finish()?;
    let mut out = vec![0x02u8];
    out.extend_from_slice(&z);
    Ok(out)
}

/// `[u32 len+1][u8 len][bytes]` — a LibCrossRef string block.
fn sblock(s: &str) -> Vec<u8> {
    let b = s.as_bytes();
    let mut out = ((b.len() + 1) as u32).to_le_bytes().to_vec();
    out.push(b.len() as u8);
    out.extend_from_slice(b);
    out
}

fn lib_cross_ref(comp: &str, descr: &str, fp: &str) -> Vec<u8> {
    let one = 1u32.to_le_bytes();
    let mut o = vec![0u8];
    o.extend_from_slice(&one);
    o.extend(sblock(comp));
    o.extend(sblock(r":\SchLib\0.schlib"));
    o.extend_from_slice(&one);
    o.extend(sblock(descr));
    o.extend(sblock(&format!(r"C:\adom\{comp}.SchLib")));
    o.extend_from_slice(&one);
    o.extend(sblock(fp));
    o.extend(sblock("PCBLIB"));
    o.extend_from_slice(&one);
    o.extend(sblock(r":\PCBLib\0.pcblib"));
    o.extend(sblock(&format!(r"C:\adom\{fp}.PcbLib")));
    o
}

/// Encode a symbol + footprint into a self-contained `.IntLib`. The symbol must
/// carry `footprint = Some(footprint.name)` so its PCBLIB model resolves against
/// the bundled footprint.
pub fn encode_intlib(symbol: &Symbol, footprint: &Footprint, descr: &str) -> Result<Vec<u8>> {
    let schlib = encode_schlib(std::slice::from_ref(symbol))?;
    let pcblib = encode_pcblib(footprint)?;

    let cursor = Cursor::new(Vec::<u8>::new());
    let mut comp = cfb::CompoundFile::create(cursor).context("create IntLib CFBF")?;
    comp.create_storage("/SchLib")?;
    comp.create_storage("/PCBLib")?;

    write_stream(&mut comp, "/SchLib/0.schlib", &wrap(&schlib)?)?;
    write_stream(&mut comp, "/PCBLib/0.pcblib", &wrap(&pcblib)?)?;
    write_stream(
        &mut comp,
        "/LibCrossRef.Txt",
        &lib_cross_ref(&symbol.name, descr, &footprint.name),
    )?;
    write_stream(&mut comp, "/Version.Txt", &[0x00, 0x02, 0x00, 0x00, 0x00])?;

    // Parameters   .bin — [u8 0x00] then [u32 len][text\0] records:
    // one for the symbol's parameters, then one for the footprint's. (No leading
    // pipe in the text, unlike the |RECORD records.)
    let mut params = vec![0x00u8];
    let push_rec = |buf: &mut Vec<u8>, text: &str| {
        let mut p = text.as_bytes().to_vec();
        p.push(0);
        buf.extend_from_slice(&(p.len() as u32).to_le_bytes());
        buf.extend_from_slice(&p);
    };
    push_rec(
        &mut params,
        &format!(
            "Comment=*|Component Kind=Standard|Description={descr}|Footprint={}",
            footprint.name
        ),
    );
    push_rec(
        &mut params,
        &format!("Height=0mil|Pad Count={}", footprint.pads.len()),
    );
    write_stream(&mut comp, "/Parameters   .bin", &params)?;

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

fn write_stream<F: Read + Write + std::io::Seek>(
    comp: &mut cfb::CompoundFile<F>,
    path: &str,
    bytes: &[u8],
) -> Result<()> {
    let mut s = comp
        .create_stream(path)
        .with_context(|| format!("create {path}"))?;
    s.write_all(bytes)?;
    s.flush()?;
    Ok(())
}

/// Decode an `.IntLib`: inflate the embedded libs and parse each with the
/// existing readers. Returns `(symbol_json, footprint_json)`.
pub fn decode_intlib(bytes: &[u8]) -> Result<(serde_json::Value, serde_json::Value)> {
    let (sch, pcb) = extract_intlib_libs(bytes)?;
    Ok((crate::decode_schlib_first(&sch)?, crate::decode_pcblib(&pcb)?))
}

/// Extract the raw (inflated) embedded `.SchLib` + `.PcbLib` byte images from an
/// IntLib — for callers that want to re-decode them into the neutral model.
pub fn extract_intlib_libs(bytes: &[u8]) -> Result<(Vec<u8>, Vec<u8>)> {
    use flate2::read::ZlibDecoder;
    let cursor = Cursor::new(bytes.to_vec());
    let mut comp = cfb::CompoundFile::open(cursor).context("open IntLib")?;
    let inflate = |comp: &mut cfb::CompoundFile<Cursor<Vec<u8>>>, path: &str| -> Result<Vec<u8>> {
        let mut raw = Vec::new();
        comp.open_stream(path)?.read_to_end(&mut raw)?;
        // Payload is a 0x02 marker byte + zlib stream — verify, don't assume.
        match raw.first() {
            None => anyhow::bail!("{path}: empty embedded stream"),
            Some(0x02) => {}
            Some(b) => anyhow::bail!("{path}: unexpected marker byte {b:#04x} (expected 0x02)"),
        }
        let mut out = Vec::new();
        ZlibDecoder::new(&raw[1..]).read_to_end(&mut out).with_context(|| format!("{path}: inflate"))?;
        Ok(out)
    };
    let sch = inflate(&mut comp, "/SchLib/0.schlib")?;
    let pcb = inflate(&mut comp, "/PCBLib/0.pcblib")?;
    Ok((sch, pcb))
}