1use egui::{FontFamily, Grid, RichText, TextEdit, Ui, Window};
2use std::ops::{BitOr, BitOrAssign};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct GenericBreakpoint<T> {
6    pub start_address: T,
7    pub end_address: T,
8    pub read: bool,
9    pub write: bool,
10    pub execute: bool,
11}
12
13#[derive(Debug, Clone)]
14pub struct BreakpointsWidget<T> {
15    id: String,
16    breakpoints: Vec<GenericBreakpoint<T>>,
17    breakpoints_changed: bool,
18    start_address: String,
19    end_address: String,
20    read: bool,
21    write: bool,
22    execute: bool,
23}
24
25impl<T> BreakpointsWidget<T> {
26    pub fn new(id: impl Into<String>) -> Self {
27        Self {
28            id: id.into(),
29            breakpoints: Vec::new(),
30            breakpoints_changed: false,
31            start_address: String::new(),
32            end_address: String::new(),
33            read: true,
34            write: false,
35            execute: false,
36        }
37    }
38}
39
40pub trait BreakpointAddress: Sized + Copy + Eq + Ord {
41    fn from_hex_str(value: &str) -> Option<Self>;
42
43    fn format_hex(self) -> String;
44}
45
46impl BreakpointAddress for u16 {
47    fn from_hex_str(value: &str) -> Option<Self> {
48        Self::from_str_radix(value, 16).ok()
49    }
50
51    fn format_hex(self) -> String {
52        format!("{self:04X}")
53    }
54}
55
56impl BreakpointAddress for u32 {
57    fn from_hex_str(value: &str) -> Option<Self> {
58        Self::from_str_radix(value, 16).ok()
59    }
60
61    fn format_hex(self) -> String {
62        format!("{self:08X}")
63    }
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
67pub struct U24(u32);
68
69impl U24 {
70    pub fn new(value: u32) -> Self {
71        Self(value & 0xFFFFFF)
72    }
73
74    pub fn get(self) -> u32 {
75        self.0
76    }
77}
78
79impl BreakpointAddress for U24 {
80    fn from_hex_str(value: &str) -> Option<Self> {
81        u32::from_hex_str(value).map(Self::new)
82    }
83
84    fn format_hex(self) -> String {
85        format!("{:06X}", self.0)
86    }
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub enum BreakpointWindowResponse {
91    NotChanged,
92    Changed,
93}
94
95impl BreakpointWindowResponse {
96    pub fn from_changed(changed: bool) -> Self {
97        if changed { Self::Changed } else { Self::NotChanged }
98    }
99}
100
101impl BitOr for BreakpointWindowResponse {
102    type Output = Self;
103
104    fn bitor(self, rhs: Self) -> Self::Output {
105        match (self, rhs) {
106            (Self::Changed, _) | (_, Self::Changed) => Self::Changed,
107            (Self::NotChanged, Self::NotChanged) => Self::NotChanged,
108        }
109    }
110}
111
112impl BitOrAssign for BreakpointWindowResponse {
113    fn bitor_assign(&mut self, rhs: Self) {
114        *self = *self | rhs;
115    }
116}
117
118impl<T> BreakpointsWidget<T>
119where
120    T: BreakpointAddress,
121{
122    #[must_use]
123    pub fn show_window(
124        &mut self,
125        ctx: &egui::Context,
126        window_title: impl Into<egui::WidgetText>,
127        window_open: &mut bool,
128        additional_breakpoints: impl FnOnce(&mut Ui) -> BreakpointWindowResponse,
129    ) -> BreakpointWindowResponse {
130        let mut response = BreakpointWindowResponse::from_changed(self.breakpoints_changed);
131        self.breakpoints_changed = false;
132
133        Window::new(window_title)
134            .open(window_open)
135            .constrain(false)
136            .resizable([true, true])
137            .default_pos(crate::rand_window_pos())
138            .show(ctx, |ui| {
139                response |= self.render(ui);
140
141                response |= additional_breakpoints(ui);
142            });
143
144        response
145    }
146
147    #[must_use]
148    pub fn breakpoints(&self) -> &[GenericBreakpoint<T>] {
149        &self.breakpoints
150    }
151
152    fn render(&mut self, ui: &mut Ui) -> BreakpointWindowResponse {
153        let initial_breakpoints = self.breakpoints.clone();
154
155        if !self.breakpoints.is_empty() {
156            Grid::new(format!("{}_breakpoints", self.id)).show(ui, |ui| {
157                ui.heading("Addresses");
158                ui.heading("R");
159                ui.heading("W");
160                ui.heading("X");
161                ui.label("");
162                ui.end_row();
163
164                let mut remove_idx: Option<usize> = None;
165                for (i, breakpoint) in self.breakpoints.iter_mut().enumerate() {
166                    if breakpoint.start_address == breakpoint.end_address {
167                        ui.label(
168                            RichText::new(format!("${}", breakpoint.start_address.format_hex()))
169                                .family(FontFamily::Monospace),
170                        );
171                    } else {
172                        ui.label(
173                            RichText::new(format!(
174                                "${}-${}",
175                                breakpoint.start_address.format_hex(),
176                                breakpoint.end_address.format_hex()
177                            ))
178                            .family(FontFamily::Monospace),
179                        );
180                    }
181
182                    for value in
183                        [&mut breakpoint.read, &mut breakpoint.write, &mut breakpoint.execute]
184                    {
185                        ui.checkbox(value, "");
186                    }
187
188                    if ui.button("Remove").clicked() {
189                        remove_idx = Some(i);
190                    }
191
192                    ui.end_row();
193                }
194
195                if let Some(remove_idx) = remove_idx
196                    && remove_idx < self.breakpoints.len()
197                {
198                    self.breakpoints.remove(remove_idx);
199                }
200
201                macro_rules! check_all_button {
202                    ($label:literal, $field:ident = $value:literal) => {
203                        if ui.button($label).clicked() {
204                            for breakpoint in &mut self.breakpoints {
205                                breakpoint.$field = $value;
206                            }
207                        }
208                    };
209                }
210
211                ui.label("");
212                check_all_button!("Check all", read = true);
213                check_all_button!("Check all", write = true);
214                check_all_button!("Check all", execute = true);
215                ui.end_row();
216
217                ui.label("");
218                check_all_button!("Uncheck all", read = false);
219                check_all_button!("Uncheck all", write = false);
220                check_all_button!("Uncheck all", execute = false);
221                ui.end_row();
222            });
223
224            ui.separator();
225        }
226
227        let mut enter_pressed = false;
228
229        ui.heading("Add Breakpoint");
230        ui.horizontal(|ui| {
231            ui.label("$");
232            let start_resp =
233                ui.add(TextEdit::singleline(&mut self.start_address).desired_width(80.0));
234            ui.label("-");
235            ui.label("$");
236            let end_resp = ui.add(TextEdit::singleline(&mut self.end_address).desired_width(80.0));
237
238            ui.checkbox(&mut self.read, "Read");
239            ui.checkbox(&mut self.write, "Write");
240            ui.checkbox(&mut self.execute, "Execute");
241
242            enter_pressed = (start_resp.lost_focus() || end_resp.lost_focus())
243                && ui.input(|i| i.key_pressed(egui::Key::Enter));
244        });
245
246        let button_resp = ui.button("Add");
247        if (button_resp.clicked() || enter_pressed)
248            && let Some(start_address) = T::from_hex_str(&self.start_address)
249        {
250            if self.end_address.is_empty() {
251                self.breakpoints.push(GenericBreakpoint {
252                    start_address,
253                    end_address: start_address,
254                    read: self.read,
255                    write: self.write,
256                    execute: self.execute,
257                });
258
259                self.start_address.clear();
260            } else if let Some(end_address) = T::from_hex_str(&self.end_address)
261                && end_address >= start_address
262            {
263                self.breakpoints.push(GenericBreakpoint {
264                    start_address,
265                    end_address,
266                    read: self.read,
267                    write: self.write,
268                    execute: self.execute,
269                });
270
271                self.start_address.clear();
272                self.end_address.clear();
273            }
274        }
275
276        if initial_breakpoints != self.breakpoints {
277            BreakpointWindowResponse::Changed
278        } else {
279            BreakpointWindowResponse::NotChanged
280        }
281    }
282
283    pub fn has_execute_breakpoint(&self, address: T) -> bool {
284        self.breakpoints.iter().any(|breakpoint| {
285            breakpoint.execute
286                && breakpoint.start_address == address
287                && breakpoint.end_address == address
288        })
289    }
290
291    pub fn toggle_execute_breakpoint(&mut self, address: T) {
292        let mut value: Option<bool> = None;
293        let mut to_remove = Vec::new();
294        for (i, breakpoint) in self.breakpoints.iter_mut().enumerate() {
295            if breakpoint.start_address != address || breakpoint.end_address != address {
296                continue;
297            }
298
299            let value = *value.get_or_insert(!breakpoint.execute);
300            breakpoint.execute = value;
301
302            if !breakpoint.read && !breakpoint.write && !breakpoint.execute {
303                to_remove.push(i);
304            }
305        }
306
307        if value.is_none() {
308            self.breakpoints.push(GenericBreakpoint {
309                start_address: address,
310                end_address: address,
311                read: false,
312                write: false,
313                execute: true,
314            });
315        }
316
317        for remove_idx in to_remove.into_iter().rev() {
318            if remove_idx < self.breakpoints.len() {
319                self.breakpoints.remove(remove_idx);
320            }
321        }
322
323        self.breakpoints_changed = true;
324    }
325
326    // Render a widget that can be clicked on to toggle execute breakpoints at a specific address.
327    //
328    // Draws a red circle if there exists a breakpoint at the specified address, otherwise draws
329    // an empty circle whenever the widget is hovered over.
330    pub fn render_clickable_widget(
331        &mut self,
332        address: T,
333        interact_id: impl Into<egui::Id>,
334        ui: &mut Ui,
335    ) {
336        let max_rect = ui.max_rect();
337
338        let interact_resp = ui.interact(max_rect, interact_id.into(), egui::Sense::click());
339
340        let has_execute_breakpoint = self.has_execute_breakpoint(address);
341        if interact_resp.clicked() {
342            self.toggle_execute_breakpoint(address);
343        } else if interact_resp.hovered() || has_execute_breakpoint {
344            let fill_color = if interact_resp.hovered() && has_execute_breakpoint {
345                egui::Color32::LIGHT_RED
346            } else if has_execute_breakpoint {
347                egui::Color32::RED
348            } else {
349                ui.visuals().panel_fill
350            };
351            let stroke_color = ui.visuals().text_color();
352
353            let radius = if max_rect.width() < max_rect.height() {
354                0.5 * max_rect.width()
355            } else {
356                0.5 * max_rect.height()
357            };
358
359            ui.painter().circle(
360                [
361                    0.5 * max_rect.width() + max_rect.left(),
362                    0.5 * max_rect.height() + max_rect.top(),
363                ]
364                .into(),
365                radius,
366                fill_color,
367                egui::Stroke::new(1.0_f32, stroke_color),
368            );
369        }
370    }
371}