cube-tui 0.1.7

Terminal UI timer and session manager for speedcubing, with optional web dashboard and BLE (GAN) timer support.
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph, Widget, Wrap};

use crate::model::settings::ThemeSettings;
use crate::widgets::history::{History, Time};

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MeanType {
    Mo3,
    Ao5,
}

pub struct MeanDetailWidget {
    mean_type: MeanType,
    solve_index: usize,
    mean_value: String,
    times: Vec<(usize, Time)>,
    selected_index: usize,
}

impl MeanDetailWidget {
    pub fn new(history: &History, solve_index: usize, col: usize, selected_index: usize) -> Self {
        let (mean_type, mean_value, times) = if col == 0 {
            let val = history
                .mo3_at(solve_index)
                .unwrap_or_else(|| "-".to_string());
            let ts = history
                .mo3_times_at(solve_index)
                .map_or_else(Vec::new, |slice| {
                    let start = solve_index.saturating_sub(2);
                    slice
                        .iter()
                        .enumerate()
                        .map(|(i, t)| (start + i + 1, t.clone()))
                        .collect()
                });
            (MeanType::Mo3, val, ts)
        } else {
            let val = history
                .ao5_at(solve_index)
                .unwrap_or_else(|| "-".to_string());
            let ts = history
                .ao5_times_at(solve_index)
                .map_or_else(Vec::new, |slice| {
                    let start = solve_index.saturating_sub(4);
                    slice
                        .iter()
                        .enumerate()
                        .map(|(i, t)| (start + i + 1, t.clone()))
                        .collect()
                });
            (MeanType::Ao5, val, ts)
        };

        Self {
            mean_type,
            solve_index,
            mean_value,
            times,
            selected_index,
        }
    }

    pub fn render_with_theme(self, area: Rect, buf: &mut Buffer, theme: &ThemeSettings) {
        let type_name = match self.mean_type {
            MeanType::Mo3 => "Mean of 3",
            MeanType::Ao5 => "Average of 5",
        };

        let title = format!(
            "{} at solve #{} (Enter: open time, Esc: back)",
            type_name,
            self.solve_index + 1
        );

        let block = Block::default()
            .title(title)
            .borders(Borders::ALL)
            .border_style(Style::default().fg(theme.border()));

        let mut lines: Vec<Line> = Vec::new();

        lines.push(Line::from(vec![
            Span::styled(
                format!("{type_name}: "),
                Style::default()
                    .fg(theme.text())
                    .add_modifier(Modifier::BOLD),
            ),
            Span::styled(
                self.mean_value.clone(),
                Style::default()
                    .fg(theme.text())
                    .add_modifier(Modifier::BOLD),
            ),
        ]));
        lines.push(Line::from(""));

        let trimmed = compute_trimmed_indices(&self.times, self.mean_type);

        lines.push(Line::from(Span::styled(
            "Times:",
            Style::default()
                .fg(theme.text())
                .add_modifier(Modifier::BOLD),
        )));

        for (i, (num, time)) in self.times.iter().enumerate() {
            let is_trimmed = trimmed.contains(&i);
            let is_selected = i == self.selected_index;
            let time_display = time.to_string();

            let annotation = if is_trimmed {
                if time.effective_ms().is_none() {
                    " ← worst (trimmed)"
                } else {
                    let effective = time.effective_ms().unwrap_or(u64::MAX);
                    let all_effective: Vec<u64> = self
                        .times
                        .iter()
                        .filter_map(|(_, t)| t.effective_ms())
                        .collect();
                    if let Some(&min) = all_effective.iter().min() {
                        if effective == min {
                            "  best"
                        } else {
                            "  worst"
                        }
                    } else {
                        ""
                    }
                }
            } else {
                ""
            };

            let style = if is_selected {
                Style::default()
                    .bg(theme.selection())
                    .fg(theme.selection_text())
                    .add_modifier(Modifier::BOLD)
            } else {
                Style::default().fg(theme.text())
            };

            let annotation_style = Style::default().fg(theme.text());

            lines.push(Line::from(vec![
                Span::styled(format!("  #{num}: "), Style::default().fg(theme.text())),
                Span::styled(
                    if is_trimmed {
                        format!("({time_display})")
                    } else {
                        time_display
                    },
                    style,
                ),
                Span::styled(annotation.to_string(), annotation_style),
            ]));
        }

        Paragraph::new(lines)
            .block(block)
            .wrap(Wrap { trim: true })
            .render(area, buf);
    }
}

fn compute_trimmed_indices(times: &[(usize, Time)], mean_type: MeanType) -> Vec<usize> {
    if mean_type != MeanType::Ao5 || times.len() != 5 {
        return vec![];
    }

    let effective: Vec<Option<u64>> = times.iter().map(|(_, t)| t.effective_ms()).collect();
    let dnf_count = effective.iter().filter(|e| e.is_none()).count();

    if dnf_count >= 2 {
        return vec![];
    }

    let mut trimmed = Vec::new();

    if dnf_count == 1 {
        let dnf_idx = effective.iter().position(Option::is_none).unwrap();
        trimmed.push(dnf_idx);

        let mut best_val = u64::MAX;
        let mut best_idx = 0;
        for (i, e) in effective.iter().enumerate() {
            if let Some(v) = e
                && *v < best_val
            {
                best_val = *v;
                best_idx = i;
            }
        }
        trimmed.push(best_idx);
    } else {
        let mut best_val = u64::MAX;
        let mut best_idx = 0;
        let mut worst_val = 0;
        let mut worst_idx = 0;

        for (i, e) in effective.iter().enumerate() {
            if let Some(v) = e {
                if *v < best_val {
                    best_val = *v;
                    best_idx = i;
                }
                if *v > worst_val {
                    worst_val = *v;
                    worst_idx = i;
                }
            }
        }

        if best_idx == worst_idx {
            trimmed.push(0);
            trimmed.push(4);
        } else {
            trimmed.push(best_idx);
            trimmed.push(worst_idx);
        }
    }

    trimmed
}