mod menus;

use std::{
    fmt::Debug,
    fs::File,
    io::{self, Read, Write},
    num::{NonZeroU32, NonZeroUsize},
    path::PathBuf,
    time::Duration,
};

use crossterm::{
    cursor::{self, MoveTo},
    event::KeyboardEnhancementFlags,
    style,
    terminal::{self, Clear, ClearType},
    ExecutableCommand,
};

use falling_tetromino_engine::{
    Board, Button, DelayParameters, ExtDuration, Game, GameBuilder, GameEndCause, InGameTime,
    Input, Notification, NotificationFeed, NotificationLevel, Stat, Tetromino,
};

use crate::{
    application::menus::{Menu, MenuUpdate},
    game_modes::{self, game_modifiers, GameMode},
    gameplay_settings::*,
    graphics_settings::*,
    keybinds::*,
    palette::*,
};

pub type Slots<T> = Vec<(String, T)>;

pub type UncompressedInputHistory = Vec<(InGameTime, Input)>;

#[derive(
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Clone,
    Debug,
    Default,
    serde::Serialize,
    serde::Deserialize,
)]
pub struct CompressedInputHistory(Vec<u128>);

impl CompressedInputHistory {
    // How many bits it takes to encode a `ButtonChange`:
    // - 1 bit for Press/Release,
    // - At time of writing: 4 bits for the 11 `Button` variants.
    pub const BUTTON_CHANGE_BITSIZE: usize =
        1 + Button::VARIANTS.len().next_power_of_two().ilog2() as usize;

    pub fn new(game_input_history: &UncompressedInputHistory) -> Self {
        let mut compressed_inputs = Vec::new();

        if let Some((mut update_time_0, button_change)) = game_input_history.first() {
            let i = Self::compress_input((update_time_0, *button_change));

            // Add first input.
            compressed_inputs.push(i);

            for (update_time_1, button_change) in game_input_history.iter().skip(1) {
                let time_diff = update_time_1.saturating_sub(update_time_0);
                let i = Self::compress_input((time_diff, *button_change));

                // Add further input.
                compressed_inputs.push(i);

                update_time_0 = *update_time_1;
            }
        };

        Self(compressed_inputs)
    }

    pub fn decompress(&self) -> UncompressedInputHistory {
        let mut decompressed_inputs = Vec::new();

        if let Some(i) = self.0.first() {
            let (mut update_time_0, button_change) = Self::decompress_input(*i);

            // Add first input.
            decompressed_inputs.push((update_time_0, button_change));

            for i in self.0.iter().skip(1) {
                let (time_diff, button_change) = Self::decompress_input(*i);
                let update_time_1 = update_time_0.saturating_add(time_diff);

                // Add further input.
                decompressed_inputs.push((update_time_1, button_change));

                update_time_0 = update_time_1;
            }
        }

        decompressed_inputs
    }

    // For serialization reasons, we encode a single user input as `u128` instead of
    // `(GameTime, ButtonChange)`, which would have a verbose direct string representation.
    fn compress_input((update_target_time, button_change): (InGameTime, Input)) -> u128 {
        // Encode `GameTime = std::time::Duration` using `std::time::Duration::as_millis`.
        // NOTE: We actually use `millis` not `nanos` as a convention which is upheld by `play_game.rs`!
        let millis: u128 = update_target_time.as_millis();
        // Encode `falling_tetromino_engine::ButtonChange` using `Self::encode_button_change`.
        let bc_bits: u8 = Self::compress_buttonchange(&button_change);
        (millis << Self::BUTTON_CHANGE_BITSIZE) | u128::from(bc_bits)
    }

    fn decompress_input(i: u128) -> (InGameTime, Input) {
        let mask = u128::MAX >> (128 - Self::BUTTON_CHANGE_BITSIZE);
        let bc_bits = u8::try_from(i & mask).unwrap();
        let millis = u64::try_from(i >> Self::BUTTON_CHANGE_BITSIZE).unwrap();
        (
            std::time::Duration::from_millis(millis),
            Self::decompress_buttonchange(bc_bits),
        )
    }

    fn compress_buttonchange(button_change: &Input) -> u8 {
        match button_change {
            Input::Deactivate(button) => (*button as u8) << 1,
            Input::Activate(button) => ((*button as u8) << 1) | 1,
        }
    }

