// A game is a self-contained struct that holds everything that an instance of
// Tetris needs to run, except for something to tick the time forward.

use crate::playfield::PlayField;
use crate::srs::RotationSystem;
use crate::srs::SRS;
use crate::tetromino::Position;
use crate::TICKS_PER_SECOND;
use log::{error, info, trace};
use std::fmt;

// I think this was correct, can't find source
const LINE_CLEAR_DELAY: u64 = TICKS_PER_SECOND as u64 * 41 / 60;

#[derive(Debug, Clone, Copy)]
pub enum LossReason {
    TopOut,
    LockOut,
    PieceLimitReached,
    TickLimitReached,
    BlockOut(Position),
}

#[derive(Clone)]
// Logic is based on 60 ticks / second
pub struct Game {
    playfield: PlayField,
    rotation_system: SRS,
    level: u8,
    score: u32,
    tick: u64,
    next_gravity_tick: u64,
    next_lock_tick: u64,
    next_spawn_tick: u64,
    is_game_over: Option<LossReason>,
    /// The last clear action performed, used for determining if a back-to-back
    /// bonus is needed.
    last_clear_action: ClearAction,
    pub line_clears: u32,
    // used if we set a limit on how long a game can last.
    pieces_placed: usize,
    piece_limit: usize,

    // used if we set a limit on how long the game can be played.
    tick_limit: u64,
}

impl fmt::Debug for Game {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "level: {}, points: {}", self.level, self.score)?;
        writeln!(f, "tick: {}", self.tick)?;
        write!(f, "{:?}", self.playfield)
    }
}

impl Default for Game {
    fn default() -> Self {
        Game {
            playfield: PlayField::new(),
            rotation_system: SRS::default(),
            level: 1,
            score: 0,
            tick: 0,
            next_gravity_tick: 60,
            next_lock_tick: 0,
            next_spawn_tick: 0,
            is_game_over: None,
            last_clear_action: ClearAction::Single, // Doesn't matter what it's initialized to
            line_clears: 0,
            pieces_placed: 0,
            piece_limit: 0,
            tick_limit: 0,
        }
    }
}

pub trait Tickable {
    fn tick(&mut self);
}

impl Tickable for Game {
    fn tick(&mut self) {
        if self.is_game_over().is_some() {
            return;
        }
        self.tick += 1;

        match self.tick {
            t if t == self.next_spawn_tick => {
                trace!("Spawn tick was met, spawning new Tetromino!");
                self.spawn_tetromino();
            }
            t if t == self.next_lock_tick => {
                trace!("Lock tick was met, trying to locking tetromino");
                if self.try_lock_tetromino() {
                    trace!("Successfully locked Tetromino");
                } else {
                    trace!("Failed to lock Tetromino.");
                }
            }
            t if t == self.next_gravity_tick => {
                if self.playfield.active_piece.is_some() {
                    self.playfield.tick_gravity();
                    trace!("Ticking gravity");
                    if !self.playfield.can_active_piece_move_down() {
                        self.update_lock_tick();
                    }
                }

                self.update_gravity_tick();
            }
            _ => (),
        }

        if self.tick == self.tick_limit {
            self.is_game_over = Some(LossReason::TickLimitReached);
        }
    }
}

#[derive(Clone, Copy)]
enum ClearAction {
    Single,
    Double,
    Triple,
    Tetris,
    MiniTSpin,
    TSpin,
    TSpinSingle,
    TSpinDouble,
    TSpinTriple,
}

impl Game {
    pub fn score(&self) -> u32 {
        self.score
    }

    pub fn is_game_over(&self) -> Option<LossReason> {
        self.is_game_over.or_else(|| {
            self.playfield
                .is_active_piece_in_valid_position()
                .map(|p| LossReason::BlockOut(p))
        })
    }

    fn update_gravity_tick(&mut self) {
        self.next_gravity_tick = self.tick + TICKS_PER_SECOND as u64;
    }

    fn update_lock_tick(&mut self) {
        self.next_lock_tick = self.tick + TICKS_PER_SECOND as u64 / 2;
    }

    fn spawn_tetromino(&mut self) {
        self.playfield.spawn_tetromino();
        self.update_gravity_tick();
    }

    /// Returns if some lines were cleared
    fn clear_lines(&mut self) -> usize {
        let rows = self
            .playfield
            .active_piece
            .map(|t| t.get_cur_occupied_spaces())
            .map(|i| {
                let mut a = i.iter().map(|p| p.y).collect::<Vec<_>>();
                a.sort();
                a
            })
            .unwrap_or_default();

        let mut rows_cleared = 0;
        for row in rows {
            if self.playfield.try_clear_row(row as usize).is_ok() {
                rows_cleared += 1;
            }
        }

        rows_cleared
    }

