use eframe::egui;
use crate::system::{
    ThreadOrder,
    System,
};
use crate::uistate::{
    UIBackend,
    UIMode,
    ThreadID 
};
use crate::strings;
use crate::shortcuts;
use crate::util;
use crate::cpumap::CPUMap;
use crate::uifrontend::UIFrontend;
use crate::topologycache::{
    TopologyObjectID,
    TopologyObjectInfo,
    TopologyObjectChildren,
    TopologyObjectLabelStyle
};
use util::ShorcutControllable;

// TODO eventually move all direct interactions with System into Backend, but for that,
// Backend needs to keep/cache more System state

impl eframe::App for CPUMap {
    /// Main GUI update
    fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
        if self.backend.is_first_update() {
            match self.frontend {
                UIFrontend::Egui(..) => {
                    EguiFrontend::setup(ctx);
                },
                _ => {
                    panic!("egui update called for non-egui frontend");
                }
            }
        }

        // Handle any events from other threads.
        self.backend.handle_events(&mut self.system);

        match self.frontend {
            UIFrontend::Egui(ref mut egui) => {
                egui.update( &mut self.backend, ctx, frame, &mut self.system );
            },
            _ => {
                panic!("egui update called for non-egui frontend");
            }
        }
    }
}

pub struct EguiFrontend {
    /// Keyboard shortcuts controlling UI elements.
    shortcuts: shortcuts::Shortcuts,
    /// If true, show help information instead of CPU info on the right panel.
    ///
    /// Frontend-side instead of backend side as different frontends may lay out help/CPU info
    /// differently/maybe even in parallel.
    show_help:                 bool,
    /// True during the first frame/update so we can e.g. set initial focus, false after that.
    first_frame:               bool,
    /// Object ID and thread widget ID of the last mouse-hovered 'framed' topology object.
    ///
    /// The thread widget ID is None when rendering process topology, Some when rendering a
    /// thread's topology.
    /// 
    /// If the mouse is on top of multiple such overlapping objects, this is the deepest (e.g. an L2
    /// on top of an L3). Used to highlight clickable frames which can be used to select multiple
    /// cores at a time.
    last_hovered_frame_object: Option<(TopologyObjectID, Option<ThreadID>)>,
}

impl EguiFrontend {
    /// Construct without launching
    pub fn new() -> Self {
        Self {
            shortcuts:                 shortcuts::Shortcuts::new(),
            show_help:                 false,
            first_frame:               true,
            last_hovered_frame_object: None,
        }
    }

    /// Launch the frontend with an EFrame (EGUI) window.
    pub fn run(creator: eframe::AppCreator) 
        -> anyhow::Result<()>
    {
        let options = eframe::NativeOptions {
            // Currently `initial_window_size` seems to not work on Linux, may get fixed in future.
            initial_window_size: Some(egui::vec2(1024.0, 768.0)),
            min_window_size:     Some(egui::vec2(720.0, 320.0)),
            ..Default::default()
        };

        if let Err(e) = eframe::run_native("cpumap", options, creator) {
            return Err(anyhow::anyhow!("Failed to initialize GUI: {}", e));
        } else {
            Ok(())
        }
    }

    /// First-frame egui setup, configuring fonts/styles.
    pub fn setup(ctx: &egui::Context) {
        // Custom small font for L1/L2 caches.
        let small_cache_style = egui::TextStyle::Name("SmallCache".into());
        let small_font = egui::FontId {
            size: 11.0,
            family: egui::FontFamily::Proportional,
        };
        // Custom small font for thread topology views.
        let thread_view_style = egui::TextStyle::Name("ThreadView".into());
        let very_small_font = egui::FontId {
            // TODO bold with text size 10.0 as soon as egui supports bold
            size: 11.0,
            family: egui::FontFamily::Proportional,
        };

        // Add the small font to the GUI style.
        let mut style = (*ctx.style()).clone();
        style.text_styles.insert(small_cache_style, small_font);
        style.text_styles.insert(thread_view_style, very_small_font);
        ctx.set_style(style);
    }

    /// Run a single GUI update. Calls and updates backend state as needed.
    pub fn update(&mut self, ui: &mut UIBackend, ctx: &egui::Context, frame: &mut eframe::Frame, system: &mut System) {
        // Statusbar. First so it takes up the entire bottom of the screen.
        self.statusbar(ui, ctx);

        // Process selection panel.
        self.process_selection_panel(ui, ctx, system);
        if frame.info().window_info.size.x >= 800.0 {
            self.right_panel(ui, ctx, system);
        }
        // Get core status (after process selection so we always check currently selected PID,
        // avoiding a 1-frame delay)
        ui.update_current_process_data(system);

        // Main part of the UI; CPU topology view.
        self.topology_view(ui, ctx, system);
        ui.handle_timeouts(system);
        ui.end_update();
        self.first_frame = false;
    }

