//! Board state, piece movement, line clearing, and lock metadata.

use crate::engine::piece::PieceState;
use crate::engine::rotation::get_kicks;

pub const BOARD_ROWS: usize = 40;
pub const BOARD_COLS: usize = 10;
pub const VISIBLE_ROW: i32 = 20;
pub const SPAWN_ROW: i32 = 18;
pub const SPAWN_COL: i32 = 4;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SpinKind {
    None,
    Full,
    Mini,
}

#[derive(Clone, Debug)]
pub struct LockMetadata {
    pub piece: char,
    pub rot: u8,
    pub row: i32,
    pub col: i32,
    pub lines_cleared: u32,
    pub hard_drop_cells: i32,
    pub soft_drop_cells: i32,
    pub all_clear: bool,
    pub spin_kind: SpinKind,
    pub last_rotation_delta: Option<i8>,
    pub last_rotation_kick_index: Option<usize>,
    pub last_rotation_kick: Option<(i32, i32)>,
}

#[derive(Clone)]
pub struct Board {
    pub grid: Vec<Vec<Option<char>>>,
    pub queue: Vec<char>,
    pub hold: Option<char>,
    pub hold_used: bool,
    pub pieces_placed: u32,
    pub lines_cleared: u32,
    pub drop_score_points: i64,
    pub lock_history: Vec<(char, u8, i32, i32, u32)>,
    pub lock_metadata: Vec<LockMetadata>,
    pub current: Option<PieceState>,
    overlap_exempt: std::collections::HashSet<(i32, i32)>,
    last_successful_action: Option<&'static str>,
    current_soft_drop_cells: i32,
    current_hard_drop_cells: i32,
    last_rotation_delta: Option<i8>,
    last_rotation_kick_index: Option<usize>,
    last_rotation_kick: Option<(i32, i32)>,
}

impl Board {
    pub fn new(piece_queue: Vec<char>) -> Board {
        let mut b = Board {
            grid: vec![vec![None; BOARD_COLS]; BOARD_ROWS],
            queue: piece_queue,
            hold: None,
            hold_used: false,
            pieces_placed: 0,
            lines_cleared: 0,
            drop_score_points: 0,
            lock_history: Vec::new(),
            lock_metadata: Vec::new(),
            current: None,
            overlap_exempt: std::collections::HashSet::new(),
            last_successful_action: None,
            current_soft_drop_cells: 0,
            current_hard_drop_cells: 0,
            last_rotation_delta: None,
            last_rotation_kick_index: None,
            last_rotation_kick: None,
        };
        b.spawn();
        b
    }

    pub fn row_string(&self, r: usize) -> String {
        (0..BOARD_COLS)
            .map(|c| self.grid[r][c].unwrap_or('.'))
            .collect()
    }

    fn spawn(&mut self) -> bool {
        if self.queue.is_empty() {
            return false;
        }
        let piece = self.queue.remove(0);
        let ps = PieceState {
            piece,
            rot: 0,
            row: SPAWN_ROW,
            col: SPAWN_COL,
        };
        if !self.valid(&ps) {
            self.current = Some(ps);
            self.overlap_exempt = ps
                .cells()
                .into_iter()
                .filter(|&(r, c)| {
                    r >= 0
                        && (r as usize) < BOARD_ROWS
                        && c >= 0
                        && (c as usize) < BOARD_COLS
                        && self.grid[r as usize][c as usize].is_some()
                })
                .collect();
            return false;
        }
        self.current = Some(ps);
        self.overlap_exempt.clear();
        self.hold_used = false;
        self.last_successful_action = None;
        self.current_soft_drop_cells = 0;
        self.current_hard_drop_cells = 0;
        self.last_rotation_delta = None;
        self.last_rotation_kick_index = None;
        self.last_rotation_kick = None;
        true
    }