    fn decompress_buttonchange(b: u8) -> Input {
        (if b.is_multiple_of(2) {
            Input::Deactivate
        } else {
            Input::Activate
        })(Button::VARIANTS[usize::from(b >> 1)])
    }
}

#[derive(
    PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct GameRestorationData<T> {
    builder: GameBuilder,
    mod_ids_args: Vec<(String, String)>,
    input_history: T,
    forfeit: Option<InGameTime>,
}

impl<T> GameRestorationData<T> {
    fn new(game: &Game, input_history: T, forfeit: Option<InGameTime>) -> GameRestorationData<T> {
        let (builder, mod_ids_args) = game.blueprint();

        GameRestorationData {
            builder,
            mod_ids_args,
            input_history,
            forfeit,
        }
    }

    fn map<U>(self, f: impl Fn(T) -> U) -> GameRestorationData<U> {
        GameRestorationData::<U> {
            builder: self.builder,
            mod_ids_args: self.mod_ids_args,
            input_history: f(self.input_history),
            forfeit: self.forfeit,
        }
    }
}

impl GameRestorationData<UncompressedInputHistory> {
    fn restore(&self, input_index: usize) -> Game {
        // Step 1: Prepare builder.
        let builder = self.builder.clone();
        // Step 2: Build actual game by possibly reconstructing mods to finalize builder with.
        let mut game = if self.mod_ids_args.is_empty() {
            builder.build()
        } else {
            match game_modes::game_modifiers::reconstruct_build_modded(&builder, &self.mod_ids_args)
            {
                Ok((mut modded_game, unrecognized_mod_ids)) => {
                    if !unrecognized_mod_ids.is_empty() {
                        // Add warning messages if certain mods could not be recognized.
                        // This should never happen in our application.
                        let warn_messages = unrecognized_mod_ids
                            .into_iter()
                            .map(|mod_desc| format!("WARNING: idk mod {mod_desc:?}"))
                            .collect();

                        let print_warn_msgs_mod =
                            game_modifiers::PrintMsgs::modifier(warn_messages);

                        modded_game.modifiers.push(print_warn_msgs_mod);
                    }

                    modded_game
                }
                Err(msg) => {
                    let error_messages = vec![format!("ERROR: {msg}")];

                    let print_error_msg_mod = game_modifiers::PrintMsgs::modifier(error_messages);

                    builder.build_modded(vec![print_error_msg_mod])
                }
            }
        };

        // Step 3: Reenact recorded game inputs.
        let restore_notification_level = game.config.notification_level;

        game.config.notification_level = NotificationLevel::Silent;
        for (update_time, button_change) in self.input_history.iter().take(input_index) {
            // FIXME: Handle UpdateGameError? If not, why not?
            let _v = game.update(*update_time, Some(*button_change));
        }

        game.config.notification_level = restore_notification_level;

        game
    }
}

#[derive(
    PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct GameMetaData {
    pub datetime: String,
    pub title: String,
    pub comparison_stat: (Stat, bool),
}

#[derive(
    PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct GameSave<T> {
    game_meta_data: GameMetaData,
    game_restoration_data: GameRestorationData<T>,
    inputs_to_load: usize,
}

impl<T> GameSave<T> {
    fn map<U>(self, f: impl Fn(T) -> U) -> GameSave<U> {
        GameSave {
            game_restoration_data: self.game_restoration_data.map(f),
            game_meta_data: self.game_meta_data,
            inputs_to_load: self.inputs_to_load,
        }
    }
}

#[derive(
    PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct ScoresEntry {
    game_meta_data: GameMetaData,
    end_cause: GameEndCause,
    is_win: bool,
    time_elapsed: InGameTime,
    lineclears: u32,
    points_scored: u32,
    pieces_locked: [u32; Tetromino::VARIANTS.len()],
    fall_delay_reached: ExtDuration,
    lock_delay_reached: Option<ExtDuration>,
}

#[derive(
    PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Debug, serde::Serialize, serde::Deserialize,
)]
pub enum ScoresSorting {
    Chronological,
    Scoring,
}

#[derive(
    PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct ScoresAndReplays {
    sorting: ScoresSorting,
    entries: Vec<(
        ScoresEntry,
        Option<GameRestorationData<CompressedInputHistory>>,
    )>,
}

impl Default for ScoresAndReplays {
    fn default() -> Self {
        Self {
            sorting: ScoresSorting::Scoring,
            entries: Vec::new(),
        }
    }
}

