use std::sync::mpsc;
use std::collections::HashMap;
use std::time::{
    Duration,
    Instant
};
use hwloc2;
use log::*;

use crate::topologycache::TopologyObjectInfo;
use crate::util;
use util::CpuSetExt;
use crate::ordered_core_selections::OrderedCoreSelections;
use crate::system::{
    ThreadOrder,
    System,
};
use crate::cpumap::Event;
use crate::strings;

/// Default value for `UIMode::thread_apply_time`
pub const THREAD_APPLY_TIME_DEFAULT: f64 = 1.2;

/// Possible states the 'run' mode may be in.
#[derive(Clone)]
enum RunState {
    /// Initial state before pressing 'run' - editing affinity for the process about to be run.
    EditAffinity,
    /// Entered after clicking 'run' with a thread order or specified thread affinities;
    /// waiting for fixed duration to give the process time to start threads; when done waiting,
    /// sets affinities for any threads started by that time.
    WaitForThreadStart{
        /// Time when we started waiting.
        wait_start:    Instant,
        /// Duration to wait for.
        wait_duration: Duration,
        /// Processes started by the `run` command - will set thread affinities for all of them.
        /// This is almost always a single process.
        processes:     Vec<sysinfo::Pid>
    }
}

/// Mode that allows to launch a new process with specified affinities.
#[derive(Clone)]
pub struct UIModeRun {
    /// Current core (PU) selection to determine affinity for the process that will be run.
    selected_cores_process: hwloc2::CpuSet,
    /// Threads with 'manually set affinity' (threads shown in the 'threads' collapsible in Run 
    /// mode).
    ///
    /// In Run mode, we can't know how many threads the process we run will have, but we can
    /// set affinities for e.g. 4 threads and those will be set for the (up to) first 4 threads
    /// spawned by the process.
    ///
    /// None means there is no manually set affinity yet for the thread at this index (i.e.
    /// only the process affinity applies to that thread).
    selected_cores_threads: OrderedCoreSelections,
    /// During CPU topology rendering, this is the thread we're currently rendering, or None if
    /// rendering the process.
    currently_rendering_thread_index: Option<usize>,
    /// Current command to run.
    command:               String,
    /// If non-empty, recursively look for subprocess/es of launched command that fuzzily match
    /// this pattern, and set affinity for those subprocess/es instead of process/es directly
    /// launched by the command.
    subprocess_pattern:    String,
    /// User-enterable delay (seconds) to apply thread affinities after launching the process.
    thread_apply_time:     f64,
    /// Current 'run state', to differentiate between core selecting and waiting to set thread
    /// affinities after launching process.
    state:                 RunState,
}

impl UIModeRun {
    /// Used in Run order to determine if we have any per-thread affinities to set.
    ///
    /// `current_order` is current thread order passed from `UIBackend`.
    pub fn have_thread_affinities(&self, current_order: Option<ThreadOrder>) -> bool {
        current_order.is_some() || !self.selected_cores_threads.all_default()
    }

    /// True if the 'run' button should be enabled (if we have all parameters we need to launch a process).
    ///
    /// `current_order` is current thread order passed from `UIBackend`.
    pub fn is_run_enabled(&self, current_order: Option<ThreadOrder>) -> bool {
        let selections_valid = self.selected_cores_threads.validate_selections();
        !self.selected_cores_process.is_empty() &&
        (!self.have_thread_affinities(current_order) || selections_valid) &&
        !self.command.is_empty() && self.wait_for_thread_start_ratio().is_none()
    }

    /// If waiting before setting thread affinities after starting a process, get progress (0.0 to 1.0) of that wait.
    ///
    /// Otherwise returns None.
    pub fn wait_for_thread_start_ratio(&self) -> Option<f32> {
        if let RunState::WaitForThreadStart{wait_start, wait_duration, ..} = self.state {
            let now = Instant::now();
            let duration_so_far = now.duration_since(wait_start);
            let duration_ratio = duration_so_far.as_micros() as f64 / wait_duration.as_micros() as f64;
            Some(duration_ratio as f32)
        } else {
            None
        }
    }

    /// Get status of selection of given PU in the thread/process view we're currently rendering.
    ///
    /// A PU may be selected in a process view, selected in a thread view, or not selected at all.
    fn get_pu_selection_status_in_currently_rendering_thread(&self, pu_id: u32) -> PUSelectionStatus {
        let rendering_thread = self.currently_rendering_thread_index;
        if let Some(ref cores) = rendering_thread.and_then(|index|{self.selected_cores_threads.get(index)}) {
            // If we have a thread core selection, only those affinities apply - process affinities are ignored for the thread.
            return if cores.is_set(pu_id) {PUSelectionStatus::SelectedInThread} else {PUSelectionStatus::NotSelected}
        }
        let is_selected_in_process = self.selected_cores_process.is_set(pu_id);
        if is_selected_in_process {PUSelectionStatus::SelectedInProcess} else {PUSelectionStatus::NotSelected}
    }

    /// True if thread with specified index has edited affinities.
    fn is_thread_edited(&self, thread_index: usize) -> bool {
        self.selected_cores_threads.get(thread_index).is_some()
    }

    /// Get number of threads we're setting affinities for (before launching the process).
    fn get_thread_count(&self) -> usize {
        self.selected_cores_threads.len()
    }

    /// Read/write access to command to run (to allow frontend to change it).
    pub fn get_command_mut(&mut self) -> &mut String {
        &mut self.command
    }

    /// Read/write access to pattern of the subprocess to set affinities for (to allow frontend to change it).
    pub fn get_subprocess_pattern_mut(&mut self) -> &mut String {
        &mut self.subprocess_pattern
    }

    /// Read/write access to time delay before applying thread affinities (to allow frontend to change it).
    pub fn get_thread_apply_time_mut(&mut self) -> &mut f64 {
        &mut self.thread_apply_time
    }

