use crate::components::constants::lookup_team_by_id;
use crate::components::date_selector::DateSelector;
use crate::components::stats::search::SearchState;
use crate::components::stats::table::{
PLAYER_COLUMN_NAME, StatType, StatsTable, TEAM_COLUMN_NAME, TableData, TeamOrPlayer,
};
use crate::state::messages::NetworkRequest;
use crate::state::player_profile::PlayerProfileState;
use crate::state::team_page::TeamPageState;
use chrono::{Datelike, NaiveDate};
use chrono_tz::Tz;
use mlbt_api::client::StatGroup;
use mlbt_api::player::PeopleResponse;
use mlbt_api::schedule::ScheduleResponse;
use mlbt_api::season::GameType;
use mlbt_api::stats::StatsResponse;
use mlbt_api::team::{RosterResponse, RosterType, TransactionsResponse};
use std::sync::Arc;
use tui::widgets::TableState;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub enum ActivePane {
#[default]
Data,
Options,
}
pub struct StatsState {
pub options_state: TableState,
pub data_state: TableState,
pub active_pane: ActivePane,
pub search_previous_pane: Option<ActivePane>,
pub stat_type: StatType,
pub table: StatsTable,
pub show_options: bool,
pub date_selector: DateSelector,
pub visible_rows: usize,
pub search: SearchState,
pub player_profile: Option<PlayerProfileState>,
pub team_page: Option<TeamPageState>,
}
impl Default for StatsState {
fn default() -> Self {
let mut ss = StatsState {
options_state: TableState::default(),
data_state: TableState::default(),
active_pane: ActivePane::default(),
search_previous_pane: None,
stat_type: StatType {
group: StatGroup::Hitting,
team_player: TeamOrPlayer::Player,
},
table: StatsTable::default(),
show_options: true,
date_selector: DateSelector::default(),
visible_rows: 0,
search: SearchState::default(),
player_profile: None,
team_page: None,
};
ss.options_state.select(Some(0));
ss.data_state.select(Some(0));
ss
}
}
impl StatsState {
pub fn update(&mut self, stats: &StatsResponse) {
self.player_profile = None;
self.table.load(stats, self.stat_type.team_player);
self.data_state.select(Some(0));
self.options_state.select(Some(0));
self.search.close();
self.search_previous_pane = None;
}
pub fn has_player_profile(&self) -> bool {
self.player_profile.is_some()
}
pub fn has_team_page(&self) -> bool {
self.team_page.is_some()
}
pub fn close_overlay(&mut self) {
if let Some(tp) = &mut self.team_page {
if tp.player_profile.is_some() {
tp.player_profile = None;
} else {
self.team_page = None;
}
} else {
self.player_profile = None;
}
}
pub fn update_team_page(
&mut self,
team_id: u16,
date: NaiveDate,
schedule: &ScheduleResponse,
roster: &RosterResponse,
transactions: &TransactionsResponse,
tz: Tz,
) {
let team = lookup_team_by_id(team_id).unwrap_or_default();
self.team_page = Some(TeamPageState::from_response(
team,
date,
schedule,
roster,
transactions,
tz,
));
}
pub fn update_team_roster(
&mut self,
team_id: u16,
roster: &RosterResponse,
roster_type: RosterType,
) {
if let Some(tp) = &mut self.team_page
&& tp.team.id == team_id
{
tp.update_roster(roster, roster_type);
}
}
pub fn update_team_player_profile(&mut self, data: Arc<PeopleResponse>, game_type: GameType) {
if let Some(tp) = &mut self.team_page {
tp.update_player_profile(data, game_type);
}
}
pub fn update_player_profile(&mut self, data: Arc<PeopleResponse>, game_type: GameType) {
let season_year = self.date_selector.date.year();
self.player_profile =
PlayerProfileState::from_response(data, self.stat_type.group, game_type, season_year);
}
pub fn open_selected_request(&self) -> Option<NetworkRequest> {
if self.stat_type.team_player == TeamOrPlayer::Team {
let team_id = self.get_selected_id()? as u16;
return Some(NetworkRequest::TeamPage {
team_id,
date: self.date_selector.date,
});
}
let player_id = self.get_selected_id()?;
Some(NetworkRequest::PlayerProfile {
player_id,
group: self.stat_type.group,
date: self.date_selector.date,
game_type: GameType::RegularSeason,
})
}
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)
}
pub fn update_search_matches(&mut self) {
self.table.invalidate_cache();
let name_key = match self.stat_type.team_player {
TeamOrPlayer::Team => TEAM_COLUMN_NAME,
TeamOrPlayer::Player => PLAYER_COLUMN_NAME,
};
let empty = Vec::new();
let names = self
.table
.columns
.get(name_key)
.map(|entry| &entry.rows)
.unwrap_or(&empty);
self.search.update_matches(names);
}
pub fn open_search(&mut self) {
if !self.search.is_open {
self.search_previous_pane = Some(self.active_pane);
}
self.active_pane = ActivePane::Data;
self.data_state.select(Some(0));
self.search.open();
}
pub fn submit_search(&mut self) {
self.search.submit();
self.search_previous_pane = None;
self.active_pane = ActivePane::Data;
}
pub fn cancel_search(&mut self) {
self.search.close();
self.table.invalidate_cache();
if let Some(previous) = self.search_previous_pane.take() {
self.active_pane = if previous == ActivePane::Options && !self.show_options {
ActivePane::Data
} else {
previous
};
}
}
pub fn generate_table(&mut self) -> Arc<TableData> {
let filter = if self.search.is_filtering() {
Some(self.search.matched_indices.as_slice())
} else {
None
};
self.table.generate(filter)
}
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,
};
}
pub fn toggle_stat(&mut self) {
let idx = self.options_state.selected().unwrap_or_default();
self.table.toggle_stat(idx);
}
pub fn store_sort_column(&mut self) {
let Some(idx) = self.options_state.selected() else {
return;
};
self.table.store_sort_column(idx);
}
pub fn get_selected_id(&self) -> Option<u64> {
let selected = self.data_state.selected()?;
let (_, ids, _) = self.table.cached()?.as_ref();
ids.get(selected).copied()
}
pub fn total_row_count(&self) -> usize {
self.table.total_row_count()
}
fn row_count(&self) -> usize {
if self.search.is_filtering() {
return self.search.matched_indices.len();
}
self.total_row_count()
}
pub fn reset_data_selection(&mut self) {
if self.row_count() > 0 {
self.data_state.select(Some(0));
} else {
self.data_state.select(None);
}
}
pub fn next(&mut self) {
let len = match self.active_pane {
ActivePane::Data => self.row_count(),
ActivePane::Options => self.table.columns.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.table.columns.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));
}
}