use std::f32::INFINITY;

use crate::{board::{piece::Piece, color::Color, CastlingSide}, bitboard::{Bitboard, BitboardFns}, square::Square};

use super::Grossmeister;

/// Score in pawns (not centipawns)
pub type Score = f32;

const PAWN_BONUS: [Score; 64] = [
  //  A      B      C      D      E      F      G      H
     0.00,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00,
     0.50,  0.50,  0.50,  0.50,  0.50,  0.50,  0.50,  0.50,
     0.10,  0.10,  0.20,  0.30,  0.30,  0.20,  0.10,  0.10,
     0.05,  0.05,  0.10,  0.25,  0.25,  0.10,  0.05,  0.05,
     0.00,  0.00,  0.00,  0.20,  0.20,  0.00,  0.00,  0.00,
     0.05, -0.05, -0.10,  0.00,  0.00, -0.10, -0.05,  0.05,
     0.05,  0.10,  0.10, -0.20, -0.20,  0.10,  0.10,  0.05,
     0.00,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00
];

const PAWN_BONUS_PASSER_ENDGAME: [Score; 64] = [
  //  A      B      C      D      E      F      G      H
     0.00,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00,
     2.00,  2.00,  2.00,  2.00,  2.00,  2.00,  2.00,  2.00,
     1.00,  1.00,  1.00,  1.00,  1.00,  1.00,  1.00,  1.00,
     0.70,  0.70,  0.70,  0.70,  0.70,  0.70,  0.70,  0.70,
     0.50,  0.50,  0.50,  0.50,  0.50,  0.50,  0.50,  0.50,
     0.30,  0.30,  0.30,  0.30,  0.30,  0.30,  0.30,  0.30,
     0.10,  0.10,  0.10,  0.10,  0.10,  0.10,  0.10,  0.10,
     0.00,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00,
];

const KNIGHT_BONUS: [Score; 64] = [
  //  A      B      C      D      E      F      G      H
    -0.50, -0.40, -0.30, -0.30, -0.30, -0.30, -0.40, -0.50,
    -0.40, -0.20,  0.00,  0.00,  0.00,  0.00, -0.20, -0.40,
    -0.30,  0.00,  0.10,  0.15,  0.15,  0.10,  0.00, -0.30,
    -0.30,  0.05,  0.15,  0.20,  0.20,  0.15,  0.05, -0.30,
    -0.30,  0.00,  0.15,  0.20,  0.20,  0.15,  0.00, -0.30,
    -0.30,  0.05,  0.10,  0.15,  0.15,  0.10,  0.05, -0.30,
    -0.40, -0.20,  0.00,  0.05,  0.05,  0.00, -0.20, -0.40,
    -0.50, -0.40, -0.30, -0.30, -0.30, -0.30, -0.40, -0.50,
];

const BISHOP_BONUS: [Score; 64] = [
  //  A      B      C      D      E      F      G      H
    -0.20, -0.10, -0.10, -0.10, -0.10, -0.10, -0.10, -0.20,
    -0.10,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00, -0.10,
    -0.10,  0.00,  0.05,  0.10,  0.10,  0.05,  0.00, -0.10,
    -0.10,  0.05,  0.05,  0.10,  0.10,  0.05,  0.05, -0.10,
    -0.10,  0.00,  0.10,  0.10,  0.10,  0.10,  0.00, -0.10,
    -0.10,  0.10,  0.10,  0.10,  0.10,  0.10,  0.10, -0.10,
    -0.10,  0.25,  0.00,  0.00,  0.00,  0.00,  0.25, -0.10,
    -0.20, -0.10, -0.10, -0.10, -0.10, -0.10, -0.10, -0.20,
];

const ROOK_BONUS: [Score; 64] = [
  //  A      B      C      D      E      F      G      H
     0.00,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00,
     0.05,  0.30,  0.30,  0.30,  0.30,  0.30,  0.30,  0.05,
    -0.05,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00, -0.05,
    -0.05,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00, -0.05,
    -0.05,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00, -0.05,
    -0.05,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00, -0.05,
    -0.05,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00, -0.05,
     0.00,  0.00,  0.00,  0.05,  0.05,  0.00,  0.00,  0.00
];

