use crate::{bitboard::{Bitboard, serialize_bitboard}, moves::Move, attacks::Attacks};

pub static DEFAULT_FEN: &str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Board {
    pub pieces: [Bitboard; 12],

    pub occupancy: Bitboard,
    pub ply: u16,

    attacks: Attacks,
}


#[derive(Debug, Clone, Copy, PartialEq, Eq, num_enum::FromPrimitive)]
#[repr(usize)]
pub enum PieceType {
    #[default]
    Pawn,
    Knight,
    Bishop,
    Rook,
    Queen,
    King,
    PawnBlack,
    KnightBlack,
    BishopBlack,
    RookBlack,
    QueenBlack,
    KingBlack,
}

const PIECE_CHARS: [&str; 12] = [
    "♟︎", "♞", "♝", "♜", "♛", "♚",
    "♙", "♘", "♗", "♖", "♕", "♔",
];


#[allow(unused)]
impl Board {
    #[allow(non_snake_case)]
    pub fn from_FEN(fen: String) -> Self {
        let mut pieces = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];

        let mut rank = 7;
        let mut file = 0i32;

        for character in fen.chars() {
            let index = rank * 8 + file;
            let position = 1 << index.clamp(0, 63);

            if character.is_numeric() {
                let digit = match character.to_digit(10) {
                    None => todo!("What to do here?"),
                    Some(digit) => digit,
                };
                if digit > 0 && digit <= 8 {
                    file += digit as i32;
                }
            } else {
                match character {
                    'P' => pieces[PieceType::Pawn as usize] |= position,
                    'N' => pieces[PieceType::Knight as usize] |= position,
                    'B' => pieces[PieceType::Bishop as usize] |= position,
                    'R' => pieces[PieceType::Rook as usize] |= position,
                    'Q' => pieces[PieceType::Queen as usize] |= position,
                    'K' => pieces[PieceType::King as usize] |= position,
                    'p' => pieces[PieceType::PawnBlack as usize] |= position,
                    'n' => pieces[PieceType::KnightBlack as usize] |= position,
                    'b' => pieces[PieceType::BishopBlack as usize] |= position,
                    'r' => pieces[PieceType::RookBlack as usize] |= position,
                    'q' => pieces[PieceType::QueenBlack as usize] |= position,
                    'k' => pieces[PieceType::KingBlack as usize] |= position,
                    '/' => {
                        rank -= 1;
                        file = -1; // So it becomes 0
                    },
                    ' ' => { break }, // TODO: break for now, parse everything else later
                    '-' => {}, // TODO
                    'w' => {}, // TODO
                    _ => todo!("Unexpected character!"),
                }
                file += 1;
            }
        }

        let mut board = Self { pieces, occupancy: 0, ply: 0, attacks: Attacks::new() };
        board.update_occupancy();
        board
    }

    fn update_occupancy(&mut self) {
        self.occupancy = 0;
        // TODO: reduce
        for piece in self.pieces {
            self.occupancy |= piece;
        }
    }

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

    fn pieces_by_color(&self, color: Color) -> [Bitboard; 6] {
        let mut pieces = [0; 6];
        for piece_type in 0..6 {
            pieces[piece_type] = self.pieces[piece_type + 6 * color as usize];
        }
        pieces
    }

    fn color_occupancy(&self, color: Color) -> Bitboard {
        let mut occupancy = 0;
        for piece in self.pieces_by_color(color) {
            occupancy |= piece;
        }
        occupancy
    }

    pub fn print(&self) {
        println!();
        for rank in (0..8).rev() {
            print!("{}|", rank + 1);
            for file in 0..8 {
                let index = rank * 8 + file;
                let position: Bitboard = 1 << index;
                let mut found = false;
                for (piece_type, piece_bitboard) in self.pieces.iter().enumerate() {
                    if (piece_bitboard & position) > 0 {
                        found = true;
                        print!("{} ", PIECE_CHARS[piece_type]);
                    }
                }
                if !found {
                    print!(". ");
                }
            }
            println!();
        }
        println!("  a b c d e f g h");
    }

    pub fn generate_moves(&self, color: Color) -> Vec<Move> {
        let mut moves = Vec::with_capacity(1024);
        let opponent_occupancy = self.color_occupancy(Color::from(1 - color as u8));
        let available_targets = opponent_occupancy | self.empty();
        for (piece_type, piece) in self.pieces_by_color(color).iter().enumerate() {
            match PieceType::from(piece_type) {
                PieceType::Pawn => {
                    for source in serialize_bitboard(*piece) {
                        for target in serialize_bitboard(self.attacks.pawn[color as usize][source as usize] & opponent_occupancy) {
                            moves.push(Move { source, target });
                        };
                        for target in serialize_bitboard(self.attacks.pawn_pushes[color as usize][source as usize] & available_targets) {
                            moves.push(Move { source, target });
                        };
                    }
                }
                PieceType::King => {
                    for source in serialize_bitboard(*piece) {
                        for target in serialize_bitboard(self.attacks.king[source as usize] & available_targets) {
                            moves.push(Move { source, target });
                        };
                    }
                }
                PieceType::Knight => {
                    for source in serialize_bitboard(*piece) {
                        for target in serialize_bitboard(self.attacks.knight[source as usize] & available_targets) {
                            moves.push(Move { source, target });
                        };
                    }
                }
                PieceType::Bishop => {
                    for source in serialize_bitboard(*piece) {
                        for target in serialize_bitboard(self.attacks.bishop(self.occupancy, source) & available_targets) {
                            moves.push(Move { source, target });
                        };
                    }
                }
                PieceType::Rook => {
                    for source in serialize_bitboard(*piece) {
                        for target in serialize_bitboard(self.attacks.rook(self.occupancy, source) & available_targets) {
                            moves.push(Move { source, target });
                        };
                    }
                }
                PieceType::Queen => {
                    for source in serialize_bitboard(*piece) {
                        for target in serialize_bitboard(self.attacks.queen(self.occupancy, source) & available_targets) {
                            moves.push(Move { source, target });
                        };
                    }
                }
                _ => todo!("Incorrect piece type")
            }
        }
        moves
    }

    /// *Blindlessly* apply a move without any validation
    /// Move should be validated beforehand
    pub fn make_move(&mut self, mov: Move) -> Option<PieceType> {
        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 captured_piece = match self.pieces
            .iter()
            .enumerate()
            .find(|(piece_type, bitboard)| *bitboard & mov.target.to_bitboard() > 0)
            {
                Some((target_piece, _)) => {
                    self.pieces[target_piece] ^= move_target_bb;
                    Some(PieceType::from(target_piece))
                },
                None => None,
            };

        // Move a piece from source square to target
        match self.pieces
            .iter()
            .enumerate()
            .find(|(piece_type, bitboard)| *bitboard & mov.source.to_bitboard() > 0)
            {
                Some((source_piece, _)) => {
                    self.pieces[source_piece] ^= move_source_bb;
                    self.occupancy            ^= move_source_bb;

                    self.pieces[source_piece] |= move_target_bb;
                    self.occupancy            |= move_target_bb;
                },
                None => panic!("Move is malformed: source piece not found"),
            };

        self.ply += 1;

        captured_piece
    }

    /// Completely reverse make_move as if it never happened
    pub fn unmake_move(&mut self, mov: Move, captured_piece: Option<PieceType>) {
        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.pieces
            .iter()
            .enumerate()
            .find(|(piece_type, bitboard)| *bitboard & mov.target.to_bitboard() > 0)
            {
                Some((source_piece, _)) => {
                    self.pieces[source_piece] ^= move_target_bb;
                    self.occupancy            ^= move_target_bb;

                    self.pieces[source_piece] |= 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"),
            };

        // Return captured piece to target square
        match captured_piece {
            Some(target_piece) => {
                self.pieces[target_piece as usize] |= move_target_bb;
                self.occupancy                     |= move_target_bb;
            },
            None => {}
        }

        self.ply -= 1;
    }
}


