1use crate::genesis::m68kdebug::M68kDebugMemoryMap;
2use crate::genesis::widgets::{BreakpointWindowResponse, BreakpointsWidget};
3use crate::{AddressSet, non_selectable_label};
4use egui::style::ScrollStyle;
5use egui::{Align, CentralPanel, Grid, Layout, Panel, RichText, TextEdit, Ui, Window};
6use egui_extras::{Column, TableBuilder};
7use genesis_core::api::debug::{GenesisDebugState, Z80BreakStatus, Z80Breakpoint};
8use z80_emu::{DisassembledInstruction, MemoryAccess, Z80};
9
10const DISASSEMBLY_WINDOW_TITLE: &str = "Z80 Disassembly";
11const BREAKPOINTS_WINDOW_TITLE: &str = "Z80 Breakpoints";
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Z80BreakCommand {
15    Pause,
16    Resume,
17    Step,
18}
19
20pub struct Z80DebugWindowState {
21    pub disassembly_open: bool,
22    pub disassembly_address: u16,
23    pub disassembly_end_address: Option<u16>,
24    pub disassembly_addr_changed: bool,
25    pub jump_to_address: String,
26    pub disassembly_table_offset: f32,
27    pub disassembly_table_height: f32,
28    pub disassembly_selected_pcs: AddressSet<u16>,
29    pub break_status_last_frame: Z80BreakStatus,
30    pub breakpoints_open: bool,
31    pub breakpoints: BreakpointsWidget<u16>,
32}
33
34impl Z80DebugWindowState {
35    pub fn new() -> Self {
36        Self {
37            disassembly_open: false,
38            disassembly_address: 0,
39            disassembly_end_address: None,
40            disassembly_addr_changed: false,
41            jump_to_address: String::new(),
42            disassembly_table_offset: 0.0,
43            disassembly_table_height: 1.0,
44            disassembly_selected_pcs: AddressSet::new(),
45            break_status_last_frame: Z80BreakStatus::default(),
46            breakpoints_open: false,
47            breakpoints: BreakpointsWidget::new("z80_breakpoints"),
48        }
49    }
50
51    pub fn open_disassembly_window(&mut self, ctx: &egui::Context) {
52        self.disassembly_open = true;
53        crate::move_to_top(ctx, DISASSEMBLY_WINDOW_TITLE);
54    }
55
56    pub fn open_breakpoints_window(&mut self, ctx: &egui::Context) {
57        self.breakpoints_open = true;
58        crate::move_to_top(ctx, BREAKPOINTS_WINDOW_TITLE);
59    }
60
61    fn maybe_change_disassembly_address(&mut self, address: u16) {
62        if self
63            .disassembly_end_address
64            .is_none_or(|end_addr| !(self.disassembly_address..end_addr).contains(&address))
65        {
66            self.change_disassembly_address(address);
67        }
68    }
69
70    fn change_disassembly_address(&mut self, address: u16) {
71        self.disassembly_address = address;
72        self.disassembly_addr_changed = true;
73    }
74}
75
76pub trait Z80MemoryMap {
77    fn peek(&self, address: u16) -> Option<u8>;
78}
79
80pub struct GenesisZ80MemoryMap<'a> {
81    pub audio_ram: &'a [u8],
82    pub z80_memory_bank: u32,
83    pub m68k_map: &'a dyn M68kDebugMemoryMap,
84}
85
86impl<'a> GenesisZ80MemoryMap<'a> {
87    pub fn new(debug_state: &'a GenesisDebugState, m68k_map: &'a dyn M68kDebugMemoryMap) -> Self {
88        Self {
89            audio_ram: debug_state.audio_ram.as_ref(),
90            z80_memory_bank: debug_state.z80_bank_number,
91            m68k_map,
92        }
93    }
94}
95
96impl Z80MemoryMap for GenesisZ80MemoryMap<'_> {
97    fn peek(&self, address: u16) -> Option<u8> {
98        match address {
99            0x0000..=0x3FFF => Some(self.audio_ram[(address & 0x1FFF) as usize]),
100            0x8000..=0xFFFF => {
101                let m68k_addr = (self.z80_memory_bank << 15) | u32::from(address & 0x7FFF);
102                self.m68k_map.peek(m68k_addr).map(|word| word.to_be_bytes()[(address & 1) as usize])
103            }
104            _ => None,
105        }
106    }
107}
108
109pub fn render_disassembly_window(
110    ctx: &egui::Context,
111    z80: &Z80,
112    memory_map: impl Z80MemoryMap,
113    state: &mut Z80DebugWindowState,
114    break_status: Z80BreakStatus,
115    handle_command: Option<impl FnMut(Z80BreakCommand)>,
116) {
117    if break_status.breaking && break_status != state.break_status_last_frame {
118        let mut move_to_pc = break_status.pc;
119        for previous_pc in break_status.previous_pcs {
120            if previous_pc > move_to_pc || previous_pc < move_to_pc.saturating_sub(16) {
121                break;
122            }
123            move_to_pc = previous_pc;
124        }
125
126        state.maybe_change_disassembly_address(move_to_pc);
127        state.disassembly_open = true;
128        crate::move_to_top(ctx, DISASSEMBLY_WINDOW_TITLE);
129    }
130    state.break_status_last_frame = break_status;
131
132    let mut open = state.disassembly_open;
133    Window::new(DISASSEMBLY_WINDOW_TITLE)
134        .open(&mut open)
135        .constrain(false)
136        .resizable([true, true])
137        .default_pos(crate::rand_window_pos())
138        .default_width(700.0)
139        .show(ctx, |ui| {
140            if let Some(handle_command) = handle_command {
141                render_disasm_top_panel(state, handle_command, ui);
142            }
143
144            render_disasm_right_panel(z80, state, ui);
145            render_disasm_central_panel(z80, memory_map, state, break_status, ui);
146        });
147    state.disassembly_open = open;
148}
149
150fn render_disasm_top_panel(
151    state: &mut Z80DebugWindowState,
152    mut handle_command: impl FnMut(Z80BreakCommand),
153    ui: &mut Ui,
154) {
155    Panel::top("z80_top_panel").show_inside(ui, |ui| {
156        ui.horizontal(|ui| {
157            if ui.button("Pause").clicked() {
158                handle_command(Z80BreakCommand::Pause);
159            }
160
161            if ui.button("Resume").clicked() {
162                handle_command(Z80BreakCommand::Resume);
163            }
164
165            if ui.button("Step").clicked() {
166                handle_command(Z80BreakCommand::Step);
167            }
168
169            ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
170                if ui.button("Breakpoints").clicked() {
171                    state.open_breakpoints_window(ui.ctx());
172                }
173            });
174        });
175
176        ui.add_space(3.0);
177    });
178}
179
180fn render_disasm_right_panel(z80: &Z80, state: &mut Z80DebugWindowState, ui: &mut Ui) {
181    Panel::right("z80_right_panel").show_inside(ui, |ui| {
182        ui.horizontal(|ui| {
183            let text_resp =
184                ui.add(TextEdit::singleline(&mut state.jump_to_address).desired_width(40.0));
185            let button_resp = ui.button("Jump to address");
186
187            let should_jump = button_resp.clicked()
188                || (text_resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)));
189            if should_jump && let Ok(address) = u16::from_str_radix(&state.jump_to_address, 16) {
190                state.change_disassembly_address(address);
191            }
192        });
193
194        ui.add_space(3.0);
195
196        if ui.button("Jump to PC").clicked() {
197            state.change_disassembly_address(z80.pc());
198        }
199
200        ui.separator();
201
202        let registers = z80.registers();
203        Grid::new("z80_registers_grid").show(ui, |ui| {
204            for ((label0, value0), (label1, value1)) in [
205                (("A", registers.a), ("F", u8::from(registers.f))),
206                (("B", registers.b), ("C", registers.c)),
207                (("D", registers.d), ("E", registers.e)),
208                (("H", registers.h), ("L", registers.l)),
209            ] {
210                ui.label(label0);
211                ui.label(monospace_u8(value0));
212                ui.label(label1);
213                ui.label(monospace_u8(value1));
214                ui.end_row();
215            }
216
217            ui.label("HL");
218            ui.label(monospace_u16(u16::from_be_bytes([registers.h, registers.l])));
219            ui.label("SP");
220            ui.label(monospace_u16(registers.sp));
221            ui.end_row();
222
223            ui.label("IX");
224            ui.label(monospace_u16(registers.ix));
225            ui.label("IY");
226            ui.label(monospace_u16(registers.iy));
227            ui.end_row();
228
229            ui.label("PC");
230            ui.label(monospace_u16(registers.pc));
231            ui.end_row();
232
233            ui.label("IFF1");
234            ui.label(monospace_bool(registers.iff1));
235            ui.label("IFF2");
236            ui.label(monospace_bool(registers.iff2));
237            ui.end_row();
238
239            ui.label("I");
240            ui.label(monospace_u8(registers.i));
241            ui.label("R");
242            ui.label(monospace_u8(registers.r));
243            ui.end_row();
244
245            for ((label0, value0), (label1, value1)) in [
246                (("A'", registers.ap), ("F'", u8::from(registers.fp))),
247                (("B'", registers.bp), ("C'", registers.cp)),
248                (("D'", registers.dp), ("E'", registers.ep)),
249                (("H'", registers.hp), ("L'", registers.lp)),
250            ] {
251                ui.label(label0);
252                ui.label(monospace_u8(value0));
253                ui.label(label1);
254                ui.label(monospace_u8(value1));
255                ui.end_row();
256            }
257        });
258
259        ui.add_space(3.0);
260
261        ui.label(
262            RichText::new(format!(
263                "C={} N={} V={} H={} Z={} S={}",
264                u8::from(registers.f.carry),
265                u8::from(registers.f.subtract),
266                u8::from(registers.f.overflow),
267                u8::from(registers.f.half_carry),
268                u8::from(registers.f.zero),
269                u8::from(registers.f.sign),
270            ))
271            .monospace(),
272        );
273    });
274}
275
276fn render_disasm_central_panel(
277    z80: &Z80,
278    memory_map: impl Z80MemoryMap,
279    state: &mut Z80DebugWindowState,
280    break_status: Z80BreakStatus,
281    ui: &mut Ui,
282) {
283    let ctx = ui.ctx().clone();
284
285    CentralPanel::default().show_inside(ui, |ui| {
286        ui.spacing_mut().scroll = ScrollStyle { bar_width: 10.0, ..ScrollStyle::solid() };
287
288        let mut table_builder = TableBuilder::new(ui)
289            .column(Column::auto().at_least(10.0))
290            .column(Column::auto().at_least(60.0))
291            .column(Column::auto().at_least(150.0))
292            .column(Column::remainder())
293            .striped(true)
294            .sense(egui::Sense::click());
295
296        if state.disassembly_addr_changed {
297            state.disassembly_addr_changed = false;
298            table_builder = table_builder.scroll_to_row(0, Some(Align::Min));
299        } else if crate::window_on_top(&ctx, DISASSEMBLY_WINDOW_TITLE) {
300            let keys = crate::scroll_keys_pressed(&ctx);
301            if let Some(offset) = keys.relative_scroll_offset(state.disassembly_table_height) {
302                table_builder =
303                    table_builder.vertical_scroll_offset(state.disassembly_table_offset + offset);
304            }
305        }
306
307        let z80_pc = if break_status.breaking { break_status.pc } else { z80.pc() };
308
309        let highlight_color = crate::highlight_color(ctx.theme());
310
311        let scroll_output = table_builder.body(|mut body| {
312            let mut pc = state.disassembly_address;
313            let mut instruction = DisassembledInstruction::new();
314
315            for _ in 0..100 {
316                body.row(15.0, |mut row| {
317                    let is_pc_row = pc == z80_pc;
318                    let original_pc = pc;
319
320                    row.set_selected(state.disassembly_selected_pcs.contains(pc));
321
322                    row.col(|ui| {
323                        state.breakpoints.render_clickable_widget(
324                            pc,
325                            format!("z80_break_row_{pc}"),
326                            ui,
327                        );
328
329                        if is_pc_row {
330                            ui.add(non_selectable_label(
331                                RichText::new("→").monospace().color(highlight_color),
332                            ));
333                        }
334                    });
335
336                    row.col(|ui| {
337                        let mut text = monospace_u16(pc);
338                        if is_pc_row {
339                            text = text.color(highlight_color);
340                        }
341
342                        ui.add(non_selectable_label(text));
343                    });
344
345                    z80_emu::disassemble_into(&mut instruction, pc, || {
346                        let byte = memory_map.peek(pc).unwrap_or(0xFF);
347                        pc = pc.wrapping_add(1);
348                        byte
349                    });
350
351                    row.col(|ui| {
352                        let mut text = RichText::new(&instruction.text).monospace();
353                        if is_pc_row {
354                            text = text.color(highlight_color);
355                        }
356
357                        ui.add(non_selectable_label(text));
358                    });
359
360                    row.col(|ui| {
361                        if let Some(memory_access) = instruction.memory_access {
362                            let address = memory_access.resolve_address(z80);
363                            let value =
364                                memory_map.peek(address).map(|value| format!("0x{value:02X}"));
365
366                            let text = match (memory_access, value) {
367                                (MemoryAccess::Absolute(..), Some(value)) => {
368                                    monospace_text(format!("; = {value}"))
369                                }
370                                (MemoryAccess::Absolute(..), None) => return,
371                                (MemoryAccess::Indirect(..), Some(value)) => {
372                                    monospace_text(format!("; ${address:04X} = {value}"))
373                                }
374                                (MemoryAccess::Indirect(..), None) => {
375                                    monospace_text(format!("; ${address:04X}"))
376                                }
377                            };
378                            ui.add(non_selectable_label(text));
379                        }
380                    });
381
382                    if row.response().clicked() {
383                        state
384                            .disassembly_selected_pcs
385                            .handle_click(original_pc, ctx.input(|i| i.modifiers));
386                    }
387                });
388            }
389
390            state.disassembly_end_address = Some(pc);
391        });
392        state.disassembly_table_offset = scroll_output.state.offset.y;
393        state.disassembly_table_height = scroll_output.inner_rect.height();
394    });
395}
396
397pub fn render_breakpoints_window(
398    ctx: &egui::Context,
399    state: &mut Z80DebugWindowState,
400    update_breakpoints: impl FnOnce(Vec<Z80Breakpoint>),
401) {
402    let response = state.breakpoints.show_window(
403        ctx,
404        BREAKPOINTS_WINDOW_TITLE,
405        &mut state.breakpoints_open,
406        |_| BreakpointWindowResponse::NotChanged,
407    );
408    if response == BreakpointWindowResponse::Changed {
409        let breakpoints = state
410            .breakpoints
411            .breakpoints()
412            .iter()
413            .map(|breakpoint| Z80Breakpoint {
414                start_address: breakpoint.start_address,
415                end_address: breakpoint.end_address,
416                read: breakpoint.read,
417                write: breakpoint.write,
418                execute: breakpoint.execute,
419            })
420            .collect();
421
422        update_breakpoints(breakpoints);
423    }
424}
425
426fn monospace_text(value: impl Into<String>) -> RichText {
427    RichText::new(value).monospace()
428}
429
430fn monospace_bool(value: bool) -> RichText {
431    RichText::new(["0", "1"][usize::from(value)]).monospace()
432}
433
434fn monospace_u8(value: u8) -> RichText {
435    RichText::new(format!("{value:02X}")).monospace()
436}
437
438fn monospace_u16(value: u16) -> RichText {
439    RichText::new(format!("{value:04X}")).monospace()
440}