    fn valid(&self, ps: &PieceState) -> bool {
        for (r, c) in ps.cells() {
            if c < 0 || c >= BOARD_COLS as i32 {
                return false;
            }
            if r >= BOARD_ROWS as i32 {
                return false;
            }
            if r >= 0
                && self.grid[r as usize][c as usize].is_some()
                && !self.overlap_exempt.contains(&(r, c))
            {
                return false;
            }
        }
        true
    }

    fn drop_row(&self, ps: &PieceState) -> i32 {
        let mut target = ps.row;
        loop {
            let test = PieceState {
                piece: ps.piece,
                rot: ps.rot,
                row: target + 1,
                col: ps.col,
            };
            if self.valid(&test) {
                target += 1;
            } else {
                break;
            }
        }
        target
    }

    fn retain_overlap_exempt(&mut self, ps: &PieceState) {
        let cells: std::collections::HashSet<(i32, i32)> = ps.cells().into_iter().collect();
        self.overlap_exempt = self.overlap_exempt.intersection(&cells).copied().collect();
    }

    fn lock(&mut self) -> u32 {
        let locked = self.current.expect("lock with no current piece");
        let spin_kind = self.classify_spin(&locked);
        for (r, c) in locked.cells() {
            if r >= 0 && (r as usize) < BOARD_ROWS {
                self.grid[r as usize][c as usize] = Some(locked.piece);
            }
        }
        self.pieces_placed += 1;
        let cleared = self.clear_lines();
        self.lines_cleared += cleared;
        self.lock_history
            .push((locked.piece, locked.rot, locked.row, locked.col, cleared));
        let all_clear = self.grid.iter().all(|row| row.iter().all(|c| c.is_none()));
        self.lock_metadata.push(LockMetadata {
            piece: locked.piece,
            rot: locked.rot,
            row: locked.row,
            col: locked.col,
            lines_cleared: cleared,
            hard_drop_cells: self.current_hard_drop_cells,
            soft_drop_cells: self.current_soft_drop_cells,
            all_clear,
            spin_kind,
            last_rotation_delta: self.last_rotation_delta,
            last_rotation_kick_index: self.last_rotation_kick_index,
            last_rotation_kick: self.last_rotation_kick,
        });
        cleared
    }

    fn clear_lines(&mut self) -> u32 {
        let full: Vec<usize> = (0..BOARD_ROWS)
            .filter(|&r| self.grid[r].iter().all(|c| c.is_some()))
            .collect();
        for r in &full {
            self.grid.remove(*r);
            self.grid.insert(0, vec![None; BOARD_COLS]);
        }
        full.len() as u32
    }

    fn corner_occupied(&self, row: i32, col: i32) -> bool {
        if col < 0 || col >= BOARD_COLS as i32 || row >= BOARD_ROWS as i32 {
            return true;
        }
        if row < 0 {
            return false;
        }
        self.grid[row as usize][col as usize].is_some()
    }

    fn classify_spin(&self, ps: &PieceState) -> SpinKind {
        if ps.piece != 'T' || self.last_successful_action != Some("rotate") {
            return SpinKind::None;
        }
        let cr = ps.row;
        let cc = ps.col;
        let corners = [
            (cr - 1, cc - 1),
            (cr - 1, cc + 1),
            (cr + 1, cc - 1),
            (cr + 1, cc + 1),
        ];
        let occupied = corners
            .iter()
            .filter(|&&(r, c)| self.corner_occupied(r, c))
            .count();
        if occupied < 3 {
            return SpinKind::None;
        }
        let front = match ps.rot {
            0 => [(cr - 1, cc - 1), (cr - 1, cc + 1)],
            1 => [(cr - 1, cc + 1), (cr + 1, cc + 1)],
            2 => [(cr + 1, cc - 1), (cr + 1, cc + 1)],
            _ => [(cr - 1, cc - 1), (cr + 1, cc - 1)],
        };
        let front_occupied = front
            .iter()
            .filter(|&&(r, c)| self.corner_occupied(r, c))
            .count();
        if front_occupied == 2 {
            return SpinKind::Full;
        }
        if self.last_rotation_delta == Some(2) {
            return SpinKind::Full;
        }
        if let Some(idx) = self.last_rotation_kick_index {
            if idx >= 4 {
                return SpinKind::Full;
            }
        }
        SpinKind::Mini
    }