#[derive(Debug, Clone, Copy, PartialEq, num_enum::FromPrimitive)]
#[repr(u8)]
pub enum Color {
    #[default]
    White,
    Black,
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{bitboard::{pop_count, bitscan, print}, square::Square};

    #[test]
    fn test_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 test_from_fen() {
        let fen = String::from(DEFAULT_FEN);
        let board = Board::from_FEN(fen);

        board.print();
        print(board.empty(), "Empty squares");

        assert_eq!(pop_count(board.pieces[PieceType::Pawn as usize]), 8);
        assert_eq!(pop_count(board.pieces[PieceType::Knight as usize]), 2);
        assert_eq!(pop_count(board.pieces[PieceType::Bishop as usize]), 2);
        assert_eq!(pop_count(board.pieces[PieceType::Rook as usize]), 2);
        assert_eq!(pop_count(board.pieces[PieceType::Queen as usize]), 1);
        assert_eq!(pop_count(board.pieces[PieceType::King as usize]), 1);

        assert_eq!(pop_count(board.pieces[PieceType::PawnBlack as usize]), 8);
        assert_eq!(pop_count(board.pieces[PieceType::KnightBlack as usize]), 2);
        assert_eq!(pop_count(board.pieces[PieceType::BishopBlack as usize]), 2);
        assert_eq!(pop_count(board.pieces[PieceType::RookBlack as usize]), 2);
        assert_eq!(pop_count(board.pieces[PieceType::QueenBlack as usize]), 1);
        assert_eq!(pop_count(board.pieces[PieceType::KingBlack as usize]), 1);

        assert_eq!(bitscan(board.pieces[PieceType::King as usize]), Square::E1);
        assert_eq!(bitscan(board.pieces[PieceType::QueenBlack as usize]), Square::D8);

        assert_eq!(pop_count(board.occupancy), 32);
        assert_eq!(pop_count(board.empty()), 32);
        assert_eq!(pop_count(board.color_occupancy(Color::White)), 16);
        assert_eq!(pop_count(board.color_occupancy(Color::Black)), 16);
    }

    #[test]
    fn test_generate_moves_starting_position() {
        let fen = String::from(DEFAULT_FEN);
        let board = Board::from_FEN(fen);
        let moves = board.generate_moves(Color::White);
        let black_moves = board.generate_moves(Color::Black);

        assert_eq!(moves.len(), 20);
        assert_eq!(black_moves.len(), 20);

        for mov in moves {
            mov.print();
        }
    }

    #[test]
    fn test_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 };
        println!("\n{:?}", black_move);

        match board.make_move(black_move) {
            Some(..) => panic!("No piece should be captured"),
            None => {},
        };

        board.print();

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

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

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

        board.print();

        assert!(board.pieces[PieceType::PawnBlack as usize] & Square::A5.to_bitboard() == 0, "Target piece should be captured");
        assert!(board.pieces[PieceType::Queen as usize] & Square::D2.to_bitboard() == 0);
        assert!(board.pieces[PieceType::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 test_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 };

        board.print();

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

        board.unmake_move(mov, captured_piece);
        board.print();

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