use crate::components::constants::TEAM_IDS;
use crate::components::date_selector::DateSelector;
use chrono::NaiveDate;
use indexmap::IndexMap;
use mlbt_api::client::StatGroup;
use mlbt_api::stats::{HittingStat, PitchingStat, StatSplit, StatsResponse};
use std::cmp::Ordering;
use std::string::ToString;
use tui::widgets::TableState;
pub const STATS_FIRST_COL_WIDTH: u16 = 28;
pub const STATS_DEFAULT_COL_WIDTH: u16 = 6;
const PLAYER_COLUMN_NAME: &str = "Player";
const TEAM_COLUMN_NAME: &str = "Team";
#[derive(Clone, Copy, Debug)]
pub struct StatType {
pub group: StatGroup,
pub team_player: TeamOrPlayer,
}
#[derive(Clone, Copy, Debug, Default)]
pub enum TeamOrPlayer {
#[default]
Team,
Player,
}
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum ActivePane {
Data,
#[default]
Options,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
pub enum Order {
#[default]
Ascending,
Descending,
}
impl std::ops::Not for Order {
type Output = Self;
fn not(self) -> Self::Output {
match self {
Order::Ascending => Order::Descending,
Order::Descending => Order::Ascending,
}
}
}
impl Order {
pub fn arrow_symbol(&self) -> &'static str {
match self {
Order::Ascending => "^",
Order::Descending => "v",
}
}
}
#[derive(Clone, Debug)]
pub struct Sort {
pub column_name: Option<String>,
pub order: Order,
}
impl Default for Sort {
fn default() -> Self {
Sort {
column_name: None,
order: Order::Ascending,
}
}
}
pub struct StatsState {
pub options_state: TableState,
pub data_state: TableState,
pub active_pane: ActivePane,
pub stat_type: StatType,
pub stats: IndexMap<String, TableEntry>,
pub sorting: Sort,
pub show_options: bool,
pub date_selector: DateSelector,
pub visible_rows: usize,
}
pub struct TableEntry {
pub description: String,
pub active: bool,
pub rows: Vec<String>,
}
impl Default for StatsState {
fn default() -> Self {
let mut ss = StatsState {
options_state: TableState::default(),
data_state: TableState::default(),
active_pane: ActivePane::default(),
stat_type: StatType {
group: StatGroup::Pitching,
team_player: TeamOrPlayer::Team,
},
stats: IndexMap::new(),
sorting: Sort::default(),
show_options: true,
date_selector: DateSelector::default(),
visible_rows: 0,
};
ss.options_state.select(Some(0));
ss
}
}
impl StatsState {
pub fn update(&mut self, stats: &StatsResponse) {
self.stats.clear();
self.create_table(stats);
self.data_state.select(Some(0));
}
pub fn set_date_from_valid_input(&mut self, date: NaiveDate) {
self.date_selector.set_date_from_valid_input(date);
self.select(Some(0));
}
fn select(&mut self, index: Option<usize>) {
match self.active_pane {
ActivePane::Data => self.data_state.select(index),
ActivePane::Options => self.options_state.select(index),
}
}
pub fn set_date_with_arrows(&mut self, forward: bool) -> NaiveDate {
self.date_selector.set_date_with_arrows(forward)
}
fn create_table(&mut self, stats: &StatsResponse) {
for stat in &stats.stats {
for split in &stat.splits {
let name = match &split.player {
Some(p) => p.full_name.clone(),
None => split.team.name.clone(),
};
let team_abbreviation = TEAM_IDS
.get(&split.team.name.as_str())
.map(|t| t.abbreviation.to_string());
match &split.stat {
StatSplit::Pitching(s) => self.load_pitching_stats(name, team_abbreviation, s),
StatSplit::Hitting(s) => self.load_hitting_stats(name, team_abbreviation, s),
};
}
}
}
pub fn generate_table(&self) -> (Vec<String>, Vec<Vec<String>>) {
if self.stats.is_empty() {
return (vec![], vec![vec![]]);
}
let len = match self.stats.first() {
Some((_, v)) => v.rows.len(),
None => 0,
};
if len == 0 {
return (vec![], vec![vec![]]);
}
let mut rows = vec![Vec::with_capacity(self.stats.len()); len];
let mut header = Vec::with_capacity(self.stats.len());
for (key, col) in &self.stats {
if col.active {
header.push(key.clone());
for (idx, val) in col.rows.iter().enumerate() {
rows[idx].push(val.clone());
}
}
}
self.sort_table(&mut rows);
(header, rows)
}
fn table_helper<T>(&mut self, key: &str, description: &str, active: bool, value: T)
where
T: ToString,
{
self.stats
.entry(key.to_string())
.and_modify(|table_entry| table_entry.rows.push(value.to_string()))
.or_insert(TableEntry {
description: description.to_string(),
active,
rows: vec![value.to_string()],
});
}
fn load_pitching_stats(
&mut self,
name: String,
team_abbreviation: Option<String>,
stat: &PitchingStat,
) {
self.format_name_columns(name, team_abbreviation);
self.table_helper("W", "wins", true, stat.wins);
self.table_helper("L", "losses", true, stat.losses);
self.table_helper("ERA", "earned run average", true, &stat.era);
self.table_helper("G", "games played", true, stat.games_played);
self.table_helper("GS", "games started", true, stat.games_started);
self.table_helper("CG", "complete games", true, stat.complete_games);
self.table_helper("SHO", "shutouts", false, stat.shutouts);
self.table_helper("SV", "saves", true, stat.saves);
self.table_helper("SVO", "save opportunities", true, stat.save_opportunities);
self.table_helper("IP", "innings pitched", true, &stat.innings_pitched);
self.table_helper("H", "hits", true, stat.hits);
self.table_helper("R", "runs", true, stat.runs);
self.table_helper("ER", "earned runs", true, stat.earned_runs);
self.table_helper("HR", "home runs", true, stat.home_runs);
self.table_helper("HB", "hit batsmen", false, stat.hit_batsmen);
self.table_helper("BB", "walks", true, stat.base_on_balls);
self.table_helper("SO", "strike outs", true, stat.strike_outs);
}
fn load_hitting_stats(
&mut self,
name: String,
team_abbreviation: Option<String>,
stat: &HittingStat,
) {
self.format_name_columns(name, team_abbreviation);
self.table_helper("G", "games played", true, stat.games_played);
self.table_helper("AB", "at bats", true, stat.at_bats);
self.table_helper("AVG", "batting avg", true, &stat.avg);
self.table_helper("OBP", "on-base percent", true, &stat.obp);
self.table_helper("SLG", "slugging percent", true, &stat.slg);
self.table_helper("OPS", "on-base + slug", true, &stat.ops);
self.table_helper("R", "runs", true, stat.runs);
self.table_helper("H", "hits", true, stat.hits);
self.table_helper("2B", "doubles", true, stat.doubles);
self.table_helper("3B", "triples", true, stat.triples);
self.table_helper("HR", "home runs", true, stat.home_runs);
self.table_helper("RBI", "runs batted in", true, stat.rbi);
self.table_helper("BB", "walks", true, stat.base_on_balls);
self.table_helper("SO", "strike outs", true, stat.strike_outs);
self.table_helper("SB", "stolen bases", true, stat.stolen_bases);
self.table_helper("CS", "caught stealing", true, stat.caught_stealing);
}
fn format_name_columns(&mut self, name: String, team_abbreviation: Option<String>) {
match self.stat_type.team_player {
TeamOrPlayer::Team => {
self.table_helper(TEAM_COLUMN_NAME, "", true, name);
}
TeamOrPlayer::Player => {
self.table_helper(PLAYER_COLUMN_NAME, "", true, name);
if let Some(abb) = team_abbreviation {
self.table_helper(TEAM_COLUMN_NAME, "", true, abb);
}
}
};
}
pub fn trim_columns(&mut self, available_width: u16) {
let mut active: Vec<usize> = self
.stats
.values()
.enumerate()
.filter(|(_, v)| v.active)
.map(|(i, _)| i)
.collect();
let mut column_width = (active.len() as u16 * STATS_DEFAULT_COL_WIDTH)
+ (STATS_FIRST_COL_WIDTH - STATS_DEFAULT_COL_WIDTH) - 2;
while column_width >= available_width && !active.is_empty() {
let key = active.pop().unwrap();
if let Some((_, v)) = self.stats.get_index_mut(key) {
v.active = false;
column_width -= STATS_DEFAULT_COL_WIDTH;
}
}
}
pub fn toggle_stat(&mut self) {
let toggled_column_index = self.options_state.selected().unwrap_or_default();
let sort_column_index = self.get_sort_column_index();
if let Some((_, v)) = self.stats.get_index_mut(toggled_column_index) {
v.active = !v.active;
if sort_column_index.is_some_and(|idx| idx == toggled_column_index) && !v.active {
self.sorting.column_name = None;
}
}
}
fn get_sort_column_index(&self) -> Option<usize> {
let sort_column = self.sorting.column_name.as_ref()?;
let mut active_idx = 0;
for (column_name, entry) in self.stats.iter() {
if column_name == sort_column {
return Some(active_idx);
}
if entry.active {
active_idx += 1;
}
}
None
}
pub fn store_sort_column(&mut self) {
let Some(selected_index) = self.options_state.selected() else {
return;
};
if let Some((column_name, entry)) = self.stats.get_index(selected_index) {
if !entry.active {
return;
}
self.sorting = Sort {
column_name: Some(column_name.clone()),
order: !self.sorting.order,
};
}
}
fn sort_table(&self, rows: &mut [Vec<String>]) {
let sort_column_index = self.get_sort_column_index();
let sort_column_name = self.sorting.column_name.as_ref();
if let (Some(sort_column_index), Some(sort_column)) = (sort_column_index, sort_column_name)
{
rows.sort_by(|a, b| {
let a = a.get(sort_column_index);
let b = b.get(sort_column_index);
if let (Some(a), Some(b)) = (a, b) {
if sort_column == TEAM_COLUMN_NAME || sort_column == PLAYER_COLUMN_NAME {
match self.sorting.order {
Order::Ascending => a.cmp(b),
Order::Descending => b.cmp(a),
}
} else {
let a: f64 = a.parse().unwrap_or_default();
let b: f64 = b.parse().unwrap_or_default();
match self.sorting.order {
Order::Ascending => a.partial_cmp(&b).unwrap_or(Ordering::Equal),
Order::Descending => b.partial_cmp(&a).unwrap_or(Ordering::Equal),
}
}
} else {
Ordering::Equal
}
});
}
}
pub fn toggle_options(&mut self) {
self.show_options = !self.show_options;
if !self.show_options {
self.active_pane = ActivePane::Data;
}
}
pub fn switch_pane(&mut self) {
if !self.show_options {
return;
}
self.active_pane = match self.active_pane {
ActivePane::Data => ActivePane::Options,
ActivePane::Options => ActivePane::Data,
};
}
fn row_count(&self) -> usize {
self.stats
.values()
.next()
.map(|entry| entry.rows.len())
.unwrap_or(0)
}
pub fn next(&mut self) {
let len = match self.active_pane {
ActivePane::Data => self.row_count(),
ActivePane::Options => self.stats.len(),
};
if len == 0 {
return;
}
let selected = match self.active_pane {
ActivePane::Data => self.data_state.selected(),
ActivePane::Options => self.options_state.selected(),
};
let i = match selected {
Some(i) => {
if i >= len - 1 {
0
} else {
i + 1
}
}
None => 0,
};
self.select(Some(i));
}
pub fn page_down(&mut self) {
if self.active_pane != ActivePane::Data {
return;
}
let len = self.row_count();
if len == 0 || self.visible_rows == 0 {
return;
}
let offset = self.data_state.offset();
let last_visible = (offset + self.visible_rows - 1).min(len - 1);
*self.data_state.offset_mut() = last_visible;
self.data_state.select(Some(last_visible));
}
pub fn page_up(&mut self) {
if self.active_pane != ActivePane::Data {
return;
}
let len = self.row_count();
if len == 0 || self.visible_rows == 0 {
return;
}
let offset = self.data_state.offset();
let new_offset = offset.saturating_sub(self.visible_rows - 1);
*self.data_state.offset_mut() = new_offset;
self.data_state.select(Some(new_offset));
}
pub fn previous(&mut self) {
let len = match self.active_pane {
ActivePane::Data => self.row_count(),
ActivePane::Options => self.stats.len(),
};
if len == 0 {
return;
}
let selected = match self.active_pane {
ActivePane::Data => self.data_state.selected(),
ActivePane::Options => self.options_state.selected(),
};
let i = match selected {
Some(i) => {
if i == 0 {
len - 1
} else {
i - 1
}
}
None => 0,
};
self.select(Some(i));
}
}