#[derive(
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Clone,
    Debug,
    Default,
    serde::Serialize,
    serde::Deserialize,
)]
pub struct Statistics {
    total_new_games_started: u32,
    total_games_ended: u32,
    total_play_time: Duration,
    total_pieces_locked: u32,
    total_points_scored: u32,
    total_lines_cleared: u32,
    total_mono: u32,
    total_duo: u32,
    total_tri: u32,
    total_tetra: u32,
    total_spin: u32,
    total_perfect_clear: u32,
    total_combo: u32,
}

impl Statistics {
    // This simple blacklist is used to prevent certain game modes from being counted toward stats (e.g. Puzzle's perfect clears).
    const BLACKLIST_TITLE_PREFIXES: &[&str] = &[GameMode::TITLE_PUZZLE, GameMode::TITLE_COMBO];

    fn accumulate_from_feed(&mut self, feed: &NotificationFeed) {
        for (notification, _notif_time) in feed {
            match notification {
                Notification::PieceLocked { .. } => {
                    self.total_pieces_locked += 1;
                }

                Notification::Accolade {
                    score_bonus,
                    lineclears,
                    combo,
                    is_spin,
                    is_perfect_clear,
                    tetromino: _,
                } => {
                    self.total_points_scored += score_bonus;
                    self.total_lines_cleared += lineclears;
                    match lineclears {
                        1 => self.total_mono += 1,
                        2 => self.total_duo += 1,
                        3 => self.total_tri += 1,
                        4 => self.total_tetra += 1,
                        _ => {}
                    }
                    self.total_spin += if *is_spin { 1 } else { 0 };
                    self.total_perfect_clear += if *is_perfect_clear { 1 } else { 0 };
                    self.total_combo += if *combo > 1 { 1 } else { 0 };
                }

                _ => {}
            }
        }
    }

    fn accumulate(&mut self, other: &Statistics) {
        let Statistics {
            total_new_games_started,
            total_games_ended,
            total_play_time,
            total_pieces_locked,
            total_points_scored,
            total_lines_cleared,
            total_mono,
            total_duo,
            total_tri,
            total_tetra,
            total_spin,
            total_perfect_clear,
            total_combo,
        } = self;

        *total_new_games_started += other.total_new_games_started;
        *total_games_ended += other.total_games_ended;
        *total_play_time += other.total_play_time;
        *total_pieces_locked += other.total_pieces_locked;
        *total_points_scored += other.total_points_scored;
        *total_lines_cleared += other.total_lines_cleared;
        *total_mono += other.total_mono;
        *total_duo += other.total_duo;
        *total_tri += other.total_tri;
        *total_tetra += other.total_tetra;
        *total_spin += other.total_spin;
        *total_perfect_clear += other.total_perfect_clear;
        *total_combo += other.total_combo;
    }
}

#[derive(
    PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Debug, serde::Serialize, serde::Deserialize,
)]
pub struct NewGameSettings {
    custom_fall_delay_params: DelayParameters,
    custom_win_condition: Option<Stat>,
    custom_seed: Option<u64>,
    custom_encoded_board: Option<String>, // For more compact serialization of NewGameSettings, we store an encoded `Board` (see `encode_board`).

    cheese_tiles_per_line: NonZeroUsize,
    cheese_fall_lock_delays: (ExtDuration, ExtDuration),
    cheese_limit: Option<NonZeroU32>,

    combo_limit: Option<NonZeroU32>,
    /// Custom starting layout when playing Combo mode (4-wide rows), encoded as binary.
    /// Example: '▀▄▄▀' => 0b_1001_0110 = 150
    combo_initial_layout: u16,

    master_mode_unlocked: bool,
    experimental_mode_unlocked: bool,
}

impl Default for NewGameSettings {
    fn default() -> Self {
        Self {
            custom_fall_delay_params: DelayParameters::standard_fall(),
            custom_win_condition: None,
            custom_seed: None,
            custom_encoded_board: None,

            cheese_limit: Some(NonZeroU32::try_from(20).unwrap()),
            cheese_fall_lock_delays: (ExtDuration::Infinite, ExtDuration::Infinite),
            cheese_tiles_per_line: NonZeroUsize::new(Game::WIDTH - 1).unwrap(),

            combo_limit: Some(NonZeroU32::try_from(30).unwrap()),
            combo_initial_layout: game_modifiers::Combo::LAYOUTS[0],

            master_mode_unlocked: false,
            experimental_mode_unlocked: false,
        }
    }
}

