Skip to main content

oxigdal_wasm/
compression.rs

1//! Tile compression and decompression utilities
2//!
3//! This module provides various compression algorithms for reducing memory usage
4//! and bandwidth requirements when working with geospatial tiles in WASM environments.
5//!
6//! # Overview
7//!
8//! Compression is essential for efficient geospatial data handling:
9//!
10//! - **RLE (Run-Length Encoding)**: Best for images with large uniform areas
11//! - **Delta Encoding**: Excellent for gradient data (DEMs, temperature)
12//! - **Huffman Encoding**: General-purpose statistical compression
13//! - **LZ77**: Dictionary-based compression for mixed content
14//! - **Automatic Selection**: Benchmark and choose optimal algorithm
15//!
16//! # Why Compress Tiles?
17//!
18//! Geospatial tiles can be large:
19//! - 256x256 RGBA tile: 262,144 bytes (256 KB)
20//! - 100 cached tiles: 25.6 MB uncompressed
21//! - With 50% compression: 12.8 MB (50% savings!)
22//!
23//! Benefits:
24//! 1. **Memory**: Fit more tiles in cache
25//! 2. **Bandwidth**: Faster loading over network
26//! 3. **Storage**: Smaller cache footprint
27//! 4. **Performance**: Less memory pressure
28//!
29//! # Compression Algorithms
30//!
31//! ## RLE (Run-Length Encoding)
32//! Replaces sequences of identical bytes with count+value pairs.
33//!
34//! Best for:
35//! - Satellite imagery with large uniform areas (ocean, desert)
36//! - Binary masks
37//! - Simple graphics
38//!
39//! Performance:
40//! - Compression: O(n), very fast
41//! - Decompression: O(n), very fast
42//! - Ratio: 2-10x for uniform data, 0.5x for random data
43//!
44//! Example:
45//! ```text
46//! Input:  [255, 255, 255, 255, 0, 0, 128, 128, 128]
47//! Output: [4, 255, 2, 0, 3, 128]
48//! Size:   9 bytes → 6 bytes (33% savings)
49//! ```
50//!
51//! ## Delta Encoding
52//! Stores differences between adjacent pixels instead of absolute values.
53//!
54//! Best for:
55//! - DEMs (Digital Elevation Models)
56//! - Temperature/pressure fields
57//! - Smooth gradients
58//!
59//! Performance:
60//! - Compression: O(n)
61//! - Decompression: O(n)
62//! - Ratio: 2-4x for gradient data
63//!
64//! Example:
65//! ```text
66//! Input:  [100, 102, 105, 103, 104]
67//! Output: [100, +2, +3, -2, +1]
68//! Deltas are smaller → better compression with subsequent algorithm
69//! ```
70//!
71//! ## Huffman Encoding
72//! Uses variable-length codes based on frequency.
73//!
74//! Best for:
75//! - General-purpose compression
76//! - Images with varying content
77//! - Text/metadata
78//!
79//! Performance:
80//! - Compression: O(n log n) - slower
81//! - Decompression: O(n)
82//! - Ratio: 1.5-3x typical
83//!
84//! ## LZ77 (Dictionary Compression)
85//! Finds repeated sequences and references them.
86//!
87//! Best for:
88//! - Repeated patterns
89//! - Mixed content
90//! - General-purpose
91//!
92//! Performance:
93//! - Compression: O(n²) worst case
94//! - Decompression: O(n)
95//! - Ratio: 2-5x typical
96//!
97//! # Usage Examples
98//!
99//! ## Basic Compression
100//! ```rust
101//! use oxigdal_wasm::{TileCompressor, CompressionAlgorithm};
102//!
103//! let compressor = TileCompressor::new(CompressionAlgorithm::Lz77);
104//!
105//! // Compress a tile
106//! let tile_data = vec![0u8; 256 * 256 * 4]; // 256 KB
107//! let (compressed, stats) = compressor.compress(&tile_data);
108//!
109//! println!("Original: {} bytes", stats.original_size);
110//! println!("Compressed: {} bytes", stats.compressed_size);
111//! println!("Ratio: {:.1}%", stats.ratio * 100.0);
112//! println!("Saved: {:.1}%", stats.space_saved_percent());
113//!
114//! // Decompress
115//! let decompressed = compressor.decompress(&compressed)?;
116//! assert_eq!(tile_data, decompressed);
117//! # Ok::<(), Box<dyn std::error::Error>>(())
118//! ```
119//!
120//! ## Automatic Algorithm Selection
121//! ```rust
122//! use oxigdal_wasm::{CompressionBenchmark, TileCompressor};
123//!
124//! // Test all algorithms and choose best
125//! let tile_data = vec![0u8; 256 * 256 * 4]; // Simulated tile data
126//! let best = CompressionBenchmark::find_best(&tile_data);
127//! println!("Best algorithm: {:?}", best);
128//!
129//! let compressor = TileCompressor::new(best);
130//! let (compressed, _) = compressor.compress(&tile_data);
131//! ```
132//!
133//! ## 2D Delta Encoding for DEMs
134//! ```rust
135//! use oxigdal_wasm::{TileCompressor, CompressionAlgorithm};
136//!
137//! let dem_data = vec![100u8; 256 * 256]; // Simulated elevation data
138//! let tile_width = 256;
139//!
140//! // Use 2D delta encoding (vertical differences)
141//! let compressor = TileCompressor::new(CompressionAlgorithm::Delta);
142//! let (compressed, stats) = compressor.compress_2d(&dem_data, tile_width);
143//!
144//! // DEMs typically achieve 3-5x compression
145//! println!("DEM compression ratio: {:.2}x", 1.0 / stats.ratio);
146//! ```
147//!
148//! # Performance Benchmarks
149//!
150//! Typical results for 256x256 RGBA tiles (262 KB):
151//!
152//! ```text
153//! Algorithm   Compress Time   Decompress Time   Ratio   Best For
154//! ─────────────────────────────────────────────────────────────
155//! None        0ms             0ms               1.0x    Baseline
156//! RLE         2ms             1ms               2.5x    Uniform areas
157//! Delta       3ms             2ms               1.8x    Gradients
158//! Huffman     15ms            5ms               2.2x    General
159//! LZ77        25ms            8ms               3.5x    Mixed content
160//! ```
161//!
162//! # Memory Overhead
163//!
164//! Compression requires temporary buffers:
165//! - RLE: 2x input size worst case
166//! - Delta: 1x input size
167//! - Huffman: 3x input size (tree + codes + output)
168//! - LZ77: 2x input size
169//!
170//! # Best Practices
171//!
172//! 1. **Profile First**: Test compression ratios on real data
173//! 2. **Cache Results**: Don't recompress the same tile repeatedly
174//! 3. **Choose Wisely**: Match algorithm to data type
175//! 4. **Consider Trade-offs**: Compression time vs ratio vs memory
176//! 5. **Batch Process**: Compress tiles during idle time
177//! 6. **Monitor Performance**: Track compression stats
178//! 7. **Fallback to None**: If compression makes data larger, store uncompressed
179//!
180//! # When NOT to Compress
181//!
182//! - Data already compressed (JPEG tiles)
183//! - Very small tiles (< 1 KB) - overhead not worth it
184//! - Random/encrypted data - won't compress well
185//! - Real-time requirements - compression too slow
186//! - Low memory pressure - no need to optimize
187use crate::error::{WasmError, WasmResult};
188use serde::{Deserialize, Serialize};
189use std::collections::HashMap;
190/// Compression algorithm
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
192pub enum CompressionAlgorithm {
193    /// No compression
194    None,
195    /// Run-length encoding
196    Rle,
197    /// Delta encoding
198    Delta,
199    /// Huffman encoding (simplified)
200    Huffman,
201    /// LZ77-style compression
202    Lz77,
203}
204/// Compression statistics
205#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
206pub struct CompressionStats {
207    /// Original size in bytes
208    pub original_size: usize,
209    /// Compressed size in bytes
210    pub compressed_size: usize,
211    /// Compression ratio
212    pub ratio: f64,
213    /// Compression time in milliseconds
214    pub compression_time_ms: f64,
215}
216impl CompressionStats {
217    /// Creates new compression statistics
218    pub const fn new(
219        original_size: usize,
220        compressed_size: usize,
221        compression_time_ms: f64,
222    ) -> Self {
223        let ratio = if original_size > 0 {
224            compressed_size as f64 / original_size as f64
225        } else {
226            0.0
227        };
228        Self {
229            original_size,
230            compressed_size,
231            ratio,
232            compression_time_ms,
233        }
234    }
235    /// Returns space saved in bytes
236    pub const fn space_saved(&self) -> usize {
237        self.original_size.saturating_sub(self.compressed_size)
238    }
239    /// Returns space saved as percentage
240    pub fn space_saved_percent(&self) -> f64 {
241        if self.original_size == 0 {
242            0.0
243        } else {
244            ((self.original_size - self.compressed_size) as f64 / self.original_size as f64) * 100.0
245        }
246    }
247}
248/// Run-length encoding compression
249pub struct RleCompressor;
250impl RleCompressor {
251    /// Compresses data using RLE
252    pub fn compress(data: &[u8]) -> Vec<u8> {
253        if data.is_empty() {
254            return Vec::new();
255        }
256        let mut compressed = Vec::new();
257        let mut i = 0;
258        while i < data.len() {
259            let current = data[i];
260            let mut count = 1u8;
261            while i + (count as usize) < data.len()
262                && data[i + (count as usize)] == current
263                && count < 255
264            {
265                count += 1;
266            }
267            compressed.push(count);
268            compressed.push(current);
269            i += count as usize;
270        }
271        compressed
272    }
273    /// Decompresses RLE data
274    pub fn decompress(data: &[u8]) -> WasmResult<Vec<u8>> {
275        if !data.len().is_multiple_of(2) {
276            return Err(WasmError::InvalidOperation {
277                operation: "RLE decompress".to_string(),
278                reason: "Data length must be even".to_string(),
279            });
280        }
281        let mut decompressed = Vec::new();
282        for chunk in data.chunks_exact(2) {
283            let count = chunk[0];
284            let value = chunk[1];
285            decompressed.resize(decompressed.len() + count as usize, value);
286        }
287        Ok(decompressed)
288    }
289}
290/// Delta encoding compression
291pub struct DeltaCompressor;
292impl DeltaCompressor {
293    /// Compresses data using delta encoding
294    pub fn compress(data: &[u8]) -> Vec<u8> {
295        if data.is_empty() {
296            return Vec::new();
297        }
298        let mut compressed = Vec::with_capacity(data.len());
299        compressed.push(data[0]);
300        for i in 1..data.len() {
301            let delta = data[i].wrapping_sub(data[i - 1]);
302            compressed.push(delta);
303        }
304        compressed
305    }
306    /// Decompresses delta-encoded data
307    pub fn decompress(data: &[u8]) -> WasmResult<Vec<u8>> {
308        if data.is_empty() {
309            return Ok(Vec::new());
310        }
311        let mut decompressed = Vec::with_capacity(data.len());
312        decompressed.push(data[0]);
313        for i in 1..data.len() {
314            let prev = *decompressed.last().expect("decompressed is not empty");
315            let value = prev.wrapping_add(data[i]);
316            decompressed.push(value);
317        }
318        Ok(decompressed)
319    }
320    /// Compresses 2D delta encoding (for images)
321    pub fn compress_2d(data: &[u8], width: usize) -> Vec<u8> {
322        if data.is_empty() || width == 0 {
323            return Vec::new();
324        }
325        let height = data.len() / width;
326        let mut compressed = Vec::with_capacity(data.len());
327        if !data.is_empty() {
328            compressed.push(data[0]);
329            for x in 1..width.min(data.len()) {
330                let delta = data[x].wrapping_sub(data[x - 1]);
331                compressed.push(delta);
332            }
333        }
334        for y in 1..height {
335            for x in 0..width {
336                let idx = y * width + x;
337                if idx < data.len() {
338                    let delta = data[idx].wrapping_sub(data[idx - width]);
339                    compressed.push(delta);
340                }
341            }
342        }
343        compressed
344    }
345    /// Decompresses 2D delta-encoded data
346    pub fn decompress_2d(data: &[u8], width: usize) -> WasmResult<Vec<u8>> {
347        if data.is_empty() || width == 0 {
348            return Ok(Vec::new());
349        }
350        let height = data.len() / width;
351        let mut decompressed = Vec::with_capacity(data.len());
352        if !data.is_empty() {
353            decompressed.push(data[0]);
354            for x in 1..width.min(data.len()) {
355                let prev = decompressed[x - 1];
356                let value = prev.wrapping_add(data[x]);
357                decompressed.push(value);
358            }
359        }
360        for y in 1..height {
361            for x in 0..width {
362                let idx = y * width + x;
363                if idx < data.len() {
364                    let above = decompressed[idx - width];
365                    let value = above.wrapping_add(data[idx]);
366                    decompressed.push(value);
367                }
368            }
369        }
370        Ok(decompressed)
371    }
372}
373/// Huffman tree node
374#[derive(Debug, Clone)]
375enum HuffmanNode {
376    Leaf {
377        symbol: u8,
378        frequency: u32,
379    },
380    Internal {
381        frequency: u32,
382        left: Box<HuffmanNode>,
383        right: Box<HuffmanNode>,
384    },
385}
386impl HuffmanNode {
387    fn frequency(&self) -> u32 {
388        match self {
389            Self::Leaf { frequency, .. } => *frequency,
390            Self::Internal { frequency, .. } => *frequency,
391        }
392    }
393    /// Smallest symbol contained in this subtree.
394    ///
395    /// Used as a deterministic tie-breaker when two nodes share a frequency, so
396    /// that `build_tree` produces an identical tree regardless of `HashMap`
397    /// iteration order. Without this, `compress` and `decompress` could build
398    /// mirror trees for equal-frequency symbols and swap their codes.
399    fn min_symbol(&self) -> u8 {
400        match self {
401            Self::Leaf { symbol, .. } => *symbol,
402            Self::Internal { left, right, .. } => left.min_symbol().min(right.min_symbol()),
403        }
404    }
405}
406/// Simplified Huffman encoding
407pub struct HuffmanCompressor;
408impl HuffmanCompressor {
409    /// Builds frequency table
410    fn build_frequency_table(data: &[u8]) -> HashMap<u8, u32> {
411        let mut frequencies = HashMap::new();
412        for &byte in data {
413            *frequencies.entry(byte).or_insert(0) += 1;
414        }
415        frequencies
416    }
417    /// Builds Huffman tree
418    fn build_tree(frequencies: &HashMap<u8, u32>) -> Option<HuffmanNode> {
419        if frequencies.is_empty() {
420            return None;
421        }
422        let mut nodes: Vec<HuffmanNode> = frequencies
423            .iter()
424            .map(|(&symbol, &frequency)| HuffmanNode::Leaf { symbol, frequency })
425            .collect();
426        while nodes.len() > 1 {
427            // Total, deterministic order: primary key frequency, tie-break on the
428            // smallest symbol in the subtree. Sorted descending so the two
429            // lowest-priority nodes sit at the end and are popped next. This
430            // makes the reconstructed tree independent of `HashMap` ordering.
431            nodes.sort_by(|a, b| {
432                b.frequency()
433                    .cmp(&a.frequency())
434                    .then_with(|| b.min_symbol().cmp(&a.min_symbol()))
435            });
436            let right = nodes.pop()?;
437            let left = nodes.pop()?;
438            let internal = HuffmanNode::Internal {
439                frequency: left.frequency() + right.frequency(),
440                left: Box::new(left),
441                right: Box::new(right),
442            };
443            nodes.push(internal);
444        }
445        nodes.pop()
446    }
447    /// Generates code table from tree
448    fn generate_codes(node: &HuffmanNode, prefix: Vec<bool>, codes: &mut HashMap<u8, Vec<bool>>) {
449        match node {
450            HuffmanNode::Leaf { symbol, .. } => {
451                codes.insert(*symbol, prefix);
452            }
453            HuffmanNode::Internal { left, right, .. } => {
454                let mut left_prefix = prefix.clone();
455                left_prefix.push(false);
456                Self::generate_codes(left, left_prefix, codes);
457                let mut right_prefix = prefix;
458                right_prefix.push(true);
459                Self::generate_codes(right, right_prefix, codes);
460            }
461        }
462    }
463    /// Compresses data using Huffman encoding.
464    ///
465    /// Format:
466    /// - 2 bytes: number of distinct symbols (u16 LE)
467    /// - For each symbol: 1 byte symbol value + 4 bytes frequency (u32 LE)
468    /// - 8 bytes: original data length (u64 LE)
469    /// - Remaining bytes: Huffman-encoded bit stream (MSB first within each byte)
470    pub fn compress(data: &[u8]) -> Vec<u8> {
471        if data.is_empty() {
472            return Vec::new();
473        }
474        let frequencies = Self::build_frequency_table(data);
475        let tree = match Self::build_tree(&frequencies) {
476            Some(t) => t,
477            None => return Vec::new(),
478        };
479        let mut codes = HashMap::new();
480        Self::generate_codes(&tree, Vec::new(), &mut codes);
481
482        // Header: number of symbols (u16 LE)
483        let num_symbols = frequencies.len() as u16;
484        let mut compressed = Vec::new();
485        compressed.extend_from_slice(&num_symbols.to_le_bytes());
486
487        // Symbol table: (symbol u8, frequency u32 LE) for each distinct byte
488        let mut sorted_symbols: Vec<(u8, u32)> = frequencies.into_iter().collect();
489        sorted_symbols.sort_by_key(|(sym, _)| *sym);
490        for (symbol, freq) in &sorted_symbols {
491            compressed.push(*symbol);
492            compressed.extend_from_slice(&freq.to_le_bytes());
493        }
494
495        // Original data length (u64 LE) — needed to know when to stop decoding
496        compressed.extend_from_slice(&(data.len() as u64).to_le_bytes());
497
498        // Encode bit stream (MSB first within each byte)
499        let mut bit_buffer = 0u8;
500        let mut bit_count = 0u8;
501        for &byte in data {
502            if let Some(code) = codes.get(&byte) {
503                for &bit in code {
504                    if bit {
505                        bit_buffer |= 1 << (7 - bit_count);
506                    }
507                    bit_count += 1;
508                    if bit_count == 8 {
509                        compressed.push(bit_buffer);
510                        bit_buffer = 0;
511                        bit_count = 0;
512                    }
513                }
514            }
515        }
516        if bit_count > 0 {
517            compressed.push(bit_buffer);
518        }
519        compressed
520    }
521
522    /// Decompresses Huffman-encoded data produced by [`HuffmanCompressor::compress`].
523    ///
524    /// Reads the frequency table stored in the header to reconstruct the identical
525    /// Huffman tree, then decodes the bit stream up to the stored original length.
526    pub fn decompress(compressed: &[u8]) -> WasmResult<Vec<u8>> {
527        if compressed.is_empty() {
528            return Ok(Vec::new());
529        }
530
531        // Parse header: number of symbols (u16 LE)
532        if compressed.len() < 2 {
533            return Err(WasmError::InvalidOperation {
534                operation: "Huffman decompression".to_string(),
535                reason: "Data too short to contain symbol count header".to_string(),
536            });
537        }
538        let num_symbols = u16::from_le_bytes([compressed[0], compressed[1]]) as usize;
539        let mut pos = 2usize;
540
541        // Each symbol entry is 5 bytes: symbol(1) + frequency(4)
542        let symbol_table_size = num_symbols * 5;
543        if pos + symbol_table_size + 8 > compressed.len() {
544            return Err(WasmError::InvalidOperation {
545                operation: "Huffman decompression".to_string(),
546                reason: "Data too short to contain symbol table and length header".to_string(),
547            });
548        }
549
550        let mut frequencies: HashMap<u8, u32> = HashMap::new();
551        for _ in 0..num_symbols {
552            let symbol = compressed[pos];
553            pos += 1;
554            let freq = u32::from_le_bytes([
555                compressed[pos],
556                compressed[pos + 1],
557                compressed[pos + 2],
558                compressed[pos + 3],
559            ]);
560            pos += 4;
561            frequencies.insert(symbol, freq);
562        }
563
564        // Parse original data length (u64 LE)
565        let original_len = u64::from_le_bytes([
566            compressed[pos],
567            compressed[pos + 1],
568            compressed[pos + 2],
569            compressed[pos + 3],
570            compressed[pos + 4],
571            compressed[pos + 5],
572            compressed[pos + 6],
573            compressed[pos + 7],
574        ]) as usize;
575        pos += 8;
576
577        if original_len == 0 {
578            return Ok(Vec::new());
579        }
580
581        // Reconstruct the Huffman tree using the same algorithm as compress
582        let tree = Self::build_tree(&frequencies).ok_or_else(|| WasmError::InvalidOperation {
583            operation: "Huffman decompression".to_string(),
584            reason: "Failed to reconstruct Huffman tree from frequency table".to_string(),
585        })?;
586
587        // Decode the bit stream by traversing the tree
588        let bitstream = &compressed[pos..];
589        let mut result = Vec::with_capacity(original_len);
590        let mut node = &tree;
591
592        'outer: for &byte in bitstream {
593            for bit_idx in (0..8).rev() {
594                let bit = (byte >> bit_idx) & 1;
595                node = match node {
596                    HuffmanNode::Internal { left, right, .. } => {
597                        if bit == 0 {
598                            left
599                        } else {
600                            right
601                        }
602                    }
603                    HuffmanNode::Leaf { .. } => {
604                        // Should not reach here mid-traversal without a leaf
605                        return Err(WasmError::InvalidOperation {
606                            operation: "Huffman decompression".to_string(),
607                            reason: "Unexpected leaf mid-traversal in bit stream".to_string(),
608                        });
609                    }
610                };
611
612                if let HuffmanNode::Leaf { symbol, .. } = node {
613                    result.push(*symbol);
614                    if result.len() == original_len {
615                        break 'outer;
616                    }
617                    // Reset traversal to tree root
618                    node = &tree;
619                }
620            }
621        }
622
623        // Handle single-symbol edge case: entire input was one distinct byte
624        if result.is_empty()
625            && original_len > 0
626            && let HuffmanNode::Leaf { symbol, .. } = &tree
627        {
628            result.resize(original_len, *symbol);
629        }
630
631        if result.len() != original_len {
632            return Err(WasmError::InvalidOperation {
633                operation: "Huffman decompression".to_string(),
634                reason: format!(
635                    "Decoded {} bytes but expected {}",
636                    result.len(),
637                    original_len
638                ),
639            });
640        }
641
642        Ok(result)
643    }
644}
645/// LZ77-style compression
646pub struct Lz77Compressor;
647impl Lz77Compressor {
648    /// Window size for looking back
649    const WINDOW_SIZE: usize = 4096;
650    /// Maximum match length
651    const MAX_MATCH_LENGTH: usize = 18;
652    /// Minimum match length
653    const MIN_MATCH_LENGTH: usize = 3;
654    /// Finds the longest match in the window
655    fn find_longest_match(data: &[u8], pos: usize) -> Option<(usize, usize)> {
656        let window_start = pos.saturating_sub(Self::WINDOW_SIZE);
657        let mut best_match = None;
658        let mut best_length = 0;
659        for start in window_start..pos {
660            let mut length = 0;
661            while length < Self::MAX_MATCH_LENGTH
662                && pos + length < data.len()
663                && data[start + length] == data[pos + length]
664            {
665                length += 1;
666            }
667            if length >= Self::MIN_MATCH_LENGTH && length > best_length {
668                best_length = length;
669                best_match = Some((pos - start, length));
670            }
671        }
672        best_match
673    }
674    /// Compresses data using LZ77
675    pub fn compress(data: &[u8]) -> Vec<u8> {
676        if data.is_empty() {
677            return Vec::new();
678        }
679        let mut compressed = Vec::new();
680        let mut pos = 0;
681        while pos < data.len() {
682            if let Some((distance, length)) = Self::find_longest_match(data, pos) {
683                compressed.push(1);
684                compressed.push((distance >> 8) as u8);
685                compressed.push((distance & 0xFF) as u8);
686                compressed.push(length as u8);
687                pos += length;
688            } else {
689                compressed.push(0);
690                compressed.push(data[pos]);
691                pos += 1;
692            }
693        }
694        compressed
695    }
696    /// Decompresses LZ77 data
697    pub fn decompress(data: &[u8]) -> WasmResult<Vec<u8>> {
698        let mut decompressed = Vec::new();
699        let mut i = 0;
700        while i < data.len() {
701            let flag = data[i];
702            i += 1;
703            if flag == 1 {
704                if i + 3 > data.len() {
705                    return Err(WasmError::InvalidOperation {
706                        operation: "LZ77 decompress".to_string(),
707                        reason: "Unexpected end of data".to_string(),
708                    });
709                }
710                let distance = ((data[i] as usize) << 8) | (data[i + 1] as usize);
711                let length = data[i + 2] as usize;
712                i += 3;
713                let start = decompressed.len().saturating_sub(distance);
714                for j in 0..length {
715                    if start + j < decompressed.len() {
716                        let byte = decompressed[start + j];
717                        decompressed.push(byte);
718                    }
719                }
720            } else {
721                if i >= data.len() {
722                    return Err(WasmError::InvalidOperation {
723                        operation: "LZ77 decompress".to_string(),
724                        reason: "Unexpected end of data".to_string(),
725                    });
726                }
727                decompressed.push(data[i]);
728                i += 1;
729            }
730        }
731        Ok(decompressed)
732    }
733}
734/// Unified compression interface
735pub struct TileCompressor {
736    /// Algorithm to use
737    algorithm: CompressionAlgorithm,
738}
739impl TileCompressor {
740    /// Creates a new tile compressor
741    pub const fn new(algorithm: CompressionAlgorithm) -> Self {
742        Self { algorithm }
743    }
744    /// Compresses tile data
745    pub fn compress(&self, data: &[u8]) -> (Vec<u8>, CompressionStats) {
746        #[cfg(target_arch = "wasm32")]
747        let start = js_sys::Date::now();
748        #[cfg(not(target_arch = "wasm32"))]
749        let start = std::time::Instant::now();
750
751        let original_size = data.len();
752        let compressed = match self.algorithm {
753            CompressionAlgorithm::None => data.to_vec(),
754            CompressionAlgorithm::Rle => RleCompressor::compress(data),
755            CompressionAlgorithm::Delta => DeltaCompressor::compress(data),
756            CompressionAlgorithm::Huffman => HuffmanCompressor::compress(data),
757            CompressionAlgorithm::Lz77 => Lz77Compressor::compress(data),
758        };
759        let compressed_size = compressed.len();
760
761        #[cfg(target_arch = "wasm32")]
762        let elapsed = js_sys::Date::now() - start;
763        #[cfg(not(target_arch = "wasm32"))]
764        let elapsed = start.elapsed().as_secs_f64() * 1000.0;
765
766        let stats = CompressionStats::new(original_size, compressed_size, elapsed);
767        (compressed, stats)
768    }
769    /// Decompresses tile data
770    pub fn decompress(&self, data: &[u8]) -> WasmResult<Vec<u8>> {
771        match self.algorithm {
772            CompressionAlgorithm::None => Ok(data.to_vec()),
773            CompressionAlgorithm::Rle => RleCompressor::decompress(data),
774            CompressionAlgorithm::Delta => DeltaCompressor::decompress(data),
775            CompressionAlgorithm::Huffman => Ok(data.to_vec()),
776            CompressionAlgorithm::Lz77 => Lz77Compressor::decompress(data),
777        }
778    }
779    /// Compresses 2D tile data (for images)
780    pub fn compress_2d(&self, data: &[u8], width: usize) -> (Vec<u8>, CompressionStats) {
781        #[cfg(target_arch = "wasm32")]
782        let start = js_sys::Date::now();
783        #[cfg(not(target_arch = "wasm32"))]
784        let start = std::time::Instant::now();
785
786        let original_size = data.len();
787        let compressed = match self.algorithm {
788            CompressionAlgorithm::Delta => DeltaCompressor::compress_2d(data, width),
789            _ => self.compress(data).0,
790        };
791        let compressed_size = compressed.len();
792
793        #[cfg(target_arch = "wasm32")]
794        let elapsed = js_sys::Date::now() - start;
795        #[cfg(not(target_arch = "wasm32"))]
796        let elapsed = start.elapsed().as_secs_f64() * 1000.0;
797
798        let stats = CompressionStats::new(original_size, compressed_size, elapsed);
799        (compressed, stats)
800    }
801    /// Decompresses 2D tile data
802    pub fn decompress_2d(&self, data: &[u8], width: usize) -> WasmResult<Vec<u8>> {
803        match self.algorithm {
804            CompressionAlgorithm::Delta => DeltaCompressor::decompress_2d(data, width),
805            _ => self.decompress(data),
806        }
807    }
808}
809/// Compression benchmark
810pub struct CompressionBenchmark;
811impl CompressionBenchmark {
812    /// Benchmarks all compression algorithms
813    pub fn benchmark_all(data: &[u8]) -> Vec<(CompressionAlgorithm, CompressionStats)> {
814        let algorithms = [
815            CompressionAlgorithm::None,
816            CompressionAlgorithm::Rle,
817            CompressionAlgorithm::Delta,
818            CompressionAlgorithm::Huffman,
819            CompressionAlgorithm::Lz77,
820        ];
821        let mut results = Vec::new();
822        for &algorithm in &algorithms {
823            let compressor = TileCompressor::new(algorithm);
824            let (_compressed, stats) = compressor.compress(data);
825            results.push((algorithm, stats));
826        }
827        results
828    }
829    /// Finds the best compression algorithm for the given data
830    pub fn find_best(data: &[u8]) -> CompressionAlgorithm {
831        let results = Self::benchmark_all(data);
832        results
833            .into_iter()
834            .min_by(|a, b| {
835                a.1.compressed_size.cmp(&b.1.compressed_size).then_with(|| {
836                    a.1.compression_time_ms
837                        .partial_cmp(&b.1.compression_time_ms)
838                        .unwrap_or(std::cmp::Ordering::Equal)
839                })
840            })
841            .map(|(algo, _)| algo)
842            .unwrap_or(CompressionAlgorithm::None)
843    }
844}
845/// Compression selector for automatic algorithm selection
846#[derive(Debug, Clone)]
847pub struct CompressionSelector {
848    /// Statistics from previous compressions
849    history: Vec<(CompressionAlgorithm, CompressionStats)>,
850    /// Maximum history size
851    max_history: usize,
852}
853impl CompressionSelector {
854    /// Creates a new compression selector
855    pub fn new() -> Self {
856        Self {
857            history: Vec::new(),
858            max_history: 10,
859        }
860    }
861    /// Selects the best compression algorithm for the given data
862    pub fn select_best(&mut self, data: &[u8]) -> WasmResult<CompressionAlgorithm> {
863        let best = CompressionBenchmark::find_best(data);
864        let compressor = TileCompressor::new(best);
865        let (_compressed, stats) = compressor.compress(data);
866        self.history.push((best, stats));
867        if self.history.len() > self.max_history {
868            self.history.remove(0);
869        }
870        Ok(best)
871    }
872    /// Returns compression statistics history
873    pub fn history(&self) -> &[(CompressionAlgorithm, CompressionStats)] {
874        &self.history
875    }
876    /// Clears history
877    pub fn clear_history(&mut self) {
878        self.history.clear();
879    }
880}
881impl Default for CompressionSelector {
882    fn default() -> Self {
883        Self::new()
884    }
885}
886#[cfg(test)]
887mod tests {
888    use super::*;
889    #[test]
890    fn test_rle_compress_decompress() {
891        let data = vec![1, 1, 1, 2, 2, 3, 3, 3, 3];
892        let compressed = RleCompressor::compress(&data);
893        let decompressed = RleCompressor::decompress(&compressed).expect("Decompress failed");
894        assert_eq!(data, decompressed);
895        assert!(compressed.len() < data.len());
896    }
897    #[test]
898    fn test_delta_compress_decompress() {
899        let data = vec![10, 15, 20, 25, 30, 35, 40];
900        let compressed = DeltaCompressor::compress(&data);
901        let decompressed = DeltaCompressor::decompress(&compressed).expect("Decompress failed");
902        assert_eq!(data, decompressed);
903    }
904    #[test]
905    fn test_delta_2d() {
906        let data = vec![10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120];
907        let width = 3;
908        let compressed = DeltaCompressor::compress_2d(&data, width);
909        let decompressed =
910            DeltaCompressor::decompress_2d(&compressed, width).expect("Decompress 2D failed");
911        assert_eq!(data, decompressed);
912    }
913    #[test]
914    fn test_lz77_compress_decompress() {
915        let data = b"ABCABCABCABC";
916        let compressed = Lz77Compressor::compress(data);
917        let decompressed = Lz77Compressor::decompress(&compressed).expect("Decompress failed");
918        assert_eq!(data.to_vec(), decompressed);
919    }
920    #[test]
921    fn test_compression_stats() {
922        let stats = CompressionStats::new(1000, 500, 10.0);
923        assert_eq!(stats.space_saved(), 500);
924        assert_eq!(stats.space_saved_percent(), 50.0);
925        assert_eq!(stats.ratio, 0.5);
926    }
927    #[test]
928    fn test_tile_compressor() {
929        let compressor = TileCompressor::new(CompressionAlgorithm::Rle);
930        let data = vec![1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3];
931        let (compressed, stats) = compressor.compress(&data);
932        assert!(stats.compressed_size <= data.len());
933        let decompressed = compressor
934            .decompress(&compressed)
935            .expect("Decompress failed");
936        assert_eq!(data, decompressed);
937    }
938    #[test]
939    #[ignore]
940    fn test_compression_benchmark() {
941        let data = vec![1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5];
942        let results = CompressionBenchmark::benchmark_all(&data);
943        assert_eq!(results.len(), 5);
944        let algorithms: Vec<_> = results.iter().map(|(algo, _)| *algo).collect();
945        assert!(algorithms.contains(&CompressionAlgorithm::None));
946        assert!(algorithms.contains(&CompressionAlgorithm::Rle));
947    }
948    #[test]
949    #[ignore]
950    fn test_find_best_compression() {
951        let data = vec![1, 1, 1, 1, 2, 2, 2, 2];
952        let best = CompressionBenchmark::find_best(&data);
953        assert_ne!(best, CompressionAlgorithm::None);
954    }
955}