    pub fn move_left(&mut self) -> bool {
        let cur = self.current.expect("move_left with no current piece");
        let test = PieceState {
            piece: cur.piece,
            rot: cur.rot,
            row: cur.row,
            col: cur.col - 1,
        };
        if self.valid(&test) {
            self.current = Some(test);
            self.retain_overlap_exempt(&test);
            self.last_successful_action = Some("move");
            return true;
        }
        false
    }

    pub fn move_right(&mut self) -> bool {
        let cur = self.current.expect("move_right with no current piece");
        let test = PieceState {
            piece: cur.piece,
            rot: cur.rot,
            row: cur.row,
            col: cur.col + 1,
        };
        if self.valid(&test) {
            self.current = Some(test);
            self.retain_overlap_exempt(&test);
            self.last_successful_action = Some("move");
            return true;
        }
        false
    }

    pub fn rotate(&mut self, delta: i8) -> bool {
        let cur = self.current.expect("rotate with no current piece");
        let new_rot = ((cur.rot as i8 + delta).rem_euclid(4)) as u8;
        let kicks = get_kicks(cur.piece, cur.rot, new_rot);
        for (kick_index, &(dr, dc)) in kicks.iter().enumerate() {
            let test = PieceState {
                piece: cur.piece,
                rot: new_rot,
                row: cur.row + dr,
                col: cur.col + dc,
            };
            if self.valid(&test) {
                self.current = Some(test);
                self.retain_overlap_exempt(&test);
                self.last_successful_action = Some("rotate");
                self.last_rotation_delta = Some(delta);
                self.last_rotation_kick_index = Some(kick_index);
                self.last_rotation_kick = Some((dr, dc));
                return true;
            }
        }
        false
    }

    pub fn step_down(&mut self) -> u32 {
        let cur = self.current.expect("step_down with no current piece");
        let test = PieceState {
            piece: cur.piece,
            rot: cur.rot,
            row: cur.row + 1,
            col: cur.col,
        };
        if self.valid(&test) {
            self.current = Some(test);
            self.retain_overlap_exempt(&test);
            return 1;
        }
        0
    }

    pub fn sonic_drop(&mut self, sd_cell_adjust: i32) -> i32 {
        let cur = self.current.expect("sonic_drop with no current piece");
        let target = self.drop_row(&cur);
        let dropped = target - cur.row;
        if dropped != 0 {
            let np = PieceState {
                piece: cur.piece,
                rot: cur.rot,
                row: target,
                col: cur.col,
            };
            self.current = Some(np);
            self.retain_overlap_exempt(&np);
            let scored = (dropped - sd_cell_adjust).max(0);
            self.current_soft_drop_cells += scored;
            self.drop_score_points += scored as i64;
            self.last_successful_action = Some("softDrop");
        }
        dropped
    }

    pub fn gravity_drop(&mut self, max_cells: Option<i32>) -> i32 {
        let cur = self.current.expect("gravity_drop with no current piece");
        match max_cells {
            None => {
                let target = self.drop_row(&cur);
                let dropped = target - cur.row;
                if dropped != 0 {
                    let np = PieceState {
                        piece: cur.piece,
                        rot: cur.rot,
                        row: target,
                        col: cur.col,
                    };
                    self.current = Some(np);
                    self.retain_overlap_exempt(&np);
                }
                dropped
            }
            Some(n) => {
                let mut dropped = 0;
                for _ in 0..n.max(0) {
                    if self.step_down() == 0 {
                        break;
                    }
                    dropped += 1;
                }
                dropped
            }
        }
    }

    pub fn soft_drop(&mut self) -> u32 {
        let dropped = self.step_down();
        if dropped != 0 {
            self.current_soft_drop_cells += dropped as i32;
            self.drop_score_points += dropped as i64;
            self.last_successful_action = Some("softDrop");
        }
        dropped
    }