impl NewGameSettings {
    #[allow(dead_code)]
    pub fn encode_board(board: &Board) -> String {
        board
            .iter()
            .map(|line| {
                line.iter()
                    .map(|tile| if tile.is_some() { 'X' } else { ' ' })
                    .collect::<String>()
            })
            .collect::<String>()
            .trim_end()
            .to_owned()
    }

    pub fn decode_board(board_str: &str) -> Board {
        let grey_tile = Some(std::num::NonZeroU8::try_from(254).unwrap());

        let mut new_board = Board::default();

        let mut chars = board_str.chars();

        for line in &mut new_board {
            for tile in line {
                for char in chars.by_ref() {
                    if char == ' ' {
                        // Space = empty tile.
                        *tile = None;
                        break;
                    } else if char == '\n' {
                        // Newline = ignore, stay at tile but move on to next char.
                        continue;
                    } else {
                        // Otherwise = filled tile.
                        *tile = grey_tile;
                        break;
                    }
                }
            }
        }

        new_board
    }
}

#[serde_with::serde_as]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Settings {
    graphics_slot_active: usize,
    keybinds_slot_active: usize,
    gameplay_slot_active: usize,
    graphics_slots_that_should_not_be_changed: usize,
    palette_slots_that_should_not_be_changed: usize,
    keybinds_slots_that_should_not_be_changed: usize,
    gameplay_slots_that_should_not_be_changed: usize,
    graphics_slots: Slots<GraphicsSettings>,
    palette_slots: Slots<Palette>,
    gameplay_slots: Slots<GameplaySettings>,
    // NOTE: Reconsider #[serde_as(as = "Vec<(_, std::collections::HashMap<serde_with::json::JsonString, _>)>")]
    #[serde_as(as = "Vec<(_, Vec<(_, _)>)>")]
    keybinds_slots: Slots<Keybinds>,
    new_game: NewGameSettings,
}

impl Default for Settings {
    fn default() -> Self {
        let graphics_slots = vec![
            ("Default".to_owned(), GraphicsSettings::default()),
            ("Focus+".to_owned(), GraphicsSettings::extra_focus()),
            ("Guideline".to_owned(), GraphicsSettings::guideline()),
            ("High Compat.".to_owned(), GraphicsSettings::compatibility()),
            (
                "Elektronika 60".to_owned(),
                GraphicsSettings::elektronika_60(),
            ),
        ];
        let palette_slots = vec![
            ("Monochrome".to_owned(), Palette::monochrome()), // NOTE: The slot at index 0 is the special 'monochrome'/no palette slot.
            ("ANSI".to_owned(), Palette::ansi()),
            ("Fullcolor".to_owned(), Palette::fullcolor()),
            ("Okpalette".to_owned(), Palette::okpalette()),
            ("Gruvbox".to_owned(), Palette::gruvbox()),
            ("Solarized".to_owned(), Palette::solarized()),
            ("Terafox".to_owned(), Palette::terafox()),
            ("Fahrenheit".to_owned(), Palette::fahrenheit()),
            ("The Matrix".to_owned(), Palette::matrix()),
            ("Sequoia".to_owned(), Palette::sequoia()),
        ];
        let keybinds_slots = vec![
            ("Default".to_owned(), Keybinds::default_tetro()),
            ("Control+".to_owned(), Keybinds::extra_control()),
            ("Guideline".to_owned(), Keybinds::guideline()),
            ("Vim".to_owned(), Keybinds::vim()),
        ];
        let gameplay_slots = vec![
            ("Default".to_owned(), GameplaySettings::default()),
            ("Finesse+".to_owned(), GameplaySettings::extra_finesse()),
            ("Guideline".to_owned(), GameplaySettings::guideline()),
            ("NES".to_owned(), GameplaySettings::nes()),
            ("Gameboy".to_owned(), GameplaySettings::gameboy()),
        ];

        Self {
            graphics_slot_active: 0,
            keybinds_slot_active: 0,
            gameplay_slot_active: 0,
            graphics_slots_that_should_not_be_changed: graphics_slots.len(),
            palette_slots_that_should_not_be_changed: palette_slots.len(),
            keybinds_slots_that_should_not_be_changed: keybinds_slots.len(),
            gameplay_slots_that_should_not_be_changed: gameplay_slots.len(),
            graphics_slots,
            palette_slots,
            keybinds_slots,
            gameplay_slots,
            new_game: NewGameSettings::default(),
        }
    }
}