    /// Render the statusbar with current status message.
    fn statusbar(&self, backend: &UIBackend, ctx: &egui::Context) {
        egui::TopBottomPanel::bottom("statusbar").show(ctx, |ui| {
            ui.add_space(4.0);
            ui.label(backend.get_status().as_str());
        });
    }

    /// Render and handle input for the process selection panel.
    /// 
    /// Can change the currently selected process.
    fn process_selection_panel(&self, backend: &mut UIBackend, ctx: &egui::Context, system: &System) {
        if backend.get_hide_process_selection_panel() {
            egui::SidePanel::left("processes_dummy").exact_width(22.0).show(ctx, |ui| {
                ui.spacing_mut().button_padding.x = 3.0;
                *backend.get_hide_process_selection_panel_mut() = 
                    !ui.small_button(">>").on_hover_text(strings::TTIP_PROCLIST_SHOW).clicked_or_shortcut(ui, self.shortcuts.toggle_left);
            });
            return;
        }
        egui::SidePanel::left("processes").width_range(100.0 ..= 600.0).default_width(300.0).show(ctx, |ui| {
            ui.horizontal(|ui| {
                ui.spacing_mut().button_padding.x = 3.0;
                // Button to hide/collapse the process selection panel.
                *backend.get_hide_process_selection_panel_mut() = 
                    ui.small_button("<<").on_hover_text(strings::TTIP_PROCLIST_HIDE).clicked_or_shortcut(ui, self.shortcuts.toggle_left);
                let filter_label = ui.label(strings::filter());

                // Filter pattern entry.
                ui.text_edit_singleline(backend.get_filter_mut()).labelled_by(filter_label.id).focus_on_shortcut(ui, self.shortcuts.filter);
            });
            let processes = backend.get_filtered_user_processes( system );
            let total_rows = processes.len();
            // Process count label.
            ui.label(strings::processes(total_rows));
            let text_style = egui::TextStyle::Body;
            let row_height = ui.text_style_height(&text_style);
            // `show_rows` only renders the visible part of the list.
            egui::ScrollArea::both().auto_shrink([false, true]).show_rows(ui, row_height, total_rows, |ui, row_range| {
                // Display matching processes' ID, name.
                for row in row_range {
                    self.process_selection_entry(ui, backend, system, processes[row].0, System::get_process_name(processes[row].1));
                }
            });
            self.process_selection_panel_keyboard_control(ui, backend, system);
        });
    }

    /// Render and handle input for a single process entry in the process selection panel.
    ///
    /// `pid` is the ID of the process shown by the entry.
    fn process_selection_entry(&self, ui: &mut egui::Ui, backend: &mut UIBackend, system: &System, pid: &sysinfo::Pid, name: &str){
        ui.horizontal(|ui|{
            // First column, show PID.
            let pid_width = ui.label(format!("{}", pid)).rect.width();
            // Ensures entries are aligned *except* when some are very long.
            ui.add_space(f32::max(0.0, 50.0 - pid_width));

            let mut selected_pid = backend.get_selected_pid();
            let cmd = UIBackend::get_process_cmd(*pid, system);
            // Second column, process name.

            let response = ui.selectable_value(&mut selected_pid, *pid, name).on_hover_text(name);
            let name_width = response.rect.width();

            if response.clicked() {
                backend.set_next_mode_view( system, selected_pid );
            }

            // Ensures entries are aligned *except* when some are very long.
            ui.add_space(f32::max(0.0, 180.0 - name_width));
            // Third column, process command.
            ui.label(&cmd).on_hover_text(&cmd);
        });
    }

    /// Keyboard controls to allow moving between processes in process selection panel.
    ///
    /// Far from ideal, but we'd need some kind of ListView widget to do much better; e.g. 
    /// we could manually control scrolling with keyboard, but it'd conflict with mouse scrolling.
    fn process_selection_panel_keyboard_control(&self, ui: &egui::Ui, backend: &mut UIBackend, system: &System) {
        let process_prevnext_pressed =
            if      ui.input_mut(|i| i.consume_shortcut(&self.shortcuts.process_previous)) { Some(true) }
            else if ui.input_mut(|i| i.consume_shortcut(&self.shortcuts.process_next))     { Some(false) }
            else { None };
        backend.process_selection_panel_keyboard_control(process_prevnext_pressed, system);
    }

