use std::io::{self, stdout};
use std::rc::Rc;
use log::*;
use crate::uistate::{
    UIBackend,
    UIMode,
    THREAD_APPLY_TIME_DEFAULT
};
use crate::system::{
    ThreadOrder,
    System,
};
use crate::strings;
use crossterm::{
    event::{
        self,
        Event,
        KeyEvent,
        KeyCode,
        KeyModifiers,
        DisableMouseCapture,
        EnableMouseCapture
    },
    terminal::{
        disable_raw_mode,
        enable_raw_mode,
        EnterAlternateScreen,
        LeaveAlternateScreen,
    },
};
use crate::topologycache::{
    TopologyObjectID,
    TopologyObjectInfo,
    TopologyObjectLabelStyle
};
use crate::util;
use ratatui::{prelude::*, widgets::*};
use tui_textarea::{Input, Key, TextArea};

/// Shortcut to split a rect into vertical chunks using ratatui layouting.
fn chunks_vertical(rect: Rect, constraints: &[Constraint]) -> Rc<[Rect]> { 
    // TODO ratatui 0.26: use Layout::vertical()
    Layout::default().direction(Direction::Vertical).constraints(constraints).split(rect)
}

/// Shortcut to split a rect into horizontal chunks using ratatui layouting.
fn chunks_horizontal(rect: Rect, constraints: &[Constraint]) -> Rc<[Rect]> { 
    // TODO ratatui 0.26: use Layout::vertical()
    Layout::default().direction(Direction::Horizontal).constraints(constraints).split(rect)
}

/// Shortcut to draw a scrollbar in a consistent 'cpumap style'.
fn scrollbar(frame: &mut Frame, rect: Rect, state: &mut ScrollbarState) {
    Scrollbar::default()
        .begin_symbol(None)
        .end_symbol(None)
        .track_symbol(None)
        .thumb_symbol("▐")
        .render(rect, frame.buffer_mut(), state);
}

/// Fills a rectangle with spaces, used to clear space to be overdrawn.
fn fill_rect(frame: &mut Frame, rect: Rect) {
    // TODO ratatui 0.26: use rect.positions() instead of nested for loop
    for fix_y in rect.y .. rect.y + rect.height {
        for fix_x in rect.x .. rect.x + rect.width {
            frame.buffer_mut().get_mut(fix_x, fix_y).set_char(' ');
        }
    }
}

/// Base function for `radio_value()` and `checkbox()`.
fn checkbox_base(frame: &mut Frame, rect: Rect, selected: bool, label: &str, hint: Option<&str>, check: &str) {
    let hint_part = if let Some(hint) = hint {format!("<{}> ", hint)} else {"".to_string()};
    let checkbox_line = Line::from(vec![
        Span::styled("[", Style::default().fg(Color::Gray)),
        if selected {
            Span::styled(check, Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
        } else {
            Span::styled(" ", Style::default())
        },
        Span::styled("]", Style::default().fg(Color::Gray)),
        Span::styled(format!("  {}{}", hint_part, label), Style::default().fg(Color::White)),
    ]);
    frame.render_widget(Paragraph::new(checkbox_line), rect);
}

/// Render a GUI-like 'radio' value that possibly be currently selected, with a label to the right.
fn radio_value(frame: &mut Frame, rect: Rect, selected: bool, label: &str) {
    checkbox_base(frame, rect, selected, label, None, "*");
}

/// Render a GUI-like 'checkbox' that can be currently selected (checked) with a label to the right.
fn checkbox(frame: &mut Frame, rect: Rect, selected: bool, label: &str, hint: &str) {
    checkbox_base(frame, rect, selected, label, Some(hint), "x");
}

/// Style of `textarea` to active (currently being edited).
fn set_active_textarea_style<'a>(textarea: &mut TextArea<'a>) {
    textarea.set_block(
        Block::default()
            .borders(Borders::RIGHT | Borders::LEFT)
            .style(Style::default().add_modifier(Modifier::BOLD))
            .bg(Color::Yellow)
            .fg(Color::Black),
    ); 
}

/// Reset style of passed textarea to default/inactive style.
fn set_inactive_textarea_style<'a>(textarea: &mut TextArea<'a>) {
    textarea.set_cursor_line_style(Style::default());
    textarea.set_cursor_style(Style::default());
    textarea.set_block(
        Block::default()
            .borders(Borders::RIGHT | Borders::LEFT)
            .bg(Color::Indexed(234)),
    ); 
}

/// Types of input that may be accepted by a TextInput.
enum TextInputType {
    /// String - any characters are OK.
    String,
    /// Unsigned int - only digits are accepted.
    UInt,
    /// 'Unsigned; float - digits and one decimal point are accepted, but no sign.
    UFloat
}
      
/// Text area for input in RatatuiFrontend and any related fields describing it.
struct TextInput<'a> {
    /// Text area itself.
    textarea: TextArea<'a>,
    /// Specifies what input is allowed.
    input_type: TextInputType
}

impl TextInput<'_> {
    /// Extends `TextArea::input()` with checking to e.g. only accept numeric digits for integer inputs.
    fn input(&mut self, input: Input) {
        let key = input.key;
        let first_line = &self.textarea.lines()[0];
        let filter = match (&self.input_type, key) {
            (TextInputType::String, _) => true,
            (TextInputType::UFloat, Key::Char(c)) =>
                c.is_digit(10) || (c == '.' && !first_line.is_empty() && !first_line.contains('.')),
            (TextInputType::UInt, Key::Char(c)) =>
                c.is_digit(10) && first_line.len() <= 9,
            (_, Key::Backspace) => true,
            (_, _) => false
        };
        if filter {
            self.textarea.input(input);
        }
    }
}

/// Ratatui (TUI) UI frontend.
pub struct RatatuiFrontend {
    /// Index of the currently active text input, if any.
    active_textinput:          Option<usize>,
    /// Set if if the 'previous'/'next' process key has been pressed this frame. True is 'previous'.
    process_prevnext_pressed:  Option<bool>,
    /// Set if 'j' or 'k' have been pressed, acting as 'down' or 'up' for thread selection. True is 'k/up'.
    thread_updown_pressed:     Option<bool>,
    /// Index of the current update, starting at 0.
    update_idx:                usize,
    /// Index of the currently displayed tab on the right sidebar (CPU info vs help)>
    current_right_tab_index:   usize,
    /// Index of the help page currently shown on the help tab of the right sidebar.
    current_help_page_index:   usize,
    /// Currently selected topology object.
    ///
    /// Only None during initialization, always set after first update 
    /// (TODO pass topology cache on init so we can set it and remove Option<> here)
    selected_object:           Option<TopologyObjectID>,
    /// Threads scrolling: index of the row to start drawing at the top of threads view.
    threads_current_row_index: usize,
    /// Index of thread currently focused for editing, or `None` if no thread focused.
    ///
    /// May be `Some()` even if currently editing process - used to toggle between a thread and the
    /// process.
    selected_thread:           Option<usize>,
    /// True if the process is focused for editing.
    process_is_focused:        bool,
    /// If true, blink the entire UI for the next frame - used for debugging.
    debug_blink:               bool,
    /// Rectangle to draw any small debug widgets into. Unused by default.
    rect_debug:                Option<Rect>
}

/// Indices into the `text_inputs` array passed throughout RatatuiFrontend, 
/// Process filter text input.
const TEXTINPUT_FILTER:            usize = 0;
/// Run mode command text input.
const TEXTINPUT_RUN:               usize = 1;
/// Run mode subprocess text input.
const TEXTINPUT_SUBPROCESS:        usize = 2;
/// Run mode thread apply time text input.
const TEXTINPUT_THREAD_APPLY_TIME: usize = 3;
/// Thread count text input, can only be active in run mode.
const TEXTINPUT_THREAD_COUNT:      usize = 4;

// Using indices instead of an enum because that's what ratatui `Tabs::select()` requires.

/// Index of the 'help' tab on the right sidebar
const RIGHT_TAB_INDEX_HELP:    usize = 0;
/// Index of the 'CPU info' tab on the right sidebar
const RIGHT_TAB_INDEX_CPUINFO: usize = 1;

