use crate::gldf::GldfProduct;
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::io::{Cursor, Read, Write};
use zip::write::SimpleFileOptions;
use zip::{ZipArchive, ZipWriter};
const MAX_HISTORY_SIZE: usize = 50;
#[derive(Clone)]
struct GldfSnapshot {
product_json: String,
}
#[derive(Clone)]
pub struct EditableGldf {
pub product: GldfProduct,
pub embedded_files: HashMap<String, Vec<u8>>,
history: Vec<GldfSnapshot>,
history_index: usize,
is_modified: bool,
original_path: Option<String>,
}
impl Default for EditableGldf {
fn default() -> Self {
Self::new()
}
}
impl EditableGldf {
pub fn new() -> Self {
Self {
product: GldfProduct::default(),
embedded_files: HashMap::new(),
history: Vec::new(),
history_index: 0,
is_modified: false,
original_path: None,
}
}
pub fn from_product(product: GldfProduct) -> Self {
Self {
product,
embedded_files: HashMap::new(),
history: Vec::new(),
history_index: 0,
is_modified: false,
original_path: None,
}
}
pub fn from_gldf(path: &str) -> Result<Self> {
let file_buf = std::fs::read(path).context("Failed to read GLDF file")?;
let mut editable = Self::from_buf(file_buf)?;
editable.original_path = Some(path.to_string());
editable.product.path = path.to_string();
Ok(editable)
}
pub fn from_buf(gldf_buf: Vec<u8>) -> Result<Self> {
let zip_buf = Cursor::new(gldf_buf);
let mut zip = ZipArchive::new(zip_buf).context("Failed to open GLDF as ZIP archive")?;
let mut xmlfile = zip
.by_name("product.xml")
.context("product.xml not found in GLDF archive")?;
let mut xml_str = String::new();
xmlfile
.read_to_string(&mut xml_str)
.context("Failed to read product.xml")?;
let product = GldfProduct::from_xml(&xml_str).context("Failed to parse product.xml")?;
drop(xmlfile);
let mut embedded_files = HashMap::new();
let mut filename_to_id: HashMap<String, String> = HashMap::new();
for file_def in &product.general_definitions.files.file {
if file_def.type_attr != "url" {
let zip_path =
Self::get_zip_path_for_file(&file_def.content_type, &file_def.file_name);
filename_to_id.insert(zip_path, file_def.id.clone());
}
}
for i in 0..zip.len() {
if let Ok(mut file) = zip.by_index(i) {
if file.is_file() {
let file_name = file.name().to_string();
if file_name == "product.xml" {
continue;
}
let mut buf = Vec::new();
if file.read_to_end(&mut buf).is_ok() {
if let Some(file_id) = filename_to_id.get(&file_name) {
embedded_files.insert(file_id.clone(), buf);
} else {
embedded_files.insert(file_name, buf);
}
}
}
}
}
Ok(Self {
product,
embedded_files,
history: Vec::new(),
history_index: 0,
is_modified: false,
original_path: None,
})
}
pub fn from_json(json_str: &str) -> Result<Self> {
let product = GldfProduct::from_json(json_str)?;
Ok(Self::from_product(product))
}
fn get_zip_path_for_file(content_type: &str, file_name: &str) -> String {
let folder = match content_type {
ct if ct.starts_with("ldc") => "ldc",
ct if ct.starts_with("geo") => "geo",
ct if ct.starts_with("image") => "image",
ct if ct.starts_with("document") => "doc",
ct if ct.starts_with("spectrum") => "spectrum",
ct if ct.starts_with("sensor") => "sensor",
ct if ct.starts_with("symbol") => "symbol",
_ => "other",
};
format!("{}/{}", folder, file_name)
}
pub fn add_embedded_file(&mut self, id: &str, data: Vec<u8>) {
self.embedded_files.insert(id.to_string(), data);
self.is_modified = true;
}
pub fn remove_embedded_file(&mut self, id: &str) -> Option<Vec<u8>> {
self.is_modified = true;
self.embedded_files.remove(id)
}
pub fn get_embedded_file(&self, id: &str) -> Option<&[u8]> {
self.embedded_files.get(id).map(|v| v.as_slice())
}
pub fn has_embedded_file(&self, id: &str) -> bool {
self.embedded_files.contains_key(id)
}
pub fn embedded_file_ids(&self) -> Vec<&str> {
self.embedded_files.keys().map(|s| s.as_str()).collect()
}
pub fn checkpoint(&mut self) {
if let Ok(json) = self.product.to_json() {
let snapshot = GldfSnapshot { product_json: json };
if self.history_index < self.history.len() {
self.history.truncate(self.history_index);
}
self.history.push(snapshot);
self.history_index = self.history.len();
if self.history.len() > MAX_HISTORY_SIZE {
self.history.remove(0);
self.history_index = self.history.len();
}
}
}
pub fn undo(&mut self) -> bool {
if self.history_index > 0 {
if self.history_index == self.history.len() {
if let Ok(json) = self.product.to_json() {
self.history.push(GldfSnapshot { product_json: json });
}
}
self.history_index -= 1;
if let Some(snapshot) = self.history.get(self.history_index) {
if let Ok(product) = GldfProduct::from_json(&snapshot.product_json) {
self.product = product;
self.is_modified = true;
return true;
}
}
}
false
}
pub fn redo(&mut self) -> bool {
if self.history_index < self.history.len().saturating_sub(1) {
self.history_index += 1;
if let Some(snapshot) = self.history.get(self.history_index) {
if let Ok(product) = GldfProduct::from_json(&snapshot.product_json) {
self.product = product;
self.is_modified = true;
return true;
}
}
}
false
}
pub fn can_undo(&self) -> bool {
self.history_index > 0
}
pub fn can_redo(&self) -> bool {
self.history_index < self.history.len().saturating_sub(1)
}
pub fn clear_history(&mut self) {
self.history.clear();
self.history_index = 0;
}
pub fn is_modified(&self) -> bool {
self.is_modified
}
pub fn mark_modified(&mut self) {
self.is_modified = true;
}
pub fn mark_saved(&mut self) {
self.is_modified = false;
}
pub fn original_path(&self) -> Option<&str> {
self.original_path.as_deref()
}
pub fn save_to_file(&mut self, path: &str) -> Result<()> {
let buf = self.save_to_buf()?;
std::fs::write(path, buf).context("Failed to write GLDF file")?;
self.original_path = Some(path.to_string());
self.is_modified = false;
Ok(())
}
pub fn save_to_buf(&self) -> Result<Vec<u8>> {
let cursor = Cursor::new(Vec::new());
let mut zip = ZipWriter::new(cursor);
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o644);
let xml = self
.product
.to_xml()
.context("Failed to serialize product to XML")?;
zip.start_file("product.xml", options)
.context("Failed to start product.xml in ZIP")?;
zip.write_all(xml.as_bytes())
.context("Failed to write product.xml")?;
for file_def in &self.product.general_definitions.files.file {
if file_def.type_attr == "url" {
continue;
}
if let Some(content) = self.embedded_files.get(&file_def.id) {
let zip_path =
Self::get_zip_path_for_file(&file_def.content_type, &file_def.file_name);
zip.start_file(&zip_path, options)
.with_context(|| format!("Failed to start {} in ZIP", zip_path))?;
zip.write_all(content)
.with_context(|| format!("Failed to write {} to ZIP", zip_path))?;
}
}
let cursor = zip.finish().context("Failed to finalize ZIP archive")?;
Ok(cursor.into_inner())
}
pub fn to_json(&self) -> Result<String> {
self.product.to_json()
}
pub fn to_pretty_json(&self) -> Result<String> {
self.product.to_pretty_json()
}
pub fn to_xml(&self) -> Result<String> {
self.product.to_xml()
}
pub fn stats(&self) -> EditableGldfStats {
let file_count = self.product.general_definitions.files.file.len();
let embedded_count = self.embedded_files.len();
let total_embedded_size: usize = self.embedded_files.values().map(|v| v.len()).sum();
let variant_count = self
.product
.product_definitions
.variants
.as_ref()
.map(|v| v.variant.len())
.unwrap_or(0);
let light_source_count = self
.product
.general_definitions
.light_sources
.as_ref()
.map(|ls| ls.fixed_light_source.len() + ls.changeable_light_source.len())
.unwrap_or(0);
EditableGldfStats {
file_definition_count: file_count,
embedded_file_count: embedded_count,
total_embedded_size,
variant_count,
light_source_count,
is_modified: self.is_modified,
history_depth: self.history.len(),
}
}
}
#[derive(Debug, Clone)]
pub struct EditableGldfStats {
pub file_definition_count: usize,
pub embedded_file_count: usize,
pub total_embedded_size: usize,
pub variant_count: usize,
pub light_source_count: usize,
pub is_modified: bool,
pub history_depth: usize,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_editable() {
let editable = EditableGldf::new();
assert!(!editable.is_modified());
assert!(editable.embedded_files.is_empty());
assert!(!editable.can_undo());
assert!(!editable.can_redo());
}
#[test]
fn test_embedded_file_management() {
let mut editable = EditableGldf::new();
editable.add_embedded_file("test_file", vec![1, 2, 3, 4]);
assert!(editable.has_embedded_file("test_file"));
assert_eq!(
editable.get_embedded_file("test_file"),
Some(&[1, 2, 3, 4][..])
);
assert!(editable.is_modified());
let removed = editable.remove_embedded_file("test_file");
assert_eq!(removed, Some(vec![1, 2, 3, 4]));
assert!(!editable.has_embedded_file("test_file"));
}
#[test]
fn test_checkpoint_and_undo() {
let mut editable = EditableGldf::new();
editable.product.header.author = "Original".to_string();
editable.checkpoint();
editable.product.header.author = "Modified".to_string();
assert!(editable.undo());
assert_eq!(editable.product.header.author, "Original");
assert!(editable.redo());
assert_eq!(editable.product.header.author, "Modified");
}
#[test]
fn test_stats() {
let editable = EditableGldf::new();
let stats = editable.stats();
assert_eq!(stats.file_definition_count, 0);
assert_eq!(stats.embedded_file_count, 0);
assert_eq!(stats.variant_count, 0);
}
}