    fn try_lock_tetromino(&mut self) -> bool {
        // It's possible that the player moved the piece in the meantime.
        if !self.playfield.can_active_piece_move_down() {
            let positions = self.playfield.lock_active_piece();
            if self.pieces_placed < self.piece_limit {
                self.pieces_placed += 1;
                if self.pieces_placed >= self.piece_limit {
                    trace!("Loss due to piece limit!");
                    self.is_game_over = Some(LossReason::PieceLimitReached);
                }
            }

            self.is_game_over = self.is_game_over.or_else(|| {
                if positions.iter().map(|p| p.y).all(|y| y < 20) {
                    trace!("Loss due to lockout! {:?}", positions);
                    Some(LossReason::LockOut)
                } else {
                    None
                }
            });

            let cleared_lines = self.clear_lines();
            if cleared_lines > 0 {
                trace!("Lines were cleared.");
                self.line_clears += cleared_lines as u32;
                self.score += (cleared_lines * 100 * self.level as usize) as u32;
                self.level = (self.line_clears / 10) as u8;

                self.playfield.active_piece = None;
                self.next_spawn_tick = self.tick + LINE_CLEAR_DELAY;
            } else {
                self.spawn_tetromino();
            }

            true
        } else {
            false
        }
    }

    pub fn set_piece_limit(&mut self, size: usize) {
        self.piece_limit = size;
    }

    pub fn playfield(&self) -> &PlayField {
        &self.playfield
    }

    pub fn set_time_limit(&mut self, duration: std::time::Duration) {
        self.tick_limit = duration.as_secs() * TICKS_PER_SECOND as u64;
    }
}

pub trait Controllable {
    fn move_left(&mut self);
    fn move_right(&mut self);
    fn move_down(&mut self);
    fn rotate_left(&mut self);
    fn rotate_right(&mut self);
    fn hard_drop(&mut self);
    fn hold(&mut self);
    fn get_legal_actions(&self) -> Vec<Action>;
}

#[derive(Hash, Eq, PartialEq, Copy, Clone, Debug)]
pub enum Action {
    Nothing, // Default value
    MoveLeft,
    MoveRight,
    SoftDrop,
    HardDrop,
    Hold,
    RotateLeft,
    RotateRight,
}

impl Controllable for Game {
    fn move_left(&mut self) {
        if self.playfield.active_piece.is_none() {
            return;
        }

        if self.playfield.move_offset(-1, 0) && !self.playfield.can_active_piece_move_down() {
            self.update_lock_tick();
        }
    }

    fn move_right(&mut self) {
        if self.playfield.active_piece.is_none() {
            return;
        }

        if self.playfield.move_offset(1, 0) && !self.playfield.can_active_piece_move_down() {
            self.update_lock_tick();
        }
    }

    fn move_down(&mut self) {
        if self.playfield.active_piece.is_none() {
            return;
        }

        if self.playfield.move_offset(0, 1) {
            self.score += 1;
            self.update_gravity_tick();
            self.update_lock_tick();
        }
    }

    fn rotate_left(&mut self) {
        if self.playfield.active_piece.is_none() {
            return;
        }

        match self.rotation_system.rotate_left(&self.playfield) {
            Ok(Position { x, y }) => {
                let mut active_piece = self.playfield.active_piece.unwrap().clone();
                active_piece.position = active_piece.position.offset(x, y);
                active_piece.rotate_left();
                self.playfield.active_piece = Some(active_piece);
                self.update_lock_tick();
            }
            Err(_) => (),
        }
    }

    fn rotate_right(&mut self) {
        if self.playfield.active_piece.is_none() {
            return;
        }

        match self.rotation_system.rotate_right(&self.playfield) {
            Ok(Position { x, y }) => {
                let mut active_piece = self.playfield.active_piece.unwrap().clone();
                active_piece.position = active_piece.position.offset(x, y);
                active_piece.rotate_right();
                self.playfield.active_piece = Some(active_piece);
                self.update_lock_tick();
            }
            Err(_) => (),
        }
    }

    fn hard_drop(&mut self) {
        if self.playfield.active_piece.is_none() {
            return;
        }

        let mut lines_fallen = 0;
        trace!("Score before hard drop: {}", self.score);
        while self.playfield.can_active_piece_move_down() {
            self.score += 2;
            lines_fallen += 1;
            self.playfield.move_offset(0, 1);
        }
        trace!(
            "Score after hard dropping {} lines: {}",
            lines_fallen,
            self.score
        );

        trace!(
            "Found active piece {:?}, trying to lock it",
            self.playfield.active_piece
        );
        if self.try_lock_tetromino() {
            trace!("Successfully locked piece, disabling lock tick.");
            self.next_lock_tick = std::u64::MAX;
        } else {
            error!("couldn't lock tetromino despite hard dropping!");
        }
    }

    fn hold(&mut self) {
        if self.playfield.active_piece.is_none() {
            return;
        }

        let _ = self.playfield.try_swap_hold();
    }

    fn get_legal_actions(&self) -> Vec<Action> {
        let mut legal_actions = vec![
            Action::RotateLeft,
            Action::RotateRight,
            Action::SoftDrop,
            Action::HardDrop,
            Action::Nothing,
            Action::MoveLeft,
            Action::MoveRight,
        ];

        if self.playfield.can_swap() {
            legal_actions.push(Action::Hold);
        }

        legal_actions
    }
}
