use crate::{bitboard::{Bitboard, BitboardFns}, moves::{Move, MoveKind}, attacks::Attacks, square::Square, board::io::IO, clock::Clock};

use self::{zobrist::{ZobristSeed, Zobrist, ZobristHash}, piece::Piece, color::Color};
pub mod io;
pub mod color;
pub mod piece;
pub mod move_generation;
mod zobrist;

#[derive(Debug, Clone, Copy)]
pub enum CastlingSide {
    King,
    Queen,
}

/// Chess board is an main interface to the internal game state.
/// Board defines rules of the game and manages players actions.
#[derive(Debug, Clone, PartialEq)]
pub struct Board {
    pub ply: u16,
    pub piece_sets: [Bitboard; 12],
    /// Castling rights indexed by Color and CastlingSide
    pub castling_rights: [[bool; 2]; 2],
    /// En passsant target square
    pub ep_target: Option<Square>,

    pub clock: Clock,

    /// List of all positions to determine repetitions
    pub positions: Vec<ZobristHash>,

    // Computed values
    pub occupancy: Bitboard,
    /// Zobrist hash of the current position
    pub hash: ZobristHash,
    zobrist_seed: ZobristSeed,


    pub attacks: Attacks,
}

impl Default for Board {
    fn default() -> Self {
        Board::new()
    }

}

