use egui::{Color32, DragValue, Rect, ScrollArea, Sense, Ui, Vec2};

use crate::allocator::{free_list_allocator::MemoryChunk, resolve_backtrace};

use super::ColorScheme;

pub(crate) struct MemoryChunksVisualizationSettings {
    pub width_in_bytes: u64,
    pub show_backtraces: bool,
}

impl Default for MemoryChunksVisualizationSettings {
    fn default() -> Self {
        Self {
            width_in_bytes: 1024,
            show_backtraces: false,
        }
    }
}

impl MemoryChunksVisualizationSettings {
    pub fn ui(&mut self, ui: &mut Ui, store_stack_traces: bool) {
        if store_stack_traces {
            ui.checkbox(&mut self.show_backtraces, "Show backtraces");
        }

        // Slider for changing the 'zoom' level of the visualizer.
        const BYTES_PER_UNIT_MIN: i32 = 1;
        const BYTES_PER_UNIT_MAX: i32 = 1024 * 1024;

        ui.horizontal(|ui| {
            ui.add(
                DragValue::new(&mut self.width_in_bytes)
                    .clamp_range(BYTES_PER_UNIT_MIN..=BYTES_PER_UNIT_MAX)
                    .speed(10.0),
            );
            ui.label("Bytes per line");
        });
    }
}

pub(crate) fn render_memory_chunks_ui<'a>(
    ui: &mut Ui,
    color_scheme: &ColorScheme,
    settings: &MemoryChunksVisualizationSettings,
    total_size_in_bytes: u64,
    data: impl IntoIterator<Item = &'a MemoryChunk>,
) {
    let line_height = ui.text_style_height(&egui::TextStyle::Body);
    let number_of_rows =
        (total_size_in_bytes as f32 / settings.width_in_bytes as f32).ceil() as usize;

    ScrollArea::new([false, true]).show_rows(ui, line_height, number_of_rows, |ui, range| {
        // Let range be in bytes
        let start_in_bytes = range.start as u64 * settings.width_in_bytes;
        let end_in_bytes = range.end as u64 * settings.width_in_bytes;

        let mut data = data
            .into_iter()
            .filter(|chunk| {
                (chunk.offset + chunk.size) > start_in_bytes && chunk.offset < end_in_bytes
            })
            .collect::<Vec<_>>();
        data.sort_by_key(|chunk| chunk.offset);

        let screen_width = ui.available_width();
        let mut cursor_idx = 0;
        let mut bytes_required = data[cursor_idx].offset + data[cursor_idx].size - start_in_bytes;

        for _ in range {
            ui.horizontal(|ui| {
                let mut bytes_left = settings.width_in_bytes;
                let mut cursor = ui.cursor().min;

                while cursor_idx < data.len() && bytes_left > 0 {
                    // Block is depleted, so reset for more chunks
                    while bytes_required == 0 {
                        cursor_idx += 1;
                        if cursor_idx < data.len() {
                            bytes_required = data[cursor_idx].size;
                        }
                        continue;
                    }

                    let bytes_used = bytes_required.min(bytes_left);
                    let width_used =
                        bytes_used as f32 * screen_width / settings.width_in_bytes as f32;

                    // Draw the rectangle
                    let resp = ui.allocate_rect(
                        Rect::from_min_size(cursor, Vec2::new(width_used, line_height)),
                        Sense::click(),
                    );

                    if ui.is_rect_visible(resp.rect) {
                        ui.painter().rect(
                            resp.rect,
                            egui::Rounding::none(),
                            color_scheme
                                .get_allocation_type_color(data[cursor_idx].allocation_type),
                            egui::Stroke::new(1.0, Color32::BLACK),
                        );

                        resp.on_hover_ui_at_pointer(|ui| {
                            let chunk = &data[cursor_idx];
                            ui.label(format!("id: {}", chunk.chunk_id));
                            ui.label(format!("offset: 0x{:x}", chunk.offset));
                            ui.label(format!("size: 0x{:x}", chunk.size));
                            ui.label(format!(
                                "allocation_type: {}",
                                chunk.allocation_type.as_str()
                            ));
                            if let Some(name) = &chunk.name {
                                ui.label(format!("name: {}", name));
                            }
                            if settings.show_backtraces && chunk.backtrace.is_some() {
                                ui.label(format!(
                                    "backtrace: {}",
                                    resolve_backtrace(&chunk.backtrace)
                                ));
                            }
                        });
                    }

                    // Update our cursors
                    cursor.x += width_used;
                    bytes_left -= bytes_used;
                    bytes_required -= bytes_used;
                }
            });
        }
    });
}
