use alloc::{format, string::ToString, vec, vec::Vec};
use std::io::Write;
use std::path::Path;
use crate::{
domain::{ByteOrder, Channel, ChannelData, Datafile, Journal, Marker, MarkerStyle, Timestamp},
error::BiopacError,
parser::interleaved::compute_sample_pattern,
};
const DEFAULT_REVISION: i32 = 43;
const COMPRESSED_REVISION: i32 = 68;
const REVISION_TIMESTAMP: i32 = 77;
const PRE4_GRAPH_HDR_LEN: usize = 256;
const CHAN_HDR_LEN: usize = 252;
const REVISION_V30R: i32 = 44;
const REVISION_POST4: i32 = 68;
const POST4_GRAPH_HDR_LEN: usize = 1944;
const POST4_COMPRESSED_FLAG_OFFSET: usize = 1936;
const MARKER_HDR_BYTES: i32 = 8;
const MARKER_FIXED_BYTES: i32 = 14;
const MARKER_TIMESTAMP_BYTES: i32 = 8;
#[derive(Debug, Clone)]
pub struct WriteOptions {
pub revision: i32,
pub compressed: bool,
pub byte_order: ByteOrder,
}
impl WriteOptions {
pub const fn new() -> Self {
Self {
revision: DEFAULT_REVISION,
compressed: false,
byte_order: ByteOrder::LittleEndian,
}
}
#[must_use]
pub const fn compressed(mut self, compressed: bool) -> Self {
self.compressed = compressed;
self
}
#[must_use]
pub const fn revision(mut self, revision: i32) -> Self {
self.revision = revision;
self
}
#[must_use]
pub const fn byte_order(mut self, byte_order: ByteOrder) -> Self {
self.byte_order = byte_order;
self
}
pub fn write_file(&self, df: &Datafile, path: impl AsRef<Path>) -> Result<(), BiopacError> {
let file = std::fs::File::create(path).map_err(BiopacError::Io)?;
let mut w = std::io::BufWriter::new(file);
self.write_stream(df, &mut w)
}
pub fn write_stream<W: Write>(&self, df: &Datafile, w: &mut W) -> Result<(), BiopacError> {
if self.compressed {
write_compressed(df, w, self)
} else {
write_uncompressed(df, w, self)
}
}
}
impl Default for WriteOptions {
fn default() -> Self {
Self::new()
}
}
pub fn write_file(df: &Datafile, path: impl AsRef<Path>) -> Result<(), BiopacError> {
WriteOptions::default().write_file(df, path)
}
pub fn write_stream<W: Write>(df: &Datafile, w: &mut W) -> Result<(), BiopacError> {
WriteOptions::default().write_stream(df, w)
}
fn write_uncompressed<W: Write>(
df: &Datafile,
w: &mut W,
opts: &WriteOptions,
) -> Result<(), BiopacError> {
let le = opts.byte_order == ByteOrder::LittleEndian;
let n_ch = df.channels.len();
write_pre4_graph_header(w, opts.revision, n_ch, df.metadata.samples_per_second, le)?;
for ch in &df.channels {
write_channel_header(w, ch, le, opts.revision)?;
}
write_foreign_data_section(w, le)?;
for ch in &df.channels {
write_dtype_header(w, &ch.data, le)?;
}
write_interleaved_data(w, df, le)?;
write_marker_section(w, &df.markers, opts.revision, le)?;
write_journal_section(w, df.journal.as_ref(), le)?;
Ok(())
}
fn write_compressed<W: Write>(
df: &Datafile,
w: &mut W,
_opts: &WriteOptions,
) -> Result<(), BiopacError> {
let le = true;
let n_ch = df.channels.len();
write_post4_graph_header(w, COMPRESSED_REVISION, n_ch, df.metadata.samples_per_second)?;
for ch in &df.channels {
write_channel_header(w, ch, le, COMPRESSED_REVISION)?;
}
write_foreign_data_section(w, le)?;
for ch in &df.channels {
write_dtype_header(w, &ch.data, le)?;
}
write_marker_section(w, &df.markers, COMPRESSED_REVISION, le)?;
write_journal_section(w, df.journal.as_ref(), le)?;
for ch in &df.channels {
write_compressed_channel(w, ch)?;
}
Ok(())
}
fn write_pre4_graph_header<W: Write>(
w: &mut W,
revision: i32,
n_channels: usize,
samples_per_second: f64,
le: bool,
) -> Result<(), BiopacError> {
let mut buf = [0u8; PRE4_GRAPH_HDR_LEN];
let n_ch = i16::try_from(n_channels).map_err(|_| {
BiopacError::Validation(format!("too many channels to write as Pre-4: {n_channels}"))
})?;
let sample_time_ms = 1000.0_f64 / samples_per_second;
let chan_hdr_len = i16::try_from(CHAN_HDR_LEN).unwrap_or(i16::MAX);
put_i32(&mut buf, 2, revision, le); put_i32(
&mut buf,
6,
i32::try_from(PRE4_GRAPH_HDR_LEN).unwrap_or(i32::MAX),
le,
); put_i16(&mut buf, 10, n_ch, le); put_f64(&mut buf, 16, sample_time_ms, le); put_i16(&mut buf, 252, chan_hdr_len, le);
w.write_all(&buf).map_err(BiopacError::Io)
}
fn write_post4_graph_header<W: Write>(
w: &mut W,
revision: i32,
n_channels: usize,
samples_per_second: f64,
) -> Result<(), BiopacError> {
let mut buf = [0u8; POST4_GRAPH_HDR_LEN];
let n_ch = i16::try_from(n_channels).map_err(|_| {
BiopacError::Validation(format!(
"too many channels to write as Post-4: {n_channels}"
))
})?;
let graph_hdr_len = i32::try_from(POST4_GRAPH_HDR_LEN).unwrap_or(i32::MAX);
let sample_time_ms = 1000.0_f64 / samples_per_second;
put_i32(&mut buf, 2, revision, true); put_i32(&mut buf, 6, graph_hdr_len, true); put_i16(&mut buf, 10, n_ch, true); put_f64(&mut buf, 16, sample_time_ms, true); if let Some(flag) = buf.get_mut(POST4_COMPRESSED_FLAG_OFFSET) {
*flag = 1; }
w.write_all(&buf).map_err(BiopacError::Io)
}
fn write_channel_header<W: Write>(
w: &mut W,
ch: &Channel,
le: bool,
revision: i32,
) -> Result<(), BiopacError> {
let mut buf = [0u8; CHAN_HDR_LEN];
let chan_hdr_len = i32::try_from(CHAN_HDR_LEN).unwrap_or(i32::MAX);
let sample_count = i32::try_from(ch.data.len()).map_err(|_| {
BiopacError::Validation(format!("channel '{}' has too many samples", ch.name))
})?;
let (scale, offset_val) = channel_calibration(ch);
let var_sample_divider = i16::try_from(ch.frequency_divider).unwrap_or(i16::MAX);
put_i32(&mut buf, 0, chan_hdr_len, le);
let name_bytes = ch.name.as_bytes();
let name_len = name_bytes.len().min(39);
if let (Some(dst), Some(src)) = (buf.get_mut(6..6 + name_len), name_bytes.get(..name_len)) {
dst.copy_from_slice(src);
}
let units_bytes = ch.units.as_bytes();
let units_len = units_bytes.len().min(19);
if let (Some(dst), Some(src)) = (
buf.get_mut(68..68 + units_len),
units_bytes.get(..units_len),
) {
dst.copy_from_slice(src);
}
put_i32(&mut buf, 88, sample_count, le);
put_f64(&mut buf, 92, scale, le);
put_f64(&mut buf, 100, offset_val, le);
if revision >= REVISION_POST4 {
put_i16(&mut buf, 152, var_sample_divider, le);
} else if revision >= REVISION_V30R {
put_i16(&mut buf, 250, var_sample_divider, le);
}
w.write_all(&buf).map_err(BiopacError::Io)
}
const fn channel_calibration(ch: &Channel) -> (f64, f64) {
match &ch.data {
ChannelData::Scaled { scale, offset, .. } => (*scale, *offset),
_ => (1.0, 0.0),
}
}
fn write_foreign_data_section<W: Write>(w: &mut W, le: bool) -> Result<(), BiopacError> {
let bytes = if le {
0i32.to_le_bytes()
} else {
0i32.to_be_bytes()
};
w.write_all(&bytes).map_err(BiopacError::Io)
}
fn write_dtype_header<W: Write>(
w: &mut W,
data: &ChannelData,
le: bool,
) -> Result<(), BiopacError> {
let n_size: u16 = 4;
let n_type: u16 = match data {
ChannelData::Float(_) => 1,
ChannelData::Raw(_) | ChannelData::Scaled { .. } => 2,
};
let mut buf = [0u8; 4];
put_u16(&mut buf, 0, n_size, le);
put_u16(&mut buf, 2, n_type, le);
w.write_all(&buf).map_err(BiopacError::Io)
}
fn write_interleaved_data<W: Write>(w: &mut W, df: &Datafile, le: bool) -> Result<(), BiopacError> {
let dividers: Vec<u16> = df.channels.iter().map(|ch| ch.frequency_divider).collect();
let pattern = compute_sample_pattern(÷rs);
if pattern.is_empty() {
return Ok(());
}
let n_ch = df.channels.len();
let mut indices = vec![0usize; n_ch];
let totals: Vec<usize> = df.channels.iter().map(|ch| ch.data.len()).collect();
loop {
let mut any_written = false;
for &ch_idx in &pattern {
let Some(&cur) = indices.get(ch_idx) else {
continue;
};
let Some(&total) = totals.get(ch_idx) else {
continue;
};
if cur < total {
let Some(ch) = df.channels.get(ch_idx) else {
continue;
};
write_one_sample(w, &ch.data, cur, le)?;
if let Some(idx) = indices.get_mut(ch_idx) {
*idx += 1;
}
any_written = true;
}
}
if !any_written || indices.iter().zip(totals.iter()).all(|(i, t)| i >= t) {
break;
}
}
Ok(())
}
fn write_one_sample<W: Write>(
w: &mut W,
data: &ChannelData,
idx: usize,
le: bool,
) -> Result<(), BiopacError> {
match data {
ChannelData::Raw(v) | ChannelData::Scaled { raw: v, .. } => {
let sample = v.get(idx).copied().unwrap_or(0);
let bytes = if le {
sample.to_le_bytes()
} else {
sample.to_be_bytes()
};
w.write_all(&bytes).map_err(BiopacError::Io)
}
ChannelData::Float(v) => {
let sample = v.get(idx).copied().unwrap_or(0.0);
let bytes = if le {
sample.to_le_bytes()
} else {
sample.to_be_bytes()
};
w.write_all(&bytes).map_err(BiopacError::Io)
}
}
}
fn write_marker_section<W: Write>(
w: &mut W,
markers: &[Marker],
revision: i32,
le: bool,
) -> Result<(), BiopacError> {
let has_timestamp = revision >= REVISION_TIMESTAMP;
let total_bytes = compute_marker_section_bytes(markers, has_timestamp)?;
let num_markers = i32::try_from(markers.len())
.map_err(|_| BiopacError::Validation("too many markers to write".to_string()))?;
write_i32(w, total_bytes, le)?; write_i32(w, num_markers, le)?;
for m in markers {
write_marker_record(w, m, has_timestamp, le)?;
}
Ok(())
}
fn compute_marker_section_bytes(
markers: &[Marker],
has_timestamp: bool,
) -> Result<i32, BiopacError> {
let mut total: i32 = MARKER_HDR_BYTES;
for m in markers {
let text_len = i32::try_from(m.label.len()).map_err(|_| {
BiopacError::Validation(format!(
"marker label '{}' is too long to serialise",
m.label
))
})?;
let fixed = MARKER_FIXED_BYTES;
let ts = if has_timestamp {
MARKER_TIMESTAMP_BYTES
} else {
0
};
total = total
.checked_add(fixed)
.and_then(|v| v.checked_add(text_len))
.and_then(|v| v.checked_add(ts))
.ok_or_else(|| BiopacError::Validation("marker section too large".to_string()))?;
}
Ok(total)
}
fn write_marker_record<W: Write>(
w: &mut W,
m: &Marker,
has_timestamp: bool,
le: bool,
) -> Result<(), BiopacError> {
let sample = i32::try_from(m.global_sample_index).map_err(|_| {
BiopacError::Validation(format!(
"marker sample index {} overflows i32",
m.global_sample_index
))
})?;
let n_channel: i16 = match m.channel {
None => -1i16,
Some(ch) => i16::try_from(ch).map_err(|_| {
BiopacError::Validation(format!("marker channel index {ch} overflows i16"))
})?,
};
let text_bytes = m.label.as_bytes();
let text_len = i32::try_from(text_bytes.len())
.map_err(|_| BiopacError::Validation("marker label too long".to_string()))?;
write_i32(w, sample, le)?;
write_i16(w, n_channel, le)?;
w.write_all(&marker_style_code(&m.style))
.map_err(BiopacError::Io)?;
write_i32(w, text_len, le)?;
w.write_all(text_bytes).map_err(BiopacError::Io)?;
if has_timestamp {
let ts = m.created_at.map_or(0i64, Timestamp::as_secs);
write_i64(w, ts, le)?;
}
Ok(())
}
fn marker_style_code(style: &MarkerStyle) -> [u8; 4] {
match style {
MarkerStyle::Append => *b"apnd",
MarkerStyle::UserEvent => *b"usr1",
MarkerStyle::Waveform => *b"wave",
MarkerStyle::GlobalEvent => *b"glbl",
MarkerStyle::Unknown(s) => {
let mut code = [0u8; 4];
for (i, b) in s.bytes().take(4).enumerate() {
if let Some(c) = code.get_mut(i) {
*c = b;
}
}
code
}
}
}
fn write_journal_section<W: Write>(
w: &mut W,
journal: Option<&Journal>,
le: bool,
) -> Result<(), BiopacError> {
match journal {
None => write_i32(w, 0, le),
Some(j) => {
let text = j.as_text();
let len = i32::try_from(text.len()).map_err(|_| {
BiopacError::Validation("journal text too large to write".to_string())
})?;
write_i32(w, len, le)?;
w.write_all(text.as_bytes()).map_err(BiopacError::Io)
}
}
}
fn write_compressed_channel<W: Write>(w: &mut W, ch: &Channel) -> Result<(), BiopacError> {
use flate2::{Compression, write::ZlibEncoder};
let raw_bytes = channel_samples_to_le_bytes(ch);
let uncompressed_len = i32::try_from(raw_bytes.len()).map_err(|_| {
BiopacError::Validation(format!("channel '{}' data too large to compress", ch.name))
})?;
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&raw_bytes).map_err(BiopacError::Io)?;
let compressed = encoder.finish().map_err(BiopacError::Io)?;
let compressed_len = i32::try_from(compressed.len()).map_err(|_| {
BiopacError::Validation(format!("channel '{}' compressed data too large", ch.name))
})?;
write_i32(w, uncompressed_len, true)?;
write_i32(w, compressed_len, true)?;
write_i32(w, 0, true)?;
w.write_all(&compressed).map_err(BiopacError::Io)
}
fn channel_samples_to_le_bytes(ch: &Channel) -> Vec<u8> {
match &ch.data {
ChannelData::Raw(v) | ChannelData::Scaled { raw: v, .. } => {
let mut bytes = Vec::with_capacity(v.len() * 2);
for &s in v {
bytes.extend_from_slice(&s.to_le_bytes());
}
bytes
}
ChannelData::Float(v) => {
let mut bytes = Vec::with_capacity(v.len() * 8);
for &s in v {
bytes.extend_from_slice(&s.to_le_bytes());
}
bytes
}
}
}
fn write_i16<W: Write>(w: &mut W, v: i16, le: bool) -> Result<(), BiopacError> {
let bytes = if le { v.to_le_bytes() } else { v.to_be_bytes() };
w.write_all(&bytes).map_err(BiopacError::Io)
}
fn write_i32<W: Write>(w: &mut W, v: i32, le: bool) -> Result<(), BiopacError> {
let bytes = if le { v.to_le_bytes() } else { v.to_be_bytes() };
w.write_all(&bytes).map_err(BiopacError::Io)
}
fn write_i64<W: Write>(w: &mut W, v: i64, le: bool) -> Result<(), BiopacError> {
let bytes = if le { v.to_le_bytes() } else { v.to_be_bytes() };
w.write_all(&bytes).map_err(BiopacError::Io)
}
fn put_i16(buf: &mut [u8], offset: usize, v: i16, le: bool) {
let bytes = if le { v.to_le_bytes() } else { v.to_be_bytes() };
if let Some(dst) = buf.get_mut(offset..offset + 2) {
dst.copy_from_slice(&bytes);
}
}
fn put_i32(buf: &mut [u8], offset: usize, v: i32, le: bool) {
let bytes = if le { v.to_le_bytes() } else { v.to_be_bytes() };
if let Some(dst) = buf.get_mut(offset..offset + 4) {
dst.copy_from_slice(&bytes);
}
}
fn put_f64(buf: &mut [u8], offset: usize, v: f64, le: bool) {
let bytes = if le { v.to_le_bytes() } else { v.to_be_bytes() };
if let Some(dst) = buf.get_mut(offset..offset + 8) {
dst.copy_from_slice(&bytes);
}
}
fn put_u16(buf: &mut [u8], offset: usize, v: u16, le: bool) {
let bytes = if le { v.to_le_bytes() } else { v.to_be_bytes() };
if let Some(dst) = buf.get_mut(offset..offset + 2) {
dst.copy_from_slice(&bytes);
}
}