impl Board {
    pub fn new() -> Self {
        let default_fen = String::from("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
        Self::from_FEN(default_fen)
    }

    /// Color to move at this ply
    pub fn color(&self) -> Color {
        Color::from(self.ply as u8 % 2)
    }

    fn update_occupancy(&mut self) {
        self.occupancy = self.piece_sets.iter().fold(0, |acc, bitboard| acc | bitboard)
    }

    pub fn empty(&self) -> Bitboard {
        !self.occupancy
    }

    pub fn pieces_by_color(&self, color: Color) -> &[Bitboard] {
        match color {
            Color::White => &self.piece_sets[0..6],
            Color::Black => &self.piece_sets[6..12],
        }
    }

    pub fn color_occupancy(&self, color: Color) -> Bitboard {
        self.pieces_by_color(color).iter().fold(0, |acc, bitboard| acc | bitboard)
    }

    pub fn piece_by_square(&self, square: Square) -> Option<Piece> {
        let square_bb = square.to_bitboard();
        self.piece_sets
            .iter()
            .enumerate()
            .find(|(_, bitboard)| *bitboard & square_bb > 0)
            .map(|(pt, _)| Piece::from(pt))
    }

    pub fn ep_bitboard(&self) -> Bitboard {
        let color = self.color();
        match self.ep_target {
            Some(square) => {
                let rank = square.rank();
                if (rank == 2 && color == Color::Black) || (rank == 5 && color == Color::White) {
                    square.to_bitboard()
                } else {
                    0
                }
            }
            None => 0,
        }
    }

    /// *Blindlessly* apply a move without any validation
    /// Legality test should still be performed
    pub fn make_move(&mut self, mov: Move) -> Option<Piece> {
        let move_source_bb = mov.source.to_bitboard();
        let move_target_bb = mov.target.to_bitboard();

        // Remove existing piece (if any) from target square
        let mut captured_piece = match self.piece_by_square(mov.target) {
            Some(target_piece) => {
                self.piece_sets[target_piece as usize] ^= move_target_bb;
                self.zobrist_toggle_piece(target_piece, mov.target);
                Some(target_piece)
            },
            None => None,
        };

        // En Passant captures diffirently
        if mov.kind == MoveKind::EnPassant {
            debug_assert!(captured_piece.is_none(), "No capture should be found at this point");
            let captured_square = Square::from_coords(mov.source.rank(), mov.target.file());
            captured_piece = match self.piece_by_square(captured_square) {
                Some(pawn_type) => {
                    let captured_bb = captured_square.to_bitboard();
                    self.piece_sets[pawn_type as usize] ^= captured_bb;
                    self.occupancy                      ^= captured_bb;
                    self.zobrist_toggle_piece(pawn_type, captured_square);
                    Some(pawn_type)
                },
                None => panic!("Pawn captured by En Passant was not found"),
            }
        }

        // Move a piece from source square to target
        let source_piece = match self.piece_by_square(mov.source) {
            Some(source_piece) => {
                let source_id = source_piece as usize;
                match mov.kind {
                    MoveKind::Promotion(promotion_piece) => {
                        let promo_id = promotion_piece as usize;
                        self.piece_sets[source_id]    ^= move_source_bb;
                        self.occupancy                ^= move_source_bb;
                        self.zobrist_toggle_piece(source_piece, mov.source);

                        self.piece_sets[promo_id]     |= move_target_bb;
                        self.occupancy                |= move_target_bb;
                        self.zobrist_toggle_piece(promotion_piece, mov.target);
                    },
                    _ => {
                        self.piece_sets[source_id]    ^= move_source_bb;
                        self.occupancy                ^= move_source_bb;
                        self.zobrist_toggle_piece(source_piece, mov.source);

                        self.piece_sets[source_id]    |= move_target_bb;
                        self.occupancy                |= move_target_bb;
                        self.zobrist_toggle_piece(source_piece, mov.target);
                    }
                }
                source_piece
            },
            None => {
                self.print();
                panic!("{:?} is malformed: source piece not found", mov);
            }
        };

        // When castling, also move a rook
        if mov.kind == MoveKind::Castle {
            debug_assert!(mov.source.file() == 4, "Castle can only be done from E file");
            let (rook_source_file, rook_target_file) = match mov.target.file() {
                2 => (0, 3),
                6 => (7, 5),
                _ => panic!("Malformed castle, target square invalid: {:?}", mov),
            };
            let rook_source_square = Square::from_coords(mov.target.rank(), rook_source_file);
            let rook_source_bb = rook_source_square.to_bitboard();

            let rook_target_square = Square::from_coords(mov.target.rank(), rook_target_file);
            let rook_target_bb = rook_target_square.to_bitboard();

            match self.piece_by_square(rook_source_square) {
                Some(rook_type) => {
                    let rook_id = rook_type as usize;
                    self.piece_sets[rook_id] ^= rook_source_bb;
                    self.occupancy         ^= rook_source_bb;
                    self.zobrist_toggle_piece(rook_type, rook_source_square);

                    self.piece_sets[rook_id] |= rook_target_bb;
                    self.occupancy         |= rook_target_bb;
                    self.zobrist_toggle_piece(rook_type, rook_target_square);
                },
                None => panic!("Rook was not found when castling"),
            }
        }

        // Double push should set En Passant target square
        self.ep_target = if mov.kind == MoveKind::DoublePush {
            self.zobrist_toggle_ep_square(mov.source);
            match mov.source.rank() {
                1 => Some(mov.source.nort_one()),
                6 => Some(mov.source.sout_one()),
                rank => panic!("Double-push was used from invalid rank({}) when trying to make {:?}", rank, mov),
            }
        } else { None };

        // Withdraw castling rights when moving rooks or king
        let source_color = Color::from_piece(source_piece);
        match source_piece.without_color() {
            Piece::King => {
                let king_side = &mut self.castling_rights[source_color as usize][CastlingSide::King as usize];
                if *king_side {
                    *king_side = false;
                    self.zobrist_toggle_castling_right(source_color, CastlingSide::King);
                }

                let queen_side = &mut self.castling_rights[source_color as usize][CastlingSide::Queen as usize];
                if *queen_side {
                    *queen_side = false;
                    self.zobrist_toggle_castling_right(source_color, CastlingSide::Queen);
                }
            },
            Piece::Rook => {
                match mov.source.file() {
                    0 => {
                        let queen_side = &mut self.castling_rights[source_color as usize][CastlingSide::Queen as usize];
                        if *queen_side {
                            *queen_side = false;
                            self.zobrist_toggle_castling_right(source_color, CastlingSide::Queen);
                        }
                    }
                    7 => {
                        let king_side = &mut self.castling_rights[source_color as usize][CastlingSide::King as usize];
                        if *king_side {
                            *king_side = false;
                            self.zobrist_toggle_castling_right(source_color, CastlingSide::King);
                        }
                    }
                    _ => {},
                }
            },
            _ => {},
        }

        self.ply += 1;
        self.zobrist_toggle_color();
        self.positions.push(self.hash);

        captured_piece
    }

    /// Completely reverse make_move as if it never happened
    pub fn unmake_move(
        &mut self,
        mov: Move,
        captured_piece: Option<Piece>,
        previous_ep_target: Option<Square>,
        previous_castling_rights: [[bool; 2]; 2],
        previous_hash: ZobristHash,
    ) {
        let move_source_bb = mov.source.to_bitboard();
        let move_target_bb = mov.target.to_bitboard();

        // Move a piece from target square back to source square
        match self.piece_by_square(mov.target) {
            Some(source_piece) => {
                match mov.kind {
                    MoveKind::Promotion(promotion_piece) => {
                        let promo_id = promotion_piece as usize;
                        self.piece_sets[promo_id]  ^= move_target_bb;
                        self.occupancy         ^= move_target_bb;

                        let source_id = match Color::from_piece(promotion_piece) {
                            Color::White => Piece::Pawn,
                            Color::Black => Piece::PawnBlack,
                        } as usize;

                        self.piece_sets[source_id] |= move_source_bb;
                        self.occupancy         |= move_source_bb;
                    }
                    _ => {
                        let source_id = source_piece as usize;
                        self.piece_sets[source_id] ^= move_target_bb;
                        self.occupancy         ^= move_target_bb;

                        self.piece_sets[source_id] |= move_source_bb;
                        self.occupancy         |= move_source_bb;
                    }
                }
            },
            None => panic!("Trying to unmake move which was not made: no piece was found on target square"),
        };

        // If unmaking castle, also return rook to its place
        if mov.kind == MoveKind::Castle {
            let (rook_source_file, rook_target_file) = match mov.target.file() {
                2 => (0, 3),
                6 => (7, 5),
                _ => panic!("Malformed castle, target square invalid: {:?}", mov),
            };
            let rook_source_bb = Square::from_coords(mov.target.rank(), rook_source_file).to_bitboard();
            let rook_target_bb = Square::from_coords(mov.target.rank(), rook_target_file).to_bitboard();

            match self.piece_sets
                .iter()
                .enumerate()
                .find(|(_, bitboard)| *bitboard & rook_target_bb > 0)
                {
                    Some((rook_type, _)) => {
                        self.piece_sets[rook_type] |= rook_source_bb;
                        self.occupancy         |= rook_source_bb;

                        self.piece_sets[rook_type] ^= rook_target_bb;
                        self.occupancy         ^= rook_target_bb;
                    },
                    None => panic!("Rook was not found when castling"),
                }
        }

        // Return captured piece to target square
        if let Some(target_piece) = captured_piece {
            match mov.kind {
                // Return pawn captured by En Passant pawn if needed
                MoveKind::EnPassant => {
                    let original_dead_pawn_bb = Square::from_coords(mov.source.rank(), mov.target.file()).to_bitboard();
                    self.piece_sets[target_piece as usize] |= original_dead_pawn_bb;
                    self.occupancy                     |= original_dead_pawn_bb;
                },
                _ => {
                    self.piece_sets[target_piece as usize] |= move_target_bb;
                    self.occupancy                     |= move_target_bb;
                },
            }
        }


        self.ep_target = previous_ep_target;
        self.castling_rights = previous_castling_rights;
        self.hash = previous_hash;
        self.ply -= 1;
        self.positions.pop();
    }

    pub fn is_square_attacked(&self, square: Square, attacker_color: Color) -> bool {
        for (piece_type, piece) in self.pieces_by_color(attacker_color).iter().enumerate() {
            match Piece::from(piece_type) {
                Piece::Queen => {
                    if self.attacks.queen(self.occupancy, square) & piece > 0 {
                        return true
                    }
                }
                Piece::Knight => {
                    if self.attacks.knight[square as usize] & piece > 0 {
                        return true
                    }
                }
                Piece::Bishop => {
                    if self.attacks.bishop(self.occupancy, square) & piece > 0 {
                        return true
                    }
                }
                Piece::Rook => {
                    if self.attacks.rook(self.occupancy, square) & piece > 0 {
                        return true
                    }
                }
                Piece::Pawn => {
                    if self.attacks.pawn[attacker_color.flip() as usize][square as usize] & piece > 0 {
                        return true
                    }
                }
                Piece::King => {
                    if self.attacks.king[square as usize] & piece > 0 {
                        return true
                    }
                }
                _ => panic!("Unexpected piece type! Pieces by color should be considered white")
            }
        }
        false
    }

    pub fn is_king_in_check(&self, color: Color) -> bool {
        let king_bb = match color {
            Color::White => self.piece_sets[Piece::King as usize],
            Color::Black => self.piece_sets[Piece::KingBlack as usize],
        };
        let square = king_bb.bitscan();
        self.is_square_attacked(square, color.flip())
    }

    pub fn threefold_repetition(&self) -> bool {
        self.positions.iter().filter(|&&p| p == self.hash).count() >= 2
    }

    pub fn is_theoretically_winnable(&self, color: Color) -> bool {
        if [
            Piece::Pawn,
            Piece::Rook,
            Piece::Queen,
        ].iter().any(|&piece| {
            self.piece_sets[piece.colored(color) as usize].pop_count() > 0
        }) {
            return true;
        };

        let minor_pieces = [
            Piece::Knight,
            Piece::Bishop,
        ].iter().fold(0, |acc, &piece| {
            acc + self.piece_sets[piece.colored(color) as usize].pop_count()
        });

        minor_pieces > 1
    }
}


#[cfg(test)]
mod tests {
    use super::*;
    use crate::{square::Square, board::zobrist::Zobrist};