/// Number of help pages we can switch between.
const HELP_PAGE_COUNT: usize = 2;
/// 'FPS' to draw the UI at. Higher values reduce battery life, improve responsiveness.
const TUI_FRAMERATE: u64 = 12;

impl RatatuiFrontend {
    /// Construct a RatatuiFrontend.
    pub fn new() -> Self {
        Self {
            active_textinput:          None,
            process_prevnext_pressed:  None,
            thread_updown_pressed:     None,
            update_idx:                0,
            current_right_tab_index:   0,
            current_help_page_index:   0,
            selected_object:           None,
            threads_current_row_index: 0,
            selected_thread:           None,
            process_is_focused:        true,
            debug_blink:               false,
            rect_debug:                None
        }
    }

    /// Run the UI using passed backend and system - this is the 'UI application entry point'.
    pub fn run(&mut self, backend: &mut UIBackend, system: &mut System) 
        -> anyhow::Result<()>
    {
        // Terminal setup
        enable_raw_mode()?;
        crossterm::execute!(stdout(), EnterAlternateScreen, EnableMouseCapture)?;
        let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;

        // Storing TextAreas in a field would force us to propagate their lifetime upwards through 
        // all structs that own a RatatuiFrontend, so instead store them on the stack.
        // TODO try to find a less hacky way to do this. Cell? Rc?
        let mut text_inputs = [Self::text_input("</>,<?> Process filter text ...", TextInputType::String),
                               Self::text_input("<:> Command...", TextInputType::String),
                               Self::text_input("<!> subprocess (optional)...", TextInputType::String),
                               Self::text_input(format!("{}", THREAD_APPLY_TIME_DEFAULT ), TextInputType::UFloat),
                               Self::text_input(format!("<t> {}", strings::THREADS_COUNT), TextInputType::UInt),
        ];
        
        // Main UI loop.
        let mut should_quit = false;
        while !should_quit {
            let mut ui = |frame: &mut Frame| {
                self.update(backend, system, frame, &mut text_inputs);
            };

            terminal.draw(&mut ui)?;
            // Handle events from the tick thread.
            backend.handle_events(system);
            // Handle terminal input.
            should_quit = self.handle_events_term(backend, system, &mut text_inputs).unwrap();
        }

        // Terminal teardown
        disable_raw_mode()?;
        crossterm::execute!(
            terminal.backend_mut(),
            LeaveAlternateScreen,
            DisableMouseCapture
        )?;
        terminal.show_cursor()?;
        Ok(())
    }