impl Settings {
    pub fn graphics(&self) -> &GraphicsSettings {
        &self.graphics_slots[self.graphics_slot_active].1
    }
    pub fn keybinds(&self) -> &Keybinds {
        &self.keybinds_slots[self.keybinds_slot_active].1
    }
    pub fn gameplay(&self) -> &GameplaySettings {
        &self.gameplay_slots[self.gameplay_slot_active].1
    }
    fn graphics_mut(&mut self) -> &mut GraphicsSettings {
        &mut self.graphics_slots[self.graphics_slot_active].1
    }
    fn keybinds_mut(&mut self) -> &mut Keybinds {
        &mut self.keybinds_slots[self.keybinds_slot_active].1
    }
    fn gameplay_mut(&mut self) -> &mut GameplaySettings {
        &mut self.gameplay_slots[self.gameplay_slot_active].1
    }

    pub fn palette(&self) -> &Palette {
        &self.palette_slots[self.graphics().palette_active].1
    }
    pub fn palette_lockedtiles(&self) -> &Palette {
        &self.palette_slots[self.graphics().palette_active_lockedtiles].1
    }
}

#[derive(
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Clone,
    Copy,
    Debug,
    Default,
    serde::Serialize,
    serde::Deserialize,
)]
pub enum SavefileGranularity {
    #[default]
    NoSavefile,
    RememberSettings,
    RememberSettingsScores,
    RememberSettingsScoresReplays,
}

#[derive(
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    Hash,
    Clone,
    Debug,
    Default,
    serde::Serialize,
    serde::Deserialize,
)]
pub struct TemporaryAppData {
    pub custom_terminal_state_initialized: bool,
    pub kitty_detected: bool,
    pub kitty_assumed: bool,
    pub blindfold_enabled: bool,
    pub renderernumber: usize,
    pub save_on_exit: SavefileGranularity,
    pub savefile_path: PathBuf, // This should technically be the same for a given compiled binary, but we compute it at runtime.
}

// FIXME: Move tui application into `main` instead of artifically having it in one module below `tetro-tui::main`.
#[derive(PartialEq, Clone, Debug)]
pub struct Application<T: Write> {
    term: T,
    temp_data: TemporaryAppData,
    settings: Settings,
    scores_and_replays: ScoresAndReplays,
    // FIXME: Currently one can only access one without resorting to manually editing the savefile.
    game_saves: (usize, Vec<GameSave<UncompressedInputHistory>>),
    statistics: Statistics,
}

impl<T: Write> Drop for Application<T> {
    fn drop(&mut self) {
        // (Try to) undo terminal setup. Ignore errors cuz atp it's too late to take any flak from Crossterm.
        let _ = self.deinitialize_terminal_state();

        if self.temp_data.save_on_exit != SavefileGranularity::NoSavefile {
            // If the user wants any of their data stored, try to do so.
            if let Err(e) = self.store_to_savefile() {
                eprintln!("{e}");
            }
        } else if self
            .temp_data
            .savefile_path
            .try_exists()
            .is_ok_and(|exists| exists)
        {
            // Otherwise explicitly check for savefile and try to make sure we don't leave it around.
            if let Err(e) = std::fs::remove_file(self.temp_data.savefile_path.clone()) {
                eprintln!("{e}");
            }
        }
    }
}

impl<T: Write> Application<T> {
    // FIXME: What the... Maybe do less hardcoding here?
    // pub const W_MAIN: u16 = 80;
    // pub const H_MAIN: u16 = 24;
    pub const W_MAIN: u16 = 62;
    pub const H_MAIN: u16 = 23;

    // FIXME: Could we ever get any undesirable results from pushing *all* enhancement flags?
    pub const KEYBOARD_ENHANCEMENT_FLAGS: KeyboardEnhancementFlags =
        KeyboardEnhancementFlags::all();

    pub(crate) fn fetch_main_xy() -> (u16, u16) {
        let (w_console, h_console) = terminal::size().unwrap_or((0, 0));
        (
            w_console.saturating_sub(Self::W_MAIN) / 2,
            h_console.saturating_sub(Self::H_MAIN) / 2,
        )
    }

