wow_m2/
anim.rs

1use crate::io_ext::{ReadExt, WriteExt};
2use std::fs::File;
3use std::io::{Read, Seek, SeekFrom, Write};
4use std::path::Path;
5
6use crate::common::{C3Vector, Quaternion};
7use crate::error::{M2Error, Result};
8use crate::version::M2Version;
9
10/// Magic signature for Modern Anim files ("MAOF")
11pub const ANIM_MAGIC: [u8; 4] = *b"MAOF";
12
13/// ANIM file format types
14///
15/// ANIM files evolved significantly between World of Warcraft expansions:
16/// - Legacy format was used from Vanilla through Warlords of Draenor
17/// - Modern format was introduced in Legion and continues through current versions
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19pub enum AnimFormat {
20    /// Legacy format (Vanilla through Warlords of Draenor)
21    ///
22    /// Features:
23    /// - Raw binary data without magic headers
24    /// - Variable structure depending on M2 model
25    /// - Requires context from associated M2 file for proper parsing
26    Legacy,
27    /// Modern format (Legion and later)
28    ///
29    /// Features:
30    /// - "MAOF" magic header for identification
31    /// - Self-contained chunked structure
32    /// - Standardized format across different models
33    Modern,
34}
35
36/// ANIM format detector
37pub struct AnimFormatDetector;
38
39impl AnimFormatDetector {
40    /// Detect ANIM format by examining file content
41    ///
42    /// This method examines the first 4 bytes of the file to detect the format:
43    /// - If they match "MAOF", it's a modern format file
44    /// - Otherwise, it's assumed to be a legacy format file
45    ///
46    /// The reader position is restored after detection.
47    ///
48    /// # Errors
49    ///
50    /// Returns an error if:
51    /// - The file is too small to read the magic bytes
52    /// - I/O errors occur during detection
53    pub fn detect_format<R: Read + Seek>(reader: &mut R) -> Result<AnimFormat> {
54        let initial_pos = reader.stream_position().map_err(M2Error::Io)?;
55
56        // Try to read first 4 bytes to check for MAOF magic
57        let mut magic = [0u8; 4];
58        match reader.read_exact(&mut magic) {
59            Ok(()) => {
60                // Reset position for subsequent parsing
61                reader
62                    .seek(SeekFrom::Start(initial_pos))
63                    .map_err(M2Error::Io)?;
64
65                if magic == ANIM_MAGIC {
66                    Ok(AnimFormat::Modern)
67                } else {
68                    // If first 4 bytes are not MAOF, assume legacy format
69                    // Legacy files start with raw data (typically offset tables)
70                    Ok(AnimFormat::Legacy)
71                }
72            }
73            Err(e) => {
74                // If we can't read 4 bytes, the file is too small or corrupted
75                reader
76                    .seek(SeekFrom::Start(initial_pos))
77                    .map_err(|_| M2Error::Io(e))?;
78                Err(M2Error::AnimFormatError(
79                    "File too small to determine ANIM format - need at least 4 bytes".to_string(),
80                ))
81            }
82        }
83    }
84
85    /// Detect format based on M2 version (heuristic approach)
86    pub fn detect_format_by_version(version: M2Version) -> AnimFormat {
87        match version {
88            // Pre-Legion versions use legacy format
89            M2Version::Vanilla
90            | M2Version::TBC
91            | M2Version::WotLK
92            | M2Version::Cataclysm
93            | M2Version::MoP
94            | M2Version::WoD => AnimFormat::Legacy,
95            // Legion and later use modern format
96            _ => AnimFormat::Modern,
97        }
98    }
99}
100
101/// ANIM file header (Modern format)
102#[derive(Debug, Clone)]
103pub struct AnimHeader {
104    /// Magic signature ("MAOF")
105    pub magic: [u8; 4],
106    /// Anim version
107    pub version: u32,
108    /// The number of AFID IDs in this file
109    pub id_count: u32,
110    /// Unknown
111    pub unknown: u32,
112    /// Offset to animation entries
113    pub anim_entry_offset: u32,
114}
115
116impl AnimHeader {
117    /// Parse an ANIM header from a reader
118    pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
119        // Read and check magic
120        let mut magic = [0u8; 4];
121        reader.read_exact(&mut magic)?;
122
123        if magic != ANIM_MAGIC {
124            return Err(M2Error::InvalidMagic {
125                expected: String::from_utf8_lossy(&ANIM_MAGIC).to_string(),
126                actual: String::from_utf8_lossy(&magic).to_string(),
127            });
128        }
129
130        // Read other header fields
131        let version = reader.read_u32_le()?;
132        let id_count = reader.read_u32_le()?;
133        let unknown = reader.read_u32_le()?;
134        let anim_entry_offset = reader.read_u32_le()?;
135
136        Ok(Self {
137            magic,
138            version,
139            id_count,
140            unknown,
141            anim_entry_offset,
142        })
143    }
144
145    /// Write an ANIM header to a writer
146    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
147        writer.write_all(&self.magic)?;
148        writer.write_u32_le(self.version)?;
149        writer.write_u32_le(self.id_count)?;
150        writer.write_u32_le(self.unknown)?;
151        writer.write_u32_le(self.anim_entry_offset)?;
152
153        Ok(())
154    }
155}
156
157/// Animation entry header
158#[derive(Debug, Clone)]
159pub struct AnimEntry {
160    /// Animation ID "AFID"
161    pub id: u32,
162    /// Start offset of the animation section
163    pub offset: u32,
164    /// Size of the animation section
165    pub size: u32,
166}
167
168impl AnimEntry {
169    /// Parse an animation entry from a reader
170    pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
171        let id = reader.read_u32_le()?;
172        let offset = reader.read_u32_le()?;
173        let size = reader.read_u32_le()?;
174
175        Ok(Self { id, offset, size })
176    }
177
178    /// Write an animation entry to a writer
179    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
180        writer.write_u32_le(self.id)?;
181        writer.write_u32_le(self.offset)?;
182        writer.write_u32_le(self.size)?;
183
184        Ok(())
185    }
186}
187
188/// Animation section header "AFID"
189#[derive(Debug, Clone)]
190pub struct AnimSectionHeader {
191    /// "AFID" magic
192    pub magic: [u8; 4],
193    /// Animation ID
194    pub id: u32,
195    /// Start frames for this section
196    pub start: u32,
197    /// End frames for this section
198    pub end: u32,
199}
200
201impl AnimSectionHeader {
202    /// Parse an animation section header from a reader
203    pub fn parse<R: Read>(reader: &mut R) -> Result<Self> {
204        let mut magic = [0u8; 4];
205        reader.read_exact(&mut magic)?;
206
207        if magic != *b"AFID" {
208            return Err(M2Error::InvalidMagic {
209                expected: "AFID".to_string(),
210                actual: String::from_utf8_lossy(&magic).to_string(),
211            });
212        }
213
214        let id = reader.read_u32_le()?;
215        let start = reader.read_u32_le()?;
216        let end = reader.read_u32_le()?;
217
218        Ok(Self {
219            magic,
220            id,
221            start,
222            end,
223        })
224    }
225
226    /// Write an animation section header to a writer
227    pub fn write<W: Write>(&self, writer: &mut W) -> Result<()> {
228        writer.write_all(&self.magic)?;
229        writer.write_u32_le(self.id)?;
230        writer.write_u32_le(self.start)?;
231        writer.write_u32_le(self.end)?;
232
233        Ok(())
234    }
235}
236
237/// Translation data for an animation
238#[derive(Debug, Clone)]
239pub struct AnimTranslation {
240    /// Animation timelines
241    pub timestamps: Vec<u32>,
242    /// Translation vectors
243    pub translations: Vec<C3Vector>,
244}
245
246/// Rotation data for an animation
247#[derive(Debug, Clone)]
248pub struct AnimRotation {
249    /// Animation timelines
250    pub timestamps: Vec<u32>,
251    /// Rotation quaternions
252    pub rotations: Vec<Quaternion>,
253}
254
255/// Scaling data for an animation
256#[derive(Debug, Clone)]
257pub struct AnimScaling {
258    /// Animation timelines
259    pub timestamps: Vec<u32>,
260    /// Scaling vectors
261    pub scalings: Vec<C3Vector>,
262}
263
264/// Animation data for a single bone
265#[derive(Debug, Clone)]
266pub struct AnimBoneAnimation {
267    /// Bone ID
268    pub bone_id: u32,
269    /// Translation animation
270    pub translation: Option<AnimTranslation>,
271    /// Rotation animation
272    pub rotation: Option<AnimRotation>,
273    /// Scaling animation
274    pub scaling: Option<AnimScaling>,
275}
276
277/// Animation data for a section
278#[derive(Debug, Clone)]
279pub struct AnimSection {
280    /// Section header
281    pub header: AnimSectionHeader,
282    /// Animations for each bone
283    pub bone_animations: Vec<AnimBoneAnimation>,
284}
285
286impl AnimSection {
287    /// Parse an animation section from a reader
288    pub fn parse<R: Read>(reader: &mut R, size: u32) -> Result<Self> {
289        let header = AnimSectionHeader::parse(reader)?;
290
291        // Determine the bone count
292        let header_size = 16; // "AFID" + id + start + end
293        let remaining_size = size - header_size;
294        let bone_count = remaining_size / 4; // Each bone animation reference is 4 bytes
295
296        // Read bone animation offsets
297        let mut bone_offsets = Vec::with_capacity(bone_count as usize);
298        for _ in 0..bone_count {
299            bone_offsets.push(reader.read_u32_le()?);
300        }
301
302        // Read bone animations
303        let mut bone_animations = Vec::with_capacity(bone_count as usize);
304
305        for &offset in &bone_offsets {
306            if offset > 0 {
307                // Bone has animation data
308                let bone_id = reader.read_u32_le()?;
309
310                // Read flags
311                let flags = reader.read_u32_le()?;
312
313                // Read translation data if present
314                let translation = if (flags & 0x1) != 0 {
315                    let timestamp_count = reader.read_u32_le()?;
316
317                    let mut timestamps = Vec::with_capacity(timestamp_count as usize);
318                    for _ in 0..timestamp_count {
319                        timestamps.push(reader.read_u32_le()?);
320                    }
321
322                    let mut translations = Vec::with_capacity(timestamp_count as usize);
323                    for _ in 0..timestamp_count {
324                        translations.push(C3Vector::parse(reader)?);
325                    }
326
327                    Some(AnimTranslation {
328                        timestamps,
329                        translations,
330                    })
331                } else {
332                    None
333                };
334
335                // Read rotation data if present
336                let rotation = if (flags & 0x2) != 0 {
337                    let timestamp_count = reader.read_u32_le()?;
338
339                    let mut timestamps = Vec::with_capacity(timestamp_count as usize);
340                    for _ in 0..timestamp_count {
341                        timestamps.push(reader.read_u32_le()?);
342                    }
343
344                    let mut rotations = Vec::with_capacity(timestamp_count as usize);
345                    for _ in 0..timestamp_count {
346                        rotations.push(Quaternion::parse(reader)?);
347                    }
348
349                    Some(AnimRotation {
350                        timestamps,
351                        rotations,
352                    })
353                } else {
354                    None
355                };
356
357                // Read scaling data if present
358                let scaling = if (flags & 0x4) != 0 {
359                    let timestamp_count = reader.read_u32_le()?;
360
361                    let mut timestamps = Vec::with_capacity(timestamp_count as usize);
362                    for _ in 0..timestamp_count {
363                        timestamps.push(reader.read_u32_le()?);
364                    }
365
366                    let mut scalings = Vec::with_capacity(timestamp_count as usize);
367                    for _ in 0..timestamp_count {
368                        scalings.push(C3Vector::parse(reader)?);
369                    }
370
371                    Some(AnimScaling {
372                        timestamps,
373                        scalings,
374                    })
375                } else {
376                    None
377                };
378
379                bone_animations.push(AnimBoneAnimation {
380                    bone_id,
381                    translation,
382                    rotation,
383                    scaling,
384                });
385            } else {
386                // No animation data for this bone
387                bone_animations.push(AnimBoneAnimation {
388                    bone_id: 0,
389                    translation: None,
390                    rotation: None,
391                    scaling: None,
392                });
393            }
394        }
395
396        Ok(Self {
397            header,
398            bone_animations,
399        })
400    }
401
402    /// Write an animation section to a writer
403    pub fn write<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
404        // Write section header
405        self.header.write(writer)?;
406
407        // Write bone animation offsets (placeholders for now)
408        let bone_offsets_pos = writer.stream_position()?;
409
410        for _ in 0..self.bone_animations.len() {
411            writer.write_u32_le(0)?; // Placeholder
412        }
413
414        // Write bone animations and update offsets
415        let mut bone_offsets = Vec::with_capacity(self.bone_animations.len());
416
417        for bone_animation in &self.bone_animations {
418            if bone_animation.translation.is_some()
419                || bone_animation.rotation.is_some()
420                || bone_animation.scaling.is_some()
421            {
422                // Bone has animation data
423                let offset = writer.stream_position()? as u32;
424                bone_offsets.push(offset);
425
426                // Write bone ID
427                writer.write_u32_le(bone_animation.bone_id)?;
428
429                // Determine flags
430                let mut flags = 0u32;
431                if bone_animation.translation.is_some() {
432                    flags |= 0x1;
433                }
434                if bone_animation.rotation.is_some() {
435                    flags |= 0x2;
436                }
437                if bone_animation.scaling.is_some() {
438                    flags |= 0x4;
439                }
440
441                // Write flags
442                writer.write_u32_le(flags)?;
443
444                // Write translation data if present
445                if let Some(ref translation) = bone_animation.translation {
446                    writer.write_u32_le(translation.timestamps.len() as u32)?;
447
448                    for &timestamp in &translation.timestamps {
449                        writer.write_u32_le(timestamp)?;
450                    }
451
452                    for translation in &translation.translations {
453                        translation.write(writer)?;
454                    }
455                }
456
457                // Write rotation data if present
458                if let Some(ref rotation) = bone_animation.rotation {
459                    writer.write_u32_le(rotation.timestamps.len() as u32)?;
460
461                    for &timestamp in &rotation.timestamps {
462                        writer.write_u32_le(timestamp)?;
463                    }
464
465                    for rotation in &rotation.rotations {
466                        rotation.write(writer)?;
467                    }
468                }
469
470                // Write scaling data if present
471                if let Some(ref scaling) = bone_animation.scaling {
472                    writer.write_u32_le(scaling.timestamps.len() as u32)?;
473
474                    for &timestamp in &scaling.timestamps {
475                        writer.write_u32_le(timestamp)?;
476                    }
477
478                    for scaling in &scaling.scalings {
479                        scaling.write(writer)?;
480                    }
481                }
482            } else {
483                // No animation data for this bone
484                bone_offsets.push(0);
485            }
486        }
487
488        // Update bone offsets
489        let current_pos = writer.stream_position()?;
490        writer.seek(SeekFrom::Start(bone_offsets_pos))?;
491
492        for &offset in &bone_offsets {
493            writer.write_u32_le(offset)?;
494        }
495
496        // Restore position
497        writer.seek(SeekFrom::Start(current_pos))?;
498
499        Ok(())
500    }
501}
502
503/// Format-specific metadata
504#[derive(Debug, Clone)]
505pub enum AnimMetadata {
506    Legacy {
507        /// Total file size
508        file_size: u32,
509        /// Number of animations detected
510        animation_count: u32,
511        /// Detected structure hints for validation
512        structure_hints: LegacyStructureHints,
513    },
514    Modern {
515        /// Original MAOF header
516        header: AnimHeader,
517        /// Animation entries
518        entries: Vec<AnimEntry>,
519    },
520}
521
522/// Structure hints for legacy ANIM files
523#[derive(Debug, Clone)]
524pub struct LegacyStructureHints {
525    /// Whether the file appears to have valid structure
526    pub appears_valid: bool,
527    /// Estimated data blocks found
528    pub estimated_blocks: u32,
529    /// File appears to contain timestamps
530    pub has_timestamps: bool,
531}
532
533/// Memory usage statistics for ANIM files
534#[derive(Debug, Clone, Default)]
535pub struct MemoryUsage {
536    /// Number of animation sections
537    pub sections: usize,
538    /// Total number of bone animations
539    pub bone_animations: usize,
540    /// Total translation keyframes
541    pub translation_keyframes: usize,
542    /// Total rotation keyframes
543    pub rotation_keyframes: usize,
544    /// Total scaling keyframes
545    pub scaling_keyframes: usize,
546    /// Approximate memory usage in bytes
547    pub approximate_bytes: usize,
548}
549
550impl MemoryUsage {
551    /// Create new empty memory usage statistics
552    pub fn new() -> Self {
553        Self::default()
554    }
555
556    /// Calculate approximate memory usage in bytes
557    pub fn calculate_approximate_bytes(&self) -> usize {
558        let mut bytes = 0;
559
560        // Section headers
561        bytes += self.sections * std::mem::size_of::<AnimSectionHeader>();
562
563        // Bone animation structures
564        bytes += self.bone_animations * std::mem::size_of::<AnimBoneAnimation>();
565
566        // Keyframe data (timestamps + values)
567        bytes += self.translation_keyframes
568            * (std::mem::size_of::<u32>() + std::mem::size_of::<C3Vector>());
569        bytes += self.rotation_keyframes
570            * (std::mem::size_of::<u32>() + std::mem::size_of::<Quaternion>());
571        bytes +=
572            self.scaling_keyframes * (std::mem::size_of::<u32>() + std::mem::size_of::<C3Vector>());
573
574        bytes
575    }
576
577    /// Get total keyframes across all animation types
578    pub fn total_keyframes(&self) -> usize {
579        self.translation_keyframes + self.rotation_keyframes + self.scaling_keyframes
580    }
581}
582
583/// Unified ANIM file representation
584#[derive(Debug, Clone)]
585pub struct AnimFile {
586    /// Detected format type
587    pub format: AnimFormat,
588    /// Animation sections (unified regardless of source format)
589    pub sections: Vec<AnimSection>,
590    /// Format-specific metadata
591    pub metadata: AnimMetadata,
592}
593
594/// ANIM parser factory for format-specific parsing
595pub struct AnimParser;
596
597impl AnimParser {
598    /// Parse ANIM file with automatic format detection
599    pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<AnimFile> {
600        let format = AnimFormatDetector::detect_format(reader)?;
601
602        match format {
603            AnimFormat::Legacy => Self::parse_legacy(reader),
604            AnimFormat::Modern => Self::parse_modern(reader),
605        }
606    }
607
608    /// Parse with explicit format specification
609    pub fn parse_with_format<R: Read + Seek>(
610        reader: &mut R,
611        format: AnimFormat,
612    ) -> Result<AnimFile> {
613        match format {
614            AnimFormat::Legacy => Self::parse_legacy(reader),
615            AnimFormat::Modern => Self::parse_modern(reader),
616        }
617    }
618
619    /// Parse legacy format ANIM file
620    fn parse_legacy<R: Read + Seek>(reader: &mut R) -> Result<AnimFile> {
621        // Get file size for metadata
622        let file_size = reader.seek(SeekFrom::End(0))? as u32;
623        reader.seek(SeekFrom::Start(0))?;
624
625        // Legacy format analysis:
626        // Based on examination of real Cataclysm ANIM files, they appear to start
627        // with raw animation data rather than a count. The structure seems to be:
628        // 1. Header/offset information (variable size)
629        // 2. Raw animation timeline and value data
630
631        // For legacy ANIM files, we'll attempt to parse as raw animation data
632        // Since the exact structure varies, we'll create a minimal representation
633
634        // Try to detect if this looks like legacy animation data
635        let mut header_bytes = [0u8; 16];
636        reader.read_exact(&mut header_bytes)?;
637        reader.seek(SeekFrom::Start(0))?;
638
639        // Check if this looks like raw animation data (starts with zeros or small values)
640        let _first_value = u32::from_le_bytes([
641            header_bytes[0],
642            header_bytes[1],
643            header_bytes[2],
644            header_bytes[3],
645        ]);
646
647        // For legacy files, create a placeholder animation section
648        // Real parsing would require understanding the specific M2 model this ANIM belongs to
649        let animation_id = Self::extract_anim_id_from_legacy_data(&header_bytes);
650
651        // Analyze the structure for better metadata
652        let structure_hints = Self::analyze_legacy_structure(reader, file_size)?;
653
654        // Create a single animation section representing this legacy ANIM file
655        // In practice, legacy ANIM files contain raw data for a single animation
656        let sections = vec![Self::create_legacy_animation_section(
657            reader,
658            animation_id,
659            file_size,
660        )?];
661
662        Ok(AnimFile {
663            format: AnimFormat::Legacy,
664            sections,
665            metadata: AnimMetadata::Legacy {
666                file_size,
667                animation_count: 1, // Legacy files typically contain one animation
668                structure_hints,
669            },
670        })
671    }
672
673    /// Extract animation ID from legacy data (heuristic)
674    fn extract_anim_id_from_legacy_data(_header_bytes: &[u8; 16]) -> u32 {
675        // Since legacy files don't have a clear header with ID,
676        // we'll use a default or try to extract from context
677        // In practice, this would come from the filename pattern
678        // For now, return a default animation ID
679        1
680    }
681
682    /// Create a legacy animation section from raw data
683    fn create_legacy_animation_section<R: Read + Seek>(
684        reader: &mut R,
685        animation_id: u32,
686        _file_size: u32,
687    ) -> Result<AnimSection> {
688        // Reset to beginning
689        reader.seek(SeekFrom::Start(0))?;
690
691        // For legacy files, we create a placeholder section since the exact
692        // structure varies and requires context from the associated M2 model
693        let header = AnimSectionHeader {
694            magic: *b"AFID",
695            id: animation_id,
696            start: 0,
697            end: 0, // Would need to be extracted from actual data
698        };
699
700        // Legacy files contain raw animation data that would need to be
701        // parsed with knowledge of the bone structure from the M2 file
702        // For now, we create an empty placeholder
703        let bone_animations = Vec::new();
704
705        Ok(AnimSection {
706            header,
707            bone_animations,
708        })
709    }
710
711    /// Analyze legacy ANIM structure to provide better metadata
712    fn analyze_legacy_structure<R: Read + Seek>(
713        reader: &mut R,
714        file_size: u32,
715    ) -> Result<LegacyStructureHints> {
716        reader.seek(SeekFrom::Start(0))?;
717
718        let mut appears_valid = true;
719        let mut estimated_blocks = 0;
720        let mut has_timestamps = false;
721
722        // Read the first 1KB to analyze structure
723        let mut buffer = vec![0u8; (file_size as usize).min(1024)];
724        let bytes_read = reader.read(&mut buffer)?;
725
726        if bytes_read < 16 {
727            appears_valid = false;
728        } else {
729            // Look for patterns that suggest this is animation data
730            // Check for sequences of increasing numbers (timestamps)
731            let u32_values: Vec<u32> = buffer
732                .chunks_exact(4)
733                .map(|chunk| u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]))
734                .collect();
735
736            for window in u32_values.windows(3) {
737                if window.len() == 3 {
738                    let (val1, val2, val3) = (window[0], window[1], window[2]);
739
740                    // Check for increasing sequence (possible timestamps)
741                    if val1 < val2 && val2 < val3 && val1 < 100000 {
742                        has_timestamps = true;
743                        estimated_blocks += 1;
744                    }
745                }
746            }
747
748            // Estimate blocks based on file size and patterns
749            if estimated_blocks == 0 {
750                estimated_blocks = (file_size / 1000).max(1); // Rough estimate
751            }
752        }
753
754        reader.seek(SeekFrom::Start(0))?; // Reset for subsequent operations
755
756        Ok(LegacyStructureHints {
757            appears_valid,
758            estimated_blocks,
759            has_timestamps,
760        })
761    }
762
763    /// Parse modern format ANIM file (adapted from existing implementation)
764    fn parse_modern<R: Read + Seek>(reader: &mut R) -> Result<AnimFile> {
765        // Parse header
766        let header = AnimHeader::parse(reader)?;
767
768        // Parse animation entries
769        reader.seek(SeekFrom::Start(header.anim_entry_offset as u64))?;
770
771        let mut entries = Vec::with_capacity(header.id_count as usize);
772        for _ in 0..header.id_count {
773            entries.push(AnimEntry::parse(reader)?);
774        }
775
776        // Parse animation sections
777        let mut sections = Vec::with_capacity(entries.len());
778
779        for entry in &entries {
780            reader.seek(SeekFrom::Start(entry.offset as u64))?;
781            sections.push(AnimSection::parse(reader, entry.size)?);
782        }
783
784        Ok(AnimFile {
785            format: AnimFormat::Modern,
786            sections,
787            metadata: AnimMetadata::Modern { header, entries },
788        })
789    }
790}
791
792impl AnimFile {
793    /// Parse an ANIM file from a reader with automatic format detection
794    pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self> {
795        AnimParser::parse(reader)
796    }
797
798    /// Parse ANIM file with validation
799    pub fn parse_validated<R: Read + Seek>(reader: &mut R) -> Result<Self> {
800        let anim_file = Self::parse(reader)?;
801        anim_file.validate()?;
802        Ok(anim_file)
803    }
804
805    /// Validate the parsed ANIM file structure
806    pub fn validate(&self) -> Result<()> {
807        // Validate sections
808        if self.sections.is_empty() {
809            return Err(M2Error::ValidationError(
810                "ANIM file must contain at least one section".to_string(),
811            ));
812        }
813
814        // Format-specific validation
815        match (&self.format, &self.metadata) {
816            (
817                AnimFormat::Legacy,
818                AnimMetadata::Legacy {
819                    structure_hints, ..
820                },
821            ) => {
822                if !structure_hints.appears_valid {
823                    return Err(M2Error::ValidationError(
824                        "Legacy ANIM file structure appears invalid".to_string(),
825                    ));
826                }
827            }
828            (AnimFormat::Modern, AnimMetadata::Modern { header, entries }) => {
829                if header.id_count as usize != entries.len() {
830                    return Err(M2Error::ValidationError(format!(
831                        "Header ID count ({}) doesn't match entries count ({})",
832                        header.id_count,
833                        entries.len()
834                    )));
835                }
836
837                if header.id_count as usize != self.sections.len() {
838                    return Err(M2Error::ValidationError(format!(
839                        "Header ID count ({}) doesn't match sections count ({})",
840                        header.id_count,
841                        self.sections.len()
842                    )));
843                }
844            }
845            _ => {
846                return Err(M2Error::ValidationError(
847                    "Format and metadata type mismatch".to_string(),
848                ));
849            }
850        }
851
852        Ok(())
853    }
854
855    /// Load an ANIM file from a file with automatic format detection
856    pub fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
857        let mut file = File::open(path)?;
858        Self::parse(&mut file)
859    }
860
861    /// Load ANIM file with version hint for format detection
862    pub fn load_with_version<P: AsRef<Path>>(path: P, version: M2Version) -> Result<Self> {
863        let mut file = File::open(path)?;
864        let format = AnimFormatDetector::detect_format_by_version(version);
865        AnimParser::parse_with_format(&mut file, format)
866    }
867
868    /// Parse ANIM file with explicit format specification
869    pub fn parse_with_format<R: Read + Seek>(reader: &mut R, format: AnimFormat) -> Result<Self> {
870        AnimParser::parse_with_format(reader, format)
871    }
872
873    /// Save an ANIM file to a file
874    pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<()> {
875        let mut file = File::create(path)?;
876        self.write(&mut file)
877    }
878
879    /// Write an ANIM file to a writer
880    pub fn write<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
881        match self.format {
882            AnimFormat::Modern => self.write_modern(writer),
883            AnimFormat::Legacy => self.write_legacy(writer),
884        }
885    }
886
887    /// Write modern format ANIM file
888    fn write_modern<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
889        let (header, entries) = match &self.metadata {
890            AnimMetadata::Modern { header, entries } => (header, entries),
891            _ => {
892                return Err(M2Error::InternalError(
893                    "Attempting to write modern format with legacy metadata".to_string(),
894                ));
895            }
896        };
897
898        // Calculate offsets
899        let header_size = 20; // Magic + version + id count + unknown + entry offset
900        let entry_size = 12; // ID + offset + size
901
902        let entry_offset = header_size;
903        let _section_offset = entry_offset + entries.len() as u32 * entry_size;
904
905        // Write header
906        let mut header = header.clone();
907        header.anim_entry_offset = entry_offset;
908        header.write(writer)?;
909
910        // Write entry placeholders
911        let mut updated_entries = Vec::with_capacity(entries.len());
912
913        for entry in entries {
914            let entry = AnimEntry {
915                id: entry.id,
916                offset: 0, // Placeholder
917                size: 0,   // Placeholder
918            };
919
920            entry.write(writer)?;
921            updated_entries.push(entry);
922        }
923
924        // Write sections and update entries
925        for (i, section) in self.sections.iter().enumerate() {
926            let section_start = writer.stream_position()? as u32;
927            section.write(writer)?;
928            let section_end = writer.stream_position()? as u32;
929
930            updated_entries[i].offset = section_start;
931            updated_entries[i].size = section_end - section_start;
932        }
933
934        // Update entries
935        writer.seek(SeekFrom::Start(entry_offset as u64))?;
936
937        for entry in &updated_entries {
938            entry.write(writer)?;
939        }
940
941        Ok(())
942    }
943
944    /// Write legacy format ANIM file
945    fn write_legacy<W: Write + Seek>(&self, writer: &mut W) -> Result<()> {
946        // Legacy format writing is more complex due to raw data layout
947        // For now, implement a basic structure
948
949        // Write animation count
950        writer.write_u32_le(self.sections.len() as u32)?;
951
952        // Write offset placeholders
953        let offsets_pos = writer.stream_position()?;
954        for _ in 0..self.sections.len() {
955            writer.write_u32_le(0)?; // Placeholder
956        }
957
958        // Write animation data and collect offsets
959        let mut offsets = Vec::with_capacity(self.sections.len());
960
961        for section in &self.sections {
962            let offset = writer.stream_position()? as u32;
963            offsets.push(offset);
964
965            // Write animation header data
966            writer.write_u32_le(section.header.id)?;
967            writer.write_u32_le(section.header.start)?;
968            writer.write_u32_le(section.header.end)?;
969
970            // Write bone count
971            writer.write_u32_le(section.bone_animations.len() as u32)?;
972
973            // Write bone animation data
974            for bone_anim in &section.bone_animations {
975                writer.write_u32_le(bone_anim.bone_id)?;
976
977                // Calculate flags
978                let mut flags = 0u32;
979                if bone_anim.translation.is_some() {
980                    flags |= 0x1;
981                }
982                if bone_anim.rotation.is_some() {
983                    flags |= 0x2;
984                }
985                if bone_anim.scaling.is_some() {
986                    flags |= 0x4;
987                }
988                writer.write_u32_le(flags)?;
989
990                // Write animation data
991                if let Some(ref translation) = bone_anim.translation {
992                    writer.write_u32_le(translation.timestamps.len() as u32)?;
993                    for &timestamp in &translation.timestamps {
994                        writer.write_u32_le(timestamp)?;
995                    }
996                    for translation in &translation.translations {
997                        translation.write(writer)?;
998                    }
999                }
1000
1001                if let Some(ref rotation) = bone_anim.rotation {
1002                    writer.write_u32_le(rotation.timestamps.len() as u32)?;
1003                    for &timestamp in &rotation.timestamps {
1004                        writer.write_u32_le(timestamp)?;
1005                    }
1006                    for rotation in &rotation.rotations {
1007                        rotation.write(writer)?;
1008                    }
1009                }
1010
1011                if let Some(ref scaling) = bone_anim.scaling {
1012                    writer.write_u32_le(scaling.timestamps.len() as u32)?;
1013                    for &timestamp in &scaling.timestamps {
1014                        writer.write_u32_le(timestamp)?;
1015                    }
1016                    for scaling in &scaling.scalings {
1017                        scaling.write(writer)?;
1018                    }
1019                }
1020            }
1021        }
1022
1023        // Update offsets
1024        let current_pos = writer.stream_position()?;
1025        writer.seek(SeekFrom::Start(offsets_pos))?;
1026        for offset in offsets {
1027            writer.write_u32_le(offset)?;
1028        }
1029        writer.seek(SeekFrom::Start(current_pos))?;
1030
1031        Ok(())
1032    }
1033
1034    /// Convert this ANIM file to a different version
1035    pub fn convert(&self, target_version: M2Version) -> Self {
1036        let target_format = AnimFormatDetector::detect_format_by_version(target_version);
1037
1038        if target_format == self.format {
1039            // No conversion needed
1040            return self.clone();
1041        }
1042
1043        // Convert between formats
1044        match (self.format, target_format) {
1045            (AnimFormat::Legacy, AnimFormat::Modern) => {
1046                // Convert legacy to modern
1047                let header = AnimHeader {
1048                    magic: ANIM_MAGIC,
1049                    version: 1,
1050                    id_count: self.sections.len() as u32,
1051                    unknown: 0,
1052                    anim_entry_offset: 20,
1053                };
1054
1055                let entries: Vec<AnimEntry> = self
1056                    .sections
1057                    .iter()
1058                    .map(|section| {
1059                        AnimEntry {
1060                            id: section.header.id,
1061                            offset: 0, // Will be calculated during writing
1062                            size: 0,   // Will be calculated during writing
1063                        }
1064                    })
1065                    .collect();
1066
1067                AnimFile {
1068                    format: AnimFormat::Modern,
1069                    sections: self.sections.clone(),
1070                    metadata: AnimMetadata::Modern { header, entries },
1071                }
1072            }
1073            (AnimFormat::Modern, AnimFormat::Legacy) => {
1074                // Convert modern to legacy
1075                AnimFile {
1076                    format: AnimFormat::Legacy,
1077                    sections: self.sections.clone(),
1078                    metadata: AnimMetadata::Legacy {
1079                        file_size: 0, // Will be calculated during writing
1080                        animation_count: self.sections.len() as u32,
1081                        structure_hints: LegacyStructureHints {
1082                            appears_valid: true,
1083                            estimated_blocks: self.sections.len() as u32,
1084                            has_timestamps: false, // Unknown during conversion
1085                        },
1086                    },
1087                }
1088            }
1089            _ => self.clone(), // Same format
1090        }
1091    }
1092
1093    /// Get the number of animation sections
1094    pub fn animation_count(&self) -> u32 {
1095        self.sections.len() as u32
1096    }
1097
1098    /// Check if this ANIM file uses legacy format
1099    pub fn is_legacy_format(&self) -> bool {
1100        matches!(self.format, AnimFormat::Legacy)
1101    }
1102
1103    /// Check if this ANIM file uses modern format
1104    pub fn is_modern_format(&self) -> bool {
1105        matches!(self.format, AnimFormat::Modern)
1106    }
1107
1108    /// Get memory usage statistics for this ANIM file
1109    pub fn memory_usage(&self) -> MemoryUsage {
1110        let mut usage = MemoryUsage::new();
1111
1112        // Count sections memory
1113        for section in &self.sections {
1114            usage.sections += 1;
1115            usage.bone_animations += section.bone_animations.len();
1116
1117            for bone_anim in &section.bone_animations {
1118                if let Some(ref translation) = bone_anim.translation {
1119                    usage.translation_keyframes += translation.timestamps.len();
1120                }
1121                if let Some(ref rotation) = bone_anim.rotation {
1122                    usage.rotation_keyframes += rotation.timestamps.len();
1123                }
1124                if let Some(ref scaling) = bone_anim.scaling {
1125                    usage.scaling_keyframes += scaling.timestamps.len();
1126                }
1127            }
1128        }
1129
1130        // Calculate approximate memory usage
1131        usage.approximate_bytes = usage.calculate_approximate_bytes();
1132
1133        usage
1134    }
1135
1136    /// Optimize memory usage by deduplicating identical keyframe sequences
1137    pub fn optimize_memory(&mut self) {
1138        // This is a placeholder for memory optimization
1139        // In practice, this could deduplicate identical timestamp/value sequences
1140        // across different bone animations
1141
1142        for section in &mut self.sections {
1143            // Remove empty bone animations
1144            section.bone_animations.retain(|bone_anim| {
1145                bone_anim.translation.is_some()
1146                    || bone_anim.rotation.is_some()
1147                    || bone_anim.scaling.is_some()
1148            });
1149
1150            // Shrink capacity to fit actual data
1151            section.bone_animations.shrink_to_fit();
1152
1153            for bone_anim in &mut section.bone_animations {
1154                if let Some(ref mut translation) = bone_anim.translation {
1155                    translation.timestamps.shrink_to_fit();
1156                    translation.translations.shrink_to_fit();
1157                }
1158                if let Some(ref mut rotation) = bone_anim.rotation {
1159                    rotation.timestamps.shrink_to_fit();
1160                    rotation.rotations.shrink_to_fit();
1161                }
1162                if let Some(ref mut scaling) = bone_anim.scaling {
1163                    scaling.timestamps.shrink_to_fit();
1164                    scaling.scalings.shrink_to_fit();
1165                }
1166            }
1167        }
1168    }
1169}
1170
1171/// Legacy ANIM parsing utilities
1172mod legacy_utils {
1173    use super::*;
1174
1175    /// Validate legacy ANIM file structure
1176    pub fn validate_legacy_structure<R: Read + Seek>(
1177        reader: &mut R,
1178        animation_count: u32,
1179    ) -> Result<bool> {
1180        if animation_count == 0 || animation_count > 10000 {
1181            return Ok(false);
1182        }
1183
1184        let file_size = reader.seek(SeekFrom::End(0))?;
1185        reader.seek(SeekFrom::Start(0))?;
1186
1187        // Minimum size check: count + offsets + minimal data
1188        let min_size = 4 + (animation_count * 4) + (animation_count * 16);
1189        if file_size < min_size as u64 {
1190            return Ok(false);
1191        }
1192
1193        // Skip animation count
1194        reader.seek(SeekFrom::Start(4))?;
1195
1196        // Check that offsets are reasonable
1197        for _ in 0..animation_count {
1198            let offset = reader.read_u32_le()?;
1199            if offset > 0 && (offset as u64) >= file_size {
1200                return Ok(false);
1201            }
1202        }
1203
1204        Ok(true)
1205    }
1206
1207    /// Estimate animation count from file structure heuristics
1208    #[allow(dead_code)]
1209    pub fn estimate_animation_count<R: Read + Seek>(reader: &mut R) -> Result<u32> {
1210        let file_size = reader.seek(SeekFrom::End(0))? as u32;
1211        reader.seek(SeekFrom::Start(0))?;
1212
1213        let potential_count = reader.read_u32_le()?;
1214
1215        // Validate using file size heuristics
1216        if validate_legacy_structure(reader, potential_count)? {
1217            Ok(potential_count)
1218        } else {
1219            // Fallback: try to estimate based on file size patterns
1220            // This is a simplified heuristic - real implementation may need
1221            // more sophisticated analysis
1222            let estimated = file_size / 1000; // Rough estimate
1223            Ok(estimated.clamp(1, 100))
1224        }
1225    }
1226}
1227
1228#[cfg(test)]
1229mod tests {
1230    use super::*;
1231    use std::io::Cursor;
1232
1233    #[test]
1234    fn test_anim_header_parse_write() {
1235        let header = AnimHeader {
1236            magic: ANIM_MAGIC,
1237            version: 1,
1238            id_count: 2,
1239            unknown: 0,
1240            anim_entry_offset: 20,
1241        };
1242
1243        let mut data = Vec::new();
1244        header.write(&mut data).unwrap();
1245
1246        let mut cursor = Cursor::new(data);
1247        let parsed_header = AnimHeader::parse(&mut cursor).unwrap();
1248
1249        assert_eq!(parsed_header.magic, ANIM_MAGIC);
1250        assert_eq!(parsed_header.version, 1);
1251        assert_eq!(parsed_header.id_count, 2);
1252        assert_eq!(parsed_header.unknown, 0);
1253        assert_eq!(parsed_header.anim_entry_offset, 20);
1254    }
1255
1256    #[test]
1257    fn test_format_detection_modern() {
1258        // Test modern format detection with MAOF magic
1259        let mut data = Vec::new();
1260        data.extend_from_slice(&ANIM_MAGIC);
1261        data.extend_from_slice(&[1, 0, 0, 0]); // version
1262
1263        let mut cursor = Cursor::new(data);
1264        let format = AnimFormatDetector::detect_format(&mut cursor).unwrap();
1265
1266        assert_eq!(format, AnimFormat::Modern);
1267        assert_eq!(cursor.position(), 0); // Position should be reset
1268    }
1269
1270    #[test]
1271    fn test_format_detection_legacy() {
1272        // Test legacy format detection (no MAOF magic)
1273        let mut data = Vec::new();
1274        data.extend_from_slice(&[2, 0, 0, 0]); // animation count
1275        data.extend_from_slice(&[100, 0, 0, 0]); // first offset
1276
1277        let mut cursor = Cursor::new(data);
1278        let format = AnimFormatDetector::detect_format(&mut cursor).unwrap();
1279
1280        assert_eq!(format, AnimFormat::Legacy);
1281        assert_eq!(cursor.position(), 0); // Position should be reset
1282    }
1283
1284    #[test]
1285    fn test_format_detection_by_version() {
1286        // Test version-based format detection
1287        assert_eq!(
1288            AnimFormatDetector::detect_format_by_version(M2Version::Vanilla),
1289            AnimFormat::Legacy
1290        );
1291        assert_eq!(
1292            AnimFormatDetector::detect_format_by_version(M2Version::Cataclysm),
1293            AnimFormat::Legacy
1294        );
1295        assert_eq!(
1296            AnimFormatDetector::detect_format_by_version(M2Version::Legion),
1297            AnimFormat::Modern
1298        );
1299    }
1300
1301    #[test]
1302    fn test_anim_file_format_properties() {
1303        // Test format property methods
1304        let legacy_file = AnimFile {
1305            format: AnimFormat::Legacy,
1306            sections: Vec::new(),
1307            metadata: AnimMetadata::Legacy {
1308                file_size: 1000,
1309                animation_count: 5,
1310                structure_hints: LegacyStructureHints {
1311                    appears_valid: true,
1312                    estimated_blocks: 5,
1313                    has_timestamps: false,
1314                },
1315            },
1316        };
1317
1318        assert!(legacy_file.is_legacy_format());
1319        assert!(!legacy_file.is_modern_format());
1320        assert_eq!(legacy_file.animation_count(), 0); // Based on sections count
1321
1322        let modern_file = AnimFile {
1323            format: AnimFormat::Modern,
1324            sections: Vec::new(),
1325            metadata: AnimMetadata::Modern {
1326                header: AnimHeader {
1327                    magic: ANIM_MAGIC,
1328                    version: 1,
1329                    id_count: 3,
1330                    unknown: 0,
1331                    anim_entry_offset: 20,
1332                },
1333                entries: Vec::new(),
1334            },
1335        };
1336
1337        assert!(!modern_file.is_legacy_format());
1338        assert!(modern_file.is_modern_format());
1339    }
1340
1341    #[test]
1342    fn test_anim_entry_parse_write() {
1343        let entry = AnimEntry {
1344            id: 1,
1345            offset: 100,
1346            size: 200,
1347        };
1348
1349        let mut data = Vec::new();
1350        entry.write(&mut data).unwrap();
1351
1352        let mut cursor = Cursor::new(data);
1353        let parsed_entry = AnimEntry::parse(&mut cursor).unwrap();
1354
1355        assert_eq!(parsed_entry.id, 1);
1356        assert_eq!(parsed_entry.offset, 100);
1357        assert_eq!(parsed_entry.size, 200);
1358    }
1359
1360    #[test]
1361    fn test_format_conversion() {
1362        // Test conversion between formats
1363        let legacy_file = AnimFile {
1364            format: AnimFormat::Legacy,
1365            sections: vec![AnimSection {
1366                header: AnimSectionHeader {
1367                    magic: *b"AFID",
1368                    id: 1,
1369                    start: 0,
1370                    end: 100,
1371                },
1372                bone_animations: Vec::new(),
1373            }],
1374            metadata: AnimMetadata::Legacy {
1375                file_size: 1000,
1376                animation_count: 1,
1377                structure_hints: LegacyStructureHints {
1378                    appears_valid: true,
1379                    estimated_blocks: 1,
1380                    has_timestamps: false,
1381                },
1382            },
1383        };
1384
1385        // Convert to modern format
1386        let modern_file = legacy_file.convert(M2Version::Legion);
1387        assert_eq!(modern_file.format, AnimFormat::Modern);
1388        assert_eq!(modern_file.sections.len(), 1);
1389        assert_eq!(modern_file.sections[0].header.id, 1);
1390
1391        // Convert back to legacy format
1392        let legacy_again = modern_file.convert(M2Version::Cataclysm);
1393        assert_eq!(legacy_again.format, AnimFormat::Legacy);
1394        assert_eq!(legacy_again.sections.len(), 1);
1395        assert_eq!(legacy_again.sections[0].header.id, 1);
1396    }
1397}
1398
1399#[cfg(test)]
1400mod integration_tests {
1401    use super::*;
1402    use std::path::Path;
1403
1404    #[test]
1405    fn test_real_cataclysm_anim_file() {
1406        let anim_path =
1407            "/home/danielsreichenbach/analysis/anim_samples/cataclysm/OrcFemale0064-00.anim";
1408
1409        if Path::new(anim_path).exists() {
1410            let result = AnimFile::load(anim_path);
1411            match result {
1412                Ok(anim_file) => {
1413                    println!(
1414                        "Successfully parsed ANIM file: {} sections, format: {:?}",
1415                        anim_file.sections.len(),
1416                        anim_file.format
1417                    );
1418                    assert!(
1419                        !anim_file.sections.is_empty(),
1420                        "ANIM file should have at least one section"
1421                    );
1422                }
1423                Err(e) => {
1424                    println!("Failed to parse ANIM file: {:?}", e);
1425                    // For now, allow failures during development
1426                    // assert!(false, "Should be able to parse real ANIM file: {:?}", e);
1427                }
1428            }
1429        } else {
1430            println!("Test ANIM file not found at: {}", anim_path);
1431        }
1432    }
1433
1434    #[test]
1435    fn test_all_cataclysm_anim_samples() {
1436        let samples_dir = "/home/danielsreichenbach/analysis/anim_samples/cataclysm/";
1437
1438        if Path::new(samples_dir).exists()
1439            && let Ok(entries) = std::fs::read_dir(samples_dir)
1440        {
1441            let mut success_count = 0;
1442            let mut total_count = 0;
1443            let mut memory_stats = Vec::new();
1444
1445            for entry in entries.flatten() {
1446                let path = entry.path();
1447                if path.extension().map(|s| s == "anim").unwrap_or(false) {
1448                    total_count += 1;
1449                    println!("Testing ANIM file: {:?}", path.file_name());
1450
1451                    match AnimFile::load(&path) {
1452                        Ok(mut anim_file) => {
1453                            success_count += 1;
1454
1455                            // Test validation
1456                            match anim_file.validate() {
1457                                Ok(()) => println!("  ✓ Validation: Pass"),
1458                                Err(e) => println!("  ⚠ Validation: {:?}", e),
1459                            }
1460
1461                            // Test memory usage analysis
1462                            let usage = anim_file.memory_usage();
1463                            memory_stats.push(usage.clone());
1464                            println!(
1465                                "  ✓ Success: {} sections, format: {:?}, memory: ~{} bytes",
1466                                anim_file.sections.len(),
1467                                anim_file.format,
1468                                usage.approximate_bytes
1469                            );
1470
1471                            // Test memory optimization
1472                            let before_opt = anim_file.memory_usage();
1473                            anim_file.optimize_memory();
1474                            let after_opt = anim_file.memory_usage();
1475                            if after_opt.approximate_bytes < before_opt.approximate_bytes {
1476                                println!(
1477                                    "  ✓ Optimization: {} -> {} bytes",
1478                                    before_opt.approximate_bytes, after_opt.approximate_bytes
1479                                );
1480                            }
1481
1482                            // Test format conversion (if applicable)
1483                            if anim_file.is_legacy_format() {
1484                                let converted =
1485                                    anim_file.convert(crate::version::M2Version::Legion);
1486                                assert!(
1487                                    converted.is_modern_format(),
1488                                    "Conversion to modern format failed"
1489                                );
1490                            }
1491                        }
1492                        Err(e) => {
1493                            println!("  ✗ Failed: {:?}", e);
1494                        }
1495                    }
1496                }
1497            }
1498
1499            println!(
1500                "Summary: {}/{} ANIM files parsed successfully",
1501                success_count, total_count
1502            );
1503
1504            if !memory_stats.is_empty() {
1505                let total_memory: usize = memory_stats.iter().map(|s| s.approximate_bytes).sum();
1506                let avg_memory = total_memory / memory_stats.len();
1507                let total_keyframes: usize = memory_stats.iter().map(|s| s.total_keyframes()).sum();
1508                println!(
1509                    "Memory stats: total ~{} bytes, avg ~{} bytes per file, {} total keyframes",
1510                    total_memory, avg_memory, total_keyframes
1511                );
1512            }
1513        }
1514    }
1515
1516    #[test]
1517    fn test_anim_format_detection_edge_cases() {
1518        use std::io::Cursor;
1519
1520        // Test empty file
1521        let empty_data = vec![];
1522        let mut cursor = Cursor::new(empty_data);
1523        let result = AnimFormatDetector::detect_format(&mut cursor);
1524        assert!(result.is_err(), "Empty file should return error");
1525
1526        // Test file with only 3 bytes (insufficient)
1527        let small_data = vec![0x4D, 0x41, 0x4F]; // "MAO" (incomplete)
1528        let mut cursor = Cursor::new(small_data);
1529        let result = AnimFormatDetector::detect_format(&mut cursor);
1530        assert!(result.is_err(), "File with < 4 bytes should return error");
1531
1532        // Test file with exactly 4 bytes (minimal valid)
1533        let min_data = vec![0x4D, 0x41, 0x4F, 0x46]; // "MAOF"
1534        let mut cursor = Cursor::new(min_data);
1535        let result = AnimFormatDetector::detect_format(&mut cursor);
1536        assert!(result.is_ok(), "File with exactly 4 bytes should work");
1537        assert_eq!(result.unwrap(), AnimFormat::Modern);
1538
1539        // Test position restoration
1540        let data = vec![0x00, 0x01, 0x02, 0x03, 0x04, 0x05];
1541        let mut cursor = Cursor::new(data);
1542        cursor.set_position(2);
1543        let initial_pos = cursor.position();
1544        let _result = AnimFormatDetector::detect_format(&mut cursor);
1545        assert_eq!(
1546            cursor.position(),
1547            initial_pos,
1548            "Position should be restored after detection"
1549        );
1550    }
1551
1552    #[test]
1553    fn test_anim_validation_edge_cases() {
1554        // Test empty sections validation
1555        let empty_anim = AnimFile {
1556            format: AnimFormat::Legacy,
1557            sections: Vec::new(),
1558            metadata: AnimMetadata::Legacy {
1559                file_size: 100,
1560                animation_count: 0,
1561                structure_hints: LegacyStructureHints {
1562                    appears_valid: true,
1563                    estimated_blocks: 0,
1564                    has_timestamps: false,
1565                },
1566            },
1567        };
1568
1569        let result = empty_anim.validate();
1570        assert!(result.is_err(), "Empty sections should fail validation");
1571
1572        // Test format/metadata mismatch
1573        let mismatched_anim = AnimFile {
1574            format: AnimFormat::Modern,
1575            sections: vec![],
1576            metadata: AnimMetadata::Legacy {
1577                file_size: 100,
1578                animation_count: 1,
1579                structure_hints: LegacyStructureHints {
1580                    appears_valid: true,
1581                    estimated_blocks: 1,
1582                    has_timestamps: false,
1583                },
1584            },
1585        };
1586
1587        let result = mismatched_anim.validate();
1588        assert!(
1589            result.is_err(),
1590            "Format/metadata mismatch should fail validation"
1591        );
1592    }
1593}
1594
1595#[cfg(test)]
1596mod legacy_tests {
1597    use super::*;
1598    use std::io::Cursor;
1599
1600    #[test]
1601    fn test_legacy_animation_count_validation() {
1602        // Test valid animation count
1603        let mut data = vec![2u8, 0, 0, 0]; // count = 2
1604        data.extend_from_slice(&[100u8, 0, 0, 0]); // offset 1
1605        data.extend_from_slice(&[200u8, 0, 0, 0]); // offset 2
1606        // Add some dummy data to reach minimum size
1607        data.resize(300, 0);
1608
1609        let mut cursor = Cursor::new(data);
1610        let result = legacy_utils::validate_legacy_structure(&mut cursor, 2);
1611        assert!(result.is_ok());
1612    }
1613
1614    #[test]
1615    fn test_legacy_invalid_animation_count() {
1616        let data = vec![0u8, 0, 0, 0]; // count = 0 (invalid)
1617        let mut cursor = Cursor::new(data);
1618
1619        let result = legacy_utils::validate_legacy_structure(&mut cursor, 0);
1620        assert!(result.is_ok());
1621        assert!(!result.unwrap()); // Should be false for invalid count
1622    }
1623
1624    #[test]
1625    fn test_legacy_file_too_small() {
1626        let data = vec![10u8, 0, 0, 0]; // count = 10, but file too small
1627        let mut cursor = Cursor::new(data);
1628
1629        let result = legacy_utils::validate_legacy_structure(&mut cursor, 10);
1630        assert!(result.is_ok());
1631        assert!(!result.unwrap()); // Should be false for too small file
1632    }
1633}