Skip to main content

atlas_archive_core/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![cfg_attr(feature = "simd", feature(portable_simd))]
3extern crate alloc;
4
5pub mod ans;
6pub mod huffman;
7pub mod lzma;
8pub mod mixer;
9pub mod poor_compress;
10pub mod predictor;
11pub mod transform;
12
13pub mod loom;
14
15#[cfg(feature = "std")]
16extern crate std;
17#[cfg(feature = "std")]
18use rayon::prelude::*;
19#[cfg(feature = "std")]
20use std::sync::Once;
21
22#[cfg(feature = "std")]
23static THREAD_POOL_INIT: Once = Once::new();
24
25/// Global shared runtime for background PagedLoom writes.
26#[cfg(all(feature = "std", feature = "async"))]
27pub(crate) fn get_background_runtime() -> Option<std::sync::Arc<tokio::runtime::Runtime>> {
28    use std::sync::OnceLock;
29    static RUNTIME: OnceLock<Option<std::sync::Arc<tokio::runtime::Runtime>>> = OnceLock::new();
30    RUNTIME
31        .get_or_init(|| {
32            tokio::runtime::Builder::new_multi_thread()
33                .worker_threads(4) // Increased for faster async I/O
34                .enable_all()
35                .build()
36                .ok()
37                .map(std::sync::Arc::new)
38        })
39        .clone()
40}
41
42/// Initializes the Rayon thread pool to use at most 80% of available CPU cores.
43#[cfg(feature = "std")]
44pub fn init_thread_pool() {
45    THREAD_POOL_INIT.call_once(|| {
46        let cpus = num_cpus::get();
47        let threads = (cpus * 8 / 10).max(1);
48        let _ = rayon::ThreadPoolBuilder::new()
49            .num_threads(threads)
50            .build_global();
51    });
52}
53
54use crate::ans::{RansDecoder, RansEncoder};
55use crate::loom::{LoomEnum, LoomPredictor, LoomWeaver};
56use crate::transform::{Transform, TransformChain};
57use alloc::vec::Vec;
58
59const CHUNK_SIZE: usize = 1024 * 1024; // 1MB chunks for better global context and ratio
60
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum LoomMode {
63    Off,
64    Standard,
65    Paged,
66}
67
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69pub enum LoomAggression {
70    Low,
71    Med,
72    High,
73    Ultra,
74}
75
76impl LoomAggression {
77    pub fn throttle_rate(&self) -> usize {
78        match self {
79            LoomAggression::Low => 8,
80            LoomAggression::Med => 2,
81            LoomAggression::High => 1,
82            LoomAggression::Ultra => 1,
83        }
84    }
85}
86
87/// Pruning aggressiveness - controls how much of the cache is pruned per cycle.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum PruneAggression {
90    /// Prune 2% - gradual, keeps more high-relevance nodes (for Safe Ultra)
91    Gradual,
92    /// Prune 5% - balanced (default)
93    Balanced,
94    /// Prune 10% - aggressive (for memory-constrained environments)
95    Aggressive,
96}
97
98impl PruneAggression {
99    /// Returns the percentage of nodes to prune (as denominator: 50 = 2%, 20 = 5%, 10 = 10%)
100    pub fn prune_divisor(&self) -> usize {
101        match self {
102            PruneAggression::Gradual => 50,    // 2%
103            PruneAggression::Balanced => 20,   // 5%
104            PruneAggression::Aggressive => 10, // 10%
105        }
106    }
107}
108
109/// Configuration for the Predictive Loom Compression (PLC) engine.
110#[derive(Clone, Debug)]
111pub struct PlcConfig {
112    /// Dimension of the internal predictor embeddings.
113    pub model_dim: usize,
114    /// Number of bytes to use as history for prediction.
115    pub window_size: usize,
116    /// Which Loom implementation to use.
117    pub loom_mode: LoomMode,
118    /// How aggressively to train the Loom.
119    pub aggression: LoomAggression,
120    /// Enable high-order context mixing (PAQ-style).
121    pub use_mixer: bool,
122    /// Context orders for the mixer (e.g., [1, 2, 4, 8]).
123    pub mixer_orders: Vec<usize>,
124    /// Enable LZMA-style sliding window compression.
125    pub use_lzma: bool,
126    /// Slider window size (e.g., 64KB).
127    pub lzma_window_size: usize,
128    /// Preprocessing transforms to apply.
129    pub transforms: Vec<Transform>,
130    /// Max RAM usage for Loom Cache (if paging enabled).
131    pub paged_loom_max_ram: usize,
132    /// Hint for target page size in bytes (50MB-500MB recommended).
133    pub page_size_hint: usize,
134    /// Maximum nodes per page before splitting (default 4096).
135    pub page_max_nodes: usize,
136    /// Minimum size in bytes to attempt Loom compression (default 1024).
137    pub min_loom_size: usize,
138    /// Minimum size in bytes to attempt any compression (default 32).
139    pub min_compress_size: usize,
140    /// Maximum nodes per mixer model (Ultra mode: ~1M, Default: 1024).
141    pub max_nodes: usize,
142    /// Pre-trained dictionary for priming the engine.
143    pub dictionary: Option<alloc::vec::Vec<u8>>,
144    /// Enable verbose logging for Loom operations.
145    pub verbose: bool,
146    /// Pruning aggressiveness (how much of cache is pruned per cycle).
147    pub prune_aggression: PruneAggression,
148    /// Path to a persistent paged dictionary (folder). If set, pages are loaded/saved here.
149    pub persistent_dict_path: Option<alloc::string::String>,
150}
151
152impl Default for PlcConfig {
153    fn default() -> Self {
154        Self {
155            model_dim: 32,
156            window_size: 16,
157            loom_mode: LoomMode::Standard,
158            aggression: LoomAggression::Med,
159            use_mixer: true,
160            mixer_orders: alloc::vec![1, 2, 4, 8],
161            use_lzma: true,
162            lzma_window_size: 64 * 1024,
163            transforms: alloc::vec![Transform::Bwt, Transform::Mtf],
164            paged_loom_max_ram: 1024 * 1024 * 1024, // 1GB default
165            page_size_hint: 128 * 1024 * 1024,      // 128MB default page
166            page_max_nodes: 4096,                   // 4096 nodes per page
167            min_loom_size: 0,                       // 0B min for Loom (always attempt)
168            min_compress_size: 32,                  // 32B min for compression
169            max_nodes: 1024,                        // 1024 nodes default
170            dictionary: None,
171            verbose: false,
172            prune_aggression: PruneAggression::Balanced,
173            persistent_dict_path: None,
174        }
175    }
176}
177
178impl PlcConfig {
179    /// Recommended settings for maximum compression ratio.
180    /// Optimized for ratio <0.0066 with adaptive caching and deeper prediction.
181    pub fn ultra() -> Self {
182        Self {
183            model_dim: 96,
184            window_size: 64,
185            loom_mode: LoomMode::Paged,
186            aggression: LoomAggression::Ultra,
187            use_mixer: true,
188            mixer_orders: alloc::vec![1, 2, 4, 8, 12, 16, 24, 32, 40, 48, 64],
189            use_lzma: true,
190            lzma_window_size: 1024 * 1024,
191            transforms: alloc::vec![Transform::Auto],
192            paged_loom_max_ram: 512 * 1024 * 1024, // 512MB default (Safe for multi-thread)
193            page_size_hint: 32 * 1024 * 1024,
194            page_max_nodes: 16384, // 16k nodes (~16MB per page)
195            min_loom_size: 0,
196            min_compress_size: 32,
197            max_nodes: 500_000,
198            dictionary: None,
199            verbose: false,
200            prune_aggression: PruneAggression::Balanced,
201            persistent_dict_path: None,
202        }
203    }
204
205    pub fn safe_ultra() -> Self {
206        Self {
207            model_dim: 128,
208            window_size: 64,
209            loom_mode: LoomMode::Paged,
210            aggression: LoomAggression::Ultra,
211            use_mixer: true,
212            // Extended orders: include 64 for deep pattern matching
213            mixer_orders: alloc::vec![1, 2, 4, 8, 12, 16, 24, 32, 40, 48, 56, 64],
214            use_lzma: true,
215            lzma_window_size: 1024 * 1024, // 1MB LZMA window
216            transforms: alloc::vec![Transform::Auto],
217            paged_loom_max_ram: 1024 * 1024 * 1024, // 1GB strict cap
218            page_size_hint: 32 * 1024 * 1024,       // 32MB pages (larger for better context)
219            page_max_nodes: 32768,                  // 32k nodes per page
220            min_loom_size: 0,
221            min_compress_size: 32,
222            max_nodes: 1_000_000, // 1M nodes (~1GB budget)
223            dictionary: None,
224            verbose: false,
225            prune_aggression: PruneAggression::Gradual,
226            persistent_dict_path: None,
227        }
228    }
229}
230
231/// Core trait for Atlas compressors.
232pub trait Compressor {
233    type Error;
234    fn compress(&mut self, data: &[u8]) -> Result<Vec<u8>, Self::Error>;
235}
236
237/// Core trait for Atlas decompressors.
238pub trait Decompressor {
239    type Error;
240    fn decompress(&mut self, data: &[u8], original_len: usize) -> Result<Vec<u8>, Self::Error>;
241}
242
243/// Predictive Loom Compressor (PLC) implementation (Enhanced with High-Order Context).
244pub struct LoomCompressor {
245    loom: Option<LoomEnum>,
246    config: PlcConfig,
247}
248
249impl LoomCompressor {
250    pub fn new(config: PlcConfig) -> Self {
251        // Multi-thread RAM guard: divide budget by threads to avoid OOM
252        // For Paged mode, use gentler scaling since pages are disk-backed
253        // Multi-thread RAM guard: divide budget by threads to avoid OOM
254        // For Paged mode, use gentler scaling since pages are disk-backed
255        #[cfg(feature = "std")]
256        {
257            // DISABLED: Automatic scaling breaks determinism between Compressor and Decompressor!
258            // Decompressor doesn't know we scaled down, so it uses full RAM/Nodes.
259            // This causes divergence in Pruning (Compressor prunes earlier).
260            // User must configure RAM appropriately for their thread count.
261            /*
262            let threads = (num_cpus::get() * 8 / 10).max(1);
263            if config.paged_loom_max_ram > 64 * 1024 * 1024 {
264                // Paged mode: pages spill to disk, so less aggressive scaling
265                if config.loom_mode == LoomMode::Paged {
266                    // Scale by sqrt(threads) instead of threads
267                    let scale = (threads as f64).sqrt().ceil() as usize;
268                    config.paged_loom_max_ram /= scale.max(1);
269                } else {
270                    config.paged_loom_max_ram /= threads;
271                }
272            }
273            */
274        }
275
276        let estimated_ram_bytes = config.max_nodes * 1024; // ~1KB per node overhead
277        if config.aggression == LoomAggression::Ultra
278            && estimated_ram_bytes > config.paged_loom_max_ram
279        {
280            #[cfg(feature = "std")]
281            if config.verbose {
282                std::println!(
283                    "[Atlas-Archive] Ultra mode RAM budget per thread exceeded, capping nodes."
284                );
285            }
286            // config.max_nodes = config.paged_loom_max_ram / 1024; // Also disabled to prevent divergence
287        }
288
289        let loom = if config.loom_mode == LoomMode::Off {
290            None
291        } else {
292            // Step 7: Fallback: Loom off if error
293            match LoomEnum::new(config.clone()) {
294                Ok(mut l) => {
295                    // Warm cache on page load: Prime Loom with dictionary if provided
296                    if let Some(dict) = &config.dictionary {
297                        let mut history = Vec::new();
298                        for &sym in dict {
299                            l.weave(&history, sym);
300                            history.push(sym);
301                            if history.len() > config.window_size {
302                                history.remove(0);
303                            }
304                        }
305                    }
306                    Some(l)
307                }
308                Err(e) => {
309                    #[cfg(feature = "std")]
310                    if config.verbose {
311                        std::println!(
312                            "[Atlas-Archive] Loom initialization failed, falling back to Off: {}",
313                            e
314                        );
315                    }
316                    None
317                }
318            }
319        };
320        Self { loom, config }
321    }
322}
323
324#[derive(Debug)]
325pub enum LoomError {
326    PackingError,
327    AnsError,
328    LoomInternalError,
329    InvalidArchive,
330}
331
332impl core::fmt::Display for LoomError {
333    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
334        match self {
335            LoomError::PackingError => write!(f, "Packing error: internal buffer mismatch"),
336            LoomError::AnsError => write!(f, "ANS entropy coding error"),
337            LoomError::LoomInternalError => write!(f, "Loom adaptive model error"),
338            LoomError::InvalidArchive => write!(f, "Invalid NYX archive magic or format"),
339        }
340    }
341}
342
343#[cfg(feature = "std")]
344impl std::error::Error for LoomError {}
345
346/// An entry in an Atlas archive.
347#[derive(Clone, Debug, Default)]
348pub struct ArchiveEntry {
349    pub name: Vec<u8>,
350    pub data: Vec<u8>,
351}
352
353/// A multi-file Atlas archive (.nyx format).
354#[derive(Clone, Debug, Default)]
355pub struct AtlasArchive {
356    pub entries: Vec<ArchiveEntry>,
357}
358
359impl AtlasArchive {
360    pub const MAGIC: &'static [u8; 4] = b"NYX\x01";
361
362    /// Pack the archive into a binary stream.
363    pub fn pack(&self) -> Vec<u8> {
364        let mut buf = Vec::new();
365        buf.extend_from_slice(Self::MAGIC);
366        buf.extend_from_slice(&(self.entries.len() as u32).to_le_bytes());
367
368        for entry in &self.entries {
369            buf.extend_from_slice(&(entry.name.len() as u32).to_le_bytes());
370            buf.extend_from_slice(&entry.name);
371            buf.extend_from_slice(&(entry.data.len() as u64).to_le_bytes());
372            buf.extend_from_slice(&entry.data);
373        }
374        buf
375    }
376
377    /// Unpack an archive from a binary stream.
378    pub fn unpack(data: &[u8]) -> Result<Self, LoomError> {
379        if data.len() < 8 || &data[0..4] != Self::MAGIC {
380            return Err(LoomError::InvalidArchive);
381        }
382
383        let mut entries = Vec::new();
384        let entry_count = u32::from_le_bytes(
385            data[4..8]
386                .try_into()
387                .map_err(|_| LoomError::InvalidArchive)?,
388        ) as usize;
389        let mut offset = 8;
390
391        for _ in 0..entry_count {
392            if offset + 4 > data.len() {
393                return Err(LoomError::InvalidArchive);
394            }
395            let name_len = u32::from_le_bytes(
396                data[offset..offset + 4]
397                    .try_into()
398                    .map_err(|_| LoomError::InvalidArchive)?,
399            ) as usize;
400            offset += 4;
401
402            if offset + name_len > data.len() {
403                return Err(LoomError::InvalidArchive);
404            }
405            let name = data[offset..offset + name_len].to_vec();
406            offset += name_len;
407
408            if offset + 8 > data.len() {
409                return Err(LoomError::InvalidArchive);
410            }
411            let data_len = u64::from_le_bytes(
412                data[offset..offset + 8]
413                    .try_into()
414                    .map_err(|_| LoomError::InvalidArchive)?,
415            ) as usize;
416            offset += 8;
417
418            if offset + data_len > data.len() {
419                return Err(LoomError::InvalidArchive);
420            }
421            let entry_data = data[offset..offset + data_len].to_vec();
422            offset += data_len;
423
424            entries.push(ArchiveEntry {
425                name,
426                data: entry_data,
427            });
428        }
429
430        Ok(Self { entries })
431    }
432}
433
434impl Compressor for LoomCompressor {
435    type Error = LoomError;
436
437    fn compress(&mut self, data: &[u8]) -> Result<Vec<u8>, Self::Error> {
438        if data.is_empty() {
439            return Ok(Vec::new());
440        }
441
442        // --- Multi-File Archive Handling ---
443        // For simplicity, LoomCompressor here handles single continuous byte stream.
444        // Parallel chunking happens inside.
445
446        self.compress_parallel(data)
447    }
448}
449
450impl LoomCompressor {
451    fn compress_parallel(&mut self, data: &[u8]) -> Result<Vec<u8>, LoomError> {
452        let chunk_size = CHUNK_SIZE;
453
454        #[cfg(feature = "std")]
455        {
456            // Use Rayon for parallel chunk compression with balanced workload
457            let chunks: Vec<_> = data.chunks(chunk_size).collect();
458
459            // Throttle: ensure each thread gets at least 2 chunks to amortize overhead
460            let min_chunks_per_thread = 2;
461
462            let results: Vec<Result<Vec<u8>, LoomError>> = if chunks.len() == 1 {
463                // Optimization: Avoid Rayon overhead for single chunk + Better Debugging
464                chunks
465                    .into_iter()
466                    .map(|chunk| {
467                        let mut inst = LoomCompressor::new(self.config.clone());
468                        inst.compress_chunk(chunk)
469                    })
470                    .collect()
471            } else {
472                chunks
473                    .into_par_iter()
474                    .with_min_len(min_chunks_per_thread)
475                    .map(|chunk| {
476                        let mut inst = LoomCompressor::new(self.config.clone());
477                        inst.compress_chunk(chunk)
478                    })
479                    .collect()
480            };
481
482            let mut packed = Vec::new();
483            let mut total_compressed = 0;
484            for res in results {
485                let chunk_data = res?;
486                total_compressed += chunk_data.len();
487                packed.extend_from_slice(&chunk_data);
488            }
489
490            if self.config.verbose {
491                let ratio = total_compressed as f64 / data.len() as f64;
492                std::println!(
493                    "[Atlas] Parallel compression complete. Ratio: {:.4}, Peak RAM ~{}MB",
494                    ratio,
495                    (total_compressed * 1) / (1024 * 1024) // Placeholder for better estimate if needed
496                );
497            }
498            Ok(packed)
499        }
500
501        #[cfg(not(feature = "std"))]
502        {
503            // Sequential fallback for no_std
504            let mut packed = Vec::new();
505            for chunk in data.chunks(chunk_size) {
506                let chunk_data = self.compress_chunk(chunk)?;
507                packed.extend_from_slice(&chunk_data);
508            }
509            Ok(packed)
510        }
511    }
512
513    fn compress_chunk(&mut self, data: &[u8]) -> Result<Vec<u8>, LoomError> {
514        #[cfg(feature = "std")]
515        let chunk_start = std::time::Instant::now();
516
517        // Step 1: Handle Minimum Size for Compression
518        if data.len() < self.config.min_compress_size {
519            return self.pack_raw_chunk(data);
520        }
521
522        // Step 2: Apply Preprocessing Transforms
523        let file_type = crate::poor_compress::DetectedType::detect(data);
524
525        let mut transform_list = self.config.transforms.clone();
526
527        // Auto-Heuristic: If Auto is set, adjust based on detected type
528        if transform_list.contains(&Transform::Auto) {
529            match file_type {
530                crate::poor_compress::DetectedType::Jpeg => {
531                    // JPEGs benefit from YUV before Auto chain
532                    transform_list.insert(0, Transform::Yuv);
533                }
534                crate::poor_compress::DetectedType::Png
535                | crate::poor_compress::DetectedType::Gif => {
536                    // Already compressed. Avoid heavy BWT/MTF, use Delta+Rle
537                    transform_list = vec![Transform::Delta, Transform::Rle];
538                }
539                crate::poor_compress::DetectedType::Mp4
540                | crate::poor_compress::DetectedType::Mp3 => {
541                    // Media benefit from ChannelSeparation, but skip heavy BWT
542                    transform_list = vec![
543                        Transform::ChannelSeparation(2),
544                        Transform::Delta,
545                        Transform::Rle,
546                    ];
547                }
548                crate::poor_compress::DetectedType::Encrypted => {
549                    // Don't try complex transforms on already encrypted/random data
550                    transform_list = vec![];
551                }
552                crate::poor_compress::DetectedType::Zip => {
553                    // Archives are already compressed
554                    transform_list = vec![Transform::Rle];
555                }
556                _ => {}
557            }
558        }
559
560        let chain = TransformChain::new(transform_list);
561        let (transformed, bwt_indices) = chain.apply(data.to_vec());
562
563        if self.config.verbose {
564            let est_ram = transformed.len() + (bwt_indices.len() * 8);
565            std::println!(
566                "[Atlas] Detected Type: {:?}, Final Ratio: {:.4}, Est. Transform RAM: {:.2}MB",
567                file_type,
568                transformed.len() as f64 / data.len() as f64,
569                est_ram as f64 / (1024.0 * 1024.0)
570            );
571        }
572
573        // Step 3: Handle Minimum Size for Loom
574        if transformed.len() < self.config.min_loom_size || self.loom.is_none() {
575            #[cfg(feature = "std")]
576            if self.config.verbose {
577                std::println!(
578                    "[Atlas] Chunk {}B compressed (no Loom) in {:?}",
579                    data.len(),
580                    chunk_start.elapsed()
581                );
582            }
583            return self.pack_ans_only_chunk(&transformed, bwt_indices, data.len(), file_type);
584        }
585
586        let loom = self.loom.as_mut().unwrap();
587
588        // Step 4: Two-Pass Loom Compression
589        // Pass 1: Forward weave to sync state and collect probability models
590        let mut models = Vec::with_capacity(transformed.len());
591        let mut history = Vec::with_capacity(transformed.len());
592
593        for &sym in transformed.iter() {
594            // Predict NEXT symbol probabilities
595            let model = loom.predict(&history);
596            models.push(model);
597
598            // Weave current symbol into Loom state
599            loom.weave(&history, sym);
600            history.push(sym);
601        }
602
603        // Pass 2: Reverse ANS encoding
604        let mut encoder = RansEncoder::new();
605        let mut compressed_u16 = Vec::new();
606        for (sym, model) in transformed.iter().zip(models.into_iter()).rev() {
607            encoder.encode(&model, *sym, &mut compressed_u16);
608        }
609        encoder.finish(&mut compressed_u16);
610
611        // Convert u16 buffer to bytes
612        let mut compressed_ans = Vec::with_capacity(compressed_u16.len() * 2);
613        for &val in &compressed_u16 {
614            compressed_ans.extend_from_slice(&val.to_le_bytes());
615        }
616
617        // Step 5: Pack Chunk
618        let packed = self.pack_loom_chunk(
619            &compressed_ans,
620            bwt_indices,
621            data.len(),
622            transformed.len(),
623            file_type,
624        );
625
626        // --- No Expansion Guarantee ---
627        if packed.len() >= data.len() {
628            #[cfg(feature = "std")]
629            if self.config.verbose {
630                std::println!(
631                    "[Loom] Chunk expanded ({} -> {}), falling back to Raw",
632                    data.len(),
633                    packed.len()
634                );
635            }
636            return self.pack_raw_chunk(data);
637        }
638
639        #[cfg(feature = "std")]
640        if self.config.verbose {
641            std::println!(
642                "[Loom] Chunk compressed: {} -> {} bytes (Ratio: {:.4}) in {:?}",
643                data.len(),
644                packed.len(),
645                packed.len() as f64 / data.len() as f64,
646                chunk_start.elapsed()
647            );
648        }
649
650        Ok(packed)
651    }
652
653    fn pack_raw_chunk(&self, data: &[u8]) -> Result<Vec<u8>, LoomError> {
654        let mut packed = Vec::with_capacity(data.len() + 8);
655        // Header: [raw_len: 24 bit | type: 7 bit | flag: 1 (MSB) ] [compressed_len: 32 bit]
656        let file_type = crate::poor_compress::DetectedType::detect(data);
657        let type_val = (file_type as u32) & 0x7F;
658        let len = data.len() as u32;
659        let header_flag = (len & 0x00FF_FFFF) | (type_val << 24) | 0x8000_0000;
660
661        packed.extend_from_slice(&header_flag.to_le_bytes());
662        packed.extend_from_slice(&len.to_le_bytes()); // compressed_len is original for raw
663        packed.extend_from_slice(data);
664        Ok(packed)
665    }
666
667    fn pack_ans_only_chunk(
668        &self,
669        transformed: &[u8],
670        bwt_indices: Vec<usize>,
671        original_len: usize,
672        file_type: crate::poor_compress::DetectedType,
673    ) -> Result<Vec<u8>, LoomError> {
674        // If no transforms at all, safe to use raw
675        if bwt_indices.is_empty() && self.config.transforms.is_empty() {
676            return self.pack_raw_chunk(transformed);
677        }
678
679        // Pack as a Loom chunk with compressed_len == transformed_len to signal "No ANS"
680        Ok(self.pack_loom_chunk(
681            transformed,
682            bwt_indices,
683            original_len,
684            transformed.len(),
685            file_type,
686        ))
687    }
688
689    fn pack_loom_chunk(
690        &self,
691        data: &[u8],
692        bwt_indices: Vec<usize>,
693        original_len: usize,
694        transformed_len: usize,
695        file_type: crate::poor_compress::DetectedType,
696    ) -> Vec<u8> {
697        let mut packed = Vec::new();
698        // Header: [orig_len: 24 bit | type: 7 bit | flag: 0 (MSB)] [compressed_len: u32]...
699        let type_val = (file_type as u32) & 0x7F;
700        let meta = (original_len as u32 & 0x00FF_FFFF) | (type_val << 24);
701
702        packed.extend_from_slice(&meta.to_le_bytes());
703        packed.extend_from_slice(&(data.len() as u32).to_le_bytes());
704        packed.extend_from_slice(&(transformed_len as u32).to_le_bytes());
705        packed.extend_from_slice(&(bwt_indices.len() as u32).to_le_bytes());
706        for idx in bwt_indices {
707            packed.extend_from_slice(&(idx as u32).to_le_bytes());
708        }
709        packed.extend_from_slice(data);
710        packed
711    }
712}
713
714/// Predictive Loom Decompressor.
715pub struct LoomDecompressor {
716    loom: Option<LoomEnum>,
717    config: PlcConfig,
718}
719
720impl LoomDecompressor {
721    pub fn new(config: PlcConfig) -> Self {
722        let loom = if config.loom_mode == LoomMode::Off {
723            None
724        } else {
725            LoomEnum::new(config.clone()).ok()
726        };
727        Self { loom, config }
728    }
729}
730
731impl Decompressor for LoomDecompressor {
732    type Error = LoomError;
733
734    fn decompress(&mut self, data: &[u8], original_len: usize) -> Result<Vec<u8>, Self::Error> {
735        if original_len == 0 {
736            return Ok(Vec::new());
737        }
738
739        let mut output = Vec::with_capacity(original_len);
740        let mut cursor = 0;
741
742        #[cfg(feature = "std")]
743        if self.config.verbose {
744            std::println!(
745                "[Decompress] Starting: data_len={}, original_len={}",
746                data.len(),
747                original_len
748            );
749        }
750
751        while cursor < data.len() {
752            // Header: [meta: u32][len_or_indices...]
753            if cursor + 4 > data.len() {
754                #[cfg(feature = "std")]
755                if self.config.verbose {
756                    std::println!(
757                        "[Decompress] Breaking: cursor={} + 4 > data_len={}",
758                        cursor,
759                        data.len()
760                    );
761                }
762                break;
763            }
764            let meta = u32::from_le_bytes(data[cursor..cursor + 4].try_into().unwrap());
765            cursor += 4;
766
767            #[cfg(feature = "std")]
768            if self.config.verbose {
769                std::println!(
770                    "[Decompress] Chunk meta={:#010x}, cursor={}, is_raw={}",
771                    meta,
772                    cursor,
773                    (meta & 0x8000_0000) != 0
774                );
775            }
776
777            // --- No Expansion Detection ---
778            if (meta & 0x8000_0000) != 0 {
779                // Raw Chunk
780                let _raw_len = (meta & 0x00FF_FFFF) as usize;
781                let _file_type_val = (meta >> 24) & 0x7F;
782                if cursor + 4 > data.len() {
783                    return Err(LoomError::PackingError);
784                }
785                let compressed_len_field =
786                    u32::from_le_bytes(data[cursor..cursor + 4].try_into().unwrap()) as usize;
787                cursor += 4;
788
789                if cursor + compressed_len_field > data.len() {
790                    return Err(LoomError::PackingError);
791                }
792                output.extend_from_slice(&data[cursor..cursor + compressed_len_field]);
793                cursor += compressed_len_field;
794                continue;
795            }
796
797            // Loom Chunk
798            let chunk_orig_len = (meta & 0x00FF_FFFF) as usize;
799            let file_type_val = (meta >> 24) & 0x7F;
800            let file_type: crate::poor_compress::DetectedType =
801                unsafe { core::mem::transmute(file_type_val as u8) };
802            if cursor + 12 > data.len() {
803                return Err(LoomError::PackingError);
804            }
805            let compressed_len =
806                u32::from_le_bytes(data[cursor..cursor + 4].try_into().unwrap()) as usize;
807            let transformed_len =
808                u32::from_le_bytes(data[cursor + 4..cursor + 8].try_into().unwrap()) as usize;
809            let bwt_count =
810                u32::from_le_bytes(data[cursor + 8..cursor + 12].try_into().unwrap()) as usize;
811            cursor += 12;
812
813            #[cfg(feature = "std")]
814            if self.config.verbose {
815                std::println!(
816                    "[Decompress] Chunk: orig={}, compressed={}, transformed={}, bwt_count={}",
817                    chunk_orig_len,
818                    compressed_len,
819                    transformed_len,
820                    bwt_count
821                );
822            }
823
824            let mut bwt_indices = Vec::with_capacity(bwt_count);
825            for _ in 0..bwt_count {
826                if cursor + 4 > data.len() {
827                    return Err(LoomError::PackingError);
828                }
829                bwt_indices.push(
830                    u32::from_le_bytes(data[cursor..cursor + 4].try_into().unwrap()) as usize,
831                );
832                cursor += 4;
833            }
834
835            if cursor + compressed_len > data.len() {
836                return Err(LoomError::PackingError);
837            }
838            let ans_data = &data[cursor..cursor + compressed_len];
839            let chunk_out = self.decompress_chunk(
840                ans_data,
841                chunk_orig_len,
842                transformed_len,
843                bwt_indices,
844                file_type,
845            )?;
846
847            #[cfg(feature = "std")]
848            if self.config.verbose {
849                std::println!("[Decompress] Chunk output: {} bytes", chunk_out.len());
850            }
851
852            output.extend_from_slice(&chunk_out);
853            cursor += compressed_len;
854        }
855
856        #[cfg(feature = "std")]
857        if self.config.verbose {
858            std::println!("[Decompress] Finished: output_len={}", output.len());
859        }
860
861        Ok(output)
862    }
863}
864
865impl LoomDecompressor {
866    fn decompress_chunk(
867        &mut self,
868        data: &[u8],
869        _original_len: usize,
870        transformed_len: usize,
871        bwt_indices: Vec<usize>,
872        file_type: crate::poor_compress::DetectedType,
873    ) -> Result<Vec<u8>, LoomError> {
874        if self.loom.is_none() || data.len() == transformed_len {
875            // Revert transforms only (data is raw transformed bytes, not ANS)
876            let mut transform_list = self.config.transforms.clone();
877
878            // Re-apply same logic as compressor
879            if transform_list.contains(&Transform::Auto) {
880                match file_type {
881                    crate::poor_compress::DetectedType::Jpeg => {
882                        transform_list.insert(0, Transform::Yuv);
883                    }
884                    crate::poor_compress::DetectedType::Png
885                    | crate::poor_compress::DetectedType::Gif => {
886                        transform_list = vec![Transform::Delta, Transform::Rle];
887                    }
888                    crate::poor_compress::DetectedType::Mp4
889                    | crate::poor_compress::DetectedType::Mp3 => {
890                        transform_list = vec![
891                            Transform::ChannelSeparation(2),
892                            Transform::Delta,
893                            Transform::Rle,
894                        ];
895                    }
896                    crate::poor_compress::DetectedType::Encrypted => {
897                        transform_list = vec![];
898                    }
899                    crate::poor_compress::DetectedType::Zip => {
900                        transform_list = vec![Transform::Rle];
901                    }
902                    _ => {}
903                }
904            }
905
906            let chain = TransformChain::new(transform_list);
907            return Ok(chain.inverse(data.to_vec(), bwt_indices));
908        }
909
910        let mut transform_list = self.config.transforms.clone();
911        if transform_list.contains(&Transform::Auto) {
912            match file_type {
913                crate::poor_compress::DetectedType::Jpeg => {
914                    transform_list.insert(0, Transform::Yuv);
915                }
916                crate::poor_compress::DetectedType::Png
917                | crate::poor_compress::DetectedType::Gif => {
918                    transform_list = vec![Transform::Delta, Transform::Rle];
919                }
920                crate::poor_compress::DetectedType::Mp4
921                | crate::poor_compress::DetectedType::Mp3 => {
922                    transform_list = vec![
923                        Transform::ChannelSeparation(2),
924                        Transform::Delta,
925                        Transform::Rle,
926                    ];
927                }
928                crate::poor_compress::DetectedType::Encrypted => {
929                    transform_list = vec![];
930                }
931                crate::poor_compress::DetectedType::Zip => {
932                    transform_list = vec![Transform::Rle];
933                }
934                _ => {}
935            }
936        }
937        let chain = TransformChain::new(transform_list);
938
939        // Convert input bytes back to u16
940        let mut input_u16 = Vec::with_capacity(data.len() / 2);
941        for chunk in data.chunks_exact(2) {
942            input_u16.push(u16::from_le_bytes(chunk.try_into().unwrap()));
943        }
944
945        let mut decoder = RansDecoder::new(&mut input_u16);
946        let mut transformed = Vec::with_capacity(transformed_len);
947        let loom = self.loom.as_mut().unwrap();
948
949        for _ in 0..transformed_len {
950            let model = loom.predict(&transformed);
951            let sym = decoder.decode(&model, &mut input_u16);
952
953            // Update Loom state
954            loom.weave(&transformed, sym);
955            transformed.push(sym);
956        }
957
958        Ok(chain.inverse(transformed, bwt_indices))
959    }
960}