    #[test]
    fn square_enum() {
        assert_eq!(Square::A1 as u8, 0);
        assert_eq!(Square::F1 as u8, 5);
        assert_eq!(Square::H8 as u8, 63);
    }

    #[test]
    fn make_move() {
        let fen = String::from("q1b2k2/5p1p/4p1pb/pPPp4/3N4/3nPB2/P2QKnR1/1R6 w - - 0 25");
        let mut board = Board::from_FEN(fen);
        let initial_board = board.clone();
        board.print();

        let black_move = Move { source: Square::F7, target: Square::F5, kind: MoveKind::Quiet };
        println!("\n{:?}", black_move);

        if let Some(..) = board.make_move(black_move) {
            panic!("No piece should be captured");
        }

        board.print();

        let hash = board.hash;
        board.compute_hash();
        assert_eq!(hash, board.hash, "Hash should be correctly updated after move");

        assert!(board.piece_sets[Piece::PawnBlack as usize] & Square::F7.to_bitboard() == 0);
        assert!(board.piece_sets[Piece::PawnBlack as usize] & Square::F5.to_bitboard() > 0);
        assert!(board.ply == 1);

        let white_move = Move { source: Square::D2, target: Square::A5, kind: MoveKind::Capture };
        println!("\n{:?}", white_move);

        match board.make_move(white_move) {
            Some(captured) => assert!(captured == Piece::PawnBlack),
            None => panic!("A piece should be captured"),
        };

        board.print();

        assert!(board.piece_sets[Piece::PawnBlack as usize] & Square::A5.to_bitboard() == 0, "Target piece should be captured");
        assert!(board.piece_sets[Piece::Queen as usize] & Square::D2.to_bitboard() == 0);
        assert!(board.piece_sets[Piece::Queen as usize] & Square::A5.to_bitboard() > 0);
        assert_ne!(board.occupancy, initial_board.occupancy, "Occupancy should change after make_move");
        assert!(board.ply == 2);
    }

    #[test]
    fn unmake_move() {
        let fen = String::from("q1b2k2/5p1p/4p1pb/pPPp4/3N4/3nPB2/P2QKnR1/1R6 w - - 0 25");
        let mut board = Board::from_FEN(fen);
        let initial_board = board.clone();

        let mov = Move { source: Square::D2, target: Square::A5, kind: MoveKind::Capture };

        board.print();

        let captured_piece = board.make_move(mov);
        board.print();

        board.unmake_move(mov, captured_piece, None, board.castling_rights, initial_board.hash);
        board.print();

        assert_eq!(board, initial_board, "Board state after unmake_move should be the same as before make_move");
    }

    #[test]
    fn is_square_attacked() {
        let board = Board::new();

        assert!(board.is_square_attacked(Square::E2, Color::White));
        assert!(!board.is_square_attacked(Square::E2, Color::Black));
        assert!(!board.is_square_attacked(Square::E4, Color::White));
        assert!(board.is_square_attacked(Square::B6, Color::Black));
    }
}