wow_adt/
api.rs

1//! High-level parser API for ADT terrain files.
2//!
3//! This module provides type-safe, version-aware parsing of World of Warcraft ADT
4//! (A Dungeon Terrain) files using the two-pass architecture:
5//!
6//! **Pass 1 (Discovery)**: Fast chunk enumeration to detect version and file type
7//! **Pass 2 (Parse)**: Type-safe extraction of chunk data into structured formats
8//!
9//! # Quick Start
10//!
11//! ```no_run
12//! use std::fs::File;
13//! use std::io::BufReader;
14//! use wow_adt::api::parse_adt;
15//!
16//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
17//! let file = File::open("world/maps/azeroth/azeroth_32_32.adt")?;
18//! let mut reader = BufReader::new(file);
19//! let adt = parse_adt(&mut reader)?;
20//!
21//! match adt {
22//!     wow_adt::api::ParsedAdt::Root(root) => {
23//!         println!("Version: {:?}", root.version);
24//!         println!("Terrain chunks: {}", root.mcnk_chunks.len());
25//!         println!("Textures: {}", root.textures.len());
26//!
27//!         if let Some(water) = &root.water_data {
28//!             println!("Has WotLK+ water: {} chunks with water",
29//!                 water.liquid_chunk_count());
30//!         }
31//!     }
32//!     _ => {}
33//! }
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! # Performance
39//!
40//! - Discovery phase: <10ms for typical ADT files (5-15 MB)
41//! - Full parse: 100 ADT files in <5 seconds (50ms per file average)
42//! - Memory: ≤2× raw file size peak
43
44use std::io::{Read, Seek};
45use std::time::{Duration, Instant};
46
47use crate::chunk_discovery::{ChunkDiscovery, discover_chunks};
48use crate::chunks::mh2o::Mh2oChunk;
49use crate::chunks::{
50    DoodadPlacement, MampChunk, MbbbChunk, MbmhChunk, MbmiChunk, MbnvChunk, McalChunk, McinChunk,
51    MclyChunk, McnkChunk, MfboChunk, MhdrChunk, MtxfChunk, MtxpChunk, WmoPlacement,
52};
53use crate::error::Result;
54use crate::file_type::AdtFileType;
55use crate::version::AdtVersion;
56
57/// Parsed root ADT file (main terrain file for all versions).
58///
59/// Contains all terrain data for a single 16×16 yard area:
60/// - Heightmaps and vertex normals
61/// - Texture layers and alpha blending
62/// - Object placements (M2 models and WMOs)
63/// - Version-specific features (water, flight bounds, etc.)
64///
65/// # Guarantees
66///
67/// - `version` correctly identifies format version
68/// - All required chunks present (MHDR, MCIN, MTEX, MCNK)
69/// - Version-specific chunks only present for compatible versions
70/// - All indices valid within their respective arrays
71/// - MCNK count ∈ [1, 256]
72///
73/// # Example
74///
75/// ```no_run
76/// use wow_adt::api::{parse_adt, ParsedAdt};
77/// use std::fs::File;
78///
79/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
80/// let mut file = File::open("terrain.adt")?;
81/// let adt = parse_adt(&mut file)?;
82///
83/// if let ParsedAdt::Root(root) = adt {
84///     println!("Version: {:?}", root.version);
85///     println!("Terrain chunks: {}", root.mcnk_chunks.len());
86///
87///     if let Some(water) = &root.water_data {
88///         // Access water data by chunk index
89///         for (idx, entry) in water.entries.iter().enumerate() {
90///             if entry.header.has_liquid() {
91///                 let row = idx / 16;
92///                 let col = idx % 16;
93///                 println!("Chunk ({}, {}) has {} water layer(s)",
94///                     row, col, entry.instances.len());
95///             }
96///         }
97///     }
98/// }
99/// # Ok(())
100/// # }
101/// ```
102#[derive(Debug, Clone)]
103pub struct RootAdt {
104    /// Detected ADT version
105    pub version: AdtVersion,
106
107    /// MHDR - Header with chunk offsets
108    pub mhdr: MhdrChunk,
109
110    /// MCIN - MCNK chunk index (256 entries)
111    pub mcin: McinChunk,
112
113    /// Texture filenames from MTEX chunk
114    pub textures: Vec<String>,
115
116    /// M2 model filenames from MMDX chunk
117    pub models: Vec<String>,
118
119    /// M2 model filename offsets from MMID chunk
120    pub model_indices: Vec<u32>,
121
122    /// WMO filenames from MWMO chunk
123    pub wmos: Vec<String>,
124
125    /// WMO filename offsets from MWID chunk
126    pub wmo_indices: Vec<u32>,
127
128    /// M2 model placements from MDDF chunk
129    pub doodad_placements: Vec<DoodadPlacement>,
130
131    /// WMO placements from MODF chunk
132    pub wmo_placements: Vec<WmoPlacement>,
133
134    /// MCNK terrain chunks (1-256 chunks)
135    pub mcnk_chunks: Vec<McnkChunk>,
136
137    /// MFBO - Flight boundaries (TBC+)
138    pub flight_bounds: Option<MfboChunk>,
139
140    /// MH2O - Advanced water system (WotLK+)
141    ///
142    /// Contains 256 entries (one per MCNK chunk) with liquid layer data.
143    /// Each entry has header, instances, and optional attributes.
144    pub water_data: Option<Mh2oChunk>,
145
146    /// MTXF - Texture flags (WotLK 3.x+)
147    ///
148    /// Rendering flags for each texture controlling specularity,
149    /// environment mapping, and animation.
150    pub texture_flags: Option<MtxfChunk>,
151
152    /// MAMP - Texture amplifier (Cataclysm+)
153    pub texture_amplifier: Option<MampChunk>,
154
155    /// MTXP - Texture parameters (MoP+)
156    pub texture_params: Option<MtxpChunk>,
157
158    /// MBMH - Blend mesh headers (MoP 5.x+)
159    ///
160    /// Headers describing blend mesh batches for smooth texture transitions.
161    /// Each entry contains map_object_id, texture_id, and index/vertex ranges.
162    pub blend_mesh_headers: Option<MbmhChunk>,
163
164    /// MBBB - Blend mesh bounding boxes (MoP 5.x+)
165    ///
166    /// Bounding boxes for visibility culling of blend meshes.
167    /// Each entry has map_object_id and min/max coordinates.
168    pub blend_mesh_bounds: Option<MbbbChunk>,
169
170    /// MBNV - Blend mesh vertices (MoP 5.x+)
171    ///
172    /// Vertex data for blend mesh system with position, normal, UV coordinates,
173    /// and 3 RGBA color channels for texture blending.
174    pub blend_mesh_vertices: Option<MbnvChunk>,
175
176    /// MBMI - Blend mesh indices (MoP 5.x+)
177    ///
178    /// Triangle indices (u16) referencing MBNV vertex array.
179    /// MCBB chunks in MCNK reference ranges within this array.
180    pub blend_mesh_indices: Option<MbmiChunk>,
181}
182
183impl RootAdt {
184    /// Check if this ADT has water data.
185    #[must_use]
186    pub fn has_water(&self) -> bool {
187        self.water_data.is_some()
188    }
189
190    /// Check if this ADT has flight boundaries.
191    #[must_use]
192    pub fn has_flight_bounds(&self) -> bool {
193        self.flight_bounds.is_some()
194    }
195
196    /// Get number of terrain chunks.
197    #[must_use]
198    pub fn terrain_chunk_count(&self) -> usize {
199        self.mcnk_chunks.len()
200    }
201
202    /// Get number of textures.
203    #[must_use]
204    pub fn texture_count(&self) -> usize {
205        self.textures.len()
206    }
207
208    /// Get number of M2 models.
209    #[must_use]
210    pub fn model_count(&self) -> usize {
211        self.models.len()
212    }
213
214    /// Get number of WMO objects.
215    #[must_use]
216    pub fn wmo_count(&self) -> usize {
217        self.wmos.len()
218    }
219
220    // ========================================================================
221    // Mutable Access Methods (for modify workflows)
222    // ========================================================================
223
224    /// Get mutable access to textures for replacement workflows.
225    ///
226    /// Use this to modify texture filenames in place. After modification,
227    /// serialize using [`AdtBuilder::from_parsed()`](crate::builder::AdtBuilder::from_parsed).
228    ///
229    /// # Example
230    ///
231    /// ```rust,no_run
232    /// use wow_adt::api::parse_adt;
233    /// # use std::fs::File;
234    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
235    /// # let mut file = File::open("terrain.adt")?;
236    /// # let adt = parse_adt(&mut file)?;
237    /// # if let wow_adt::api::ParsedAdt::Root(mut root) = adt {
238    /// root.textures_mut()[0] = "terrain/grass_new.blp".to_string();
239    /// # }
240    /// # Ok(())
241    /// # }
242    /// ```
243    pub fn textures_mut(&mut self) -> &mut Vec<String> {
244        &mut self.textures
245    }
246
247    /// Get mutable access to M2 model filenames.
248    pub fn models_mut(&mut self) -> &mut Vec<String> {
249        &mut self.models
250    }
251
252    /// Get mutable access to WMO filenames.
253    pub fn wmos_mut(&mut self) -> &mut Vec<String> {
254        &mut self.wmos
255    }
256
257    /// Get mutable access to M2 model placements.
258    ///
259    /// Use this to add, remove, or modify doodad placements.
260    ///
261    /// # Example
262    ///
263    /// ```rust,no_run
264    /// use wow_adt::api::parse_adt;
265    /// # use std::fs::File;
266    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
267    /// # let mut file = File::open("terrain.adt")?;
268    /// # let adt = parse_adt(&mut file)?;
269    /// # if let wow_adt::api::ParsedAdt::Root(mut root) = adt {
270    /// root.doodad_placements_mut()[0].position[2] += 10.0; // Z coordinate
271    /// # }
272    /// # Ok(())
273    /// # }
274    /// ```
275    pub fn doodad_placements_mut(&mut self) -> &mut Vec<DoodadPlacement> {
276        &mut self.doodad_placements
277    }
278
279    /// Get mutable access to WMO placements.
280    pub fn wmo_placements_mut(&mut self) -> &mut Vec<WmoPlacement> {
281        &mut self.wmo_placements
282    }
283
284    /// Get mutable access to MCNK terrain chunks.
285    ///
286    /// Use this to modify terrain geometry, heights, textures, etc.
287    ///
288    /// # Example
289    ///
290    /// ```rust,no_run
291    /// use wow_adt::api::parse_adt;
292    /// # use std::fs::File;
293    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
294    /// # let mut file = File::open("terrain.adt")?;
295    /// # let adt = parse_adt(&mut file)?;
296    /// # if let wow_adt::api::ParsedAdt::Root(mut root) = adt {
297    /// if let Some(heights) = &mut root.mcnk_chunks_mut()[0].heights {
298    ///     for height in &mut heights.heights {
299    ///         *height += 5.0;
300    ///     }
301    /// }
302    /// # }
303    /// # Ok(())
304    /// # }
305    /// ```
306    pub fn mcnk_chunks_mut(&mut self) -> &mut Vec<McnkChunk> {
307        &mut self.mcnk_chunks
308    }
309
310    /// Get mutable access to water data (WotLK+).
311    ///
312    /// Returns `None` if this ADT has no water data.
313    pub fn water_data_mut(&mut self) -> Option<&mut Mh2oChunk> {
314        self.water_data.as_mut()
315    }
316
317    /// Get mutable access to flight boundaries (TBC+).
318    ///
319    /// Returns `None` if this ADT has no flight bounds.
320    pub fn flight_bounds_mut(&mut self) -> Option<&mut MfboChunk> {
321        self.flight_bounds.as_mut()
322    }
323
324    /// Get mutable access to texture flags (WotLK+).
325    pub fn texture_flags_mut(&mut self) -> Option<&mut MtxfChunk> {
326        self.texture_flags.as_mut()
327    }
328
329    /// Get mutable access to texture amplifier (Cataclysm+).
330    pub fn texture_amplifier_mut(&mut self) -> Option<&mut MampChunk> {
331        self.texture_amplifier.as_mut()
332    }
333
334    /// Get mutable access to texture parameters (MoP+).
335    pub fn texture_params_mut(&mut self) -> Option<&mut MtxpChunk> {
336        self.texture_params.as_mut()
337    }
338}
339
340/// MCNK texture data container for split texture files.
341///
342/// In Cataclysm+ split architecture, texture files contain simplified MCNK chunks
343/// that only hold texture layer and alpha map data. This structure represents the
344/// per-chunk texture information.
345///
346/// # Format
347///
348/// Each texture file has 256 MCNK containers (16x16 grid), one for each terrain chunk.
349/// Unlike root file MCNK chunks, these do not contain full headers or geometry data.
350///
351/// # References
352///
353/// - wowdev.wiki: ADT/v18#Texture Files (_tex0.adt)
354#[derive(Debug, Clone)]
355pub struct McnkChunkTexture {
356    /// Chunk index in 16x16 grid (0-255)
357    pub index: usize,
358
359    /// MCLY - Texture layer definitions (up to 4 layers per chunk)
360    pub layers: Option<MclyChunk>,
361
362    /// MCAL - Alpha maps for texture blending
363    pub alpha_maps: Option<McalChunk>,
364}
365
366/// MCNK object data container for split object files.
367///
368/// In Cataclysm+ split architecture, object files contain simplified MCNK chunks
369/// that only hold references to M2 models and WMO objects placed within that chunk.
370///
371/// # Format
372///
373/// Each object file has 256 MCNK containers (16x16 grid), one for each terrain chunk.
374/// The MCRD/MCRW chunks contain indices into the global MDDF/MODF arrays.
375///
376/// # References
377///
378/// - wowdev.wiki: ADT/v18#Object Files (_obj0.adt)
379#[derive(Debug, Clone)]
380pub struct McnkChunkObject {
381    /// Chunk index in 16x16 grid (0-255)
382    pub index: usize,
383
384    /// MCRD - M2 doodad reference indices (Cataclysm+)
385    ///
386    /// Indices into the global MDDF array for M2 models placed in this chunk.
387    pub doodad_refs: Vec<u32>,
388
389    /// MCRW - WMO reference indices (Cataclysm+)
390    ///
391    /// Indices into the global MODF array for WMO objects placed in this chunk.
392    pub wmo_refs: Vec<u32>,
393}
394
395/// Parsed texture file (Cataclysm+ `_tex0.adt`).
396///
397/// Contains texture-related data split from root ADT in Cataclysm+:
398/// - Texture filenames
399/// - Texture layer definitions (per-chunk)
400/// - Alpha maps for blending (per-chunk)
401/// - Texture parameters (MoP+)
402///
403/// # Format
404///
405/// Texture files contain MTEX (global texture list) and 256 MCNK containers
406/// with texture layer and alpha map data for each terrain chunk.
407///
408/// # Example
409///
410/// ```no_run
411/// use wow_adt::api::{parse_adt, ParsedAdt};
412/// use std::fs::File;
413///
414/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
415/// let mut file = File::open("terrain_tex0.adt")?;
416/// let adt = parse_adt(&mut file)?;
417///
418/// if let ParsedAdt::Tex0(tex) = adt {
419///     println!("Textures: {}", tex.textures.len());
420///     println!("Chunks with texture data: {}", tex.mcnk_textures.len());
421/// }
422/// # Ok(())
423/// # }
424/// ```
425#[derive(Debug, Clone)]
426pub struct Tex0Adt {
427    /// Detected ADT version
428    pub version: AdtVersion,
429
430    /// Texture filenames from MTEX chunk
431    pub textures: Vec<String>,
432
433    /// MTXP - Texture parameters (MoP+)
434    pub texture_params: Option<MtxpChunk>,
435
436    /// Per-chunk texture data (256 chunks in 16x16 grid)
437    pub mcnk_textures: Vec<McnkChunkTexture>,
438}
439
440/// Parsed object file (Cataclysm+ `_obj0.adt`).
441///
442/// Contains object placement data split from root ADT in Cataclysm+:
443/// - M2 model filenames and placements
444/// - WMO filenames and placements
445/// - Per-chunk object references (MCRD/MCRW)
446///
447/// # Format
448///
449/// Object files contain global object lists (MMDX/MWMO) and placement arrays
450/// (MDDF/MODF), plus 256 MCNK containers with per-chunk object references.
451///
452/// # Example
453///
454/// ```no_run
455/// use wow_adt::api::{parse_adt, ParsedAdt};
456/// use std::fs::File;
457///
458/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
459/// let mut file = File::open("terrain_obj0.adt")?;
460/// let adt = parse_adt(&mut file)?;
461///
462/// if let ParsedAdt::Obj0(obj) = adt {
463///     println!("M2 models: {}", obj.models.len());
464///     println!("WMO objects: {}", obj.wmos.len());
465///     println!("Chunks with objects: {}", obj.mcnk_objects.len());
466/// }
467/// # Ok(())
468/// # }
469/// ```
470#[derive(Debug, Clone)]
471pub struct Obj0Adt {
472    /// Detected ADT version
473    pub version: AdtVersion,
474
475    /// M2 model filenames from MMDX chunk
476    pub models: Vec<String>,
477
478    /// M2 model filename offsets from MMID chunk
479    pub model_indices: Vec<u32>,
480
481    /// WMO filenames from MWMO chunk
482    pub wmos: Vec<String>,
483
484    /// WMO filename offsets from MWID chunk
485    pub wmo_indices: Vec<u32>,
486
487    /// M2 model placements from MDDF chunk
488    pub doodad_placements: Vec<DoodadPlacement>,
489
490    /// WMO placements from MODF chunk
491    pub wmo_placements: Vec<WmoPlacement>,
492
493    /// Per-chunk object references (256 chunks in 16x16 grid)
494    pub mcnk_objects: Vec<McnkChunkObject>,
495}
496
497/// Parsed LOD file (Cataclysm+ `_lod.adt`).
498///
499/// Contains level-of-detail data for distant terrain rendering.
500/// This format is minimally documented and currently a stub implementation.
501#[derive(Debug, Clone)]
502pub struct LodAdt {
503    /// Detected ADT version
504    pub version: AdtVersion,
505}
506
507// ============================================================================
508// Type Aliases for Spec Compliance
509// ============================================================================
510//
511// The split file specification uses names like `TextureAdt` and `ObjectAdt`,
512// while our implementation uses `Tex0Adt` and `Obj0Adt` to reflect the actual
513// file naming convention (`_tex0.adt`, `_obj0.adt`).
514//
515// These type aliases provide compatibility with the specification naming
516// while preserving the more accurate implementation names.
517
518/// Type alias for texture file structure (spec-compliant naming).
519///
520/// This is an alias for [`Tex0Adt`], providing compatibility with the
521/// split file specification which uses `TextureAdt` as the canonical name.
522///
523/// # Example
524///
525/// ```rust
526/// use wow_adt::api::TextureAdt;
527/// // Equivalent to using Tex0Adt
528/// ```
529pub type TextureAdt = Tex0Adt;
530
531/// Type alias for object file structure (spec-compliant naming).
532///
533/// This is an alias for [`Obj0Adt`], providing compatibility with the
534/// split file specification which uses `ObjectAdt` as the canonical name.
535///
536/// # Example
537///
538/// ```rust
539/// use wow_adt::api::ObjectAdt;
540/// // Equivalent to using Obj0Adt
541/// ```
542pub type ObjectAdt = Obj0Adt;
543
544/// Parsed ADT file with type-safe variant access.
545///
546/// Different ADT file types are represented as enum variants, allowing
547/// type-safe access to file-specific data.
548///
549/// # Variants
550///
551/// - [`Root`](ParsedAdt::Root) - Main terrain file (all versions)
552/// - [`Tex0`](ParsedAdt::Tex0) - Texture data (Cataclysm+)
553/// - [`Tex1`](ParsedAdt::Tex1) - Additional textures (Cataclysm+)
554/// - [`Obj0`](ParsedAdt::Obj0) - Object placements (Cataclysm+)
555/// - [`Obj1`](ParsedAdt::Obj1) - Additional objects (Cataclysm+)
556/// - [`Lod`](ParsedAdt::Lod) - Level-of-detail (Cataclysm+)
557///
558/// # Example
559///
560/// ```no_run
561/// use wow_adt::api::{parse_adt, ParsedAdt};
562/// use std::fs::File;
563///
564/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
565/// let mut file = File::open("terrain.adt")?;
566/// let adt = parse_adt(&mut file)?;
567///
568/// match adt {
569///     ParsedAdt::Root(root) => {
570///         println!("Root ADT with {} chunks", root.mcnk_chunks.len());
571///     }
572///     ParsedAdt::Tex0(tex) => {
573///         println!("Texture file with {} textures", tex.textures.len());
574///     }
575///     ParsedAdt::Obj0(obj) => {
576///         println!("Object file with {} models", obj.models.len());
577///     }
578///     _ => {}
579/// }
580/// # Ok(())
581/// # }
582/// ```
583#[derive(Debug, Clone)]
584pub enum ParsedAdt {
585    /// Root ADT file (main terrain)
586    Root(Box<RootAdt>),
587
588    /// Texture file 0 (Cataclysm+)
589    Tex0(Tex0Adt),
590
591    /// Texture file 1 (Cataclysm+)
592    Tex1(Tex0Adt),
593
594    /// Object file 0 (Cataclysm+)
595    Obj0(Obj0Adt),
596
597    /// Object file 1 (Cataclysm+)
598    Obj1(Obj0Adt),
599
600    /// Level-of-detail file (Cataclysm+)
601    Lod(LodAdt),
602}
603
604impl ParsedAdt {
605    /// Check if this is a root ADT file.
606    #[must_use]
607    pub const fn is_root(&self) -> bool {
608        matches!(self, Self::Root(_))
609    }
610
611    /// Check if this is a split file (Cataclysm+).
612    #[must_use]
613    pub const fn is_split(&self) -> bool {
614        !self.is_root()
615    }
616
617    /// Get the file type.
618    #[must_use]
619    pub const fn file_type(&self) -> AdtFileType {
620        match self {
621            Self::Root(_) => AdtFileType::Root,
622            Self::Tex0(_) => AdtFileType::Tex0,
623            Self::Tex1(_) => AdtFileType::Tex1,
624            Self::Obj0(_) => AdtFileType::Obj0,
625            Self::Obj1(_) => AdtFileType::Obj1,
626            Self::Lod(_) => AdtFileType::Lod,
627        }
628    }
629
630    /// Get the ADT version.
631    #[must_use]
632    pub fn version(&self) -> AdtVersion {
633        match self {
634            Self::Root(r) => r.version,
635            Self::Tex0(t) | Self::Tex1(t) => t.version,
636            Self::Obj0(o) | Self::Obj1(o) => o.version,
637            Self::Lod(l) => l.version,
638        }
639    }
640}
641
642/// Diagnostic metadata for format research and debugging.
643///
644/// Contains performance metrics and warnings collected during parsing.
645/// Useful for format analysis, debugging, and performance profiling.
646///
647/// # Example
648///
649/// ```no_run
650/// use wow_adt::api::parse_adt_with_metadata;
651/// use std::fs::File;
652///
653/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
654/// let mut file = File::open("terrain.adt")?;
655/// let (adt, metadata) = parse_adt_with_metadata(&mut file)?;
656///
657/// println!("Version: {:?}", metadata.version);
658/// println!("Discovery: {:?}", metadata.discovery_duration);
659/// println!("Parse: {:?}", metadata.parse_duration);
660/// println!("Chunks: {}", metadata.chunk_count);
661///
662/// for warning in &metadata.warnings {
663///     eprintln!("Warning: {}", warning);
664/// }
665/// # Ok(())
666/// # }
667/// ```
668#[derive(Debug, Clone)]
669pub struct AdtMetadata {
670    /// Detected ADT version
671    pub version: AdtVersion,
672
673    /// Detected file type
674    pub file_type: AdtFileType,
675
676    /// Total number of chunks discovered
677    pub chunk_count: usize,
678
679    /// Time spent in discovery phase
680    pub discovery_duration: Duration,
681
682    /// Time spent in parse phase
683    pub parse_duration: Duration,
684
685    /// Warning messages collected during parsing
686    pub warnings: Vec<String>,
687
688    /// Full chunk discovery results
689    pub discovery: ChunkDiscovery,
690}
691
692/// Parse ADT file with automatic version detection and type-safe chunk extraction.
693///
694/// This is the primary entry point for parsing ADT files. It automatically detects
695/// the file type and version, then parses all chunks into type-safe structures.
696///
697/// # Arguments
698///
699/// * `reader` - Seekable input stream (File, Cursor, etc.)
700///
701/// # Returns
702///
703/// - `Ok(ParsedAdt)` - Successfully parsed ADT with appropriate variant
704/// - `Err(AdtError)` - Parsing failed with detailed error context
705///
706/// # Behavior
707///
708/// 1. **Discovery Phase**: Enumerate chunks, detect version and file type
709/// 2. **Parse Phase**: Extract chunk data into type-safe structures
710/// 3. **Validation**: Verify required chunks and structural integrity
711/// 4. **Error Handling**: Fail-fast on critical errors
712///
713/// # Example
714///
715/// ```no_run
716/// use wow_adt::api::{parse_adt, ParsedAdt};
717/// use std::fs::File;
718/// use std::io::BufReader;
719///
720/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
721/// let file = File::open("world/maps/azeroth/azeroth_32_32.adt")?;
722/// let mut reader = BufReader::new(file);
723/// let adt = parse_adt(&mut reader)?;
724///
725/// match adt {
726///     ParsedAdt::Root(root) => {
727///         println!("Loaded {} terrain chunks", root.mcnk_chunks.len());
728///         println!("Textures: {:?}", root.textures);
729///     }
730///     ParsedAdt::Tex0(tex) => {
731///         println!("Texture file with {} entries", tex.textures.len());
732///     }
733///     _ => {}
734/// }
735/// # Ok(())
736/// # }
737/// ```
738///
739/// # Performance
740///
741/// - Discovery: <10ms for typical files
742/// - Full parse: ~50ms average per file
743/// - Memory: ≤2× raw file size peak
744pub fn parse_adt<R: Read + Seek>(reader: &mut R) -> Result<ParsedAdt> {
745    let (adt, _metadata) = parse_adt_with_metadata(reader)?;
746    Ok(adt)
747}
748
749/// Parse ADT file with diagnostic metadata for debugging and format research.
750///
751/// Similar to [`parse_adt`] but also returns metadata containing:
752/// - Chunk discovery results
753/// - Detected version and reasoning
754/// - File type identification
755/// - Parse timing statistics
756/// - Warning messages
757///
758/// # Arguments
759///
760/// * `reader` - Seekable input stream
761///
762/// # Returns
763///
764/// - `Ok((ParsedAdt, AdtMetadata))` - Parsed ADT with metadata
765/// - `Err(AdtError)` - Parsing failed
766///
767/// # Example
768///
769/// ```no_run
770/// use wow_adt::api::parse_adt_with_metadata;
771/// use std::fs::File;
772///
773/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
774/// let mut file = File::open("terrain.adt")?;
775/// let (adt, metadata) = parse_adt_with_metadata(&mut file)?;
776///
777/// println!("Version: {:?}", metadata.version);
778/// println!("File type: {:?}", metadata.file_type);
779/// println!("Chunks discovered: {}", metadata.chunk_count);
780/// println!("Discovery time: {:?}", metadata.discovery_duration);
781/// println!("Parse time: {:?}", metadata.parse_duration);
782///
783/// for warning in metadata.warnings {
784///     eprintln!("Warning: {}", warning);
785/// }
786/// # Ok(())
787/// # }
788/// ```
789pub fn parse_adt_with_metadata<R: Read + Seek>(reader: &mut R) -> Result<(ParsedAdt, AdtMetadata)> {
790    // Discovery phase
791    let discovery_start = Instant::now();
792    let discovery = discover_chunks(reader)?;
793    let discovery_duration = discovery_start.elapsed();
794
795    // Detect version and file type
796    let version = AdtVersion::from_discovery(&discovery);
797    let file_type = AdtFileType::from_discovery(&discovery);
798
799    log::debug!(
800        "Discovered {} chunks, version: {:?}, file type: {:?}",
801        discovery.total_chunks,
802        version,
803        file_type
804    );
805
806    // Parse phase - route to appropriate parser
807    let parse_start = Instant::now();
808    let (adt, warnings) = match file_type {
809        AdtFileType::Root => {
810            let (root, warnings) = crate::root_parser::parse_root_adt(reader, &discovery, version)?;
811            (ParsedAdt::Root(Box::new(root)), warnings)
812        }
813        AdtFileType::Tex0 | AdtFileType::Tex1 => {
814            let (tex, warnings) = crate::split_parser::parse_tex_adt(reader, &discovery, version)?;
815            let adt = if file_type == AdtFileType::Tex0 {
816                ParsedAdt::Tex0(tex)
817            } else {
818                ParsedAdt::Tex1(tex)
819            };
820            (adt, warnings)
821        }
822        AdtFileType::Obj0 | AdtFileType::Obj1 => {
823            let (obj, warnings) = crate::split_parser::parse_obj_adt(reader, &discovery, version)?;
824            let adt = if file_type == AdtFileType::Obj0 {
825                ParsedAdt::Obj0(obj)
826            } else {
827                ParsedAdt::Obj1(obj)
828            };
829            (adt, warnings)
830        }
831        AdtFileType::Lod => {
832            let (lod, warnings) = crate::split_parser::parse_lod_adt(reader, &discovery, version)?;
833            (ParsedAdt::Lod(lod), warnings)
834        }
835    };
836    let parse_duration = parse_start.elapsed();
837
838    let metadata = AdtMetadata {
839        version,
840        file_type,
841        chunk_count: discovery.total_chunks,
842        discovery_duration,
843        parse_duration,
844        warnings,
845        discovery,
846    };
847
848    Ok((adt, metadata))
849}
850
851#[cfg(test)]
852mod tests {
853    use super::*;
854
855    #[test]
856    fn test_parsed_adt_is_root() {
857        let root = RootAdt {
858            version: AdtVersion::WotLK,
859            mhdr: MhdrChunk::default(),
860            mcin: McinChunk {
861                entries: McinChunk::default().entries,
862            },
863            textures: vec![],
864            models: vec![],
865            model_indices: vec![],
866            wmos: vec![],
867            wmo_indices: vec![],
868            doodad_placements: vec![],
869            wmo_placements: vec![],
870            mcnk_chunks: vec![],
871            flight_bounds: None,
872            water_data: None,
873            texture_flags: None,
874            texture_amplifier: None,
875            texture_params: None,
876            blend_mesh_headers: None,
877            blend_mesh_bounds: None,
878            blend_mesh_vertices: None,
879            blend_mesh_indices: None,
880        };
881
882        let parsed = ParsedAdt::Root(Box::new(root));
883        assert!(parsed.is_root());
884        assert!(!parsed.is_split());
885        assert_eq!(parsed.file_type(), AdtFileType::Root);
886    }
887
888    #[test]
889    fn test_parsed_adt_is_split() {
890        let tex = Tex0Adt {
891            version: AdtVersion::Cataclysm,
892            textures: vec![],
893            texture_params: None,
894            mcnk_textures: vec![],
895        };
896
897        let parsed = ParsedAdt::Tex0(tex);
898        assert!(!parsed.is_root());
899        assert!(parsed.is_split());
900        assert_eq!(parsed.file_type(), AdtFileType::Tex0);
901    }
902
903    #[test]
904    fn test_root_adt_helpers() {
905        let root = RootAdt {
906            version: AdtVersion::WotLK,
907            mhdr: MhdrChunk::default(),
908            mcin: McinChunk {
909                entries: McinChunk::default().entries,
910            },
911            textures: vec!["texture1.blp".into(), "texture2.blp".into()],
912            models: vec!["model1.m2".into()],
913            model_indices: vec![0],
914            wmos: vec![],
915            wmo_indices: vec![],
916            doodad_placements: vec![],
917            wmo_placements: vec![],
918            mcnk_chunks: vec![],
919            flight_bounds: None,
920            water_data: None,
921            texture_flags: None,
922            texture_amplifier: None,
923            texture_params: None,
924            blend_mesh_headers: None,
925            blend_mesh_bounds: None,
926            blend_mesh_vertices: None,
927            blend_mesh_indices: None,
928        };
929
930        assert_eq!(root.texture_count(), 2);
931        assert_eq!(root.model_count(), 1);
932        assert_eq!(root.wmo_count(), 0);
933        assert_eq!(root.terrain_chunk_count(), 0);
934        assert!(!root.has_water());
935        assert!(!root.has_flight_bounds());
936    }
937}