    /// Update the 'thread count' value - number of threads we're explicitly specifying affinities for.
    ///
    /// May add new items to `self.selected_cores_threads` and init them based on thread order.
    ///
    /// `current_order` is current thread order passed from `UIBackend`.
    fn update_thread_count( &mut self,
        new_count:                       usize,
        system:                          &System,
        thread_order_ignore_main_thread: bool,
        current_order:                   Option<ThreadOrder>)
    {
        let old_count = self.get_thread_count();

        // Handle change in 'thread count'.
        if new_count > old_count {
            info!("run: increasing thread count {}->{}", old_count, new_count);
            for thread_idx in old_count..new_count {
                // May seem redundant as thread order will be applied when the process 
                // starts running anyway, but doing it here to `selected_cores_threads`
                // allows us to see, in the UI, how the affinities for those threads 
                // will be set, and to tweak it.
                if let Some(order) = current_order {
                    let entry = system.apply_thread_order_single_thread(order, thread_idx, &mut self.selected_cores_process, thread_order_ignore_main_thread);
                    self.selected_cores_threads.push(entry);
                } else {
                    self.selected_cores_threads.push(None);
                }
            }
        } else if old_count > new_count {
            info!("run: decreasing thread count {}->{}", old_count, new_count);
            self.selected_cores_threads.truncate(new_count);
        }
    }

    /// Delete all explicit thread affinities.
    pub fn clear_threads(&mut self) {
        // Keep as many thread core selections as the user specified, just make them 'default'.
        info!("run: clearing threads");
        self.selected_cores_threads.set_all_default();
    }

    /// Apply changed thread order to explicitly set thread affinities.
    pub fn apply_thread_order(&mut self, order: &ThreadOrder, system: &System, ignore_main_thread: bool) {
        info!("run: applying thread order {:?}", order);
        self.selected_cores_threads.apply_thread_order(
            *order,
            system,
            &mut self.selected_cores_process,
            ignore_main_thread);
    }

    /// Disable SMT for explicitly set thread affinities, allowing only one thread per core.
    pub fn disable_affinity_smt(&mut self, system: &System) {
        info!("run: disabling SMT");
        // Disable SMT by disabling every other PU, for both the process and thread core selections.
        system.disable_affinity_smt(&mut self.selected_cores_process);
        for item in self.selected_cores_threads.iter_mut() {
            if let Some(selected_cores) = item {
                system.disable_affinity_smt(selected_cores);
            }
        }
    }
}

/// Mode to view process/thread affinities and start editing (switch to Edit mode).
#[derive(Clone)]
pub struct UIModeView {
    /// PID of the currently selected process (a process is always selected in View mode).
    selected_process: sysinfo::Pid,
    /// Affinity of the currently selected process, updated on each UI update.
    process_affinity: hwloc2::CpuSet,
    /// Affinities of all threads of the currently selected process, updated on each UI update.
    thread_affinities: HashMap<sysinfo::Pid, hwloc2::CpuSet>,
    /// During CPU topology rendering, this is the thread we're currently rendering, or None if
    /// rendering the process.
    currently_rendered_thread: Option<sysinfo::Pid>
}

impl UIModeView {
    /// True if specified PU has affinity in currently rendered thread (or process) affinity view.
    fn pu_affinity_in_currently_rendering_thread(&self, pu_id: u32) -> bool {
        if let Some(tid) = self.currently_rendered_thread {
            let affinity = self.thread_affinities.get(&tid).expect("thread_affinities entry should exist until process refresh");
            affinity.is_set(pu_id)
        } else {
            self.process_affinity.is_set(pu_id)
        }
    }

    /// Get number of threads of the currently viewed process.
    fn get_thread_count(&self) -> usize {
        self.thread_affinities.len()
    }

    /// Get PID of the curently viewed process.
    pub fn get_selected_process(&self) -> sysinfo::Pid {
        self.selected_process
    }

    /// Get affinity of the currently viewed process.
    pub fn get_process_affinity(&self) -> &hwloc2::CpuSet {
        &self.process_affinity
    }

    /// Dissable SMT for thread affinities in currently viewed process, switching to Edit mode.
    pub fn disable_affinity_smt(&self, system: &System) -> hwloc2::CpuSet {
        info!("view: disabling SMT");
        let mut selected_cores_process = self.process_affinity.clone();
        system.disable_affinity_smt(&mut selected_cores_process);
        selected_cores_process
    }

    /// Get thread/process affinities for currently viewed process with specified thread order.
    ///
    /// Returns core selections for threads (by PID) and the process.
    ///
    /// Caller can e.g. use this information to switch to the edit mode.
    pub fn apply_thread_order(&self, order: ThreadOrder, system: &System, ignore_main_thread: bool) -> (HashMap<sysinfo::Pid, hwloc2::CpuSet>, hwloc2::CpuSet) {
        info!("view: applying thread order {:?}", order);
        let mut selected_cores_threads = HashMap::new();
        let mut selected_cores_process = hwloc2::CpuSet::new();
        system.apply_thread_order(order, self.selected_process, &mut selected_cores_process, &mut selected_cores_threads, ignore_main_thread);
        (selected_cores_threads, selected_cores_process)
    }
}

/// Mode to edit and set affinities of a process and its threads.
#[derive(Clone)]
pub struct UIModeEdit {
    /// PID of the currently selected process (a process is always selected in View mode).
    selected_process: sysinfo::Pid,
    /// Currently selected cores.
    selected_cores_process: hwloc2::CpuSet,
    /// Sets of selected cores overriding `selected_cores_process` - for each thread where user
    /// has made such a manual selection.
    selected_cores_threads: HashMap<sysinfo::Pid, hwloc2::CpuSet>,
    /// During CPU topology rendering, this is the thread we're currently rendering, or None if
    /// rendering the process.
    currently_rendered_thread: Option<sysinfo::Pid>,
    /// Thread count of the edited process.
    thread_count: usize
}

impl UIModeEdit {
    /// Get status of selection of given PU in the thread/process view we're currently rendering.
    ///
    /// A PU may be selected in a process view, selected in a thread view, or not selected at all.
    fn get_pu_selection_status_in_currently_rendering_thread(&self, pu_id: u32) -> PUSelectionStatus {
        let rendering_thread = self.currently_rendered_thread;
        if let Some(ref cores) = rendering_thread.and_then(|tid|{self.selected_cores_threads.get(&tid)}) {
            // If we have a thread core selection, only those affinities apply - process affinities are ignored for the thread.
            return if cores.is_set(pu_id) {PUSelectionStatus::SelectedInThread} else {PUSelectionStatus::NotSelected}
        }
        let is_selected = self.selected_cores_process.is_set(pu_id);
        if is_selected {PUSelectionStatus::SelectedInProcess} else {PUSelectionStatus::NotSelected}
    }