const QUEEN_BONUS: [Score; 64] = [
  //  A      B      C      D      E      F      G      H
    -0.20, -0.10, -0.10, -0.05, -0.05, -0.10, -0.10, -0.20,
    -0.10,  0.00,  0.00,  0.00,  0.00,  0.00,  0.00, -0.10,
    -0.10,  0.00,  0.05,  0.05,  0.05,  0.05,  0.00, -0.10,
    -0.05,  0.00,  0.05,  0.05,  0.05,  0.05,  0.00, -0.05,
     0.00,  0.00,  0.05,  0.05,  0.05,  0.05,  0.00, -0.05,
    -0.10,  0.05,  0.05,  0.05,  0.05,  0.05,  0.00, -0.10,
    -0.10,  0.00,  0.05,  0.00,  0.00,  0.00,  0.00, -0.10,
    -0.20, -0.10, -0.10, -0.05, -0.05, -0.10, -0.10, -0.20
];

const KING_BONUS: [Score; 64] = [
  //  A      B      C      D      E      F      G      H
    -0.30, -0.40, -0.40, -0.50, -0.50, -0.40, -0.40, -0.30,
    -0.30, -0.40, -0.40, -0.50, -0.50, -0.40, -0.40, -0.30,
    -0.30, -0.40, -0.40, -0.50, -0.50, -0.40, -0.40, -0.30,
    -0.30, -0.40, -0.40, -0.50, -0.50, -0.40, -0.40, -0.30,
    -0.20, -0.30, -0.30, -0.40, -0.40, -0.30, -0.30, -0.20,
    -0.10, -0.20, -0.20, -0.20, -0.20, -0.20, -0.20, -0.10,
     0.20,  0.20,  0.00, -0.10, -0.10,  0.00,  0.20,  0.20,
     0.20,  0.10,  0.30, -0.20,  0.00,  0.10,  0.30,  0.20
];

const KING_BONUS_ENGAME: [Score; 64] = [
  //  A      B      C      D      E      F      G      H
    -0.50, -0.40, -0.30, -0.20, -0.20, -0.30, -0.40, -0.50,
    -0.30, -0.20, -0.10,  0.00,  0.00, -0.10, -0.20, -0.30,
    -0.30, -0.10,  0.20,  0.30,  0.30,  0.20, -0.10, -0.30,
    -0.30, -0.10,  0.30,  0.40,  0.40,  0.30, -0.10, -0.30,
    -0.30, -0.10,  0.30,  0.40,  0.40,  0.30, -0.10, -0.30,
    -0.30, -0.10,  0.20,  0.30,  0.30,  0.20, -0.10, -0.30,
    -0.30, -0.30,  0.00,  0.00,  0.00,  0.00, -0.30, -0.30,
    -0.50, -0.30, -0.30, -0.30, -0.30, -0.30, -0.30, -0.50
];

impl Grossmeister {
    pub fn passer_mask(&self, color: Color) -> Bitboard {
        let black_pawns = self.board.piece_sets[Piece::PawnBlack as usize];
        let white_pawns = self.board.piece_sets[Piece::Pawn as usize];

        match color {
            Color::Black => {
                let mut front_fill = white_pawns.nort_fill().nort_one();
                front_fill |= front_fill.east_one() | front_fill.west_one();
                black_pawns & !front_fill
            }
            Color::White => {
                let mut front_fill = black_pawns.sout_fill().sout_one();
                front_fill |= front_fill.east_one() | front_fill.west_one();
                white_pawns & !front_fill
            }
        }
    }

    /// Return number of pawns above the king (+left/right)
    pub fn pawn_shield(&self, color: Color) -> f32 {
        let behind_pawns = match color {
            Color::White => self.board.piece_sets[Piece::Pawn as usize].sout_one(),
            Color::Black => self.board.piece_sets[Piece::PawnBlack as usize].nort_one(),
        };

        let king = match color {
            Color::White => self.board.piece_sets[Piece::King as usize],
            Color::Black => self.board.piece_sets[Piece::KingBlack as usize],
        };

        (behind_pawns & (king | king.west_one() | king.east_one())).pop_count() as f32
    }

    /// Count the number of pawn islands of given color
    pub fn pawn_islands(&self, color: Color) -> f32 {
        let pawns = self.board.pieces_by_color(color)[Piece::Pawn as usize];
        let fileset = pawns.nort_fill().sout_fill();
        // Does not matter west/east, each island has both sides
        let islands_west_files = fileset & !fileset.east_one();
        (islands_west_files.pop_count() / 8) as f32
    }