    /// Render the right-side (CPU info/help) panel.
    fn right_panel(&mut self, backend: &mut UIBackend, ctx: &egui::Context, system: &System) {
        // Create a SidePanel with the right alignment
        egui::SidePanel::right("right_panel")
            .resizable(false)
            .width_range(200.0..=290.0)
            .show(ctx, |ui| {
                ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
                    // Tweaking style to make the mode button look at least a bit 'tab-like', to avoid
                    // confusion with the 'run' button.
                    ui.style_mut().spacing.item_spacing = egui::vec2(1.0, 0.0);
                    ui.style_mut().override_text_style = Some(egui::TextStyle::Heading);
                    if ui.add(egui::Button::new("HELP").rounding(egui::Rounding::none())).on_hover_text(strings::TTIP_HELP).clicked_or_shortcut(ui, self.shortcuts.help)  {
                        self.show_help = true;
                    }
                    if ui.add(egui::Button::new("CPU").rounding(egui::Rounding::none())).on_hover_text(strings::TTIP_CPU).clicked_or_shortcut(ui, self.shortcuts.cpu)  {
                        self.show_help = false;
                    }
                });
                if self.show_help {
                    egui::ScrollArea::vertical().show(ui, |ui| {
                        self.help(backend, ui);
                    });
                } else {
                    self.cpu_information(ui, system);
                }
            });
    }

    /// Render help information on the right panel.
    fn help(&self, backend: &UIBackend, ui: &mut egui::Ui) {
        use egui_commonmark::*;
        let (mode, mode_shortcuts, is_run) = backend.get_current_mode_help();
        let main = format!("{}{}{}\n\
                            \n\
                            ---\n\
                            ", strings::HELP_MAIN, if is_run {strings::HELP_MAIN_RUN} else {"."}
                            , strings::HELP_MAIN_SHORTCUTS_GUI);
        // Stores image handles between each frame
        let mut cache = CommonMarkCache::default();
        CommonMarkViewer::new("help-main").show(ui, &mut cache, main.as_str());
        CommonMarkViewer::new("help-mode").show(ui, &mut cache, format!("{}\n{}", mode, mode_shortcuts).as_str());
    }

    /// Render CPU information on the right panel.
    fn cpu_information(&self, ui: &mut egui::Ui, system: &System) {
        // TODO print more CPU info.
        ui.heading(strings::CPU_INFO);
        for info in system.cpu_info() {
            ui.label(format!("{}", info.name));
        }
    }

    /// Render and handle input for the topology view (the main, central panel).
    ///
    /// Includes the process view and a collapsible section with thread views.
    fn topology_view(&mut self, backend: &mut UIBackend, ctx: &egui::Context, system: &mut System) {
        egui::TopBottomPanel::bottom("topofooter").show_separator_line(false).show(ctx, |ui| {
            self.topology_footer(backend, ui, system);
        });
        // Helps with making mode switching buttons more 'tab-like'/harder to confuse with 'run' and such.
        let central_margin = egui::Margin{top:1.0, bottom: 6.0, right: 6.0, left: 6.0};
        let central_frame  = egui::Frame::central_panel(&ctx.style()).inner_margin(central_margin);
        egui::CentralPanel::default().frame(central_frame).show(ctx, |ui| {
            egui::ScrollArea::both().show(ui, |ui| {
                self.topology_header(backend, ui, system);
                ui.horizontal(|ui|{
                    self.topology_grid(backend, ui, system);
                    self.topology_side_buttons(backend, ui, system);
                });
                self.topology_buttons_process(backend, ui, system);
                ui.separator();
                self.topology_threads(backend, ui, system);
            });
        });
    }

    /// Render and handle footer buttons below the topology grid.
    ///
    /// This is where the `apply` button is in edit mode.
    ///
    /// Returns the mode to switch to if the mode should change due to user interaction.
    fn topology_footer(&self, backend: &mut UIBackend, ui: &mut egui::Ui, system: &mut System) {
        ui.spacing_mut().button_padding = egui::vec2(8.0, 4.0);
        ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
            let current_order = backend.get_current_order();
            match backend.get_mode() {
                UIMode::Run(ref run)  => {
                    let run_enabled = run.is_run_enabled(current_order);
                    if let Some(duration_ratio) = run.wait_for_thread_start_ratio() {
                        // Waiting for threads to start, draw progress bar in place of the 'run' button.
                        ui.add(egui::ProgressBar::new(duration_ratio as f32));
                    } else if ui.add_enabled(run_enabled, egui::Button::new("run"))
                                .on_hover_text(strings::TTIP_APPLY_RUN)
                                .clicked_or_shortcut(ui, self.shortcuts.apply) 
                    {
                        backend.run_shell_command_with_process_affinity(system);
                    }
                },
                // Nothing to 'apply' or 'run' in view mode.
                UIMode::View(..)  => {},
                UIMode::Edit(ref edit)  => {
                    let apply_enabled = edit.is_apply_enabled();
                    if ui.add_enabled(apply_enabled, egui::Button::new("apply"))
                         .on_hover_text(strings::TTIP_APPLY)
                         .clicked_or_shortcut(ui, self.shortcuts.apply )
                    {
                        backend.edit_apply_clicked(system);
                    }
                },
            }
        });
    }

    /// Render and handle the header of the topology view, with mode label.
    fn topology_header(&self, backend: &mut UIBackend, ui: &mut egui::Ui, system: &System) {
        ui.horizontal(|ui|{
            match backend.get_mode_mut() {
                UIMode::Run(ref mut run)  => {
                    let heading = ui.label(strings::run()).on_hover_text(strings::TTIP_RUN);
                    // Entry of the command to run.
                    let run_edit = ui.add(egui::TextEdit::singleline(run.get_command_mut()).desired_width(180.0))
                      .labelled_by(heading.id)
                      .focus_on_shortcut(ui, self.shortcuts.run);
                    if self.first_frame {
                        run_edit.request_focus();
                    }
                    let subprocess_label = ui.label(strings::subprocess())
                                             .on_hover_text(strings::TTIP_SUBPROCESS);
                    ui.add(egui::TextEdit::singleline(run.get_subprocess_pattern_mut()).desired_width(240.0))
                      .labelled_by(subprocess_label.id)
                      .focus_on_shortcut(ui, self.shortcuts.subprocess);

                },
                UIMode::View(..)  => {ui.heading("VIEW").on_hover_text(strings::TTIP_VIEW);},
                UIMode::Edit(..)  => {ui.heading("EDIT").on_hover_text(strings::TTIP_EDIT);}, 
            }
            // Mode change buttons.
            ui.with_layout(egui::Layout::right_to_left(egui::Align::Min), |ui| {
                // Tweaking style to make the mode button look at least a bit 'tab-like', to avoid
                // confusion with the 'run' button.
                ui.style_mut().spacing.item_spacing = egui::vec2(1.0, 0.0);
                ui.style_mut().override_text_style = Some(egui::TextStyle::Heading);
                match backend.get_mode() {
                    UIMode::Run(..)  => {},
                    UIMode::View(ref view)  => {
                        if ui.add(egui::Button::new("EDIT").rounding(egui::Rounding::none()))
                             .on_hover_text(strings::TTIP_EDIT_SWITCH)
                             .clicked_or_shortcut(ui, self.shortcuts.mode_edit)  
                        {
                            backend.set_next_mode_edit(view.get_selected_process(), view.get_process_affinity().clone(), None);
                        }
                        if ui.add(egui::Button::new("RUN").rounding(egui::Rounding::none()))
                             .on_hover_text(strings::TTIP_RUN_SWITCH)
                             .clicked_or_shortcut(ui, self.shortcuts.mode_run)  
                        {
                            backend.set_next_mode_run(system);
                        }
                    },
                    UIMode::Edit(ref edit)  => {
                        if ui.add(egui::Button::new("VIEW").rounding(egui::Rounding::none()))
                              .on_hover_text(strings::TTIP_VIEW_SWITCH)
                              .clicked_or_shortcut(ui, self.shortcuts.mode_view)  
                        {
                            backend.set_next_mode_view(system, edit.get_selected_process());
                        }
                        if ui.add(egui::Button::new("RUN").rounding(egui::Rounding::none()))
                             .on_hover_text(strings::TTIP_RUN_SWITCH)
                             .clicked_or_shortcut(ui, self.shortcuts.mode_run)
                        {
                            backend.set_next_mode_run(system);
                        }
                    }, 
                }
            });
        });
        ui.separator();
    }

    /// Draw and handle input for buttons at the bottom of the process topology view.
    fn topology_buttons_process(&self, backend: &mut UIBackend, ui: &mut egui::Ui, system: &System) {
        ui.horizontal(|ui|{
            // Implement behavior of 'no HT' for each mode.
            if ui.button(strings::no_smt()).on_hover_text(strings::TTIP_NO_SMT).clicked_or_shortcut(ui, self.shortcuts.no_smt) {
                backend.disable_affinity_smt(system);
            }
            let current_order = backend.get_current_order();
            // In run mode, we also have the dragvalue of the delay to apply thread affinities.
            if let UIMode::Run(ref mut run) = backend.get_mode_mut() {
                if run.have_thread_affinities(current_order) {
                    ui.label(strings::apply_thread_affinities()) 
                      .on_hover_text(strings::TTIP_APPLY_THREAD_AFFINITIES);
                    ui.add(egui::DragValue::new(run.get_thread_apply_time_mut()).clamp_range(0.0 ..= 600.0).fixed_decimals(2).suffix("s").speed(0.01))
                      .on_hover_text(strings::TTIP_APPLY_THREAD_AFFINITIES)
                      .focus_on_shortcut(ui, self.shortcuts.thread_apply_time);
                }
            }
        });
    }

    /// Draw and handle input for buttons at the right side of the process topology view.
    fn topology_side_buttons(&self, backend: &mut UIBackend, ui: &mut egui::Ui, system: &mut System) {
        ui.vertical(|ui|{
            ui.strong(strings::THREAD_ORDER).on_hover_text(strings::TTIP_ORDER);
            let shortcut_pressed = ui.checkbox(backend.get_thread_order_ignore_main_thread_mut(), strings::IGNORE_MAIN)
                                     .on_hover_text(strings::TTIP_IGNORE_MAIN)
                                     .shortcut(ui, self.shortcuts.order_ignore_main);
            if shortcut_pressed {
                *backend.get_thread_order_ignore_main_thread_mut() = !backend.get_thread_order_ignore_main_thread();
            }
            let ignore_main_thread = backend.get_thread_order_ignore_main_thread();
            let mut current_order = backend.get_current_order();
            match backend.get_mode_mut() {
                UIMode::Run(ref mut run) => {
                    // In run mode: after changing thread order, apply the new order to
                    // `selected_cores_threads`, if any
                    if Self::thread_order_radios(ui, &self.shortcuts, &mut current_order) {
                        run.apply_thread_order(&current_order.unwrap(), system, ignore_main_thread);
                    }
                },
                UIMode::View(ref view) => {
                    current_order = Self::thread_order_buttons(ui, &self.shortcuts);
                    // Applying thread order in view mode means switching to edit mode.
                    if let Some(order) = current_order {
                        let (selected_cores_threads, selected_cores_process) = view.apply_thread_order(order, system, ignore_main_thread);
                        let selected_process = view.get_selected_process();
                        backend.set_next_mode_edit_hashmap(selected_process, selected_cores_process, selected_cores_threads);
                    }
                }, 
                UIMode::Edit(ref mut edit) => {
                    current_order = Self::thread_order_buttons(ui, &self.shortcuts);
                    if let Some(order) = current_order {
                        edit.apply_thread_order(order, system, ignore_main_thread);
                    }
                }
            }
            *backend.get_current_order_mut() = current_order;
        });
    }

    /// 'Run' mode: draw and handle logic for thread order selection radios.
    ///
    /// Returns true if there is a changed thread order to apply, false if there has been no change
    /// or if None was selected (nothing to apply).
    fn thread_order_radios(ui: &mut egui::Ui, shortcuts: &shortcuts::Shortcuts, order: &mut Option<ThreadOrder>) -> bool {
        // on 'click', set affinity of each thread in this process to consecutive logical cores.
        if ui.radio_value(order, Some(ThreadOrder::ByLCore), "HW threads").on_hover_text(strings::TTIP_ORDER_PU).clicked_or_shortcut(ui, shortcuts.order_pu) {
        } else if ui.radio_value(order, Some(ThreadOrder::ByCore), "cores").on_hover_text(strings::TTIP_ORDER_CORES).clicked_or_shortcut(ui, shortcuts.order_cores) {
        } else if ui.radio_value(order, Some(ThreadOrder::ByL3), "L3").on_hover_text(strings::TTIP_ORDER_L3).clicked_or_shortcut(ui, shortcuts.order_l3) {
        } else if ui.radio_value(order, None, "none").on_hover_text(strings::TTIP_ORDER_NONE).clicked_or_shortcut(ui, shortcuts.order_none) {
            return false
        } else {
            // Nothing clicked, nothing to apply right now, but we maintain the previously selected
            // thread order for unspecified threads.
            return false
        }
        true
    }

    /// Draw and handle input for the thread order buttons.
    ///
    /// Not taking &self to avoid a borrow conflict
    fn thread_order_buttons(ui: &mut egui::Ui, shortcuts: &shortcuts::Shortcuts) -> Option<ThreadOrder> {
        // on 'click', set affinity of each thread in this process to consecutive logical cores.
        if ui.button("HW threads").on_hover_text(strings::TTIP_ORDER_PU).clicked_or_shortcut(ui, shortcuts.order_pu) {
            Some(ThreadOrder::ByLCore)
        } else if ui.button("cores").on_hover_text(strings::TTIP_ORDER_CORES).clicked_or_shortcut(ui, shortcuts.order_cores) {
            Some(ThreadOrder::ByCore)
        } else if ui.button("L3").on_hover_text(strings::TTIP_ORDER_L3).clicked_or_shortcut(ui, shortcuts.order_l3) {
            Some(ThreadOrder::ByL3)
        } else {
            None
        }
    }

    /// Render and handle input for the CPU topology grid, either for a process or a thread.
    ///
    /// This handles selection of cores for setting affinity.
    fn topology_grid(&mut self, backend: &mut UIBackend, ui: &mut egui::Ui, system: &System) {
        ui.visuals_mut().override_text_color = Some(egui::Color32::BLACK);
        self.topology_object(backend, ui, system, system.cache().root());
        ui.visuals_mut().override_text_color = None;
    }

    /// Render children of a topology object.
    ///
    /// Handles spacing and (grid) positioning of the children. Recursively calls `topology_object`.
    fn topology_object_draw_children(&mut self,
        backend:     &mut UIBackend,
        ui:          &mut egui::Ui,
        children:    TopologyObjectChildren,
        system:      &System,
        object_type: &hwloc2::ObjectType,
        horizontal:  bool)
    {
        let old_spacing = ui.spacing().item_spacing;
        let first_type = children.peek().unwrap().object_type.clone();
        if *object_type == hwloc2::ObjectType::Core {
            // Minimal readable spacing between PUs in a core.
            ui.spacing_mut().item_spacing.x = 2.0;
        } else if children.clone().all(|child| child.object_type == first_type) {
            // No spacing between cores grouped with their immediate caches.
            ui.spacing_mut().item_spacing = egui::vec2(0.0, 0.0);
        }

        // Draw children in a grid.
        let (rows, cols) = util::squarish_grid_dimensions(children.len(), horizontal);
        ui.vertical(|ui|{
            for row in 0..rows {
                ui.horizontal(|ui|{
                    for col in 0..cols {
                        self.topology_object(backend, ui, system, children.get(row * cols + col));
                    }
                });
            }
        });

        // Restore spacing.
        ui.spacing_mut().item_spacing = old_spacing;
    }

    /// Render a topology object and recursively, the entire hierarchy under it.
    ///
    /// Also handles (recursively) input for any subobjects.
    fn topology_object(
        &mut self,
        backend: &mut UIBackend,
        ui:      &mut egui::Ui,
        system:  &System,
        obj:     &TopologyObjectInfo)
    {
        if obj.object_type == hwloc2::ObjectType::PU {
            self.topology_object_pu(backend, ui, system, obj);
            return;
        }

        let draw_label = |ui: &mut egui::Ui, first_group, thread_rendering: bool| {
            // If drawing multiple child subgroups, only draw label of the parent (this node) 
            // for the first subgroup, to avoid duplicating the label.
            if first_group && !thread_rendering {
                // For main process topology view, start with the label describing the object.
                Self::topology_object_label(ui, obj);
            }
        };

        // Draws children of the object, and if the object has siblings or is otherwise important to
        // differentiate, draws a frame around them. Also tracks mouse hover state over the frame.
        let mut draw_children_auto_frame = |ui: &mut egui::Ui, children, first_group| {
            if !obj.use_frame {
                draw_label(ui, first_group, backend.get_mode().currently_rendering_a_thread());
                self.topology_object_draw_children(backend, ui, children, system, &obj.object_type, obj.use_horizontal);
                return;
            } 

            let (frame, is_hovered) = self.object_frame(backend, obj, ui.style());
            if is_hovered {
                // Clear hovered object so hover does not stay once cursor leaves. Only cleared in
                // this branch as this is recursive - if we always cleared this, hover on a sibling
                // would be cleared by next sibling.
                self.last_hovered_frame_object = None;
            }

            let response = frame.show(ui, |ui|{
                draw_label(ui, first_group, backend.get_mode().currently_rendering_a_thread());
                self.topology_object_draw_children(backend, ui, children, system, &obj.object_type, obj.use_horizontal);
            }).response.interact(egui::Sense::union(egui::Sense::hover(), egui::Sense::click()));

            let hover_depth = self.last_hovered_frame_object.as_ref().map_or_else(||0, |(id, ..)| system.cache().get_by_id(id.clone()).depth);
            // The depth check allows inner/deeper objects to 'override' outer ones.
            if response.hovered() && obj.depth > hover_depth {
                // Deeper hovered frames take priority over shallower ones - e.g. if mouse is in
                // an L2 within an L3, highlight the L2 frame.
                self.last_hovered_frame_object = 
                    Some((obj.id(), backend.get_mode().currently_rendering_thread_id()));
            }
            if response.clicked() {
                backend.handle_group_click(system, obj);
            }
        };
        let children_iter_base = system.cache().get_children(obj);
        ui.vertical(|ui| {
            let mut first_group = true;

            // Draw children in subgroups of 'similar' topology objects.
            // Ensures e.g. big and LITTLE cores are drawn separately.
            let children_iter = children_iter_base.clone();
            children_iter.by_group(|children, last|{ 
                if obj.use_horizontal {
                    ui.horizontal(|ui| { draw_children_auto_frame(ui, children, first_group); });
                } else {
                    draw_children_auto_frame(ui, children, first_group);
                }     
                first_group = false;

                if !last {
                    ui.add_space(4.0);
                }
            });
        });
    }

    /// Render a PU (logical core) and handle any relevant input.
    ///
    /// Depending on mode, affinity or selection status of the PU is rendered.
    fn topology_object_pu(&self,
        backend: &mut UIBackend,
        ui:      &mut egui::Ui,
        system:  &System,
        obj:     &TopologyObjectInfo)
    {
        let pu_id = obj.os_index;
        let core_color = self.topology_pu_color(backend, pu_id);
        // Small buttons for PUs, these affect the size of the entire topology GUI.
        ui.spacing_mut().button_padding = egui::vec2(2.0, 1.0);

        let response = if backend.get_mode().currently_rendering_a_thread() {
            // Allows to unreasonably decrease button Y size to keep thread view compact.
            ui.spacing_mut().interact_size.y = 0.0;
            ui.style_mut().override_text_style = Some(egui::TextStyle::Name("ThreadView".into()));
            ui.add(egui::Button::new(format!("{:0>2}", pu_id)).fill(core_color).rounding(egui::Rounding::none()))
        } else {
            ui.add(egui::Button::new(format!("{:0>2}", pu_id)).fill(core_color).small().rounding(egui::Rounding::none()))
        };
        if response.clicked() {
            backend.handle_group_click(system, obj);
        }
        ui.style_mut().override_text_style = None;
    }

    /// Determine color to draw a PU button with, based on current mode and possibly affinity or process/thread core selection.
    fn topology_pu_color(&self, backend: &mut UIBackend, pu_id: u32) -> egui::Color32 {
        let colors = (egui::Color32::WHITE, egui::Color32::GREEN, egui::Color32::YELLOW, egui::Color32::from_rgb(255,196,0));
        backend.pu_color(pu_id, colors)
    }

    /// Render a label describing a topology object.
    /// 
    /// Usually used to render CPU names and cache sizes. Some objects (e.g. cores) have no label.
    fn topology_object_label(ui: &mut egui::Ui, obj: &TopologyObjectInfo) {
        if let Some(label) = &obj.label {
            match &label.style_override {
                Some(TopologyObjectLabelStyle::SmallCache) => {
                    ui.style_mut().override_text_style = Some(egui::TextStyle::Name("SmallCache".into()));
                },
                None => {}
            }
            if obj.use_horizontal && label.lines.len() > 1 {
                // If the object is in a horizontal group with its children (to the left 
                // of the group items), wrap its label to save horizontal space.
                ui.vertical(|ui|{
                    for line in label.lines.iter() {
                        ui.label(line);
                    }
                });
                // Hack to improve readability with compact spacing.
                ui.add_space(4.0);
            }
            else if label.lines.len() > 1 {
                ui.label(label.lines.join(" "));
            } else {
                ui.label(&label.lines[0]);
            }
            ui.style_mut().override_text_style = None;
        }
    }

    /// Build a Frame to draw around specified object. 
    ///
    /// Use and modify `base_style` to highlight the frame if it's mouse-hovered.
    /// Only called for objects where `topology_object_use_frame` returned true.
    fn object_frame(&self, backend: &UIBackend, obj: &TopologyObjectInfo, base_style: &egui::Style) -> (egui::Frame, bool) {
        let thread     = backend.get_mode().currently_rendering_a_thread();
        let mut frame = egui::Frame::group(base_style);
        frame.inner_margin = match (thread, obj.use_horizontal, obj.is_single_core) {
            // In process view, frames use the same margin everywhere - no need to be super-compact.
            (false, _,     _)     => egui::Margin::same(4.0),
            // Thread view, horizontal; wider X margins for easier clicking with no label text.
            (true,  true,  _)     => egui::Margin::symmetric(6.0, 3.0),
            // Thread view, vertical - top margin 'tab' for clickability.
            (true,  false, false) => egui::Margin{ left: 3.0, right: 3.0, top: 6.0, bottom: 3.0 },
            // Thread view, (vertical) core - save space around core.
            (true,  false, true)  => egui::Margin{ left: 1.0, right: 1.0, top: 6.0, bottom: 0.0 },
        };
        frame.stroke.color = egui::Color32::from_rgb(16,16, 16);
        frame.rounding     = egui::Rounding::none();

        // Hover determined on data from previous frame as a Frame has no builtin hover/highlight
        // functionality - we get hover information only *after* drawing it.
        let is_hovered = self.last_hovered_frame_object == 
                         Some((obj.id(), backend.get_mode().currently_rendering_thread_id()));
        let (fill, hover) = if is_hovered {
            (egui::Color32::from_rgb(24, 160, 24), true)
        } else {
            (egui::Color32::from_rgb(192, 192, 192), false)
        };
        frame.fill = fill;
        (frame, hover)
    }

    /// Draw and handle input for the collapsible thread views section under the process topology view.
    fn topology_threads(&mut self, backend: &mut UIBackend, ui: &mut egui::Ui, system: &System) {
        let default_open = false;
        let id = ui.make_persistent_id("threads");
        egui::collapsing_header::CollapsingState::load_with_default_open(ui.ctx(), id, default_open)
        .show_header(ui, |ui| {
            self.topology_threads_header(backend, ui, system);
        })
        .body(|ui| {
            match backend.get_mode() {
                UIMode::Run(..) => {
                    self.topology_threads_main_run(backend, ui, system);
                },
                UIMode::View(ref view) => {
                    self.topology_threads_main(backend, ui, system, view.get_selected_process());
                },
                UIMode::Edit(ref edit) => {
                    self.topology_threads_main(backend, ui, system, edit.get_selected_process());
                }
            }
        });
    }

    /// Draw and handle input for the header of the collapsible thread views section under the process topology view.
    fn topology_threads_header(&self, backend: &mut UIBackend, ui: &mut egui::Ui, system: &System) {
        ui.horizontal(|ui|{
            ui.strong(strings::THREADS).on_hover_text(strings::TTIP_THREADS);
            let thread_order_ignore_main_thread = backend.get_thread_order_ignore_main_thread();
            let current_order = *backend.get_current_order_mut();
            let mut thread_count = backend.get_thread_count();
            match backend.get_mode_mut() {
                UIMode::Run(ref mut run) => {
                    ui.add(egui::DragValue::new(&mut thread_count)
                                           .prefix(format!("{}: ", strings::THREADS_COUNT))
                                           .clamp_range(0 ..=1024))
                      .on_hover_text(strings::TTIP_THREADS_COUNT_RUN)
                      .focus_on_shortcut(ui, self.shortcuts.thread_count);

                    if ui.button("clear").on_hover_text(strings::TTIP_THREADS_CLEAR).clicked_or_shortcut(ui, self.shortcuts.clear_threads) {
                        run.clear_threads();
                    }

                    backend.run_update_thread_count(thread_count, system);
                },
                // No selection to 'clear' in view mode.
                UIMode::View(ref view) => {
                    ui.add_enabled(false, egui::DragValue::new(&mut thread_count).prefix(strings::THREADS_COUNT))
                      .on_hover_text(strings::TTIP_THREADS_COUNT);
                }, 
                UIMode::Edit(ref mut edit) => {
                    ui.add_enabled(false, egui::DragValue::new(&mut thread_count).prefix(strings::THREADS_COUNT))
                      .on_hover_text(strings::TTIP_THREADS_COUNT);
                    if ui.button(strings::CLEAR).on_hover_text(strings::TTIP_THREADS_CLEAR).clicked_or_shortcut(ui, self.shortcuts.clear_threads) {
                        edit.clear_threads();
                    }
                }
            }
        });
    }

    /// Get number of columns to use when drawing the threads view grid.
    ///
    /// Egui does not allow exact layout in advance as we don't know the size of
    /// individual thread views, but we can at least estimate it like this.
    fn thread_cols(ui: &egui::Ui, system: &System) -> usize {
        (ui.available_size().x / (29.5 * (system.logical_core_count() as f32).sqrt())).trunc() as usize
    }

    /// Draw and handle input for the thread views under the process topology view in Run mode.
    ///
    /// This view shows affinities for threads that *may* run once the process starts - they 
    /// are applied to threads in the order they are launched in.
    fn topology_threads_main_run(&mut self, backend: &mut UIBackend, ui: &mut egui::Ui, system: &System) {
        let cols = Self::thread_cols(ui, system);
        let thread_count = backend.get_thread_count();
        let mut start_thread_index = 0;
        while start_thread_index < thread_count {
            ui.horizontal(|ui| {
                for thread_index in start_thread_index..usize::min(start_thread_index + cols, thread_count) {
                    let name = if thread_index > 0 {format!("{}", thread_index)} else {String::from("[main]")};
                    ui.vertical(|ui|{
                        let UIMode::Run(ref run) = backend.get_mode_mut() else {
                            panic!("this must be called only in Run mode");
                        };

                        let edited = backend.get_mode().is_thread_edited(thread_index.try_into().unwrap());
                        self.thread_name(ui, name.as_str(), edited);
                        backend.get_mode_mut().set_currently_rendered_thread(Some(thread_index.try_into().unwrap()));
                        self.topology_grid(backend, ui, system);
                        backend.get_mode_mut().set_currently_rendered_thread(None);
                    });
                }
                start_thread_index += cols;
            });
        }
    }

    /// Draw the name label of a thread, with a star if the thread's affinities have been edited.
    fn thread_name(&self, ui: &mut egui::Ui, name: &str, edited: bool) {
        ui.style_mut().override_text_style = Some(egui::TextStyle::Name("ThreadView".into()));
        ui.horizontal(|ui|{
            ui.add(egui::Label::new(util::truncate_and_add_ellipsis(name, 12)).wrap(false)).on_hover_text(name);
            if edited {
                ui.colored_label(egui::Color32::YELLOW, "*");
            }
        });
        ui.style_mut().override_text_style = None;
    }

    /// Draw and handle input for the thread views under the process topology view in View/Edit modes.
    fn topology_threads_main(&mut self, backend: &mut UIBackend, ui: &mut egui::Ui, system: &System, pid: sysinfo::Pid) {
        // TODO Backend should keep all threads' data, so frontend doesn't need to get them directly
        // from System. Then this code should be rewritten to use Backend similarly tu `topology_threads_main_run`
        let threads = system.get_nonmain_threads_of(pid);
        let cols = Self::thread_cols(ui, system);
        let mut all_threads_iter = std::iter::once((pid, "[main]")).chain(threads.into_iter().flatten().map(|(tid, thread)| (tid, thread.name()))).peekable();
        while all_threads_iter.peek().is_some() {
            ui.horizontal(|ui| {
                // A row of `cols` threads
                for (tid, name) in all_threads_iter.by_ref().take(cols) {
                    ui.vertical(|ui|{
                        let edited = backend.get_mode().is_thread_edited(tid.as_u32());
                        self.thread_name(ui, name, edited);
                        backend.get_mode_mut().set_currently_rendered_thread(Some(tid.as_u32()));
                        self.topology_grid(backend, ui, system);
                        backend.get_mode_mut().set_currently_rendered_thread(None);
                    });
                }
            });
        }
    }
}
