Skip to main content

monsoon_cli/cli/
output.rs

1//! Output formatting system for CLI memory dumps.
2//!
3//! This module provides an extensible output format system using traits.
4//! Adding a new output format requires:
5//! 1. Add a variant to `OutputFormat` enum
6//! 2. Implement the `MemoryFormatter` trait for the new format
7//! 3. Register it in `OutputFormat::formatter()`
8//!
9//! # Interpreted Data
10//!
11//! OAM and nametable dumps include both raw data and interpreted structures:
12//!
13//! - **OAM**: 64 sprites with position, tile index, attributes, flip flags
14//! - **Nametables**: 4 nametables with tile IDs, attribute tables, and per-tile
15//!   palettes
16//!
17//! # Example: Adding a new format
18//!
19//! ```rust,ignore
20//! // 1. Add enum variant in args.rs
21//! pub enum OutputFormat {
22//!     Hex,
23//!     Json,
24//!     Toml,
25//!     Binary,
26//!     Xml,  // New format
27//! }
28//!
29//! // 2. Implement the formatter
30//! pub struct XmlFormatter;
31//!
32//! impl MemoryFormatter for XmlFormatter {
33//!     fn format(&self, dump: &MemoryDump) -> Result<Vec<u8>, String> {
34//!         // ... format as XML ...
35//!     }
36//!
37//!     fn file_extension(&self) -> &'static str {
38//!         "xml"
39//!     }
40//! }
41//!
42//! // 3. Register in OutputFormat::formatter()
43//! OutputFormat::Xml => Box::new(XmlFormatter),
44//! ```
45
46use std::fs::{File, OpenOptions};
47use std::io::Write;
48use std::path::PathBuf;
49use std::sync::atomic::{AtomicBool, Ordering};
50
51use serde::Serialize;
52
53use crate::cli::args::OutputFormat;
54
55// =============================================================================
56// OAM Interpretation Structures
57// =============================================================================
58
59/// A single sprite entry from OAM (4 bytes interpreted).
60///
61/// NES OAM format per sprite:
62/// - Byte 0: Y position (actual Y = value + 1, values 0xEF-0xFF hide sprite)
63/// - Byte 1: Tile index
64/// - Byte 2: Attributes (palette, priority, flip)
65/// - Byte 3: X position
66#[derive(Debug, Clone, Serialize)]
67pub struct OamSprite {
68    /// Sprite index (0-63)
69    pub index: u8,
70    /// X position (0-255, values 0xF9-0xFF partially offscreen right)
71    pub x: u8,
72    /// Y position (raw value; actual screen Y = y + 1)
73    pub y: u8,
74    /// Tile index number
75    pub tile: u8,
76    /// Palette index (0-3, actual palette 4-7)
77    pub palette: u8,
78    /// Priority: false = in front of background, true = behind background
79    pub behind_background: bool,
80    /// Flip sprite horizontally
81    pub flip_h: bool,
82    /// Flip sprite vertically
83    pub flip_v: bool,
84    /// Whether sprite is visible (Y < 0xEF)
85    pub visible: bool,
86    /// Raw 4 bytes for this sprite
87    pub raw: [u8; 4],
88}
89
90impl OamSprite {
91    /// Create a sprite from 4 raw OAM bytes.
92    ///
93    /// # Panics
94    /// Panics if bytes slice has fewer than 4 elements.
95    pub fn from_bytes(index: u8, bytes: &[u8]) -> Self {
96        assert!(
97            bytes.len() >= 4,
98            "OAM sprite requires at least 4 bytes, got {}",
99            bytes.len()
100        );
101        let y = bytes[0];
102        let tile = bytes[1];
103        let attr = bytes[2];
104        let x = bytes[3];
105
106        Self {
107            index,
108            x,
109            y,
110            tile,
111            palette: attr & 0x03,
112            behind_background: (attr & 0x20) != 0,
113            flip_h: (attr & 0x40) != 0,
114            flip_v: (attr & 0x80) != 0,
115            visible: y < 0xEF,
116            raw: [bytes[0], bytes[1], bytes[2], bytes[3]],
117        }
118    }
119}
120
121/// Interpreted OAM data containing all 64 sprites.
122#[derive(Debug, Clone, Serialize)]
123pub struct InterpretedOam {
124    /// Total number of sprites (always 64)
125    pub sprite_count: u8,
126    /// Number of visible sprites (Y < 0xEF)
127    pub visible_count: u8,
128    /// All 64 sprites with interpreted fields
129    pub sprites: Vec<OamSprite>,
130}
131
132impl InterpretedOam {
133    /// Create interpreted OAM from raw OAM data (up to 256 bytes for 64
134    /// sprites).
135    pub fn from_raw(data: &[u8]) -> Self {
136        let mut sprites = Vec::with_capacity(64);
137        let mut visible_count = 0u8;
138
139        for i in 0..64 {
140            let offset = i * 4;
141            if offset + 4 <= data.len() {
142                let sprite = OamSprite::from_bytes(i as u8, &data[offset..offset + 4]);
143                if sprite.visible {
144                    visible_count += 1;
145                }
146                sprites.push(sprite);
147            }
148        }
149
150        Self {
151            sprite_count: sprites.len() as u8,
152            visible_count,
153            sprites,
154        }
155    }
156}
157
158// =============================================================================
159// Nametable Interpretation Structures
160// =============================================================================
161
162/// A single nametable (32x30 tiles + 64-byte attribute table).
163#[derive(Debug, Clone, Serialize)]
164pub struct InterpretedNametable {
165    /// Nametable index (0-3)
166    pub index: u8,
167    /// Base address in PPU memory ($2000, $2400, $2800, $2C00)
168    pub base_address: String,
169    /// 32x30 = 960 tile indices
170    pub tiles: Vec<Vec<u8>>,
171    /// 8x8 = 64 attribute bytes (each controls 4x4 tile area)
172    pub attributes: Vec<u8>,
173    /// Per-tile palette indices (derived from attribute table)
174    pub tile_palettes: Vec<Vec<u8>>,
175}
176
177impl InterpretedNametable {
178    /// Create interpreted nametable from raw 1024-byte nametable data.
179    ///
180    /// Layout:
181    /// - Bytes 0x000-0x3BF: 960 tile indices (32 columns x 30 rows)
182    /// - Bytes 0x3C0-0x3FF: 64 attribute bytes
183    pub fn from_raw(index: u8, data: &[u8]) -> Self {
184        let base_addresses = [0x2000u16, 0x2400, 0x2800, 0x2C00];
185        let base = base_addresses[index as usize % 4];
186
187        // Extract tiles as 30 rows of 32 columns
188        let mut tiles = Vec::with_capacity(30);
189        for row in 0..30 {
190            let start = row * 32;
191            let end = start + 32;
192            if end <= data.len() {
193                tiles.push(data[start..end].to_vec());
194            } else {
195                tiles.push(vec![0; 32]);
196            }
197        }
198
199        // Extract attribute table (64 bytes starting at offset 0x3C0)
200        let attr_start = 0x3C0;
201        let attributes = if attr_start + 64 <= data.len() {
202            data[attr_start..attr_start + 64].to_vec()
203        } else {
204            vec![0; 64]
205        };
206
207        // Calculate per-tile palette indices from attribute table
208        // Each attribute byte controls a 4x4 tile area (32x32 pixels)
209        // Bits: 76543210
210        //       ||||||++- Top-left 2x2 tiles
211        //       ||||++--- Top-right 2x2 tiles
212        //       ||++----- Bottom-left 2x2 tiles
213        //       ++------- Bottom-right 2x2 tiles
214        let mut tile_palettes = Vec::with_capacity(30);
215        for row in 0..30 {
216            let mut row_palettes = Vec::with_capacity(32);
217            for col in 0..32 {
218                // Which attribute byte controls this tile
219                let attr_col = col / 4;
220                let attr_row = row / 4;
221                let attr_index = attr_row * 8 + attr_col;
222
223                if attr_index < attributes.len() {
224                    let attr = attributes[attr_index];
225                    // Which 2x2 quadrant within the 4x4 area
226                    let quadrant_x = (col % 4) / 2;
227                    let quadrant_y = (row % 4) / 2;
228                    let shift = (quadrant_y * 2 + quadrant_x) * 2;
229                    let palette = (attr >> shift) & 0x03;
230                    row_palettes.push(palette);
231                } else {
232                    row_palettes.push(0);
233                }
234            }
235            tile_palettes.push(row_palettes);
236        }
237
238        Self {
239            index,
240            base_address: format!("0x{:04X}", base),
241            tiles,
242            attributes,
243            tile_palettes,
244        }
245    }
246}
247
248/// Interpreted nametable data containing all 4 nametables.
249#[derive(Debug, Clone, Serialize)]
250pub struct InterpretedNametables {
251    /// Total size in bytes (4096 for 4 nametables)
252    pub total_size: usize,
253    /// All 4 nametables
254    pub nametables: Vec<InterpretedNametable>,
255}
256
257impl InterpretedNametables {
258    /// Create interpreted nametables from raw data.
259    ///
260    /// Expects data for nametables at $2000-$2FFF (4 x 1024 bytes).
261    pub fn from_raw(data: &[u8]) -> Self {
262        let mut nametables = Vec::with_capacity(4);
263
264        for i in 0..4 {
265            let start = i * 0x400;
266            let end = start + 0x400;
267            if end <= data.len() {
268                nametables.push(InterpretedNametable::from_raw(i as u8, &data[start..end]));
269            } else if start < data.len() {
270                // Partial nametable
271                let partial = &data[start..];
272                let mut padded = partial.to_vec();
273                padded.resize(0x400, 0);
274                nametables.push(InterpretedNametable::from_raw(i as u8, &padded));
275            }
276        }
277
278        Self {
279            total_size: data.len(),
280            nametables,
281        }
282    }
283}
284
285// =============================================================================
286// Memory Dump Data Structure
287// =============================================================================
288
289/// A memory dump with metadata and optional interpreted data.
290///
291/// This is the common data structure passed to all formatters.
292/// For OAM and nametable dumps, interpreted data is also included.
293#[derive(Debug, Clone)]
294pub struct MemoryDump {
295    /// Type of memory (cpu, ppu, oam, nametables)
296    pub mem_type: MemoryType,
297    /// Start address
298    pub start_addr: u16,
299    /// End address
300    pub end_addr: u16,
301    /// Raw memory bytes
302    pub data: Vec<u8>,
303    /// Interpreted OAM data (only for OAM dumps)
304    pub interpreted_oam: Option<InterpretedOam>,
305    /// Interpreted nametable data (only for nametable dumps)
306    pub interpreted_nametables: Option<InterpretedNametables>,
307}
308
309/// Type of memory being dumped
310#[derive(Debug, Clone, Copy, PartialEq, Eq)]
311pub enum MemoryType {
312    /// CPU address space
313    Cpu,
314    /// PPU address space
315    Ppu,
316    /// OAM (sprite) memory
317    Oam,
318    /// Nametables
319    Nametables,
320    /// Palette RAM
321    PaletteRam,
322}
323
324impl MemoryType {
325    /// Get string representation
326    pub fn as_str(&self) -> &'static str {
327        match self {
328            MemoryType::Cpu => "cpu",
329            MemoryType::Ppu => "ppu",
330            MemoryType::Oam => "oam",
331            MemoryType::Nametables => "nametables",
332            MemoryType::PaletteRam => "palette_ram",
333        }
334    }
335}
336
337impl std::fmt::Display for MemoryType {
338    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339        write!(f, "{}", self.as_str())
340    }
341}
342
343impl MemoryDump {
344    /// Create a new memory dump
345    pub fn new(mem_type: MemoryType, start_addr: u16, data: Vec<u8>) -> Self {
346        let end_addr = if data.is_empty() {
347            start_addr
348        } else {
349            start_addr.saturating_add((data.len() - 1) as u16)
350        };
351        Self {
352            mem_type,
353            start_addr,
354            end_addr,
355            data,
356            interpreted_oam: None,
357            interpreted_nametables: None,
358        }
359    }
360
361    /// Create CPU memory dump
362    pub fn cpu(start_addr: u16, data: Vec<u8>) -> Self {
363        Self::new(MemoryType::Cpu, start_addr, data)
364    }
365
366    /// Create PPU memory dump
367    pub fn ppu(start_addr: u16, data: Vec<u8>) -> Self {
368        Self::new(MemoryType::Ppu, start_addr, data)
369    }
370
371    /// Create OAM memory dump with interpreted sprite data.
372    pub fn oam(data: Vec<u8>) -> Self {
373        let interpreted = InterpretedOam::from_raw(&data);
374        let mut dump = Self::new(MemoryType::Oam, 0, data);
375        dump.interpreted_oam = Some(interpreted);
376        dump
377    }
378
379    /// Create nametables memory dump with interpreted data.
380    pub fn nametables(data: Vec<u8>) -> Self {
381        let interpreted = InterpretedNametables::from_raw(&data);
382        let mut dump = Self::new(MemoryType::Nametables, 0x2000, data);
383        dump.interpreted_nametables = Some(interpreted);
384        dump
385    }
386
387    /// Create palette RAM memory dump.
388    /// Palette RAM is 32 bytes at PPU addresses $3F00-$3F1F.
389    pub fn palette_ram(data: Vec<u8>) -> Self { Self::new(MemoryType::PaletteRam, 0x3F00, data) }
390}
391
392// =============================================================================
393// Formatter Trait
394// =============================================================================
395
396/// Trait for memory dump formatters.
397///
398/// Implement this trait to add a new output format.
399pub trait MemoryFormatter: Send + Sync {
400    /// Format a memory dump into bytes ready to be written.
401    fn format(&self, dump: &MemoryDump) -> Result<Vec<u8>, String>;
402
403    /// Get the file extension for this format (without leading dot).
404    fn file_extension(&self) -> &'static str;
405
406    /// Whether this format is human-readable (affects stdout display).
407    fn is_text(&self) -> bool { true }
408}
409
410// =============================================================================
411// Built-in Formatters
412// =============================================================================
413
414/// Hexadecimal dump formatter (traditional hex dump format)
415///
416/// For OAM dumps, includes both raw hex and human-readable sprite
417/// interpretation. For nametable dumps, includes both raw hex and nametable
418/// summary.
419pub struct HexFormatter;
420
421impl MemoryFormatter for HexFormatter {
422    fn format(&self, dump: &MemoryDump) -> Result<Vec<u8>, String> {
423        let mut output = String::new();
424
425        // For OAM, add interpreted header
426        if let Some(ref oam) = dump.interpreted_oam {
427            output.push_str("=== OAM Interpretation ===\n");
428            output.push_str(&format!(
429                "Total sprites: {}, Visible: {}\n\n",
430                oam.sprite_count, oam.visible_count
431            ));
432            output.push_str("Idx |  X  |  Y  | Tile | Pal | Pri | FlipH | FlipV | Visible\n");
433            output.push_str("----+-----+-----+------+-----+-----+-------+-------+--------\n");
434            for sprite in &oam.sprites {
435                output.push_str(&format!(
436                    "{:3} | {:3} | {:3} | 0x{:02X} |  {}  | {} |   {}   |   {}   | {}\n",
437                    sprite.index,
438                    sprite.x,
439                    sprite.y,
440                    sprite.tile,
441                    sprite.palette,
442                    if sprite.behind_background { "B" } else { "F" },
443                    if sprite.flip_h { "Y" } else { "N" },
444                    if sprite.flip_v { "Y" } else { "N" },
445                    if sprite.visible { "Yes" } else { "No" }
446                ));
447            }
448            output.push_str("\n=== Raw OAM Data ===\n");
449        }
450
451        // For nametables, add interpreted header
452        if let Some(ref nt) = dump.interpreted_nametables {
453            output.push_str("=== Nametables Interpretation ===\n");
454            output.push_str(&format!("Total size: {} bytes\n\n", nt.total_size));
455            for nametable in &nt.nametables {
456                output.push_str(&format!(
457                    "Nametable {} (base: {})\n",
458                    nametable.index, nametable.base_address
459                ));
460                output.push_str("  Tiles (32x30 grid, showing first 8 rows):\n");
461                for (row_idx, row) in nametable.tiles.iter().take(8).enumerate() {
462                    output.push_str(&format!("  Row {:2}: ", row_idx));
463                    for tile in row.iter().take(32) {
464                        output.push_str(&format!("{:02X} ", tile));
465                    }
466                    output.push('\n');
467                }
468                if nametable.tiles.len() > 8 {
469                    output.push_str("  ... (22 more rows)\n");
470                }
471                output.push('\n');
472            }
473            output.push_str("=== Raw Nametable Data ===\n");
474        }
475
476        // Raw hex dump
477        for (i, chunk) in dump.data.chunks(16).enumerate() {
478            let line = format!(
479                "{:04X}: {}\n",
480                dump.start_addr as usize + i * 16,
481                chunk
482                    .iter()
483                    .map(|b| format!("{:02X}", b))
484                    .collect::<Vec<_>>()
485                    .join(" ")
486            );
487            output.push_str(&line);
488        }
489        Ok(output.into_bytes())
490    }
491
492    fn file_extension(&self) -> &'static str { "hex" }
493}
494
495/// Raw binary formatter
496pub struct BinaryFormatter;
497
498impl MemoryFormatter for BinaryFormatter {
499    fn format(&self, dump: &MemoryDump) -> Result<Vec<u8>, String> { Ok(dump.data.clone()) }
500
501    fn file_extension(&self) -> &'static str { "bin" }
502
503    fn is_text(&self) -> bool { false }
504}
505
506/// JSON formatter
507pub struct JsonFormatter;
508
509/// Structure for JSON/TOML serialization of basic memory dumps
510#[derive(Serialize)]
511struct MemoryDumpOutput {
512    memory_dump: MemoryDumpData,
513}
514
515#[derive(Serialize)]
516struct MemoryDumpData {
517    #[serde(rename = "type")]
518    mem_type: String,
519    start: String,
520    end: String,
521    data: Vec<String>,
522}
523
524/// Structure for JSON/TOML serialization of OAM dumps with interpretation
525#[derive(Serialize)]
526struct OamDumpOutput {
527    oam_dump: OamDumpData,
528}
529
530#[derive(Serialize)]
531struct OamDumpData {
532    #[serde(rename = "type")]
533    mem_type: String,
534    size: usize,
535    raw_data: Vec<String>,
536    interpretation: InterpretedOam,
537}
538
539/// Structure for JSON/TOML serialization of nametable dumps with interpretation
540#[derive(Serialize)]
541struct NametablesDumpOutput {
542    nametables_dump: NametablesDumpData,
543}
544
545#[derive(Serialize)]
546struct NametablesDumpData {
547    #[serde(rename = "type")]
548    mem_type: String,
549    start: String,
550    end: String,
551    raw_data: Vec<String>,
552    interpretation: InterpretedNametables,
553}
554
555impl MemoryFormatter for JsonFormatter {
556    fn format(&self, dump: &MemoryDump) -> Result<Vec<u8>, String> {
557        let data_hex: Vec<String> = dump.data.iter().map(|b| format!("0x{:02X}", b)).collect();
558
559        let json_str = match dump.mem_type {
560            MemoryType::Oam => {
561                if let Some(ref interp) = dump.interpreted_oam {
562                    let output = OamDumpOutput {
563                        oam_dump: OamDumpData {
564                            mem_type: dump.mem_type.to_string(),
565                            size: dump.data.len(),
566                            raw_data: data_hex,
567                            interpretation: interp.clone(),
568                        },
569                    };
570                    serde_json::to_string_pretty(&output)
571                        .map_err(|e| format!("Failed to serialize JSON: {}", e))?
572                } else {
573                    // Fallback to basic output
574                    let output = MemoryDumpOutput {
575                        memory_dump: MemoryDumpData {
576                            mem_type: dump.mem_type.to_string(),
577                            start: format!("0x{:04X}", dump.start_addr),
578                            end: format!("0x{:04X}", dump.end_addr),
579                            data: data_hex,
580                        },
581                    };
582                    serde_json::to_string_pretty(&output)
583                        .map_err(|e| format!("Failed to serialize JSON: {}", e))?
584                }
585            }
586            MemoryType::Nametables => {
587                if let Some(ref interp) = dump.interpreted_nametables {
588                    let output = NametablesDumpOutput {
589                        nametables_dump: NametablesDumpData {
590                            mem_type: dump.mem_type.to_string(),
591                            start: format!("0x{:04X}", dump.start_addr),
592                            end: format!("0x{:04X}", dump.end_addr),
593                            raw_data: data_hex,
594                            interpretation: interp.clone(),
595                        },
596                    };
597                    serde_json::to_string_pretty(&output)
598                        .map_err(|e| format!("Failed to serialize JSON: {}", e))?
599                } else {
600                    // Fallback to basic output
601                    let output = MemoryDumpOutput {
602                        memory_dump: MemoryDumpData {
603                            mem_type: dump.mem_type.to_string(),
604                            start: format!("0x{:04X}", dump.start_addr),
605                            end: format!("0x{:04X}", dump.end_addr),
606                            data: data_hex,
607                        },
608                    };
609                    serde_json::to_string_pretty(&output)
610                        .map_err(|e| format!("Failed to serialize JSON: {}", e))?
611                }
612            }
613            _ => {
614                let output = MemoryDumpOutput {
615                    memory_dump: MemoryDumpData {
616                        mem_type: dump.mem_type.to_string(),
617                        start: format!("0x{:04X}", dump.start_addr),
618                        end: format!("0x{:04X}", dump.end_addr),
619                        data: data_hex,
620                    },
621                };
622                serde_json::to_string_pretty(&output)
623                    .map_err(|e| format!("Failed to serialize JSON: {}", e))?
624            }
625        };
626
627        Ok(format!("{}\n", json_str).into_bytes())
628    }
629
630    fn file_extension(&self) -> &'static str { "json" }
631}
632
633/// TOML formatter
634pub struct TomlFormatter;
635
636impl MemoryFormatter for TomlFormatter {
637    fn format(&self, dump: &MemoryDump) -> Result<Vec<u8>, String> {
638        let data_hex: Vec<String> = dump.data.iter().map(|b| format!("0x{:02X}", b)).collect();
639
640        let toml_str = match dump.mem_type {
641            MemoryType::Oam => {
642                if let Some(ref interp) = dump.interpreted_oam {
643                    let output = OamDumpOutput {
644                        oam_dump: OamDumpData {
645                            mem_type: dump.mem_type.to_string(),
646                            size: dump.data.len(),
647                            raw_data: data_hex,
648                            interpretation: interp.clone(),
649                        },
650                    };
651                    toml::to_string_pretty(&output)
652                        .map_err(|e| format!("Failed to serialize TOML: {}", e))?
653                } else {
654                    let output = MemoryDumpOutput {
655                        memory_dump: MemoryDumpData {
656                            mem_type: dump.mem_type.to_string(),
657                            start: format!("0x{:04X}", dump.start_addr),
658                            end: format!("0x{:04X}", dump.end_addr),
659                            data: data_hex,
660                        },
661                    };
662                    toml::to_string_pretty(&output)
663                        .map_err(|e| format!("Failed to serialize TOML: {}", e))?
664                }
665            }
666            MemoryType::Nametables => {
667                if let Some(ref interp) = dump.interpreted_nametables {
668                    let output = NametablesDumpOutput {
669                        nametables_dump: NametablesDumpData {
670                            mem_type: dump.mem_type.to_string(),
671                            start: format!("0x{:04X}", dump.start_addr),
672                            end: format!("0x{:04X}", dump.end_addr),
673                            raw_data: data_hex,
674                            interpretation: interp.clone(),
675                        },
676                    };
677                    toml::to_string_pretty(&output)
678                        .map_err(|e| format!("Failed to serialize TOML: {}", e))?
679                } else {
680                    let output = MemoryDumpOutput {
681                        memory_dump: MemoryDumpData {
682                            mem_type: dump.mem_type.to_string(),
683                            start: format!("0x{:04X}", dump.start_addr),
684                            end: format!("0x{:04X}", dump.end_addr),
685                            data: data_hex,
686                        },
687                    };
688                    toml::to_string_pretty(&output)
689                        .map_err(|e| format!("Failed to serialize TOML: {}", e))?
690                }
691            }
692            _ => {
693                let output = MemoryDumpOutput {
694                    memory_dump: MemoryDumpData {
695                        mem_type: dump.mem_type.to_string(),
696                        start: format!("0x{:04X}", dump.start_addr),
697                        end: format!("0x{:04X}", dump.end_addr),
698                        data: data_hex,
699                    },
700                };
701                toml::to_string_pretty(&output)
702                    .map_err(|e| format!("Failed to serialize TOML: {}", e))?
703            }
704        };
705
706        Ok(format!("{}\n", toml_str).into_bytes())
707    }
708
709    fn file_extension(&self) -> &'static str { "toml" }
710}
711
712// =============================================================================
713// OutputFormat Extensions
714// =============================================================================
715
716impl OutputFormat {
717    /// Get the formatter for this output format.
718    ///
719    /// To add a new format, add a variant to the enum and a case here.
720    pub fn formatter(&self) -> Box<dyn MemoryFormatter> {
721        match self {
722            OutputFormat::Hex => Box::new(HexFormatter),
723            OutputFormat::Json => Box::new(JsonFormatter),
724            OutputFormat::Toml => Box::new(TomlFormatter),
725            OutputFormat::Binary => Box::new(BinaryFormatter),
726        }
727    }
728
729    /// Get the file extension for this format.
730    pub fn extension(&self) -> &'static str {
731        match self {
732            OutputFormat::Hex => "hex",
733            OutputFormat::Json => "json",
734            OutputFormat::Toml => "toml",
735            OutputFormat::Binary => "bin",
736        }
737    }
738}
739
740// =============================================================================
741// Output Writer
742// =============================================================================
743
744/// Track whether the output file has been initialized (created/truncated).
745/// Note: This is intentionally global to support multiple OutputWriter
746/// instances writing to the same file in append mode within a single CLI run.
747static OUTPUT_FILE_INITIALIZED: AtomicBool = AtomicBool::new(false);
748
749/// Manages output writing with support for file and stdout.
750pub struct OutputWriter {
751    /// Output file path (None for stdout)
752    path: Option<PathBuf>,
753    /// Output format
754    format: OutputFormat,
755}
756
757impl OutputWriter {
758    /// Create a new output writer.
759    pub fn new(path: Option<PathBuf>, format: OutputFormat) -> Self {
760        Self {
761            path,
762            format,
763        }
764    }
765
766    /// Reset the output file state (call at start of output session).
767    pub fn reset() { OUTPUT_FILE_INITIALIZED.store(false, Ordering::SeqCst); }
768
769    /// Write a memory dump using the configured format.
770    pub fn write(&self, dump: &MemoryDump) -> Result<(), String> {
771        let formatter = self.format.formatter();
772        let data = formatter.format(dump)?;
773        self.write_bytes(&data)
774    }
775
776    /// Write raw bytes to the output.
777    fn write_bytes(&self, data: &[u8]) -> Result<(), String> {
778        let mut writer = self.get_writer()?;
779        writer.write_all(data).map_err(|e| e.to_string())
780    }
781
782    /// Get the output writer (file or stdout).
783    fn get_writer(&self) -> Result<Box<dyn Write>, String> {
784        if let Some(ref path) = self.path {
785            let is_first_write = !OUTPUT_FILE_INITIALIZED.swap(true, Ordering::SeqCst);
786
787            let file = if is_first_write {
788                File::create(path)
789            } else {
790                OpenOptions::new().append(true).open(path)
791            }
792            .map_err(|e| format!("Failed to open output file: {}", e))?;
793
794            Ok(Box::new(file))
795        } else {
796            Ok(Box::new(std::io::stdout()))
797        }
798    }
799}
800
801// =============================================================================
802// Tests
803// =============================================================================
804
805#[cfg(test)]
806mod tests {
807    use super::*;
808
809    #[test]
810    fn test_hex_formatter() {
811        let dump = MemoryDump::cpu(0x0000, vec![0x00, 0x01, 0x02, 0x03]);
812        let formatter = HexFormatter;
813        let output = formatter.format(&dump).unwrap();
814        let text = String::from_utf8(output).unwrap();
815        assert!(text.contains("0000: 00 01 02 03"));
816    }
817
818    #[test]
819    fn test_binary_formatter() {
820        let data = vec![0xDE, 0xAD, 0xBE, 0xEF];
821        let dump = MemoryDump::cpu(0x1000, data.clone());
822        let formatter = BinaryFormatter;
823        let output = formatter.format(&dump).unwrap();
824        assert_eq!(output, data);
825    }
826
827    #[test]
828    fn test_json_formatter() {
829        let dump = MemoryDump::cpu(0x0000, vec![0xFF]);
830        let formatter = JsonFormatter;
831        let output = formatter.format(&dump).unwrap();
832        let text = String::from_utf8(output).unwrap();
833        assert!(text.contains("\"type\": \"cpu\""));
834        assert!(text.contains("0xFF"));
835    }
836
837    #[test]
838    fn test_toml_formatter() {
839        let dump = MemoryDump::cpu(0x0000, vec![0xFF]);
840        let formatter = TomlFormatter;
841        let output = formatter.format(&dump).unwrap();
842        let text = String::from_utf8(output).unwrap();
843        assert!(text.contains("type = \"cpu\""));
844    }
845
846    #[test]
847    fn test_output_format_formatter() {
848        assert_eq!(OutputFormat::Hex.extension(), "hex");
849        assert_eq!(OutputFormat::Json.extension(), "json");
850        assert_eq!(OutputFormat::Toml.extension(), "toml");
851        assert_eq!(OutputFormat::Binary.extension(), "bin");
852    }
853
854    // =============================================================================
855    // OAM Interpretation Tests
856    // =============================================================================
857
858    #[test]
859    fn test_oam_sprite_from_bytes() {
860        // Y=0x20, Tile=0x42, Attr=0b11100011 (pal=3, behind=true, flipH=true,
861        // flipV=true), X=0x80
862        let bytes = [0x20, 0x42, 0xE3, 0x80];
863        let sprite = OamSprite::from_bytes(5, &bytes);
864
865        assert_eq!(sprite.index, 5);
866        assert_eq!(sprite.y, 0x20);
867        assert_eq!(sprite.tile, 0x42);
868        assert_eq!(sprite.x, 0x80);
869        assert_eq!(sprite.palette, 3);
870        assert!(sprite.behind_background);
871        assert!(sprite.flip_h);
872        assert!(sprite.flip_v);
873        assert!(sprite.visible); // Y < 0xEF
874    }
875
876    #[test]
877    fn test_oam_sprite_visibility() {
878        // Hidden sprite (Y = 0xF0)
879        let hidden = OamSprite::from_bytes(0, &[0xF0, 0x00, 0x00, 0x00]);
880        assert!(!hidden.visible);
881
882        // Visible sprite (Y = 0xEE)
883        let visible = OamSprite::from_bytes(0, &[0xEE, 0x00, 0x00, 0x00]);
884        assert!(visible.visible);
885    }
886
887    #[test]
888    fn test_interpreted_oam_from_raw() {
889        // Create full 256-byte OAM data (64 sprites × 4 bytes) with all sprites hidden
890        // by default
891        let mut data = vec![0xFFu8; 256]; // Y=0xFF means hidden
892        // Sprite 0: visible
893        data[0] = 0x10; // Y
894        data[1] = 0x01; // Tile
895        data[2] = 0x00; // Attr
896        data[3] = 0x20; // X
897        // Sprite 1: explicitly hidden
898        data[4] = 0xFF; // Y (hidden)
899        data[5] = 0x02; // Tile
900        data[6] = 0x00; // Attr
901        data[7] = 0x30; // X
902
903        let interp = InterpretedOam::from_raw(&data);
904
905        assert_eq!(interp.sprite_count, 64);
906        assert_eq!(interp.visible_count, 1); // Only sprite 0 is visible
907        assert_eq!(interp.sprites.len(), 64);
908        assert_eq!(interp.sprites[0].y, 0x10);
909        assert_eq!(interp.sprites[0].tile, 0x01);
910        assert!(interp.sprites[0].visible);
911        assert!(!interp.sprites[1].visible);
912    }
913
914    #[test]
915    fn test_oam_dump_json_has_interpretation() {
916        let mut data = vec![0u8; 256];
917        data[0] = 0x10; // Y
918        data[1] = 0x42; // Tile
919        data[2] = 0x01; // Attr (palette 1)
920        data[3] = 0x50; // X
921
922        let dump = MemoryDump::oam(data);
923        let formatter = JsonFormatter;
924        let output = formatter.format(&dump).unwrap();
925        let text = String::from_utf8(output).unwrap();
926
927        // Should contain interpretation
928        assert!(text.contains("interpretation"));
929        assert!(text.contains("sprite_count"));
930        assert!(text.contains("visible_count"));
931        assert!(text.contains("sprites"));
932    }
933
934    #[test]
935    fn test_oam_dump_hex_has_interpretation() {
936        let mut data = vec![0u8; 256];
937        data[0] = 0x10;
938        data[1] = 0x42;
939        data[2] = 0x01;
940        data[3] = 0x50;
941
942        let dump = MemoryDump::oam(data);
943        let formatter = HexFormatter;
944        let output = formatter.format(&dump).unwrap();
945        let text = String::from_utf8(output).unwrap();
946
947        // Should contain interpretation header
948        assert!(text.contains("=== OAM Interpretation ==="));
949        assert!(text.contains("Total sprites:"));
950        assert!(text.contains("Visible:"));
951        assert!(text.contains("=== Raw OAM Data ==="));
952    }
953
954    // =============================================================================
955    // Nametable Interpretation Tests
956    // =============================================================================
957
958    #[test]
959    fn test_interpreted_nametable_from_raw() {
960        // Create 1KB nametable data
961        let mut data = vec![0u8; 1024];
962        // Set some tile values
963        data[0] = 0x01; // Row 0, Col 0
964        data[31] = 0x1F; // Row 0, Col 31
965        data[32] = 0x20; // Row 1, Col 0
966        // Attribute table starts at 0x3C0
967        data[0x3C0] = 0b11_10_01_00; // Different palettes for 4 quadrants
968
969        let nt = InterpretedNametable::from_raw(0, &data);
970
971        assert_eq!(nt.index, 0);
972        assert_eq!(nt.base_address, "0x2000");
973        assert_eq!(nt.tiles.len(), 30);
974        assert_eq!(nt.tiles[0][0], 0x01);
975        assert_eq!(nt.tiles[0][31], 0x1F);
976        assert_eq!(nt.tiles[1][0], 0x20);
977        assert_eq!(nt.attributes.len(), 64);
978        assert_eq!(nt.attributes[0], 0b11_10_01_00);
979
980        // Check palette calculation for first attribute byte
981        assert_eq!(nt.tile_palettes[0][0], 0); // Top-left quadrant
982        assert_eq!(nt.tile_palettes[0][2], 1); // Top-right quadrant
983        assert_eq!(nt.tile_palettes[2][0], 2); // Bottom-left quadrant
984        assert_eq!(nt.tile_palettes[2][2], 3); // Bottom-right quadrant
985    }
986
987    #[test]
988    fn test_interpreted_nametables_from_raw() {
989        // Create 4KB data for all 4 nametables
990        let data = vec![0u8; 4096];
991
992        let interp = InterpretedNametables::from_raw(&data);
993
994        assert_eq!(interp.total_size, 4096);
995        assert_eq!(interp.nametables.len(), 4);
996        assert_eq!(interp.nametables[0].base_address, "0x2000");
997        assert_eq!(interp.nametables[1].base_address, "0x2400");
998        assert_eq!(interp.nametables[2].base_address, "0x2800");
999        assert_eq!(interp.nametables[3].base_address, "0x2C00");
1000    }
1001
1002    #[test]
1003    fn test_nametables_dump_json_has_interpretation() {
1004        let data = vec![0u8; 4096];
1005        let dump = MemoryDump::nametables(data);
1006        let formatter = JsonFormatter;
1007        let output = formatter.format(&dump).unwrap();
1008        let text = String::from_utf8(output).unwrap();
1009
1010        // Should contain interpretation
1011        assert!(text.contains("interpretation"));
1012        assert!(text.contains("total_size"));
1013        assert!(text.contains("nametables"));
1014        assert!(text.contains("base_address"));
1015    }
1016
1017    #[test]
1018    fn test_nametables_dump_hex_has_interpretation() {
1019        let data = vec![0u8; 4096];
1020        let dump = MemoryDump::nametables(data);
1021        let formatter = HexFormatter;
1022        let output = formatter.format(&dump).unwrap();
1023        let text = String::from_utf8(output).unwrap();
1024
1025        // Should contain interpretation header
1026        assert!(text.contains("=== Nametables Interpretation ==="));
1027        assert!(text.contains("Nametable 0"));
1028        assert!(text.contains("=== Raw Nametable Data ==="));
1029    }
1030}