    /// Compute total bonus for well-positioned pieces
    /// according to Piece-Square Tables
    pub fn pst_bonus(&self, color: Color, is_endgame: bool) -> f32 {
        self.board.pieces_by_color(color).iter().enumerate().fold(0., |total, (piece_index, bitboard)| {
            let pst = match Piece::from(piece_index).without_color() {
                Piece::Pawn => if is_endgame && (bitboard & self.passer_mask(color) > 0) {
                    PAWN_BONUS_PASSER_ENDGAME
                } else {
                    PAWN_BONUS
                }
                Piece::Knight => KNIGHT_BONUS,
                Piece::Bishop => BISHOP_BONUS,
                Piece::Rook => ROOK_BONUS,
                Piece::Queen => QUEEN_BONUS,
                Piece::King => if is_endgame {
                    KING_BONUS_ENGAME
                } else {
                    KING_BONUS
                }
                _ => panic!("Unreachable")
            };
            total + bitboard.serialize().fold(0., |acc, square| {
                let pst_index = match color {
                    Color::White => square.mirror(),
                    Color::Black => square,
                } as usize;
                acc + pst[pst_index]
            })
        })
    }

    /// Count raw material of the given color
    pub fn material(&self, color: Color) -> f32 {
        self.board.pieces_by_color(color).iter().enumerate().fold(0., |acc, (piece_index, bitboard)| {
            acc + Piece::from(piece_index).static_eval() * bitboard.pop_count() as f32
        })
    }

    // Returns a value in [0, 240] representing how
    // much material is left in the game
    // Note: not related to actual material counting
    pub fn phase(&self) -> u8 {
        let knight_phase = 10;
        let bishop_phase = 10;
        let rook_phase = 20;
        let queen_phase = 40;

        let total_phase = knight_phase*4 + bishop_phase*4 + rook_phase*4 + queen_phase*2;
         // If you change it, make sure to update denominator in interpolation
        debug_assert_eq!(total_phase, 240);

        self.board.piece_sets.iter().enumerate().fold(0, |acc, (piece_index, &bitboard)| {
            acc + match Piece::from(piece_index).without_color() {
                Piece::King => 0,
                Piece::Pawn => 0,
                Piece::Knight => knight_phase,
                Piece::Bishop => bishop_phase,
                Piece::Rook => rook_phase,
                Piece::Queen => queen_phase,
                _ => panic!("Unreachable")
            } * bitboard.pop_count()
        })
    }


    /// Evaluate a position relative to the current player
    pub fn evaluate(&self) -> Score {
        let color = self.board.color();
        let opponent_color = color.flip();

        let winnable = self.board.is_theoretically_winnable(color);
        let loseable = self.board.is_theoretically_winnable(opponent_color);

        if !winnable && !loseable {
            return 0.0
        }

        let material_advantage = self.material(color) - self.material(opponent_color);
        let mobility_advantage = self.mobility(color) - self.mobility(opponent_color);
        let pawn_shield_advantage = self.pawn_shield(color) - self.pawn_shield(opponent_color);
        let pawn_islands_advantage = self.pawn_islands(opponent_color) - self.pawn_islands(color);

        // Middlegame eval, assumming all pieces are on the Board
        let middlegame_eval =
            mobility_advantage * 0.05 +
            pawn_shield_advantage * 0.20 +
            pawn_islands_advantage * 0.10 +
            (self.pst_bonus(color, false) - self.pst_bonus(opponent_color, false));

        // Engame eval, assuming no pieces are left on the Board (only kings and pawns)
        let endgame_eval =
            mobility_advantage * 0.03 +
            pawn_shield_advantage * 0.05 +
            pawn_islands_advantage * 0.15 +
            (self.pst_bonus(color, true) - self.pst_bonus(opponent_color, true));

        // Interpolate evalutaion based on amount of pieces to remove discontinuity
        let phase = self.phase();
        let tapered_eval = (middlegame_eval * phase as f32 + endgame_eval * (240 - phase) as f32) / 240.;

        let eval = (material_advantage + tapered_eval)
            .min(if winnable { INFINITY } else { 0.0 }) // Can not score > 0 if not winnable
            .max(if loseable { -INFINITY } else { 0.0 }); // Can not score < 0 if not loseable

        (eval * 100.0).round() / 100.0
    }