    /// Main TUI update - 'frame entry point'.
    fn update<'a>(&mut self,
        backend:     &mut UIBackend,
        system:      &mut System,
        frame:       &mut Frame,
        text_inputs: &mut [TextInput<'a>])
    {
        if backend.is_first_update() {
            self.selected_object = Some(TopologyObjectID::from_cached(system.cache().root()));
        }

        if self.debug_blink  {
            frame.render_widget(Block::default().style(Style::default().add_modifier(Modifier::REVERSED)), frame.size());
            self.debug_blink = false;
        }

        let chunks = chunks_vertical(frame.size(), &[Constraint::Min(1), Constraint::Length(1)]);
        // Everything except statusbar.
        let rect_main      = chunks[0];
        let rect_statusbar = chunks[1];
        // Let any statusbar space over 60 chars from the start be overdrawn for debug purposes.
        let chunks_bottom = chunks_horizontal(rect_statusbar, &[Constraint::Length(60), Constraint::Min(0)]);
        self.rect_debug = Some(chunks_bottom[1]);

        // Statusbar.
        self.statusbar(backend, frame, rect_statusbar);

        // Determine which panels to show, and set up layout for them.
        let show_left_panel = !backend.get_hide_process_selection_panel();
        let show_right_panel = frame.size().width > 180;
        let chunks_main = chunks_horizontal(rect_main, &[
            if show_left_panel {
                match frame.size().width {
                    0   ..=180 => Constraint::Percentage(40),
                    181 ..=360 => Constraint::Percentage(35),
                    361 ..     => Constraint::Percentage(30)
                }
            } else {
                Constraint::Length(0)
            },
            Constraint::Min(60),
            if show_right_panel {Constraint::Length(36)} else {Constraint::Length(0)},
        ]);

        // Left (process) panel.
        let rect_left   = chunks_main[0];
        // Main topology view.
        let rect_center = chunks_main[1];
        // Right (CPU info/help) panel.
        let rect_right  = chunks_main[2];

        if show_left_panel {
            self.process_selection_panel(frame, rect_left, backend, system, text_inputs);
        }
        
        // Get core status (after process selection so we always check currently selected PID,
        // avoiding a 1-frame delay)
        backend.update_current_process_data(system);
        self.topology_view(frame, rect_center, backend, system, text_inputs);
        if show_right_panel {
            self.right_panel(frame, rect_right, backend, system);
        }

        backend.handle_timeouts(system);

        // Update teardown.
        if backend.end_update() {
            // If there was a mode (i.e. selected process) change, reset threads scroll position.
            self.threads_current_row_index = 0;
        }
        self.process_prevnext_pressed = None;
        self.thread_updown_pressed    = None;
        self.update_idx += 1;
    }

    /// Activate text input with specified index (switching to text entry mode).
    ///
    /// `clear`: clear the textinput's content first.
    fn activate_text_input<'a>(&mut self, text_inputs: &mut [TextInput<'a>], index: usize, clear: bool) {
        self.active_textinput = Some(index);
        let input: &mut TextInput = &mut text_inputs[index];
        if clear {
            input.textarea = TextArea::default();
        }
        input.textarea.set_cursor_line_style(Style::default().add_modifier(Modifier::UNDERLINED));
        input.textarea.set_cursor_style(Style::default().add_modifier(Modifier::REVERSED));
        set_active_textarea_style(&mut input.textarea);
    }

    /// Deactivate currently active text input, if any (switching to main mode).
    fn deactivate_text_input<'a>(&mut self, text_inputs: &mut [TextInput<'a>]) {
        if let Some(index) = self.active_textinput {
            set_inactive_textarea_style(&mut text_inputs[index].textarea);
            self.active_textinput = None;
        }
    }

    /// Construct text input with specified placeholder content.
    fn text_input(placeholder: impl Into<String>, input_type: TextInputType) -> TextInput<'static> {
        let mut textarea = TextArea::default();
        set_inactive_textarea_style(&mut textarea);
        textarea.set_placeholder_text(placeholder);
        TextInput{ textarea, input_type }
    } 

    /// Handle terminal events in text entry mode (when a text input is active).
    fn handle_events_textinput<'a>(&mut self, input_index: usize, text_inputs: &mut [TextInput<'a>]) -> io::Result<bool> {
        let event = event::read()?;
        if let Event::Key(KeyEvent{kind: event::KeyEventKind::Press, code, modifiers, .. }) = event {
            if code == KeyCode::Char('c') && modifiers.contains(KeyModifiers::CONTROL) {
                return Ok(true)
            }
        }

        match event.into() {
            Input { key: Key::Esc, .. } | Input { key: Key::Enter, ..} => {
                self.deactivate_text_input(text_inputs)
            },
            input => {
                text_inputs[input_index].input(input);
            }
        }
        Ok(false)
    }

    /// Handle filter entry event.
    fn handle_events_filter<'a>(&mut self,
        backend:         &mut UIBackend,
        system:          &mut System,
        text_inputs:     &mut [TextInput<'a>],
        clear_textinput: bool)
    {
        self.activate_text_input(text_inputs, TEXTINPUT_FILTER, clear_textinput);
        backend.try_set_next_mode_view_anyprocess(system);
    }

    /// Handle edit mode entry event.
    fn handle_events_mode_edit<'a>(&mut self, 
        backend:     &mut UIBackend,
        system:      &mut System,
        text_inputs: &mut [TextInput<'a>])
    {
        match backend.get_mode() {
            UIMode::Run(..)  => {
                let processes = backend.get_filtered_user_processes( system );
                if let Some((pid, _process)) = processes.first() {
                    backend.set_next_mode_edit(**pid, system.affinity_full(), None );
                }
                self.activate_text_input(text_inputs, TEXTINPUT_FILTER, true);
            },
            UIMode::View(ref view) => {
                backend.set_next_mode_edit(view.get_selected_process(), view.get_process_affinity().clone(), None);
            },
            UIMode::Edit(..) =>{}
        }
    }

    /// Handle 'apply'/'run' event.
    fn handle_events_apply<'a>(&mut self, 
        backend:     &mut UIBackend,
        system:      &mut System)
    {
        let current_order = backend.get_current_order();
        match backend.get_mode() {
            UIMode::Run(ref run)  => {
                if run.is_run_enabled(current_order) {
                    backend.run_shell_command_with_process_affinity(system);
                }
            }
            UIMode::View(..)  => {},
            UIMode::Edit(ref edit)  => {
                if edit.is_apply_enabled() {
                    backend.edit_apply_clicked(system);
                }
            }
        }
    }

    /// Handle next thread order event.
    fn handle_events_rotate_thread_order(&mut self, 
        backend:            &mut UIBackend,
        system:             &mut System,
        ignore_main_thread: bool)
    {
        let mut current_order = backend.get_current_order();
        match (backend.get_mode_mut(), ThreadOrder::rotate(&mut current_order)) {
            (UIMode::Run(ref mut run), Some(order)) => {
                // In run mode: after changing thread order, apply the new order to
                // `selected_cores_threads`, if any
                run.apply_thread_order(&order, system, ignore_main_thread)
            },
            (UIMode::View(ref view), Some(order)) => {
                let selected_process = view.get_selected_process();
                let (selected_cores_threads, selected_cores_process) = view.apply_thread_order(order, system, ignore_main_thread);
                backend.set_next_mode_edit_hashmap(selected_process, selected_cores_process, selected_cores_threads);
            },
            (UIMode::View(ref view), None) => {
                let selected_process = view.get_selected_process();
                let selected_cores_process = view.get_process_affinity().clone();
                backend.set_next_mode_edit(selected_process, selected_cores_process, None);
            },
            (UIMode::Edit(ref mut edit), Some(order)) => {
                edit.apply_thread_order(order, system, ignore_main_thread)
            },
            (_, None) => {}
        }
        *backend.get_current_order_mut() = current_order;
    }

    /// Handle a keypress to change currently selected thread (up/down or right/left).
    fn handle_thread_change(&mut self,
        backend:         &UIBackend, 
        previous:        bool,
        jump_size:       usize) 
    {
        info!("handle_thread_change: selected_thread is {:?}, moving {} by {}",
              self.selected_thread, if previous { "back" } else { "forward" }, jump_size );
        let thread_count = backend.get_thread_count();
        self.selected_thread = match (previous, self.selected_thread, thread_count) {
            (true,  Some(n), c @ 1..) if n >= jump_size => Some((c - 1).min(n - jump_size)),
            (true,  _,       1..)     => None,    // 0 to None, or stay in None
            (false, None,    1..)     => Some(0), // None to first thread
            (false, Some(n), c @ 1..) => Some((c - 1).min(n + jump_size)),
            (_,     _,       0)       => None,    // No thread to select
        };
    
        self.process_is_focused = self.selected_thread.is_none();
        info!("selected thread changed to {:?}", self.selected_thread)
    }

    /// Handle any terminal events.
    fn handle_events_term<'a>(&mut self,
        backend: &mut UIBackend,
        system: &mut System,
        text_inputs: &mut [TextInput<'a>]) 
        -> io::Result<bool> 
    {
        if !event::poll(std::time::Duration::from_millis(1000/TUI_FRAMERATE))? 
        {
            // No input
            return Ok(false);
        }

        // Text input mode. Esc/Enter to exit, anything else is passed to a textarea.
        if let Some(index) = self.active_textinput {
            return self.handle_events_textinput(index, text_inputs);
        } 
        // Main/normal mode. Different keys to activate different inputs/etc.
        if let Event::Key(KeyEvent{kind: event::KeyEventKind::Press, code, modifiers, .. }) = event::read()? {
            let left_hidden = backend.get_hide_process_selection_panel() ;
            let ignore_main_thread = backend.get_thread_order_ignore_main_thread();
            match code {
                KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) =>
                    return Ok(true),
                KeyCode::Char('<') => 
                    *backend.get_hide_process_selection_panel_mut() = true ,
                KeyCode::Char('>') => 
                    *backend.get_hide_process_selection_panel_mut() = false,
                KeyCode::F(10) => 
                    *backend.get_hide_process_selection_panel_mut() = !left_hidden,
                KeyCode::F(1) => {
                    if self.current_right_tab_index != RIGHT_TAB_INDEX_HELP {
                        self.current_right_tab_index = RIGHT_TAB_INDEX_HELP;
                        self.current_help_page_index = 0;
                    } else {
                        self.current_help_page_index = (self.current_help_page_index + 1) % HELP_PAGE_COUNT;
                    }
                },
                KeyCode::F(2) => 
                    self.current_right_tab_index = RIGHT_TAB_INDEX_CPUINFO,
                KeyCode::F(3) => {
                    if let UIMode::Run(..) = backend.get_mode() {
                    } else {
                        backend.set_next_mode_run(system);
                    }
                },
                KeyCode::F(4) => {
                    backend.try_set_next_mode_view_anyprocess(system);
                    if let UIMode::Run(..) = backend.get_mode() {
                        self.activate_text_input(text_inputs, TEXTINPUT_FILTER, true);
                    }
                },
                KeyCode::F(5) => {
                    self.handle_events_mode_edit(backend, system, text_inputs);
                },
                KeyCode::Char('/') | KeyCode::Char('f') => {
                    self.handle_events_filter(backend, system, text_inputs, true);
                },
                KeyCode::Char('?') | KeyCode::Char('F') => {
                    self.handle_events_filter(backend, system, text_inputs, false);
                },
                KeyCode::Char(':') | KeyCode::Char('e') => {
                    self.activate_text_input(text_inputs, TEXTINPUT_RUN, false);
                },
                KeyCode::Char('!') | KeyCode::Char('p') => {
                    self.activate_text_input(text_inputs, TEXTINPUT_SUBPROCESS, false);
                },
                KeyCode::Char('-') => {
                    *backend.get_thread_order_ignore_main_thread_mut() =
                        !backend.get_thread_order_ignore_main_thread();
                },
                KeyCode::Char('+') => {
                    self.handle_events_rotate_thread_order(backend, system, ignore_main_thread);
                },
                // no SMT
                KeyCode::Char('n') => {
                    backend.disable_affinity_smt(system);
                },
                // thread_apply_time text edit
                KeyCode::Char('r') => {
                    if let UIMode::Run(ref _run) = backend.get_mode() {
                        self.activate_text_input(text_inputs, TEXTINPUT_THREAD_APPLY_TIME, true);
                    }
                },
                key @ KeyCode::Down | key @ KeyCode::Up => 
                    self.process_prevnext_pressed = Some(key == KeyCode::Up),
                // 'Apply/Run' key
                KeyCode::Char('a') if modifiers.contains(KeyModifiers::CONTROL) => {
                    self.handle_events_apply(backend, system);
                },
                KeyCode::Enter if modifiers.is_empty() => {
                    let id = self.selected_object.clone().unwrap();
                    let obj = system.cache().get_by_id(id);
                    let mut current_children = system.cache().get_children(obj); 
                    while let Some(child) = current_children.peek() {
                        current_children = system.cache().get_children(child);
                        if child.has_siblings || child.child_count() == 0 {
                            self.selected_object = Some(child.id());
                            break;
                        }
                    }
                },
                KeyCode::Esc => {
                    let id = self.selected_object.clone().unwrap();
                    let obj = system.cache().get_by_id(id);
                    let mut current_parent = system.cache().get_parent(obj);
                    while let Some(parent) = current_parent {
                        current_parent = system.cache().get_parent(parent);
                        if parent.has_siblings || current_parent.is_none() {
                            self.selected_object = Some(parent.id());
                            break;
                        }
                    }
                },
                key @ KeyCode::Char('w') | key @ KeyCode::Char('s') | key @ KeyCode::Char('a') | key @ KeyCode::Char('d') => {
                    if self.selected_object.as_ref().unwrap().is_root() {
                        // Note: process is 'before' the first thread.
                        match key {
                            // Handled in `topology_threads()`
                            KeyCode::Char('w') => self.thread_updown_pressed = Some(true),
                            KeyCode::Char('s') => self.thread_updown_pressed = Some(false),
                            // Previous/next thread, 
                            KeyCode::Char('a') => self.handle_thread_change(backend, true, 1),
                            KeyCode::Char('d') => self.handle_thread_change(backend, false, 1),
                            _ => panic!("unreachable")
                        }
                    } else {
                        let id = self.selected_object.clone().unwrap();
                        self.selected_object = match key {
                            KeyCode::Char('w') => Some(system.cache().sibling_up(&id)),
                            KeyCode::Char('s') => Some(system.cache().sibling_down(&id)),
                            // Left/right movement in grid, as well as moving to previous/next group.
                            KeyCode::Char('a') => system.cache().previous_by_type(id).or(self.selected_object.clone()),
                            KeyCode::Char('d') => system.cache().next_by_type(id).or(self.selected_object.clone()),
                            _ => panic!("unreachable")
                        }
                    }
                },
                key @ KeyCode::Char('k') | key @ KeyCode::Char('j') =>
                    // Handled in `topology_threads()`
                    self.thread_updown_pressed = Some(key == KeyCode::Char('k')),
                // Go to next/previous thread, and the process is 'before' the first thread.
                KeyCode::Char('h') => self.handle_thread_change(backend, true, 1),
                KeyCode::Char('l') => self.handle_thread_change(backend, false, 1),
                KeyCode::Char(' ') => {
                    let id = self.selected_object.clone().unwrap();
                    let thread_id = self.selected_thread.and_then(|t| backend.get_mode_mut().thread_index_to_id(t, system));
                    // Toggle PU/s in the currently focused thread (we're not actually rendering that thread).
                    backend.get_mode_mut().set_currently_rendered_thread(thread_id);
                    backend.handle_group_click(system, system.cache().get_by_id(id));
                    backend.get_mode_mut().set_currently_rendered_thread(None);
                },
                KeyCode::Backspace => {
                    if let UIMode::Run(ref mut run) = backend.get_mode_mut() {
                        run.clear_threads();
                    } else if let UIMode::Edit(ref mut edit) = backend.get_mode_mut() {
                        edit.clear_threads();
                    }
                },
                KeyCode::Char('t') => { 
                    if let UIMode::Run(_) = backend.get_mode_mut() {
                        self.activate_text_input(text_inputs, TEXTINPUT_THREAD_COUNT, true);
                    }
                },
                KeyCode::Tab => {
                    // Toggle between currently focused thread and the process.
                    self.process_is_focused = if self.process_is_focused == true {
                        // If we're not viewing the process, we need to be viewing *some* thread.
                        if self.selected_thread == None {
                            self.selected_thread = Some(0)
                        }
                        false
                    } else {
                        true
                    };
                }
                _ => {}
            }
        }
        Ok(false)
    }

    /// Draw the statusbar.
    fn statusbar(&self, backend: &UIBackend, frame: &mut Frame, rect: Rect) {
        let is_error = backend.get_status().to_lowercase().contains("error");
        frame.render_widget(
            Paragraph::new(Text::styled(
                backend.get_status().as_str(),
                Style::default().bg(Color::Indexed(235)).fg(if is_error { Color::Red } else { Color::Green }),
            ))
            .block(Block::default().borders(Borders::NONE).bg(Color::Indexed(235))),
            rect
        );
    }

    /// Convert a process and its ID into a process selection panel list item.
    fn process_selection_panel_line<'a>(
        pid: sysinfo::Pid, 
        process: &'a sysinfo::Process,
        system: &System)
        -> ListItem<'a>
    {
        let cmd = UIBackend::get_process_cmd(pid, system);
        // PID, process and command, in that order.
        ListItem::new(Line::from(vec![
            Span::styled(format!("{:>8} ", pid.as_u32()), Style::default().fg(Color::Gray)),
            Span::styled(System::get_process_name(process), Style::default().fg(Color::White)),
            Span::styled(format!(" {}", cmd), Style::default().fg(Color::DarkGray)),
        ]))
    }

    /// Render the (right) process selection panel and handle related input
    /// (process_prevnext_pressed)
    ///
    /// Can change the currently selected process.
    fn process_selection_panel<'a>(&self,
        frame:       &mut Frame,
        rect:        Rect,
        backend:     &mut UIBackend,
        system:      &System,
        text_inputs: &[TextInput<'a>])
    {
        let chunks_panel = chunks_vertical(rect, &[Constraint::Length(1), Constraint::Length(1), Constraint::Min(1)]);
        // Filter text area, process count, and the process list itself.
        let (rect_filter, rect_count, rect_list) = (chunks_panel[0], chunks_panel[1], chunks_panel[2]);
        
        let chunks_list = chunks_horizontal(rect_list, &[Constraint::Min(1), Constraint::Length(1)]);
        // Process list and its scrollbar.
        let (rect_list, rect_scrollbar) = (chunks_list[0], chunks_list[1]);

        // Process filter text area.
        let filter = &text_inputs[TEXTINPUT_FILTER];
        frame.render_widget(filter.textarea.widget(), rect_filter);

        // Get the filter string from the process filter text area, and get matching processes.
        let empty = String::from("");
        *backend.get_filter_mut() = filter.textarea.lines().get(0).unwrap_or(&empty).clone();

        let processes = backend.get_filtered_user_processes( system );
        // Process count label.
        frame.render_widget(
            Paragraph::new(Text::styled(
                strings::processes(processes.len()),
                Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)
            )), 
            rect_count);
        frame.render_widget(Paragraph::new("<↓>,<↑> next/previous")
             .style(Style::default().fg(Color::White)).alignment(Alignment::Right), rect_count);

        // Construct list items of the process list.
        let process_list_items: Vec<_> = processes
            .iter()
            .map(|p| { Self::process_selection_panel_line(*p.0, p.1, system) })
            .collect();

        // Get the currently selected process ID, or PID 0.
        let selected_pid = backend.get_selected_pid();
        // Get index of currently selected process in  `processes`, or None.
        let selected_index = if let UIMode::Run(..) = backend.get_mode() {
            None
        } else {
            processes.iter().position(|(pid, _process)| **pid == selected_pid)
        };

        // If we have a selected process, draw the scrollbar at the processes' position.
        if let Some(index) = selected_index {
            let mut scrollbar_state = ScrollbarState::default()
                .content_length(process_list_items.len())
                .viewport_content_length(rect_list.height.into())
                .position(index);
            scrollbar(frame, rect_scrollbar, &mut scrollbar_state);
        }

        // Draw the process list. 
        let mut process_list_state = ListState::default().with_selected(selected_index);
        frame.render_stateful_widget(
            List::new(process_list_items)
                .highlight_style(Style::default().bg(Color::Indexed(233))
                                                 .add_modifier(Modifier::BOLD)
                                                 .add_modifier(Modifier::UNDERLINED))
                .highlight_symbol(">"),
            rect_list,
            &mut process_list_state,
        );

        backend.process_selection_panel_keyboard_control(self.process_prevnext_pressed, system);
    }

    /// Render the right-side (CPU info/help) panel.
    fn right_panel( &self,
        frame:   &mut Frame,
        rect:    Rect,
        backend: &mut UIBackend,
        system:  &System)
    {
        // have current tab index somewhere
        let chunks_panel = chunks_vertical(rect, &[Constraint::Length(1), Constraint::Min(1)]);
        let (rect_tabs, rect_content) = (chunks_panel[0], chunks_panel[1]);
        let tabs = Tabs::new(vec!["<F1> HELP", "<F2> CPU"])
            .block(Block::default().borders(Borders::NONE))
            .style(Style::default().fg(Color::White))
            .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::UNDERLINED | Modifier::BOLD))
            .select(self.current_right_tab_index)
            .divider("");
        frame.render_widget(tabs, rect_tabs);

        match self.current_right_tab_index {
            RIGHT_TAB_INDEX_HELP => {
                self.help(frame, rect_content, backend);
            },
            RIGHT_TAB_INDEX_CPUINFO => {
                self.cpu_info(frame, rect_content, system);
            },
            _ => {
                panic!("Unsupported current_right_tab_index: {}", self.current_right_tab_index)
            }
        }
    }

    /// Draw help information on the right panel.
    fn help(&self, 
        frame:   &mut Frame,
        rect:    Rect,
        backend: &mut UIBackend) 
    {
        let (mode, mode_shortcuts, is_run) = backend.get_current_mode_help();
        let text = match self.current_help_page_index {
            0 => {
                format!("{}{}{}\n\
                        ", strings::HELP_MAIN, if is_run {strings::HELP_MAIN_RUN} else {""}
                        , strings::HELP_MAIN_SHORTCUTS_TUI)
            },
            1 => {
                format!("{}\n{}\n", mode, mode_shortcuts)
            },
            _ => {
                panic!("Unsupported current_help_page_index: {}", self.current_help_page_index)
            }
        };
        let chunks_help = chunks_vertical(rect, &[Constraint::Length(1), Constraint::Min(1), Constraint::Length(1)]);
        let (rect_help, rect_next) = (chunks_help[1], chunks_help[2]);

        frame.render_widget(
            Paragraph::new(text).block(Block::default().fg(Color::LightRed)),
            rect_help,
        );
        frame.render_widget(
            Paragraph::new(Text::styled(
                "<F1> next page",
                Style::default().fg(Color::Yellow)
            )), 
            rect_next);
    }

    /// Draw CPU information on the right panel.
    fn cpu_info(&self, 
        frame:   &mut Frame,
        rect:    Rect,
        system:  &System) 
    {
        let chunks_cpulist = chunks_vertical(rect, &[Constraint::Length(1), Constraint::Min(1)]);

        let rect_list = chunks_cpulist[1];
        let list_items: Vec<_> = system.cpu_info()
            .iter()
            .map(|info| {
                ListItem::new(format!("{}", info.name))
            })
            .collect();

        let mut list_state = ListState::default();
        frame.render_stateful_widget(
            List::new(list_items),
            rect_list,
            &mut list_state,
        );
    }

    /// Main - center - pane, showing CPU topology, affinity control inputs and threads.
    fn topology_view<'a>(&mut self, 
        frame:       &mut Frame,
        rect:        Rect,
        backend:     &mut UIBackend,
        system:      &System,
        text_inputs: &mut [TextInput<'a>])
    {
        // Background.
        frame.render_widget(Block::default().bg(Color::Indexed(232)), rect);

        let chunks_center = chunks_vertical(rect, &[Constraint::Length(2), Constraint::Min(1)]);
        let (rect_header, rect_grid) = (chunks_center[0], chunks_center[1]);
        self.topology_header(frame, rect_header, backend, text_inputs);
        let topology_rect = self.topology_grid(frame, rect_grid, backend, system).intersection(rect);

        let chunks_topbottom =
            chunks_vertical(rect_grid, &[Constraint::Length(topology_rect.height), Constraint::Min(1)]);
        let (rect_top, rect_bottom) = (chunks_topbottom[0], chunks_topbottom[1]);

        let rect_grid_sidebar =
            chunks_horizontal(rect_top, &[Constraint::Min(topology_rect.width), Constraint::Length(24)])[1];
        self.topology_sidebar(frame, rect_grid_sidebar, backend);

        let chunks_bottom =
            chunks_vertical(rect_bottom, &[Constraint::Length(1), Constraint::Min(1), Constraint::Length(1)]);
        let (rect_grid_buttons_bottom, rect_threads, rect_footer) = 
            (chunks_bottom[0], chunks_bottom[1], chunks_bottom[2]);

        self.topology_buttons_process(frame, rect_grid_buttons_bottom, backend, text_inputs);
        self.topology_threads(frame, rect_threads, backend, system, text_inputs);
        self.topology_footer(frame, rect_footer, backend);
    }

    /// Render the name of a thread with an indicator showing if affinities have been edited.
    fn thread_name(&self, frame: &mut Frame, rect: Rect, name: &str, edited: bool) {
        let chunks = chunks_horizontal(rect, &[Constraint::Length(1), Constraint::Min(1)]);
        let (rect_edited, rect_name) = (chunks[0], chunks[1]);
        if edited {
            let indicator = Text::styled(format!("*"), Style::default().fg(Color::Yellow));
            frame.render_widget(Paragraph::new(indicator), rect_edited);
        }
        let name_render = if name.len() <= rect_name.width.into() {
            String::from(name)
        } else {
            name[0..(rect_name.width.max(1) - 1).into()].to_string() + "…"
        };
        frame.render_widget(Paragraph::new(name_render).style(Style::default().fg(Color::Red)), rect_name);
    }

    /// Draw a scrollbar to scroll when there are more threads than fit on the threads view.
    fn threads_scrollbar(&self,
        frame:          &mut Frame,
        backend:        &UIBackend,
        rect_threads:   Rect,
        rect_topo:      Rect,
        rect_scrollbar: Rect)
        -> (usize, usize)
    {
        let threads_in_row: usize     = (rect_threads.width / rect_topo.width).into();
        let row_count                 = backend.get_thread_count().div_ceil(threads_in_row);
        let row_count_viewport: usize = rect_threads.height.div_ceil(rect_topo.height).into();
        // Ensure we don't end up out of bounds if thread count decreased recently.
        let threads_current_row_index = self.threads_current_row_index.min(row_count - 1);

        let mut scrollbar_state = ScrollbarState::default()
            .content_length(row_count)
            .viewport_content_length(row_count_viewport)
            .position(threads_current_row_index);
        scrollbar(frame, rect_scrollbar, &mut scrollbar_state);
        (threads_current_row_index, threads_in_row)
    }

    /// Draw a single thread's CPU topology grid (including the header).
    fn topology_thread(&self, 
        frame:   &mut Frame,
        backend: &mut UIBackend,
        system:  &System,
        name:    &String,
        edited:  bool,
        bounds:  Rect) -> Rect
    {
        let chunks = chunks_vertical(bounds, &[Constraint::Length(1), Constraint::Min(1)]);
        let (rect_name, rect_topo) = (chunks[0], chunks[1]);
        self.thread_name(frame, rect_name, name.as_str(), edited);
        let rect_grid = self.topology_grid(frame, rect_topo, backend, system).intersection(rect_topo);
        Rect::new(rect_name.x, rect_name.y, rect_grid.width, rect_grid.height + 1)
    }

    /// Get x/y/width/height of a thread's CPU topology grid (including the header).
    ///
    /// Used once thread width/height (`rect_topo`) are known based on the first thread.
    ///
    /// Not used for the first thread - which uses `topology_thread` directly to get the width/height.
    fn topology_thread_bounds(&self,
        row:          &mut u16,
        col:          &mut u16,
        rect_threads: Rect,
        rect_topo:    Rect) -> Rect
    {
        let max_x_offset = (*col + 1) * rect_topo.width;
        // Ran out of horizontal space; start new row.
        if max_x_offset > rect_threads.width {
            *col = 0;
            *row += 1;
        }
        let (x, y) = (rect_threads.x + *col * rect_topo.width, rect_threads.y + *row * rect_topo.height);
        *col += 1;
        // The intersection is neccessary for the case when `rect_threads` is narrower than
        // `rect_topo`, so even one thread per row is too wide.
        Rect::new(x, y, rect_topo.width, rect_topo.height).intersection(rect_threads)
    }

    /// Draw the buttons on the right of the threads view (navigation help).
    fn topology_threads_sidebar(&self, frame: &mut Frame, rect: Rect)
    {
        // `y - 1`: Hack to make use of one more row, which is empty on the top bar side.
        let rect = Rect::new(rect.x, rect.y - 1, rect.width, rect.height + 1);
        let chunks_side = chunks_vertical(rect, &[Constraint::Length(4), Constraint::Min(1)]);
        let rect_legend = chunks_side[0];
        let block_legend =
            Block::default().title(format!("thread selection")).borders(Borders::ALL);
        let legend_text = "🅷 🅹 🅺 🅻 navigation\n<Tab>   toggle process";
        frame.render_widget(
            Paragraph::new(legend_text).style(Style::default().fg(Color::White)).block(block_legend), rect_legend);
    }

    /// Draw topology grids of all threads, with a header, in a scrollable area.
    fn topology_threads<'a>(&mut self,
        frame:       &mut Frame,
        rect:        Rect,
        backend:     &mut UIBackend,
        system:      &System,
        text_inputs: &mut [TextInput<'a>])
    {
        let chunks = chunks_vertical(rect, &[Constraint::Length(1), Constraint::Min(1)]);
        let (rect_header, rect_body) = (chunks[0], chunks[1]);
        let chunks = chunks_horizontal(rect_body, &[Constraint::Min(1), Constraint::Length(1), Constraint::Length(24)]);
        let (rect_threads, rect_scrollbar, rect_sidebar) = (chunks[0], chunks[1], chunks[2]);

        self.topology_threads_sidebar(frame, rect_sidebar);
        self.topology_threads_header(frame, rect_header, backend, system, text_inputs);

        let thread_ids = backend.get_mode().get_all_thread_ids(system);
        // Render first thread - uses special name/tid and returns bounds rect, which will be used
        // for width/height of all threads in thread grid calculations. We don't know how much space
        // the first thread will need, so allow it to use the entire `rect_threads`.
        if backend.get_thread_count() == 0 {
            return;
        }
        let (thread_id, name) = (thread_ids[0].0, &thread_ids[0].1);
        let edited = backend.get_mode().is_thread_edited(thread_id);
        backend.get_mode_mut().set_currently_rendered_thread(Some(thread_id));
        let rect_topo = self.topology_thread(frame, backend, system, name, edited, rect_threads);

        // Current row index will be saved at function end to avoid ownership conflict.
        let (threads_current_row_index, threads_in_row) = 
            self.threads_scrollbar(frame, backend, rect_threads, rect_topo, rect_scrollbar);

        if let Some(up) = self.thread_updown_pressed {
            self.handle_thread_change(backend, up, threads_in_row) ;
        }
        let threads_current_row_index = if let Some(thread_idx) = self.selected_thread {
            thread_idx / threads_in_row
        } else {
            threads_current_row_index
        };

        let (mut row, mut col) = if threads_current_row_index == 0 {
            (0, 1)
        } else {
            // Overdraw the first thread, which we drew only to get thread dimensions (`rect_topo`).
            fill_rect(frame, rect_topo);
            (0, 0)
        };
        // The '-1' accounts for the first thread drawn (and possibly overdrawn) above.
        let skip_count: usize = (threads_current_row_index * threads_in_row).max(1) - 1;

        for (thread_id, name) in thread_ids.iter().skip(skip_count+1) {
            let edited = backend.get_mode().is_thread_edited(*thread_id);
            backend.get_mode_mut().set_currently_rendered_thread(Some(*thread_id));
            let bounds = self.topology_thread_bounds(&mut row, &mut col, rect_threads, rect_topo);
            self.topology_thread(frame, backend, system, &name, edited, bounds);
        }
        backend.get_mode_mut().set_currently_rendered_thread(None);

        self.threads_current_row_index = threads_current_row_index; 
    }

    /// Draw header of the threads area, with thread count and mode-specific inputs.
    fn topology_threads_header<'a>(&self,
        frame:       &mut Frame,
        rect:        Rect,
        backend:     &mut UIBackend,
        system:      &System,
        text_inputs: &mut [TextInput<'a>])
    {
        let count_len = if let UIMode::Run(_) = backend.get_mode() {
            11
        } else {
            let count_str = backend.get_thread_count().to_string();
            u16::try_from(count_str.len()).unwrap() + 1
        };

        let chunks = chunks_horizontal( rect, &[
                Constraint::Length(count_len), // thread count
                Constraint::Length(8),         // label
                Constraint::Length(12)         // <⌫  > clear
            ]);
        let (rect_count, rect_label, rect_clear) = (chunks[0], chunks[1], chunks[2]);

        let thread_count_input = &mut text_inputs[TEXTINPUT_THREAD_COUNT];
        let label_style     = Style::default().fg(Color::Green).add_modifier(Modifier::BOLD);
        let label_paragraph = Paragraph::new(Text::styled(strings::THREADS, label_style));
        let clear_style     = Style::default().fg(Color::White);
        let clear_paragraph = Paragraph::new(Text::styled("<⌫  > ".to_owned() + strings::CLEAR, clear_style));
        if let UIMode::Run(..) = backend.get_mode_mut() {
            // In Run mode, we have a text input to enter thread count.
            let count_str = thread_count_input.textarea.lines().get(0);
            if count_str.is_some_and(|s| !s.is_empty()) {
                let count = count_str.unwrap().parse::<usize>().expect("input validation checks this");
                backend.run_update_thread_count(count, system);
            }
            frame.render_widget(thread_count_input.textarea.widget(), rect_count);
            frame.render_widget(label_paragraph, rect_label);
            frame.render_widget(clear_paragraph, rect_clear);
        } else {
            // In edit/view mode, we have a label showing the thread count.
            frame.render_widget(Paragraph::new(Text::styled(backend.get_thread_count().to_string(), label_style)), rect_count);
            frame.render_widget(label_paragraph, rect_label);
            if let UIMode::Edit(_) = backend.get_mode_mut() {
                frame.render_widget(clear_paragraph, rect_clear);
            }
        }
    }

    /// Render the main CPU topology grid for the process.
    fn topology_grid(&self, 
        frame:   &mut Frame,
        rect:    Rect,
        backend: &mut UIBackend,
        system:  &System) -> Rect
    {
        self.topology_object(frame, rect, rect.x, rect.y, backend, system, system.cache().root())
    }

    /// Render a PU (logical core).
    ///
    /// Depending on mode, affinity or selection status of the PU is rendered.
    fn topology_object_pu(&self, 
        frame:     &mut Frame,
        rect_grid: Rect,
        x:         u16,
        y:         u16,
        backend:   &mut UIBackend,
        system:    &System,
        obj:       &TopologyObjectInfo) -> Rect
    {
        let pu_id    = obj.os_index;
        let pu_color = self.topology_pu_color(backend, pu_id);
        let pu_text  = format!("{:0>2}", pu_id);
        let pu_rect  = Rect::new(x, y, pu_text.len().try_into().unwrap(), 1);
        let pu_base_style = Style::default().bg(pu_color).fg(Color::Indexed(16));
        let pu_style      = self.highlight_style(backend, system, obj, pu_base_style);
        if rect_grid.intersects(pu_rect) {
            frame.render_widget(
                Paragraph::new(Text::styled(pu_text, pu_style)), 
                rect_grid.intersection(pu_rect));
        }
        pu_rect
    }

    /// Determine color to draw a PU with, based on current mode and possibly affinity or process/thread core selection.
    fn topology_pu_color(&self, backend: &mut UIBackend, pu_id: u32) -> Color {
        // unselected, affinity, process, thread
        backend.pu_color(pu_id, (Color::White, Color::Green, Color::LightYellow, Color::Indexed(172)))
    }

    /// Fixup rectangle and borders of a child object drawn in a grid. 
    ///
    /// Ensures the grid is drawn seamlessly and that there are no extraneous gaps between siblings.
    fn topology_object_child_rect_fixup(&self, 
        frame:          &mut Frame,
        child:          &TopologyObjectInfo,
        mut rect_child: Rect,
        rect_grid:      Rect,
        row:            usize, 
        col:            usize, 
        rows:           usize, 
        cols:           usize) -> Rect
    {
        if !child.use_frame  {
            // Ensure objects without frames with siblings (e.g. PU) aren't visually merged.
            if col + 1 < cols {
                rect_child.width += 1;
            }
            if row + 1 < rows {
                rect_child.height += 1;
            }
        } else {
            // Allow the right/bottom sibling to overdraw our right/bottom border to avoid duplicating.
            if col + 1 < cols {
                rect_child.width -= 1;
            }
            if row + 1 < rows {
                rect_child.height -= 1;
            }

            // Fix artifacts caused by overdrawing borders.
            if rect_grid.intersection(rect_child) == rect_child {
                if row == 0 && col >= 1 {
                    let (fix_x, fix_y) = (rect_child.x, rect_child.y);
                    frame.buffer_mut().set_string(fix_x, fix_y, "┬", Style::default());
                }
                if row > 0 && col == 0 {
                    let (fix_x, fix_y) = (rect_child.x, rect_child.y);
                    frame.buffer_mut().set_string(fix_x, fix_y, "├", Style::default());
                }
                if row > 0 && col > 0 {
                    let (fix_x, fix_y) = (rect_child.x, rect_child.y);
                    frame.buffer_mut().set_string(fix_x, fix_y, "┼", Style::default());
                }
                if row > 0 && col + 1 == cols {
                    let (fix_x, fix_y) = (rect_child.right() - 1, rect_child.y);
                    frame.buffer_mut().set_string(fix_x, fix_y, "┤", Style::default());
                }
                if row + 1 == rows && col > 0{
                    let (fix_x, fix_y) = (rect_child.x, rect_child.bottom()-1);
                    frame.buffer_mut().set_string(fix_x, fix_y, "┴", Style::default());
                }
            }
        }
        rect_child
    }

    /// Check if `obj` should be highlighted (is a/under a selected object).
    fn is_highlighted(&self, backend: &UIBackend, system: &System, obj: &TopologyObjectInfo) -> bool {
        let highlight_object = || {
            let is_root = self.selected_object.as_ref().unwrap().is_root();
            if is_root && obj.is_package() {
                system.cache().is_recursive_child_of(obj, self.selected_object.as_ref().unwrap())
            } else {
                self.selected_object == Some(TopologyObjectID::from_cached(obj))
            }
        };
        if backend.get_mode().currently_rendering_a_thread() {
            let selected_thread_id = self.selected_thread.and_then(|t| backend.get_mode().thread_index_to_id(t, system));
            let current_thread_selected = selected_thread_id == backend.get_mode().currently_rendering_thread_id();
            (!self.process_is_focused) && current_thread_selected && highlight_object()
        } else {
            // We're currently rendering the process, 
            (self.process_is_focused || self.selected_thread.is_none()) && highlight_object()
        }
    }

    /// If `obj` should be highlighted, modify `base` to a 'highlighted' styleand return the result.
    fn highlight_style(&self, backend: &UIBackend, system: &System, obj: &TopologyObjectInfo, base: Style) -> Style {
        if obj.is_pu() && self.is_highlighted(backend, system, obj)  {
            base.bg(Color::Cyan)
        }
        else if self.is_highlighted(backend, system, obj) {
            base.fg(Color::Cyan).add_modifier(Modifier::BOLD)
        } else {
            base
        }
    }

    /// Draw a label of a topology object at coords `x,y` and get its width/height.
    ///
    /// If `obj.use_horizontal == true`, the label will be on the left, split into multiple lines;
    /// otherwise it will be on top.
    ///
    /// If `compact` is true, the label will not de drawn, but the dimensions will still be
    /// returned to make enough space to draw the label in the header.
    fn topology_object_label(&self,
        frame:     &mut Frame,
        rect_grid: Rect,
        compact:   bool,
        x:         u16,
        y:         u16,
        obj:       &TopologyObjectInfo)
        -> (u16, u16) 
    {
        let Some(label) = &obj.label else {unreachable!();};
        let base_style = match &label.style_override {
            Some(TopologyObjectLabelStyle::SmallCache) => Style::default().fg(Color::Indexed(248)),
            None => Style::default()
        };

        let horizontal = obj.use_horizontal;
        if compact {
            // Nothing to draw, label will be drawn in the top frame border.
            let label_text = label.lines.join(" ");
            (label_text.len() as u16, 0)
        } else {
            let label_text: Vec<Line> = if horizontal && label.lines.len() > 1 {
                label.lines.iter().map(|s|{Line::from(s.clone())}).collect()
            } else {
                vec![Line::from(label.lines.join(" "))]
            };
            let max_length = label_text.iter().map(|s| s.width()).max().unwrap_or(0);
            let label_rect = Rect::new(x, y, max_length.try_into().unwrap(), label_text.len().try_into().unwrap());
            if rect_grid.intersects(label_rect) {
                frame.render_widget(
                    Paragraph::new(label_text).style(base_style),
                    rect_grid.intersection(label_rect));
            }
            (label_rect.width, label_rect.height)
        }
    }

    /// Render a topology object and recursively, the entire hierarchy under it.
    fn topology_object(&self, 
        frame:     &mut Frame,
        rect_grid: Rect,
        x:         u16,
        y:         u16,
        backend:   &mut UIBackend,
        system:    &System,
        obj:       &TopologyObjectInfo) -> Rect
    {
        // Base coords for drawing the next nested object, and to determine final size of this object.
        let (x_inner, mut y_inner) = if obj.use_frame { (x + 1, y + 1) } else { (x, y) };
        // 'Compact' label drawing draws label in top frame border.
        let compact = obj.use_frame && obj.object_type == hwloc2::ObjectType::Package;

        if obj.object_type == hwloc2::ObjectType::PU {
            return self.topology_object_pu( frame, rect_grid, x, y, backend, system, obj);
        }

        let children_iter_base = system.cache().get_children(obj);
        let mut first_group = true;
        let mut x_grid_start = x_inner;
        let mut max_group_x = 0;

        // 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|{ 
            let horizontal = obj.use_horizontal;
            // Topology grids for threads don't draw labels to save space
            let draw_label = first_group && !backend.get_mode().currently_rendering_a_thread();
            if draw_label && obj.label.is_some() {
                let (label_w, label_h) =
                    self.topology_object_label(frame, rect_grid, compact, x_inner, y_inner, obj);
                if horizontal {
                    x_grid_start += label_w;
                } else {
                    // Since groups are always vertically stacked, there's no need for this to 
                    // be reset between groups.
                    y_inner += label_h;
                    max_group_x = max_group_x.max(x_grid_start + label_w);
                }
            }

            // Draw children in a grid.
            let (rows, cols) = util::squarish_grid_dimensions(children.len(), horizontal);
            // this grid is fully within this `topology_object` - doesn't depend on `use_horizontal`.
            for row in 0..rows {
                let mut x_grid = x_grid_start;
                let mut max_height = 0;
                for (col, child) in children.subiter(row * cols, (row + 1) * cols).enumerate() {
                    let mut rect_child = self.topology_object(frame, rect_grid, x_grid, y_inner,
                                                              backend, system, child);

                    // Ensure objects without frames with siblings (e.g. PU) aren't visually merged.
                    rect_child = self.topology_object_child_rect_fixup(frame, child, rect_child,
                                                                       rect_grid, row, col, rows, cols);

                    x_grid += rect_child.width;
                    max_height = max_height.max(rect_child.height);
                }
                y_inner += max_height;
                max_group_x = max_group_x.max(x_grid);
            }

            first_group = false;
        });

        let mut object_rect = Rect::new(x, y, max_group_x - x, y_inner - y);
        if obj.use_frame {
            // Increase rect width/height to make space for frame border.
            object_rect.width  += 1; 
            object_rect.height += 1;
            if rect_grid.intersects(object_rect) {
                let highlight_style = self.highlight_style(backend, system, obj, Style::default());
                let block = if compact && obj.label.is_some()  {
                    let label_text = obj.label.as_ref().unwrap().lines.join(" ");
                    Block::default().border_style(highlight_style).borders(Borders::ALL).title(label_text)
                } else {
                    Block::default().border_style(highlight_style).borders(Borders::ALL)
                };
                frame.render_widget(block, rect_grid.intersection(object_rect));
            }
        }
        return object_rect;
    }

    /// Draw the header above the process topology
    ///
    /// Two lines: run/view/edit mode tabs on the top, and in run mode, text entries on the bottom.
    fn topology_header<'a>(&self, 
        frame:       &mut Frame,
        rect:        Rect,
        backend:     &mut UIBackend,
        text_inputs: &[TextInput<'a>])
    {
        let chunks_header = chunks_vertical(rect, &[Constraint::Length(1), Constraint::Length(1)]);
        let (rect_tabs, rect_mode) = (chunks_header[0], chunks_header[1]);

        let current_tab_index = match backend.get_mode() {
            UIMode::Run(..)  => 0,
            UIMode::View(..) => 1,
            UIMode::Edit(..) => 2,
        };
        let tabs = Tabs::new(vec!["<F3> RUN", "<F4> VIEW", "<F5> EDIT"])
            .block(Block::default().borders(Borders::NONE))
            .style(Style::default().fg(Color::White))
            .highlight_style(Style::default().fg(Color::Yellow).add_modifier(Modifier::UNDERLINED | Modifier::BOLD))
            .select(current_tab_index)
            .divider("");
        frame.render_widget(tabs, rect_tabs);

        // Run mode specific text inputs.
        if let UIMode::Run(ref mut run) = backend.get_mode_mut() {
            let chunks_mode = chunks_horizontal(rect_mode, &[Constraint::Percentage(60), Constraint::Percentage(40)]);
            let (rect_run, rect_subprocess) = (chunks_mode[0], chunks_mode[1]);

            let mut draw_input = |rect, input: &TextInput<'a>, target: &mut String| {
                frame.render_widget(input.textarea.widget(), rect);
                if let Some(text) = input.textarea.lines().get(0) {
                    *target = text.clone();
                }
            };
            draw_input( rect_run, &text_inputs[TEXTINPUT_RUN], run.get_command_mut());
            draw_input( rect_subprocess, &text_inputs[TEXTINPUT_SUBPROCESS], run.get_subprocess_pattern_mut());
        }
    }

    /// Draw the buttons/inputs on the bottom of the process topology view.
    fn topology_buttons_process<'a>(&self, 
        frame:       &mut Frame,
        rect:        Rect,
        backend:     &mut UIBackend,
        text_inputs: &mut[TextInput<'a>])
    {
        let chunks = chunks_horizontal(rect, &[
            Constraint::Length(15),  // "<key> no HT/SMT"                 
            Constraint::Length(30),  // "<key> apply thread affinities @" 
            Constraint::Length(10),  // 600.10 float, with an 's' suffix  
            Constraint::Length(1),   // 's'                               
            Constraint::Min(1)
        ]);

        let (rect_nosmt, rect_apply_label, rect_apply_time, rect_s) = 
            (chunks[0], chunks[1], chunks[2], chunks[3]);
        frame.render_widget(Block::default().style(Style::default().add_modifier(Modifier::UNDERLINED)), rect);
        frame.render_widget(
            Paragraph::new(Text::styled(format!("<n> {}", strings::NO_SMT), Style::default().fg(Color::White))), 
            rect_nosmt);
        let current_order = backend.get_current_order();
        // In run mode, we also have the text entry 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) {
                // Apply thread affinities hint label.
                frame.render_widget(
                    Paragraph::new(Text::styled(format!("<r> {}", strings::APPLY_THREAD_AFFINITIES), Style::default().fg(Color::White))), 
                    rect_apply_label);
                let thread_apply_time = &mut text_inputs[TEXTINPUT_THREAD_APPLY_TIME];
                let time_str = thread_apply_time.textarea.lines().get(0);

                if time_str.is_some_and(|s| !s.is_empty()) {
                    let time = time_str.unwrap().parse::<f64>().expect("input validation checks this");
                    set_active_textarea_style(&mut thread_apply_time.textarea);
                    *run.get_thread_apply_time_mut() = time 
                }
                // Apply thread affinities text input.
                frame.render_widget(thread_apply_time.textarea.widget(), rect_apply_time);
                frame.render_widget(
                    Paragraph::new(Text::styled("s", Style::default().fg(Color::White))), 
                    rect_s);
            }
        }
    }

    /// Draw the buttons on the right of the CPU topology view (thread order and navigation help).
    fn topology_sidebar(&self, 
        frame:   &mut Frame,
        rect:    Rect,
        backend: &mut UIBackend)
    {
        // `height + 1`: Hack to make use of one more row, which is empty on the sidebar side.
        let rect = Rect::new(rect.x, rect.y, rect.width, rect.height + 1);
        // On small screens, reflow the help to use less space.
        let compact = rect.height < 14;
        let chunks_side = chunks_vertical(rect, &[
                Constraint::Length(7), 
                if compact { Constraint::Length(6) } else { Constraint::Length(7) }
            ]);
        let (rect_side_buttons_block, rect_legend) = (chunks_side[0], chunks_side[1]);

        let buttons_borders = Borders::LEFT | Borders::RIGHT | Borders::TOP; // BOTTOM would be duplicated
        let block_side_buttons = Block::default().title(format!("<+> {}", strings::THREAD_ORDER))
                                                 .borders(buttons_borders);
        let rect_side_buttons = block_side_buttons.inner(rect_side_buttons_block);
        frame.render_widget(block_side_buttons, rect_side_buttons_block);

        let chunks_side_buttons = chunks_vertical(rect_side_buttons,
                                                  &[Constraint::Min(1), Constraint::Length(1)]);
        let (rect_order_radios, rect_ignore_main_thread) = (chunks_side_buttons[0], chunks_side_buttons[1]);
        let ignore_main_thread = backend.get_thread_order_ignore_main_thread();

        self.thread_order_radios(frame, rect_order_radios, backend.get_current_order());
        checkbox(frame, rect_ignore_main_thread, ignore_main_thread, strings::IGNORE_MAIN, "-");

        let block_legend =
            Block::default().title(format!("core control")).borders(Borders::ALL);
        let wasd_text = format!("{} navigation\n<Enter> select/in\n<Esc>   out\n<Space> toggle cores",
                                if compact {"🆆 🅰 🆂 🅳"} else {"  🆆  \n🅰 🆂 🅳  "});

        frame.render_widget(
            Paragraph::new(wasd_text).style(Style::default().fg(Color::White)).block(block_legend), rect_legend);

        // Fixup due to drawing a single border between the thread orders and the legend.
        frame.buffer_mut().set_string(rect_legend.x, rect_legend.y, "├", Style::default());
        frame.buffer_mut().set_string(rect_legend.right() - 1, rect_legend.y, "┤", Style::default());
    }

    /// Draw the radios showing thread order. Thread order is rotated in handle_events_term().
    fn thread_order_radios(&self, frame: &mut Frame, rect: Rect, order: Option<ThreadOrder>) {
        let chunks_radios = chunks_vertical(rect, &[
            Constraint::Length(1), 
            Constraint::Length(1), 
            Constraint::Length(1), 
            Constraint::Length(1), 
            Constraint::Min(1),  
        ]);

        radio_value(frame, chunks_radios[0], order == Some(ThreadOrder::ByLCore), "HW threads");
        radio_value(frame, chunks_radios[1], order == Some(ThreadOrder::ByCore),  "cores");
        radio_value(frame, chunks_radios[2], order == Some(ThreadOrder::ByL3),    "L3");
        radio_value(frame, chunks_radios[3], order == None,                       "none");
    }

    /// Draw the footer at the bottom of main (process/thread topology) view.
    fn topology_footer(&self, 
        frame:   &mut Frame,
        rect:    Rect,
        backend: &mut UIBackend)
    {
        let current_order = backend.get_current_order();
        let mut draw_run = |enabled, label| {
            let modifier = if enabled { Modifier::empty() } else {Modifier::CROSSED_OUT};
            let text     = Text::styled(format!("<C-a> {}", label), Style::default().add_modifier(modifier));
            let block    = Block::default().fg(if enabled { Color::Yellow } else {Color::DarkGray});
            frame.render_widget(Paragraph::new(text).alignment(Alignment::Right).block(block), rect);
        };
        match backend.get_mode() {
            UIMode::Run(ref run)  => {
                if let Some(duration_ratio) = run.wait_for_thread_start_ratio() {
                    frame.render_widget(Gauge::default().ratio(duration_ratio.min(1.0).into()).use_unicode(true), rect);
                } else {
                    draw_run(run.is_run_enabled(current_order), "Run");
                }
            },
            // Nothing to 'apply' or 'run' in view mode.
            UIMode::View(..)  => {},
            UIMode::Edit(ref edit)  => {
                draw_run(edit.is_apply_enabled(), "Apply");
            },
        }
    }
}