    pub(crate) fn initialize_terminal_state(&mut self) -> io::Result<()> {
        if !self.temp_data.custom_terminal_state_initialized {
            self.temp_data.custom_terminal_state_initialized = true;

            self.term.execute(terminal::EnterAlternateScreen)?;
            terminal::enable_raw_mode()?;
            self.term.execute(cursor::Hide)?;
            self.term
                .execute(terminal::SetTitle("Tetro Terminal User Interface"))?;
        }
        Ok(())
    }

    pub(crate) fn deinitialize_terminal_state(&mut self) -> io::Result<()> {
        if self.temp_data.custom_terminal_state_initialized {
            // (Try to) undo terminal setup.
            self.term.execute(style::ResetColor)?;
            self.term.execute(cursor::Show)?;
            terminal::disable_raw_mode()?;
            self.term.execute(terminal::LeaveAlternateScreen)?;

            self.temp_data.custom_terminal_state_initialized = true;
        }

        Ok(())
    }

    pub fn with_savefile_and_cmdlineoptions(
        term: T,
        savefile_path: PathBuf,
        custom_start_seed: Option<u64>,
        custom_start_board: Option<String>,
    ) -> Self {
        let mut new = Self {
            temp_data: TemporaryAppData::default(),
            term,
            settings: Settings::default(),
            scores_and_replays: ScoresAndReplays::default(),
            game_saves: (0, Vec::new()),
            statistics: Statistics::default(),
        };

        // Actually load in settings.
        // FIXME: Handle io::Error? If not, why not?
        if new.load_from_savefile(savefile_path.clone()).is_err() {
            //eprintln!("Could not load settings: {e}");
            //std::thread::sleep(Duration::from_secs(5));
        }

        // Now that the settings are loaded, we handle separate flags set for this session.
        let kitty_detected = terminal::supports_keyboard_enhancement().unwrap_or(false);

        new.temp_data = TemporaryAppData {
            custom_terminal_state_initialized: false,
            kitty_detected,
            kitty_assumed: kitty_detected,
            blindfold_enabled: false,
            renderernumber: 0,
            save_on_exit: new.temp_data.save_on_exit,
            savefile_path,
        };

        if custom_start_board.is_some() {
            new.settings.new_game.custom_encoded_board = custom_start_board;
        }
        if custom_start_seed.is_some() {
            new.settings.new_game.custom_seed = custom_start_seed;
        }

        new
    }

    fn load_from_savefile(&mut self, savefile_path: PathBuf) -> io::Result<()> {
        let mut file = File::open(savefile_path)?;
        let mut save_str = String::new();
        file.read_to_string(&mut save_str)?;

        let compressed_game_saves: (usize, Vec<GameSave<CompressedInputHistory>>);

        (
            self.temp_data.save_on_exit,
            self.settings,
            self.scores_and_replays,
            compressed_game_saves,
            self.statistics,
        ) = serde_json::from_str(&save_str)?;

        self.game_saves = (
            compressed_game_saves.0,
            compressed_game_saves
                .1
                .into_iter()
                .map(|save| save.map(|input_history| input_history.decompress()))
                .collect::<Vec<_>>(),
        );

        Ok(())
    }

    fn store_to_savefile(&mut self) -> io::Result<()> {
        if self.temp_data.save_on_exit < SavefileGranularity::RememberSettingsScores {
            // Clear scoreboard if no game data is wished to be stored.
            self.scores_and_replays.entries.clear();
        } else if self.temp_data.save_on_exit < SavefileGranularity::RememberSettingsScoresReplays {
            // Clear past game inputs if no game input data is wished to be stored.
            for (_entry, restoration_data) in &mut self.scores_and_replays.entries {
                restoration_data.take();
            }
        }

        let compressed_game_saves = (
            self.game_saves.0,
            self.game_saves
                .1
                .iter()
                .cloned()
                .map(|save| save.map(|input_history| CompressedInputHistory::new(&input_history)))
                .collect::<Vec<_>>(),
        );

        let save_str = serde_json::to_string(&(
            self.temp_data.save_on_exit,
            &self.settings,
            &self.scores_and_replays,
            compressed_game_saves,
            &self.statistics,
        ))?;

        let mut file = File::create(self.temp_data.savefile_path.clone())?;
        let n_written = file.write(save_str.as_bytes())?;
        // Attempt at additionally handling the case when save_str could not be written entirely.
        if n_written < save_str.len() {
            Err(std::io::Error::other(
                "attempt to write to file consumed `n < save_str.len()` bytes",
            ))
        } else {
            Ok(())
        }
    }