    pub fn grounded(&self) -> bool {
        let cur = self.current.expect("grounded with no current piece");
        let test = PieceState {
            piece: cur.piece,
            rot: cur.rot,
            row: cur.row + 1,
            col: cur.col,
        };
        !self.valid(&test)
    }

    pub fn spawn_next(&mut self) -> bool {
        self.spawn()
    }

    pub fn lock_current(&mut self, spawn: bool) -> i32 {
        let lines = self.lock() as i32;
        if !spawn {
            self.current = None;
            return lines;
        }
        if !self.spawn() {
            return -1;
        }
        lines
    }

    pub fn hard_drop(&mut self, spawn: bool, hd_cell_adjust: i32) -> i32 {
        let cur = self.current.expect("hard_drop with no current piece");
        let target = self.drop_row(&cur);
        let dropped = target - cur.row;
        let np = PieceState {
            piece: cur.piece,
            rot: cur.rot,
            row: target,
            col: cur.col,
        };
        self.current = Some(np);
        self.retain_overlap_exempt(&np);
        let scored = (dropped - hd_cell_adjust).max(0);
        self.current_hard_drop_cells += scored;
        self.drop_score_points += scored as i64 * 2;
        self.lock_current(spawn)
    }

    pub fn do_hold(&mut self) -> bool {
        let cur = self.current.expect("do_hold with no current piece");
        if self.hold_used {
            return false;
        }
        let piece = cur.piece;
        if self.hold.is_none() {
            self.hold = Some(piece);
            if !self.spawn() {
                return false;
            }
        } else {
            let swapped = self.hold.take().unwrap();
            self.hold = Some(piece);
            let ps = PieceState {
                piece: swapped,
                rot: 0,
                row: SPAWN_ROW,
                col: SPAWN_COL,
            };
            if !self.valid(&ps) {
                self.current = Some(ps);
                return false;
            }
            self.current = Some(ps);
            self.last_successful_action = None;
            self.current_soft_drop_cells = 0;
            self.current_hard_drop_cells = 0;
            self.last_rotation_delta = None;
            self.last_rotation_kick_index = None;
            self.last_rotation_kick = None;
        }
        self.hold_used = true;
        true
    }

    /// Returns projected landing cells for the current active piece.
    pub fn ghost_cells(&self) -> Vec<(i32, i32)> {
        match self.current {
            None => Vec::new(),
            Some(cur) => {
                let target = self.drop_row(&cur);
                let landed = PieceState {
                    piece: cur.piece,
                    rot: cur.rot,
                    row: target,
                    col: cur.col,
                };
                landed.cells().to_vec()
            }
        }
    }

    pub fn apply_key(&mut self, key: &str) {
        match key {
            "moveLeft" => {
                self.move_left();
            }
            "moveRight" => {
                self.move_right();
            }
            "rotateCW" => {
                self.rotate(1);
            }
            "rotateCCW" => {
                self.rotate(-1);
            }
            "rotate180" => {
                self.rotate(2);
            }
            "softDrop" => {
                self.soft_drop();
            }
            "hardDrop" => {
                self.hard_drop(true, 0);
            }
            "hold" => {
                self.do_hold();
            }
            _ => {}
        }
    }
}

#[cfg(test)]
mod ghost_tests {
    use super::*;

    #[test]
    fn ghost_cells_are_the_hard_drop_landing_of_current() {
        let mut board = Board::new(vec!['I', 'O', 'T', 'S', 'Z', 'J', 'L']);
        board.current = Some(PieceState {
            piece: 'O',
            rot: 0,
            row: SPAWN_ROW,
            col: SPAWN_COL,
        });
        let ghost = board.ghost_cells();
        assert_eq!(ghost.len(), 4);
        let max_row = ghost.iter().map(|&(r, _)| r).max().unwrap();
        assert_eq!(
            max_row,
            (BOARD_ROWS as i32) - 1,
            "O should land on the bottom row"
        );
        board.current = None;
        assert!(board.ghost_cells().is_empty());
    }
}