    /// Count pseudo-legal moves without actually generating them
    /// Also exclude all moves that put a piece under attack of a pawn - so called safe mobility
    pub fn mobility(&self, color: Color) -> f32 {
        let mut mobility = 0.;
        let opponent_occupancy = self.board.color_occupancy(color.flip());
        let player_pieces = self.board.pieces_by_color(color);

        let opponent_pawns = match color {
            Color::Black => self.board.piece_sets[Piece::Pawn as usize],
            Color::White => self.board.piece_sets[Piece::PawnBlack as usize],
        };

        let pawn_attacked_squares = opponent_pawns.serialize().fold(0u64, |acc, square| {
            acc | self.board.attacks.pawn[color.flip() as usize][square as usize]
        });

        // Exclude squares controlled by enemy pawns from mobility
        let empty = self.board.empty() & !pawn_attacked_squares;

        for (piece_type, piece) in player_pieces.iter().enumerate() {
            match Piece::from(piece_type) {
                Piece::Pawn => {
                    for source in piece.serialize() {
                        let ep_bitboard = match self.board.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,
                        };
                        mobility += (self.board.attacks.pawn[color as usize][source as usize] & (opponent_occupancy | ep_bitboard)).pop_count() as f32;
                        mobility += (self.board.attacks.pawn_pushes[color as usize][source as usize] & empty).pop_count() as f32;
                    }
                    let able_to_double_push_mask = match color {
                        Color::White => empty >> 8,
                        Color::Black => empty << 8,
                    };
                    for source in (*piece & able_to_double_push_mask).serialize() {
                        mobility += (self.board.attacks.pawn_double_pushes[color as usize][source as usize] & empty).pop_count() as f32;
                    }
                }
                Piece::King => {
                    for source in piece.serialize() {
                        mobility += (self.board.attacks.king[source as usize] & (empty | opponent_occupancy)).pop_count() as f32;

                        // Castling
                        let king_home_position = match color {
                            Color::White => Square::E1,
                            Color::Black => Square::E8,
                        };
                        if *piece == king_home_position.to_bitboard() {
                            for rook_square in player_pieces[Piece::Rook as usize]
                                .serialize()
                                .filter(|rook_square| rook_square.rank() == king_home_position.rank())
                                {
                                    match rook_square.file() {
                                        0 => {
                                            let castle_line = [
                                                king_home_position.west_one(),
                                                king_home_position.west_one().west_one(),
                                            ];

                                            let all_empty = castle_line.iter().all(|square| empty & square.to_bitboard() > 0);
                                            let any_checks = castle_line.iter().any(|square| self.board.is_square_attacked(*square, color.flip()));

                                            if all_empty && !any_checks && self.board.castling_rights[color as usize][CastlingSide::Queen as usize] {
                                                mobility += 1.;
                                            }
                                        },
                                        7 => {
                                            let castle_line = [
                                                king_home_position.east_one(),
                                                king_home_position.east_one().east_one(),
                                            ];

                                            let all_empty = castle_line.iter().all(|square| empty & square.to_bitboard() > 0);
                                            let any_checks = castle_line.iter().any(|square| self.board.is_square_attacked(*square, color.flip()));

                                            if all_empty && !any_checks && self.board.castling_rights[color as usize][CastlingSide::King as usize] {
                                                mobility += 1.;
                                            }
                                        },
                                        _ => {},
                                    }
                                }
                        }
                    }
                }
                Piece::Knight => {
                    for source in piece.serialize() {
                        mobility += (self.board.attacks.knight[source as usize] & (empty | opponent_occupancy)).pop_count() as f32;
                    }
                }
                Piece::Bishop => {
                    for source in piece.serialize() {
                        mobility += (self.board.attacks.bishop(self.board.occupancy, source) & (empty | opponent_occupancy)).pop_count() as f32;
                    }
                }
                Piece::Rook => {
                    for source in piece.serialize() {
                        mobility += (self.board.attacks.rook(self.board.occupancy, source) & (empty | opponent_occupancy)).pop_count() as f32;
                    }
                }
                Piece::Queen => {
                    // Do not account queen in mobility
                }
                incorrect_type => panic!("Incorrect piece type: {:?}", incorrect_type),
            }
        }
        mobility
    }
}

#[cfg(test)]
mod tests {
    use crate::{board::{Board, io::IO}, moves::{Move, MoveKind}};

    use super::*;

    #[test]
    fn castle_bonus() {
        assert_eq!(KING_BONUS[Square::E1.mirror() as usize], 0.0);
        assert!(KING_BONUS[Square::G1.mirror() as usize] > 0.0);
        assert!(KING_BONUS[Square::C1.mirror() as usize] > 0.0);
        assert!(KING_BONUS[Square::D1.mirror() as usize] < 0.0);
        assert!(KING_BONUS[Square::E2.mirror() as usize] < 0.0);

        assert_eq!(KING_BONUS[Square::E8 as usize], 0.0);
        assert!(KING_BONUS[Square::G8 as usize] > 0.0);
        assert!(KING_BONUS[Square::C8 as usize] > 0.0);
        assert!(KING_BONUS[Square::D8 as usize] < 0.0);
        assert!(KING_BONUS[Square::E2 as usize] < 0.0);
    }

