use core::fmt;
use std::cell::RefCell;
use std::fs::File;
use std::io::{Cursor, Read, Seek};
use std::path::Path;
use std::sync::Arc;
use cfb::CompoundFile;
use super::record::{parse_all_records, Sheet};
use super::storage::Storage;
use super::{SchDrawCtx, SchRecord};
use crate::common::split_altium_map;
use crate::draw::{Canvas, Draw};
use crate::error::AddContext;
use crate::parse::{extract_sized_buf, BufLenMatch, ParseUtf8};
use crate::{Error, ErrorKind, UniqueId};
const HEADER: &str = "Protel for Windows - Schematic Capture Binary File Version 5.0";
const DATA_STREAM: &str = "FileHeader";
pub struct SchDoc<F> {
#[allow(dead_code)]
cfile: RefCell<CompoundFile<F>>,
sheet: Sheet,
records: Vec<SchRecord>,
unique_id: UniqueId,
storage: Arc<Storage>,
}
impl SchDoc<File> {
pub fn open<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let cfile = cfb::open(&path)?;
Self::from_cfile(cfile)
.context("parsing SchLib")
.or_context(|| format!("with file {}", path.as_ref().display()))
}
}
impl<'a> SchDoc<Cursor<&'a [u8]>> {
pub fn from_buffer(buf: &'a [u8]) -> Result<Self, Error> {
let cfile = cfb::CompoundFile::open(Cursor::new(buf))?;
Self::from_cfile(cfile).context("parsing SchDoc from Cursor")
}
}
impl<F: Read + Seek> SchDoc<F> {
pub fn records(&self) -> impl Iterator<Item = &SchRecord> {
self.records.iter()
}
pub fn draw<C: Canvas>(&self, canvas: &mut C) {
let ctx = SchDrawCtx {
storage: &self.storage,
fonts: &self.sheet.fonts,
};
self.records().for_each(|r| r.draw(canvas, &ctx));
}
fn from_cfile(mut cfile: CompoundFile<F>) -> Result<Self, Error> {
let mut tmp_buf: Vec<u8> = Vec::new();
let storage = Storage::parse_cfile(&mut cfile, &mut tmp_buf)?;
tmp_buf.clear();
{
let mut stream = cfile.open_stream(DATA_STREAM).map_err(|e| {
Error::from(e).context(format!("reading required stream `{DATA_STREAM}`"))
})?;
stream.read_to_end(&mut tmp_buf).unwrap();
}
let (rest, unique_id) = parse_header(&tmp_buf)?;
let mut records = parse_all_records(rest, "SchDoc::from_cfile")?;
let sheet_pos = records
.iter()
.position(|x| matches!(x, SchRecord::Sheet(_)));
let sheet = sheet_pos
.map(|idx| {
let SchRecord::Sheet(sheet) = records.remove(idx) else {
unreachable!()
};
sheet
})
.unwrap_or_default();
Ok(Self {
cfile: RefCell::new(cfile),
records,
sheet,
storage: storage.into(),
unique_id,
})
}
}
impl<F> fmt::Debug for SchDoc<F> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SchDoc")
.field("unique_id", &self.unique_id)
.finish_non_exhaustive()
}
}
fn parse_header(buf: &[u8]) -> Result<(&[u8], UniqueId), Error> {
let mut uid = None;
let (hdr, rest) = extract_sized_buf(buf, BufLenMatch::U32, true)?;
for (key, val) in split_altium_map(hdr) {
match key {
b"HEADER" => {
if val != HEADER.as_bytes() {
return Err(ErrorKind::new_invalid_header(val, HEADER).into());
}
}
b"UniqueID" => uid = Some(val.parse_as_utf8()?),
_ => (),
}
}
let uid = uid.ok_or(ErrorKind::MissingUniqueId)?;
Ok((rest, uid))
}