1use egui::scroll_area::ScrollBarVisibility;
2use egui::style::ScrollStyle;
3use egui::{
4    Align, CentralPanel, Color32, Context, FontId, Id, LayerId, Order, Panel, RichText, TextEdit,
5    Ui, Window,
6};
7use egui_extras::{Column, TableBuilder};
8use jgenesis_common::debug::{DebugMemoryView, Endian};
9use rfd::FileDialog;
10use std::fs::File;
11use std::io::BufWriter;
12use std::path::Path;
13use std::{array, cmp, io};
14
15const MONOSPACE: FontId = FontId::monospace(12.0);
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum MemoryViewerColumns {
19    Byte,
20    Word,
21    Longword,
22}
23
24impl MemoryViewerColumns {
25    fn num_columns(self) -> usize {
26        match self {
27            Self::Byte => 16,
28            Self::Word => 8,
29            Self::Longword => 4,
30        }
31    }
32
33    fn column_width(self) -> f32 {
34        match self {
35            Self::Byte => 15.0,
36            Self::Word => 35.0,
37            Self::Longword => 80.0,
38        }
39    }
40}
41
42#[derive(Debug, Clone)]
43pub struct MemoryViewerState {
44    pub window_title: String,
45    pub default_file_name: Option<String>,
46    pub endian: Endian,
47    pub columns: MemoryViewerColumns,
48    pub open: bool,
49    pub goto_text: String,
50    pub goto_address: Option<usize>,
51    pub highlight_address: Option<usize>,
52    pub set_address_text: String,
53    pub table_offset: f32,
54    pub table_height: f32,
55    pub value_text: String,
56    pub set_invalid: bool,
57    pub editable: bool,
58}
59
60impl MemoryViewerState {
61    pub fn new(name: &str, endian: Endian) -> Self {
62        Self {
63            window_title: format!("{name} Viewer"),
64            default_file_name: None,
65            endian,
66            columns: MemoryViewerColumns::Word,
67            open: false,
68            goto_text: String::new(),
69            goto_address: None,
70            highlight_address: None,
71            set_address_text: String::new(),
72            table_offset: 0.0,
73            table_height: 1.0,
74            value_text: String::new(),
75            set_invalid: false,
76            editable: false,
77        }
78    }
79
80    pub fn with_default_file_name(mut self, name: String) -> Self {
81        self.default_file_name = Some(name);
82        self
83    }
84
85    pub fn with_editable(mut self) -> Self {
86        self.editable = true;
87        self
88    }
89
90    pub fn open_window(&mut self, ctx: &Context) {
91        self.open = true;
92        ctx.move_to_top(LayerId::new(Order::Middle, Id::new(&self.window_title)));
93    }
94}
95
96pub fn render(ctx: &Context, memory: &mut dyn DebugMemoryView, state: &mut MemoryViewerState) {
97    let memory_len = memory.len();
98    if memory_len == 0 {
99        return;
100    }
101
102    let mut open = state.open;
103
104    Window::new(&state.window_title)
105        .open(&mut open)
106        .constrain(false)
107        .default_pos([50.0, crate::rand_window_pos()[1]])
108        .default_width(800.0)
109        .show(ctx, |ui| {
110            render_right_panel(memory, state, ui);
111            render_central_panel(memory, state, memory_len, ui);
112        });
113
114    state.open = open;
115}
116
117fn render_right_panel(
118    memory: &mut dyn DebugMemoryView,
119    state: &mut MemoryViewerState,
120    ui: &mut Ui,
121) {
122    Panel::right(format!("{}_right", state.window_title)).resizable(false).show_inside(ui, |ui| {
123        ui.heading("Go to address");
124
125        ui.horizontal(|ui| {
126            let resp = ui.add(
127                TextEdit::singleline(&mut state.goto_text).desired_width(70.0).font(MONOSPACE),
128            );
129            let enter_pressed = resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
130
131            let goto_address = ui.button("Go").clicked() || enter_pressed;
132
133            if goto_address && let Ok(address) = usize::from_str_radix(&state.goto_text, 16) {
134                state.goto_address = Some(address);
135            }
136        });
137
138        if state.editable {
139            ui.add_space(20.0);
140
141            ui.heading("Edit memory");
142
143            ui.horizontal(|ui| {
144                ui.label("Address");
145
146                ui.add(
147                    TextEdit::singleline(&mut state.set_address_text)
148                        .desired_width(70.0)
149                        .font(MONOSPACE),
150                );
151            });
152
153            ui.horizontal(|ui| {
154                ui.label("Value");
155
156                ui.add(
157                    TextEdit::singleline(&mut state.value_text).desired_width(70.0).font(MONOSPACE),
158                );
159            });
160
161            if ui.button("Set byte (8-bit)").clicked() {
162                try_set_byte(memory, state);
163            }
164
165            if ui.button("Set word (16-bit)").clicked() {
166                try_set_word(memory, state);
167            }
168
169            if ui.button("Set longword (32-bit)").clicked() {
170                try_set_longword(memory, state);
171            }
172
173            if state.set_invalid {
174                ui.colored_label(Color32::RED, "Invalid address or value");
175            }
176        }
177
178        ui.add_space(20.0);
179
180        ui.heading("Column size");
181        ui.radio_value(&mut state.columns, MemoryViewerColumns::Byte, "Byte (8-bit)");
182        ui.radio_value(&mut state.columns, MemoryViewerColumns::Word, "Word (16-bit)");
183        ui.radio_value(&mut state.columns, MemoryViewerColumns::Longword, "Longword (32-bit)");
184
185        ui.add_space(20.0);
186
187        ui.heading("Endianness");
188        ui.radio_value(&mut state.endian, Endian::Big, "Big-endian");
189        ui.radio_value(&mut state.endian, Endian::Little, "Little-endian");
190
191        ui.add_space(20.0);
192
193        if ui.button("Export to file...").clicked() {
194            export_to_file(memory, state);
195        }
196    });
197}
198
199fn render_central_panel(
200    memory: &mut dyn DebugMemoryView,
201    state: &mut MemoryViewerState,
202    memory_len: usize,
203    ui: &mut Ui,
204) {
205    const ROW_HEIGHT: f32 = 15.0;
206
207    let ctx = ui.ctx().clone();
208
209    let highlight_color = crate::highlight_color(ctx.theme());
210
211    CentralPanel::default().show_inside(ui, |ui| {
212        ui.spacing_mut().scroll = ScrollStyle { bar_width: 10.0, ..ScrollStyle::solid() };
213
214        let table_rows = memory_len / 16 + usize::from(!memory_len.is_multiple_of(16));
215        let address_len = format!("{:X}", memory_len - 1).len();
216
217        let num_columns = state.columns.num_columns();
218        let column_width = state.columns.column_width();
219
220        let mut builder = TableBuilder::new(ui)
221            .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible)
222            .column(Column::auto().at_least(60.0)) // Address
223            .columns(Column::auto().at_least(column_width), num_columns) // Byte/word/longword values
224            .column(Column::exact(10.0)) // Spacing
225            .column(Column::auto().at_least(125.0)) // ASCII representation
226            .column(Column::remainder().at_least(10.0)); // Spacing
227
228        if let Some(address) = state.goto_address.take() {
229            builder =
230                builder.scroll_to_row(cmp::min(table_rows - 1, address / 16), Some(Align::Center));
231            state.highlight_address = Some(address);
232        } else if crate::window_on_top(&ctx, &state.window_title) {
233            let keys = crate::scroll_keys_pressed(&ctx);
234            if let Some(offset) = keys.relative_scroll_offset(state.table_height) {
235                builder = builder.vertical_scroll_offset(state.table_offset + offset);
236            }
237        }
238
239        let scroll_output = builder.body(|body| {
240            body.rows(ROW_HEIGHT, table_rows, |mut row| {
241                let address = 16 * row.index();
242                let data: [_; 16] = array::from_fn(|i| memory.read(address + i));
243
244                row.col(|ui| {
245                    ui.label(RichText::new(fmt_address(address, address_len)).monospace());
246                });
247
248                match state.columns {
249                    MemoryViewerColumns::Byte => {
250                        for (i, byte) in data.into_iter().enumerate() {
251                            if address + i < memory_len {
252                                row.col(|ui| {
253                                    let mut text = RichText::new(format!("{byte:02X}")).monospace();
254                                    if state.highlight_address == Some(address + i) {
255                                        text = text.color(highlight_color);
256                                    }
257                                    ui.label(text.monospace());
258                                });
259                            }
260                        }
261                    }
262                    MemoryViewerColumns::Word => {
263                        let to_bytes = match state.endian {
264                            Endian::Big => u16::from_be_bytes,
265                            Endian::Little => u16::from_le_bytes,
266                        };
267
268                        for (i, &chunk) in data.as_chunks::<2>().0.iter().enumerate() {
269                            if address + 2 * i + 1 < memory_len {
270                                let word = to_bytes(chunk);
271
272                                row.col(|ui| {
273                                    let mut text = RichText::new(format!("{word:04X}")).monospace();
274                                    if state
275                                        .highlight_address
276                                        .is_some_and(|highlight| highlight & !1 == address + 2 * i)
277                                    {
278                                        text = text.color(highlight_color);
279                                    }
280                                    ui.label(text);
281                                });
282                            }
283                        }
284                    }
285                    MemoryViewerColumns::Longword => {
286                        let to_bytes = match state.endian {
287                            Endian::Big => u32::from_be_bytes,
288                            Endian::Little => u32::from_le_bytes,
289                        };
290
291                        for (i, &chunk) in data.as_chunks::<4>().0.iter().enumerate() {
292                            if address + 4 * i + 3 < memory_len {
293                                let longword = to_bytes(chunk);
294
295                                row.col(|ui| {
296                                    let mut text =
297                                        RichText::new(format!("{longword:08X}")).monospace();
298                                    if state
299                                        .highlight_address
300                                        .is_some_and(|highlight| highlight & !3 == address + 4 * i)
301                                    {
302                                        text = text.color(highlight_color);
303                                    }
304                                    ui.label(text);
305                                });
306                            }
307                        }
308                    }
309                }
310
311                // Empty column for spacing
312                row.col(|_ui| {});
313
314                row.col(|ui| {
315                    let ascii_text = data
316                        .into_iter()
317                        .map(|byte| {
318                            if byte.is_ascii() && !byte.is_ascii_control() {
319                                byte as char
320                            } else {
321                                '.'
322                            }
323                        })
324                        .collect::<String>();
325
326                    ui.label(RichText::new(ascii_text).monospace());
327                });
328
329                // Hack to make scroll bar not overlap the rightmost data column
330                row.col(|_ui| {});
331            });
332        });
333        state.table_offset = scroll_output.state.offset.y;
334        state.table_height = scroll_output.inner_rect.height();
335    });
336}
337
338fn fmt_address(address: usize, len: usize) -> String {
339    use std::fmt::Write;
340
341    let mut s = String::with_capacity(len);
342    let _ = write!(s, "{address:X}");
343    while s.len() < len {
344        s.insert(0, '0');
345    }
346
347    s
348}
349
350fn try_set_byte(memory: &mut dyn DebugMemoryView, state: &mut MemoryViewerState) {
351    let Ok(address) = usize::from_str_radix(&state.set_address_text, 16) else {
352        state.set_invalid = true;
353        return;
354    };
355
356    let Ok(byte) = u8::from_str_radix(&state.value_text, 16) else {
357        state.set_invalid = true;
358        return;
359    };
360
361    memory.write(address, byte);
362
363    state.goto_address = Some(address);
364    state.set_invalid = false;
365}
366
367fn try_set_word(memory: &mut dyn DebugMemoryView, state: &mut MemoryViewerState) {
368    let Ok(address) = usize::from_str_radix(&state.set_address_text, 16) else {
369        state.set_invalid = true;
370        return;
371    };
372
373    let Ok(word) = u16::from_str_radix(&state.value_text, 16) else {
374        state.set_invalid = true;
375        return;
376    };
377
378    let [first, second] = match state.endian {
379        Endian::Big => word.to_be_bytes(),
380        Endian::Little => word.to_le_bytes(),
381    };
382
383    memory.write(address & !1, first);
384    memory.write(address | 1, second);
385
386    state.goto_address = Some(address);
387    state.set_invalid = false;
388}
389
390fn try_set_longword(memory: &mut dyn DebugMemoryView, state: &mut MemoryViewerState) {
391    let Ok(address) = usize::from_str_radix(&state.set_address_text, 16) else {
392        state.set_invalid = true;
393        return;
394    };
395
396    let Ok(longword) = u32::from_str_radix(&state.value_text, 16) else {
397        state.set_invalid = true;
398        return;
399    };
400
401    let bytes = match state.endian {
402        Endian::Big => longword.to_be_bytes(),
403        Endian::Little => longword.to_le_bytes(),
404    };
405
406    for (i, byte) in bytes.into_iter().enumerate() {
407        memory.write((address & !3) | i, byte);
408    }
409
410    state.goto_address = Some(address);
411    state.set_invalid = false;
412}
413
414fn export_to_file(memory: &mut dyn DebugMemoryView, state: &MemoryViewerState) {
415    let file_name = state.default_file_name.clone().unwrap_or("memory.bin".into());
416
417    let Some(path) =
418        FileDialog::new().set_file_name(file_name).add_filter("bin", &["bin"]).save_file()
419    else {
420        return;
421    };
422
423    if let Err(err) = try_export_to_file(&path, memory) {
424        log::error!("Error saving to path '{}': {err}", path.display());
425    }
426}
427
428fn try_export_to_file(path: &Path, memory: &mut dyn DebugMemoryView) -> io::Result<()> {
429    use std::io::Write;
430
431    let file = File::create(path)?;
432    let mut writer = BufWriter::new(file);
433
434    let len = memory.len();
435    for address in 0..len {
436        writer.write_all(&[memory.read(address)])?;
437    }
438
439    Ok(())
440}