    /// True if the apply button should be enabled, i.e. there is a valid change in affinity settings.
    pub fn is_apply_enabled(&self) -> bool {
        let no_empty_thread_selections = !self.selected_cores_threads.iter().any(|(_, selected_cores)| selected_cores.is_empty());
        // Empty set of selected cores makes no sense, the process and each of its
        // threads must have at least one core to run.
        !self.selected_cores_process.is_empty() && no_empty_thread_selections
    }

    /// True if thread with specified Pid has edited affinities.
    fn is_thread_edited(&self, tid: sysinfo::Pid) -> bool {
        self.selected_cores_threads.contains_key(&tid) 
    }

    /// Get PID of the currently edited process.
    pub fn get_selected_process(&self) -> sysinfo::Pid {
        self.selected_process
    }

    /// Apply any affinity changes to currently edited process.
    ///
    /// May fail (e.g. permissions), in which case an Err will be returned.
    fn apply(&self, system: &mut System) -> anyhow::Result<()> {
        info!("edit: applying affinity");
        let cores = self.selected_cores_process.clone();
        system.set_cpu_affinity( self.selected_process, hwloc2::CpuBindFlags::CPUBIND_PROCESS, cores )?;
        system.set_cpu_affinity_threads(&self.selected_cores_threads)?;
        Ok(())
    }

    /// Disable SMT for thread affinities in currently edited process.
    pub fn disable_affinity_smt(&mut self, system: &System) {
        info!("edit: disabling SMT");
        system.disable_affinity_smt(&mut self.selected_cores_process);
        for (_, mut selected_cores_thread) in self.selected_cores_threads.iter_mut() {
            system.disable_affinity_smt(&mut selected_cores_thread);
        }
    }

    /// Apply specified thread order to currently edited process.
    pub fn apply_thread_order(&mut self, order: ThreadOrder, system: &System, ignore_main_thread: bool) {
        info!("edit: applying thread order {:?}", order);
        system.apply_thread_order(order,
                                  self.selected_process,
                                  &mut self.selected_cores_process,
                                  &mut self.selected_cores_threads,
                                  ignore_main_thread);
    }

    /// Delete all explicit thread affinities.
    pub fn clear_threads(&mut self) {
        info!("edit: clearing thread affinities");
        self.selected_cores_threads.clear();
    }
}

/// In Run mode, this ID identifies a 'thread core selection' for a thread that will possibly run
/// when the program starts - used to manually set affinities for threads before starting them.
/// In View/Edit modes, this ID is an actual thread PID.
///
/// Can be `None`, which means "we're dealing with the process, not any of its thread"
pub type ThreadID = u32;

/// CPUMap UI modes, similar to e.g. Vim modal interface.
#[derive(Clone)]
pub enum UIMode {
    /// Run a new process with specified CPU affinities.
    Run (UIModeRun),
    /// View CPU affinities. (and TODO more CPU information, e.g. load)
    View (UIModeView),
    /// Select cores to set affinities for the current process.
    Edit(UIModeEdit)
}

impl UIMode {
    /// Construct a 'default' Run UIMode.
    fn new_run(system: &System) -> UIMode {
        UIMode::Run( UIModeRun{
            selected_cores_process:           system.affinity_full(),
            selected_cores_threads:           OrderedCoreSelections::new(),
            currently_rendering_thread_index: None,
            command:                          String::new(),
            subprocess_pattern:               String::new(),
            thread_apply_time:                THREAD_APPLY_TIME_DEFAULT,
            state:                            RunState::EditAffinity,
        })
    }

    /// Determine if affinities for a thread have been edited.
    pub fn is_thread_edited(&self, thread_id: ThreadID) -> bool {
        match self {
            UIMode::Run(ref  run)  => run.is_thread_edited(thread_id.try_into().unwrap()),
            UIMode::Edit(ref edit) => edit.is_thread_edited(sysinfo::Pid::from_u32(thread_id)),
            UIMode::View(..)       => false
        }
    }

    /// Get a vector of IDs and names of all threads (incl. main thread) of currently selected process.
    /// 
    /// Works even in Run mode, when it gets the thread indices and their string conversions.
    pub fn get_all_thread_ids(&self, system: &System) -> Vec<(ThreadID, String)> {
        let editview_get_thread_ids = |pid: sysinfo::Pid| {
            let main = std::iter::once((pid.as_u32(), "[main]".to_string()));
            let nonmain = system.get_nonmain_threads_of(pid).expect("currently selected process should exist since last sysinfo update")
                                .into_iter().map(|(tid, thread)| (tid.as_u32(), thread.name().to_string()));
            main.chain(nonmain).collect()
        };
        match self {
            UIMode::Run(ref run)   => {
                let thread_count = run.get_thread_count();
                (0..thread_count)
                    .map(|n| (n as u32, if n == 0 {"[main]".to_string()} else { n.to_string()})).collect()
            },
            UIMode::View(ref view) => {
                editview_get_thread_ids(view.selected_process)
            },
            UIMode::Edit(ref edit) => {
                editview_get_thread_ids(edit.selected_process)
            },
        }
    }

    /// Returns true if we're currently in thread topology code, regardless of mode.
    ///
    /// Used to draw topology more compactly for threads.
    pub fn currently_rendering_a_thread(&self) -> bool {
        match self {
            UIMode::Run(ref run)   => run.currently_rendering_thread_index.is_some(),
            UIMode::View(ref view) => view.currently_rendered_thread.is_some(),
            UIMode::Edit(ref edit) => edit.currently_rendered_thread.is_some()
        }
    }

    /// ID of currently rendered thread if `currently_rendering_a_thread`, otherwise None.
    pub fn currently_rendering_thread_id(&self) -> Option<ThreadID> {
        match self {
            UIMode::Run(ref run)   => run.currently_rendering_thread_index.map(|t| t.try_into().unwrap()),
            UIMode::View(ref view) => view.currently_rendered_thread.map(|t| t.as_u32()),
            UIMode::Edit(ref edit) => edit.currently_rendered_thread.map(|t| t.as_u32())
        }
    }