    #[test]
    fn mobility() {
        let board = Board::new();
        let gm = Grossmeister::new(board);
        let white = gm.mobility(Color::White);
        let black = gm.mobility(Color::Black);

        assert_eq!(white, 20.);
        assert_eq!(black, 20.);
    }

    #[test]
    fn material() {
        let board = Board::new();
        let gm = Grossmeister::new(board);
        assert_eq!(gm.material(Color::Black), gm.material(Color::White));

    }

    #[test]
    fn initial_eval() {
        let board = Board::new();
        let gm = Grossmeister::new(board);
        assert_eq!(gm.evaluate(), 0.0);
    }

    #[test]
    fn king_tropism() {
        let board = Board::new();
        let mut gm = Grossmeister::new(board);
        gm.board.make_move(Move { source: Square::D1, target: Square::F5, kind: MoveKind::Quiet });
        let score = gm.evaluate();
        gm.board.print();
        println!("Score {}", score);

        assert!(score < 0.0);
        assert!(score > -1.0);
    }

    #[test]
    fn white_winning() {
        let fen = String::from("8/5pk1/6p1/R4b1p/3P4/1P2N3/P1r2PPP/R5K1 w - - 1 27");
        let board = Board::from_FEN(fen);
        let gm = Grossmeister::new(board);
        let score = gm.evaluate();
        gm.board.print();
        println!("Score {}", score);

        assert!(score > 7.0);
    }

    #[test]
    fn black_winning() {
        let fen = String::from("8/p7/1k4K1/8/4P3/8/PP5r/8 w - - 1 38");
        let board = Board::from_FEN(fen);
        let gm = Grossmeister::new(board);
        let score = gm.evaluate();
        gm.board.print();
        println!("Score {}", score);

        assert!(score < -3.0);
    }

    #[test]
    fn encourage_center_pawns() {
        let score1 = {
            let fen = String::from("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 0 2");
            let board = Board::from_FEN(fen);
            let gm = Grossmeister::new(board);
            let score = gm.evaluate();
            gm.board.print();
            println!("Score {}", score);
            score
        };

        let score2 = {
            let fen = String::from("rnbqkbnr/pppp1ppp/8/4p3/2P5/8/PP1PPPPP/RNBQKBNR w KQkq - 0 2");
            let board = Board::from_FEN(fen);
            let gm = Grossmeister::new(board);
            let score = gm.evaluate();
            gm.board.print();
            println!("Score {}", score);
            score
        };

        assert!(score1 > score2);
    }

    #[test]
    fn discourage_edge_knights() {
        let score1 = {
            let fen = String::from("r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3");
            let board = Board::from_FEN(fen);
            let gm = Grossmeister::new(board);
            let score = gm.evaluate();
            gm.board.print();
            println!("Score {}", score);
            score
        };

        let score2 = {
            let fen = String::from("r1bqkbnr/pppp1ppp/2n5/4p3/4P3/7N/PPPP1PPP/RNBQKB1R w KQkq - 2 3");
            let board = Board::from_FEN(fen);
            let gm = Grossmeister::new(board);
            let score = gm.evaluate();
            gm.board.print();
            println!("Score {}", score);
            score
        };

        assert!(score1 > score2);
    }

    #[test]
    fn mirrored_evaluation() {
        let score1 = {
            let fen = String::from("r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1");
            let board = Board::from_FEN(fen);
            let gm = Grossmeister::new(board);
            let score = gm.evaluate();
            gm.board.print();
            println!("Score {}", score);
            score
        };

        let score2 = {
            let fen = String::from("r2q1rk1/pP1p2pp/Q4n2/bbp1p3/Np6/1B3NBn/pPPP1PPP/R3K2R b KQ - 0 1 ");
            let board = Board::from_FEN(fen);
            let gm = Grossmeister::new(board);
            let score = gm.evaluate();
            gm.board.print();
            println!("Score {}", score);
            score
        };

        assert_eq!(score1.abs(), score2.abs());
    }

    #[test]
    fn passer() {
        let fen = String::from("rnbqkbnr/ppp3pp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
        let board = Board::from_FEN(fen);
        let gm = Grossmeister::new(board);
        let mask = gm.passer_mask(Color::White);
        assert_eq!(mask, Square::E2.to_bitboard());
    }

    #[test]
    fn no_passer() {
        let board = Board::new();
        let gm = Grossmeister::new(board);
        let mask = gm.passer_mask(Color::White);
        assert_eq!(mask, 0);
    }
}