    fn sort_past_games_chronologically(&mut self) {
        self.scores_and_replays
            .entries
            .sort_by(|(pg1, _), (pg2, _)| {
                pg1.game_meta_data
                    .datetime
                    .cmp(&pg2.game_meta_data.datetime)
                    .reverse()
            });
    }

    #[rustfmt::skip]
    fn sort_past_games_semantically(&mut self) {
        self.scores_and_replays.entries.sort_by(|(pg1, _), (pg2, _)|
            // Sort by gamemode (name).
            pg1.game_meta_data.title.cmp(&pg2.game_meta_data.title).then_with(||
            // Sort by if gamemode was finished successfully.
            pg1.is_win.cmp(&pg2.is_win).reverse().then_with(|| {
                // Sort by comparison stat...
                let o = match pg1.game_meta_data.comparison_stat.0 {
                    Stat::TimeElapsed(_)    => pg1.time_elapsed.cmp(&pg2.time_elapsed),
                    Stat::PiecesLocked(_)   => pg1.pieces_locked.cmp(&pg2.pieces_locked),
                    Stat::LinesCleared(_)   => pg1.lineclears.cmp(&pg2.lineclears),
                    Stat::PointsScored(_)   => pg1.points_scored.cmp(&pg2.points_scored),
                };
                // Comparison stat is used positively/negatively (minimize or maximize) depending on
                // how comparison stat compares to 'most important'(??) (often sole) end condition.
                // This is shady, but the special order we subtly chose and never publicly document
                // makes this make sense...
                if pg1.game_meta_data.comparison_stat.1
                    { o } else { o.reverse() }
            })
            )
        );
    }