    /// Get ID of thread at specified index (from the first thread).
    ///
    /// If `index` is out of bounds, returns `None`.
    pub fn thread_index_to_id(&self, index: usize, system: &System) -> Option<ThreadID> {
        let get_id_viewedit = |pid| {
            let threads = system.get_nonmain_threads_of(pid)
                .expect("currently selected process should exist since last sysinfo update");
            if index == 0 { 
                Some(pid.as_u32())  // PID is the main (0-th) thread's ID as well
            } else if index <= threads.len() {
                Some(threads[index - 1].0.as_u32()) // Get a nonmain thread's TID
            } else {
                None // No thread at this index
            }
        };
        match self {
            UIMode::Run(ref run)   => if index < run.get_thread_count() {
                                          Some(index.try_into().unwrap()) 
                                      } else { 
                                          None 
                                      },
            UIMode::View(ref view) => get_id_viewedit(view.selected_process),
            UIMode::Edit(ref edit) => get_id_viewedit(edit.selected_process)
        }
    }

    /// Update currently rendered thread ID before/after rendering that thread's topology view.
    pub fn set_currently_rendered_thread(&mut self, opt_tid: Option<ThreadID>) {
        match self {
            UIMode::Run(ref mut run)   => run.currently_rendering_thread_index = opt_tid.map(|i| usize::try_from(i).unwrap()),
            UIMode::View(ref mut view) => view.currently_rendered_thread = opt_tid.map(|x| sysinfo::Pid::from_u32(x)),
            UIMode::Edit(ref mut edit) => edit.currently_rendered_thread = opt_tid.map(|x| sysinfo::Pid::from_u32(x))
        }
    }
}

/// Status of a PU selection for affinity in Edit/Run mode.
pub enum PUSelectionStatus {
    /// The PU is selected in process core selection.
    SelectedInProcess,
    /// The PU is selected in a thread core selection, overriding any process core selection.
    SelectedInThread,
    /// The PU is not selected.
    NotSelected
}

/// UI-related state and logic.
pub struct UIBackend {
    /// Current user-entered process name filter string.
    filter:           String,
    /// Current UI mode.
    mode:             UIMode,
    /// Mode to switch to at the end of current update (`None` to stay in current mode).
    next_mode:        Option<UIMode>,
    /// Current status string (e.g. errors), shown in the statusbar.
    status:           String,
    /// If true, don't show the process selection panel (left sidebar).
    hide_process_selection_panel:    bool,
    /// Is `ignore main thread` checkbox checked? If so, thread orders will not include the main thread.
    thread_order_ignore_main_thread: bool,
    /// Index of the current update.
    update_index:                    usize,
    /// Receiver to get out-of-threads events from.
    event_rx:                        mpsc::Receiver<Event>,
    /// Thread order, if any, for the process to run (assigns threads to cores in a specific order).
    ///
    /// Used edit and run modes, to set affinities of existing threads or to apply affinities to
    /// threads beyond those specified manually.
    current_order:                   Option<ThreadOrder>,
}

impl UIBackend {
    /// Read and write to current thread order (to alllow frontend to change it).
    pub fn get_current_order_mut(&mut self) -> &mut Option<ThreadOrder> {
        &mut self.current_order
    }

    /// Get current thread order, if any.
    pub fn get_current_order(&self) -> Option<ThreadOrder> {
        self.current_order
    }

    /// Construct an UIState using specified System.
    pub fn new(system: &System, event_rx: mpsc::Receiver<Event>) -> Self {
        Self{ 
            filter:                          String::new() ,
            mode:                            UIMode::new_run(system),
            next_mode:                       None,
            status:                          String::from("OK"),
            hide_process_selection_panel:    false,
            thread_order_ignore_main_thread: true,
            update_index:                    0,
            event_rx,
            current_order:                   None
        }
    }

    /// Handle events from the tick thread.
    pub fn handle_events(&mut self, system: &mut System) {
        self.update_index += 1;
        match self.event_rx.try_recv() {
            // Got a tick, refresh process info.
            Ok(Event::Tick) => {
                system.refresh_process_info();
                self.handle_process_refresh(system, true);
            },
            Err(mpsc::TryRecvError::Empty) => {
            },
            Err(mpsc::TryRecvError::Disconnected) => {
                panic!("tick thread should not stop");
            }
        }
    }

    /// Are we currently doing the first UI update?
    ///
    /// Used to allow some init that can only be done once e.g. egui is already running.
    pub fn is_first_update(&self) -> bool {
        self.update_index == 0
    }

