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