    pub fn run(&mut self) -> io::Result<()> {
        // Console prologue: Initialization.
        // FIXME: Handle io::Error? If not, why not?
        let _e = self.initialize_terminal_state();

        let mut menu_stack = vec![Menu::Title];
        loop {
            // Retrieve active menu, stop application if stack is empty.
            let Some(menu) = menu_stack.last_mut() else {
                break;
            };
            // Open new menu screen, then store what it returns.
            let menu_update = match menu {
                Menu::Title => self.run_menu_title(),
                Menu::NewGame => self.run_menu_new_game(),
                Menu::PlayGame {
                    game,
                    game_input_history,
                    game_meta_data,
                    game_renderer,
                } => {
                    self.run_menu_play_game(game, game_input_history, game_meta_data, game_renderer)
                }
                Menu::Pause => self.run_menu_pause(),
                Menu::Settings => self.run_menu_settings(),
                Menu::AdjustGraphics => self.run_menu_adjust_graphics(),
                Menu::AdjustKeybinds => self.run_menu_adjust_keybinds(),
                Menu::AdjustGameplay => self.run_menu_adjust_gameplay(),
                Menu::AdvancedSettings => self.run_menu_advanced_settings(),
                Menu::GameOver { game_scoring } => self.run_menu_game_ended(game_scoring),
                Menu::GameComplete { game_scoring } => self.run_menu_game_ended(game_scoring),
                Menu::ScoresAndReplays {
                    cursor_pos,
                    camera_pos,
                } => self.run_menu_scores_and_replays(cursor_pos, camera_pos),
                Menu::ReplayGame {
                    game_restoration_data,
                    game_meta_data,
                    replay_length,
                    game_renderer,
                } => self.run_menu_replay_game(
                    game_restoration_data,
                    game_meta_data,
                    *replay_length,
                    game_renderer.as_mut(),
                ),
                Menu::Statistics => self.run_menu_statistics(),
                Menu::About => self.run_menu_about(),
                Menu::Quit => break,
            }?;

            // Change screen session depending on what response screen gave.
            match menu_update {
                MenuUpdate::Pop => {
                    if menu_stack.len() > 1 {
                        menu_stack.pop();

                        // FIXME: Abandoned menu transition 2.
                        // let (x_main, y_main) = Self::fetch_main_xy();
                        // /*
                        //  0      /1   /2   /3
                        // 0,0  1,0  2,0  3,0
                        // 0,1  1,1  2,1  3,1/-4
                        // 0,2  1,2  2,2  3,2/-5
                        // 0,3  1,3  2,3  3,3/-6
                        //  */
                        // for xplusy in 0 ..= (Self::W_MAIN/2).saturating_sub(1) + (Self::H_MAIN).saturating_sub(1) {
                        //     for x in xplusy.saturating_sub((Self::H_MAIN).saturating_sub(1)) ..= (Self::W_MAIN/2).saturating_sub(1) {
                        //         let y = xplusy.saturating_sub(x);
                        //         self.term
                        //             .queue(MoveTo(x_main + 2 * x, y_main + y))?
                        //             .queue(Print("  "))?;
                        //     }
                        //     self.term.flush()?;
                        //     std::thread::sleep(Duration::from_secs_f32(1./120.0));
                        // }

                        // FIXME: Abandoned menu transition 1.
                        // let (x_main, y_main) = Self::fetch_main_xy();
                        // for x in 0..Self::W_MAIN/2 {
                        //     for y in 0..Self::H_MAIN {
                        //         self.term
                        //             .queue(MoveTo(x_main + 2 * x, y_main + y))?
                        //             .queue(Print("  "))?;
                        //     }
                        //     self.term.flush()?;
                        //     std::thread::sleep(Duration::from_secs_f32(1./240.0));
                        // }
                    }
                }
                MenuUpdate::Push(menu) => {
                    if matches!(menu, Menu::Quit) {
                        break;
                    }

                    if matches!(
                        menu,
                        Menu::Title
                            | Menu::PlayGame { .. }
                            | Menu::GameOver { .. }
                            | Menu::GameComplete { .. }
                    ) {
                        menu_stack.clear();
                    }

                    if matches!(menu, Menu::GameOver { .. }) {
                        let h_console = terminal::size()?.1;
                        for y in (0..h_console).rev() {
                            self.term
                                .execute(MoveTo(0, y))?
                                .execute(Clear(ClearType::CurrentLine))?;
                            std::thread::sleep(Duration::from_secs_f32(1. / 60.0));
                        }
                    } else if matches!(menu, Menu::GameComplete { .. }) {
                        let h_console = terminal::size()?.1;
                        for y in 0..h_console {
                            self.term
                                .execute(MoveTo(0, y))?
                                .execute(Clear(ClearType::CurrentLine))?;
                            std::thread::sleep(Duration::from_secs_f32(1. / 60.0));
                        }
                    } else {
                        // FIXME: Abandoned menu transition 2.
                        // let (x_main, y_main) = Self::fetch_main_xy();
                        // /*
                        //  0      /1   /2   /3
                        // 0,0  1,0  2,0  3,0
                        // 0,1  1,1  2,1  3,1/-4
                        // 0,2  1,2  2,2  3,2/-5
                        // 0,3  1,3  2,3  3,3/-6
                        //  */
                        // for xplusy in (0 ..= (Self::W_MAIN/2).saturating_sub(1) + (Self::H_MAIN).saturating_sub(1)).rev() {
                        //     for x in xplusy.saturating_sub((Self::H_MAIN).saturating_sub(1)) ..= (Self::W_MAIN/2).saturating_sub(1) {
                        //         let y = xplusy.saturating_sub(x);
                        //         self.term
                        //             .queue(MoveTo(x_main + 2 * x, y_main + y))?
                        //             .queue(Print("  "))?;
                        //     }
                        //     self.term.flush()?;
                        //     std::thread::sleep(Duration::from_secs_f32(1./120.0));
                        // }

                        // FIXME: Abandoned menu transition 1.
                        // let (x_main, y_main) = Self::fetch_main_xy();
                        // for x in (0..Self::W_MAIN/2).rev() {
                        //     for y in 0..Self::H_MAIN {
                        //         self.term
                        //             .queue(MoveTo(x_main + 2 * x, y_main + y))?
                        //             .queue(Print("  "))?;
                        //     }
                        //     self.term.flush()?;
                        //     std::thread::sleep(Duration::from_secs_f32(1./240.0));
                        // }
                    }

                    menu_stack.push(menu);
                }
            }
        }

        // Console epilogue: Deinitialization.
        // FIXME: Handle io::Error? If not, why not?
        self.deinitialize_terminal_state()
    }
}