    /// Get processes currently visible on process panel based on the filter entered by user.
    pub fn get_filtered_user_processes<'a>(&self, system: &'a System) -> 
        Vec<(&'a sysinfo::Pid, &'a sysinfo::Process)>
    {
        system.get_process_filter_matches(self.filter.as_str(), false)
    }

    /// Returns help, shortcut info for current mode and a bool that is true only in Run mode.
    pub fn get_current_mode_help(&self) -> (&str, &str, bool) {
        match self.mode {
            UIMode::Run(..)  => { (strings::HELP_RUN,  strings::HELP_RUN_SHORTCUTS, true)  },
            UIMode::View{..} => { (strings::HELP_VIEW, strings::HELP_VIEW_SHORTCUTS, false) },
            UIMode::Edit{..} => { (strings::HELP_EDIT, strings::HELP_EDIT_SHORTCUTS, false) }
        }
    }

    /// Get current mode.
    pub fn get_mode(&self) -> &UIMode {
        &self.mode
    }

    /// Get current value of the 'ignore main' thread order checkbox, i.e. whether the thread order should include the main thread.
    pub fn get_thread_order_ignore_main_thread(&self) -> bool {
        self.thread_order_ignore_main_thread
    }

    /// Get current status string (what to show in the statusbar).
    pub fn get_status(&self) -> &String {
        &self.status
    }

    /// True if the process selection panel should be hidden.
    pub fn get_hide_process_selection_panel(&self) -> bool {
        self.hide_process_selection_panel
    }

    /// Get current selected process regardless of mode (if any).
    pub fn get_selected_pid(&self) -> sysinfo::Pid {
        match self.mode {
            UIMode::Run(..)        => sysinfo::Pid::from(0),
            UIMode::View(ref view) => view.selected_process,
            UIMode::Edit(ref edit) => edit.selected_process,
        }
    }

    /// Get the number of threads (or virtual 'threads' in Run mode).
    pub fn get_thread_count(&self) -> usize {
        match self.mode {
            UIMode::Run(ref run)   => run.get_thread_count(),
            UIMode::View(ref view) => view.get_thread_count(),
            UIMode::Edit(ref edit) => edit.thread_count
        }
    }

    /// Get read-write access to the process filter string.
    pub fn get_filter_mut(&mut self) -> &mut String {
        &mut self.filter
    }

    /// Get read-write access to current UI mode, allowing to modify mode variables.
    pub fn get_mode_mut(&mut self) -> &mut UIMode {
        &mut self.mode
    }

    /// Get read-write access to the 'ignore main thread' thread order flag.
    pub fn get_thread_order_ignore_main_thread_mut(&mut self) -> &mut bool {
        &mut self.thread_order_ignore_main_thread
    }

    /// Get read-write access to flag determining whether the process selection panel should be hidden.
    pub fn get_hide_process_selection_panel_mut(&mut self) -> &mut bool {
        &mut self.hide_process_selection_panel
    }

    /// In run mode, update thread count (add/remove threads to set affinities when we run the process).
    ///
    /// May only be called in run mode; makes no sense in edit/view modes.
    ///
    /// May immediately set affinities for any added threads, depending on thread order.
    pub fn run_update_thread_count(&mut self, new_count: usize, system: &System) {
        let UIMode::Run(ref mut run) = self.mode else {
            panic!("run_update_thread_count can only be called in Run mode");
        };
        // We could expose this directly, but then the caller would have to explicitly get/pass
        // thread order related fields.
        run.update_thread_count(
            new_count,
            system,
            self.thread_order_ignore_main_thread,
            self.current_order);
    }

    /// Run mode: Launch a shell command with process affinity specified by run mdoe state.
    ///
    /// May start a wait (still in run mode) to set thread affinities after delay.
    pub fn run_shell_command_with_process_affinity(&mut self, system: &mut System) {
        let mut next_run_state = None;
        let UIMode::Run(ref run) = self.mode else {
            panic!("run_shell_command_with_process_affinity can only be called in Run mode");
        };
        info!("running a command '{}' with affinity", run.command);
        match system.run_shell_command_and_set_process_affinity(&run.selected_cores_process, &run.command, &run.subprocess_pattern) {
            Ok(shell_children) => {
                // The command is running now, determine if we need to wait to set
                // thread affinities and if so, change state to `WaitForThreadStart`
                let thread_apply_time = Duration::from_secs_f64(run.thread_apply_time);
                let state_change = 
                    self.handle_started_command(
                        system,
                        shell_children,
                        self.current_order,
                        &run.selected_cores_threads.clone(), // clone to allow mutable borrow of `self`
                        thread_apply_time);
                if let Some(new_state) = state_change {
                    next_run_state = Some(new_state);
                }
            },
            Err(error) => {
                self.set_status(format!("{}: {}", strings::ERROR, error));
            }
        }

        // `run_shell_command_and_set_process_affinity` refreshes `sysinfo` process info so we need to handle that.
        self.handle_process_refresh( system, false );

        // Handle any 'Run' mode state change.
        if let Some(next_state) = next_run_state {
            let UIMode::Run(ref mut run, ..) = self.mode else {
                panic!("next_run_state can only be set when we're in Run state");
            };
            run.state = next_state;
        }
    }

    /// Called when `apply` is clicked in Edit mode.
    ///
    /// Applies any affinity changes and switches mode back to View.
    pub fn edit_apply_clicked(&mut self, system: &mut System) {
        info!("edit: 'apply' clicked");
        let UIMode::Edit(ref edit) = self.mode else {
            panic!("edit_apply_clicked can only be called in Edit mode");
        };
        let selected_process = edit.get_selected_process();
        if let Err(error) = edit.apply(system) {
            self.set_status(format!("{}: {:?}", strings::STATUS_FAILED_SET_AFFINITY, error));
        }
        // After applying changes in Edit, go back to viewing the same process.
        self.set_next_mode_view(system, selected_process);
    }

    /// Shared logic for keyboard input for the process selection panel.
    ///
    /// Changes selected process based on keyboard input.
    ///
    /// * `prevnext_pressed` is true if the `previous process` shortcut has been pressed, false 
    ///   if the `next process` shortcut has been pressed, and `None` otherwise.
    ///                                                                        /
    /// If no process is currently selected, selects the first process in list 
    /// (if any) and sets next mode to View.
    pub fn process_selection_panel_keyboard_control(&mut self, prevnext_pressed: Option<bool>, system: &System)
    {
        if let UIMode::Run(..) = self.get_mode() {
            // In run mode, any change in process selection will enter the view mode.
            // There is no selected PID in run mode so we select the first one.
            if prevnext_pressed.is_some() {
                // In Run mode, there is no selected process, so we have to start from processes[0] in
                // process_selection_panel instead of from selected_pid's position
                self.try_set_next_mode_view_anyprocess(system);
            }
        } else {
            let processes = self.get_filtered_user_processes( system );
            // Get current selected process regardless of mode (if any).
            let selected_pid = self.get_selected_pid();

            if let Some(selected_pid_idx) = processes.iter().position(|i| *i.0 == selected_pid) {
                // There is a selected process and it exists; move to previous/next based on key pressed.
                if prevnext_pressed == Some(true) {
                    let new_selected_pid = processes[selected_pid_idx.saturating_sub(1)].0;
                    self.set_next_mode_view(system, *new_selected_pid);
                } else if prevnext_pressed == Some(false) {
                    let new_selected_pid = processes[usize::min(processes.len() - 1, selected_pid_idx + 1)].0;
                    self.set_next_mode_view(system, *new_selected_pid);
                }
            } else {
                // Either no selected process or the selected process does not exist / is not in filtered
                // processes.
                self.try_set_next_mode_view_anyprocess(system);
            }
        }
    }

    /// Get command that launched process with specified PID.
    pub fn get_process_cmd(pid: sysinfo::Pid, system: &System) -> String {
        let process = system.get_process(pid)
                            .expect("no process info update since we got the Pid, process must exist");
        process.cmd().join(" ")
    }

    /// Set statusbar status to specified string and write that string to stderr.
    pub fn set_status(&mut self, status: String) {
        info!("status: '{}'", status);
        self.status = status; 
    }

    /// Called at the end of an UI update to change UI mode if `next_mode` was set during the update.
    ///
    /// Returns `true` if there was a mode change.
    pub fn end_update(&mut self) -> bool {
        // Apply mode change, if any.
        if let Some(mode) = self.next_mode.take() {
            self.mode = mode;
            return true;
        }
        false
    }

    /// Check completion of, and handle any timeouts in the UI.
    ///
    /// Currently only the wait in Run mode to set thread affinities.
    pub fn handle_timeouts(&mut self, system: &mut System) {
        let UIMode::Run(ref mut run) = self.mode else {
            return;
        };

        let RunState::WaitForThreadStart{wait_start, wait_duration, ref processes} = run.state else {
            return;
        };

        let now = Instant::now();
        let duration_so_far = now.duration_since(wait_start);
         
        // Command used in 'run' may launch multiple proceses, in which case we set affinities for all of them.
        let active_processes: Vec<(sysinfo::Pid, String)> =
            processes.iter() 
                     .map(|pid| system.get_process(*pid))
                     .filter_map(|p| p)
                     .map(|p| (p.pid(), String::from(System::get_process_name(p))))
                     .collect();

        let no_overrides = run.selected_cores_threads.all_default();
        // Any proceses launched by 'run' have died while waiting, reset Run mode.
        if active_processes.len() == 0 {
            self.set_status(String::from(strings::STATUS_PROCESSES_DIED_EARLY));
            self.set_next_mode_run(system);
        // Thread order has been deselected, nothing to set thread affinities from anymore, go to View launched process.
        } else if self.current_order.is_none() && no_overrides {
            self.set_status(String::from(strings::STATUS_THREAD_AFFINITIES_CANCELED));
            let pid = active_processes.first().expect("empty active_processes is handled above").0;
            self.set_next_mode_view(system, pid);
        // Timeout has expired - apply thread affinities.
        } else if duration_so_far >= wait_duration {
            info!("applying run mode thread affinities");
            let mut success = true;
            // Apply affinity to each active process.
            for (pid, name) in active_processes.iter() {
                // Unused as we only care about applying thread affinities; no process affinities to
                // store unlike edit mode (process affinities in OS will get updated automatically)
                let mut dummy_selected_cores_process = hwloc2::CpuSet::new();
                let mut apply_selected_cores_threads = HashMap::new();
                // First apply thread order (if any) - it was initially applied when adding items to
                // `selected_cores_threads`, but there may be more threads than specified there.
                // Then override that thread order by any non-optional `selected_cores_threads`.
                if let Some(order) = self.current_order {
                    system.apply_thread_order(order, *pid, &mut dummy_selected_cores_process, &mut apply_selected_cores_threads, self.thread_order_ignore_main_thread);
                }
                apply_selected_cores_threads = system.override_core_selections(*pid, apply_selected_cores_threads, &run.selected_cores_threads);
                if let Err(error) = system.set_cpu_affinity_threads(&apply_selected_cores_threads) {
                    self.set_status(strings::status_failed_to_set_thread_affinities(pid, name, &error));
                    success = false;
                    break;
                }
            }

            if success {
                let process_names: String = 
                    active_processes.iter().map(|(_pid, name)| name.as_str()).collect::<Vec<&str>>().join(", ");
                self.set_status(format!("{} {}", strings::STATUS_THREAD_AFFINITIES_SET, process_names));
            }

            let first_pid = active_processes.first().expect("empty active_processes is handled above").0;
            self.set_next_mode_view(system, first_pid);
        }
    }

    /// Do any work needed after a process info refresh, e.g. handling death of selected process or
    /// any of its threads.
    pub fn handle_process_refresh(&mut self, system: &System, update_start: bool) {
        let handle_process_death_and_switch_mode = |this: &mut Self| {
            // Selected process died.
            this.handle_selected_process_death(system);
            if update_start {
                this.mode = this.next_mode.take()
                    .expect("this is at the beginning of a normal update; apply mode change immediately so update can rely on not starting after selected process died");
            }
        };
        match self.mode {
            UIMode::Run(..) => {
                // In Run mode we store no information about running processes
            },
            UIMode::View(ref run) => {
                if system.get_process(run.selected_process).is_none() {
                    handle_process_death_and_switch_mode(self);
                }
                // affinities will get updated on each update/frame, no need to do so here
            },
            UIMode::Edit(ref mut edit) => {
                if system.get_process(edit.selected_process).is_none() {
                    handle_process_death_and_switch_mode(self);
                } else {
                    // If any thread with a core selection dies, remove that thread's core selection.
                    let dead_tids: Vec<_> = edit.selected_cores_threads.iter().map(|(tid, _)| *tid)
                        .filter(|tid| *tid != edit.selected_process && system.get_thread_of_process(edit.selected_process, *tid).is_none())
                        .collect();
                    for tid in dead_tids {
                        edit.selected_cores_threads.remove(&tid);
                    }
                }
            },
        };
    }

    /// Select color to use to draw PU `pu_id`.
    ///
    /// `C` is the color type to use (depends on UI frontend).
    ///
    /// * `colors`: Colors used for drawing PUs, in order: 
    ///   (view/edit/run) no affinity/unselected, (view) has affinity, (edit/run) selected, 
    ///   (edit/run) selected in a thread
    pub fn pu_color<C: Copy>(&self, pu_id: u32, colors: (C,C,C,C)) -> C {
        let (unselected, affinity, selected_process, selected_thread) = colors;

        let get_color = |status: PUSelectionStatus| {
            match status {
                PUSelectionStatus::SelectedInProcess => selected_process,
                PUSelectionStatus::SelectedInThread  => selected_thread,
                PUSelectionStatus::NotSelected       => unselected
            }
        };

        match self.get_mode() {
            UIMode::Run(ref run) => {
                get_color( run.get_pu_selection_status_in_currently_rendering_thread(pu_id) )
            },
            // In view mode, show PU affinity to selected process/thread.
            UIMode::View(ref view) => {
                let has_affinity = view.pu_affinity_in_currently_rendering_thread(pu_id);
                if has_affinity { affinity } else { unselected }
            },
            // In edit mode, show process selection status for the PU.
            UIMode::Edit(ref edit) => {
                get_color( edit.get_pu_selection_status_in_currently_rendering_thread(pu_id) )
            },
        } 
    }

    /// Determine what to do when an object owning multiple PUs with specified IDs has been clicked, and do it.
    ///
    /// Used to e.g. select/toggle PUs under an L2 cache.
    pub fn handle_group_click(
        &mut self,
        system: &System,
        obj:    &TopologyObjectInfo)
    {
        info!("handling PU group click");
        let Some(ref pu_ids) = obj.cpuset else {
            self.set_status(String::from(strings::STATUS_GROUP_SELECT_FAILED));
            return;
        };
        let update_selection = |selected_cores: &mut hwloc2::CpuSet| {
            // For some reason CpuSet/Bitmap doesn't have `iter()`, only `into_iter()`, so `clone()` it.
            let all_pu_ids_selected = pu_ids.clone().into_iter().all(|id| selected_cores.is_set(id));
            *selected_cores = if all_pu_ids_selected {
                selected_cores.subtract(pu_ids)
            } else {
                selected_cores.union(pu_ids)
            };
        };
        match &mut self.mode {
            UIMode::Run(ref mut run) => {
                if let Some(index) = run.currently_rendering_thread_index {
                    // Thread click
                    let opt_selected_cores_thread = run.selected_cores_threads.get_mut(index);
                    // If no core selection for this thread yet, create selection.
                    if opt_selected_cores_thread.is_none() {
                        *opt_selected_cores_thread = Some(hwloc2::CpuSet::default());
                    }
                    update_selection(opt_selected_cores_thread.as_mut().unwrap());
                    run.selected_cores_process.synchronize_added_thread_cores_from_opt(run.selected_cores_threads.iter());
                } else {
                    // Process click
                    update_selection(&mut run.selected_cores_process);
                    // Needed for the 'subtract' case to remove removed cores from thread selections
                    run.selected_cores_process.synchronize_removed_process_cores_into_opt(run.selected_cores_threads.iter_mut());
                }
            },
            UIMode::View(ref view) => {
                let pid = view.selected_process;
                if let Some(tid) = view.currently_rendered_thread {
                    // Thread click
                    let tid_val = tid; // Avoid mutable multi-borrow of *self
                    self.handle_select_click_in_thread_view(system, pid, pu_ids, tid_val);
                } else {
                    // Process click
                    self.set_next_mode_edit(pid, pu_ids.clone(), None);
                }
            },
            // In edit mode, group items are toggled unless they are inconsistent, in which case
            // unselected group items are selected
            UIMode::Edit(ref mut edit) => {
                if let Some(tid) = edit.currently_rendered_thread {
                    // Thread click
                    update_selection(edit.selected_cores_threads.entry(tid).or_default());
                    edit.selected_cores_process.synchronize_added_thread_cores_from(edit.selected_cores_threads.values());
                } else {
                    // Process click
                    update_selection(&mut edit.selected_cores_process);
                    edit.selected_cores_process.synchronize_removed_process_cores_into( edit.selected_cores_threads.values_mut() );
                }
            },
        }
    }

    /// Update process/thread counts/affinities after the selected process is changed.
    pub fn update_current_process_data(&mut self, system: &mut System) {
        if let UIMode::View(ref mut run) = self.mode {
            let status;
            (run.process_affinity, run.thread_affinities, status) = system.get_process_and_thread_affinities(run.selected_process);
            if let Some(string) = status {
                self.set_status(string)
            }
        }
        else if let UIMode::Edit(ref mut edit) = self.mode {
            // Filters out 'missing' sysinfo threads to be consistent with everything else.
            edit.thread_count =
                system.get_nonmain_threads_of(edit.selected_process)
                      .expect("if a process does not exist in sysinfo data, it shouldn't be selected anymore")
                      .len() + 1;
        }
    }

    /// Prepare to switch mode to `Run` at the end of the update.
    ///
    /// Uses `system` to set initial core selection/affinity.
    pub fn set_next_mode_run(&mut self, system: &System) {
        info!("next mode: run");
        // Can change mode directly instead of next_node as this is outside of a normal 'update'.
        self.next_mode = Some(UIMode::new_run(system));
    }

    /// Prepare to switch mode to `View` of `selected_pid` at the end of the update.
    ///
    /// Uses `system` to get current affinities of `selected_pid`.
    pub fn set_next_mode_view(&mut self, system: &System, selected_pid: sysinfo::Pid) {
        info!("next mode: view {}", selected_pid);
        let(process_affinity, thread_affinities, status) = system.get_process_and_thread_affinities(selected_pid);
        if let Some(string) = status {
            self.set_status(string)
        }
        self.next_mode = Some(UIMode::View(UIModeView{ 
            selected_process: selected_pid,
            process_affinity,
            thread_affinities,
            currently_rendered_thread: None
        }));
    }

    /// Try to change to the View mode, without caring what process we will be viewing.
    ///
    /// May fail to change mode - if no process is found (very unlikely).
    ///
    /// Called when we're switching to View mode but don't know the process yet (e.g. when starting 
    /// text input for the viewed process' name).
    pub fn try_set_next_mode_view_anyprocess(&mut self, system: &System) {
        info!("next mode: view any process");
        let processes = self.get_filtered_user_processes( system );
        let try_set_view = |this: &mut UIBackend| {
            if let Some((pid, _process)) = processes.first() {
                this.set_next_mode_view(system, **pid);
            }
        };
        match self.get_mode() {
            UIMode::Run(..)        => {
                try_set_view(self);
            }, 
            UIMode::View(ref view)       => { 
                let pid = view.get_selected_process();
                if pid == sysinfo::Pid::from(0) || !processes.iter().any(|i| *i.0 == pid){
                    try_set_view(self);
                }
            },
            UIMode::Edit(ref edit) => {
                let pid = edit.get_selected_process();
                if pid == sysinfo::Pid::from(0) || !processes.iter().any(|i| *i.0 == pid){
                    try_set_view(self);
                } else {
                    self.set_next_mode_view(system, pid);
                }
            }
        }
    }

    /// Prepare to switch mode to `Edit` at the end of the update.
    ///
    /// Will start with `selected_cores` and optionally, one thread may also start with explicitly 
    /// selected cores with `first_thread_selection`.
    pub fn set_next_mode_edit(
        &mut self,
        selected_process:       sysinfo::Pid,
        selected_cores:         hwloc2::CpuSet,
        first_thread_selection: Option<(sysinfo::Pid, hwloc2::CpuSet)>)
    {
        info!("next mode: edit");
        let mut selected_cores_threads = HashMap::new();
        if let Some((pid, selection)) = first_thread_selection {
            selected_cores_threads.insert(pid, selection);
        }
        self.set_next_mode_edit_hashmap(selected_process, selected_cores, selected_cores_threads) ;
    }

    /// Prepare to switch mode to `Edit` at the end of the update.
    ///
    /// Same as `set_next_mode_edit`, but can start with multiple threads having selected cores.
    pub fn set_next_mode_edit_hashmap(
        &mut self,
        selected_process:       sysinfo::Pid,
        selected_cores:         hwloc2::CpuSet,
        selected_cores_threads: HashMap<sysinfo::Pid, hwloc2::CpuSet>)
    {
        self.next_mode = Some(UIMode::Edit(UIModeEdit{
            selected_process,
            selected_cores_process: selected_cores,
            selected_cores_threads,
            currently_rendered_thread: None,
            thread_count: 0
        }));
    }

    /// Apply the 'No HT/SMT' action regardless to current mode.
    ///
    /// In view/run mode, this just disables affinities for non-first SMT PUs, but in view mode it 
    /// also switches to edit mode.
    pub fn disable_affinity_smt(&mut self, system: &System) {
        match self.mode {
            UIMode::Run(ref mut run) => {
                run.disable_affinity_smt(system);
            },
            UIMode::View(ref view) => {
                let selected_process = view.get_selected_process();
                let process_affinity = view.disable_affinity_smt(system);
                // No need to set thread core selection when entering Edit mode here,
                // as no threads could have been selected/tweaked in the View mode.
                self.set_next_mode_edit(selected_process, process_affinity, None);
            },
            UIMode::Edit(ref mut edit) => {
                edit.disable_affinity_smt(system);
            }
        }
    }

    /// Handle a click on a PU/group on a thread topology in view mode.
    ///
    /// With thread views, we differentiate between threads that do and don't have selected cores;
    /// first click on a thread acts as the first click on a process in View mode, creating the
    /// first core selection as including only clicked PU/group, ignoring existing affinity.
    fn handle_select_click_in_thread_view(&mut self, system: &System, selected_process: sysinfo::Pid, pu_ids: &hwloc2::CpuSet, tid: sysinfo::Pid) {
        info!("view: PU or PU group clicked");
        // If we even get to this point, we did already get affinity in `topology_view`, so if this
        // fails even though it succeeded before, the process has probably died.
        if let Some(process_affinity) = system.get_cpu_affinity(tid, hwloc2::CpuBindFlags::CPUBIND_PROCESS) {
            // Thread view clicked in thread mode - the thread selection only includes the selected
            // PU/s, but the process selection must be initialized to also include the core
            // selected for the thread.
            self.set_next_mode_edit(selected_process, process_affinity.union(pu_ids), Some((tid, pu_ids.clone())));
        } else {
            // Need to do this as `get_cpu_affinity` accesses the process directly, at which point 
            // the process may be dead even if it existed during the last refresh.
            self.handle_selected_process_death(system);
        }
    }

    /// After launching a command in `Run`, this will either start waiting to set thread affinities
    /// or switch to 'view' if there are no thread affinities to set.
    ///
    /// `shell_children` are the processes launched by the `Run` command.
    fn handle_started_command(
        &mut self,
        system:                 &mut System,
        shell_children:         Vec<sysinfo::Pid>,
        current_order:          Option<ThreadOrder>,
        selected_cores_threads: &OrderedCoreSelections,
        thread_apply_time:      Duration) -> Option<RunState>
    {
        info!("run: command started");
        let process_names: String = 
            shell_children.iter()
                          .map(|pid| System::get_process_name( 
                                     system.get_process(*pid)
                                           .expect("no process info update since we got the children, this process should exist")))
                          .collect::<Vec<&str>>()
                          .join(", ");

        self.set_status(format!("{} {}", strings::STATUS_PROC_AFFINITY_SUCCESS, process_names));

        let have_a_core_selection = !selected_cores_threads.all_default();
        // No thread affinities to set; go straight to viewing
        if current_order.is_some() || have_a_core_selection {
            info!("wait for thread start");
            // Start waiting so we can set thread affinities based
            // on thread order - once those threads are running.
            Some(RunState::WaitForThreadStart{
                wait_start:    Instant::now(), 
                wait_duration: thread_apply_time,
                processes:     shell_children.clone()
            })
        } else {
            // We can only view one process even if multiple have
            // been launched, so view the first process.
            self.set_next_mode_view(system, shell_children[0]);
            None
        }
    }

    /// When we reach a point where we need process access and the process has died, call this.
    ///
    /// Usually we continue accessing out-of-date process data from System until the next process
    /// info refresh, but sometimes we directly access the process (e.g. getting affinity) with
    /// operations that will fail if the process doesn't exist.
    ///
    /// The current update still needs to finish, so we set a status message and the caller needs
    /// to continue without update avoiding work that needs the process to still exist.
    fn handle_selected_process_death(&mut self, system: &System) {
        info!("selected process died");
        self.set_next_mode_run(system);
        self.set_status(String::from(strings::STATUS_PROCESS_DIED));
    }
}
