use super::*;

lazy_static! {
    pub static ref HALFKP_MODEL_READER: HalfKPModelReader = {
        let mut reader = std::io::Cursor::new(include_bytes!(concat!(
            env!("OUT_DIR"),
            "/nnue_dir/nn.nnue"
        )));
        HalfKPModelReader::read(&mut reader).expect("Bad NNUE file!")
    };
}

#[derive(Clone, Debug)]
pub struct Evaluator {
    model: HalfKPModel,
    score_cache: Arc<CacheTable<Score>>,
}

impl Evaluator {
    pub fn print_info(&self) {
        print_cache_table_info(
            "Evaluation Cache Table",
            self.score_cache.len(),
            self.score_cache.get_size(),
        );
    }

    pub fn new(sub_board: &SubBoard) -> Self {
        Self {
            model: HALFKP_MODEL_READER.to_model(sub_board),
            score_cache: Arc::new(CacheTable::new(EVALUATOR_SIZE, 0)),
        }
    }

    pub fn get_model(&self) -> &HalfKPModel {
        &self.model
    }

    pub fn get_model_mut(&mut self) -> &mut HalfKPModel {
        &mut self.model
    }

    fn force_opponent_king_to_corner(
        &self,
        sub_board: &SubBoard,
        winning_side: Color,
        is_bishop_knight_endgame: bool,
    ) -> Score {
        let winning_side_king_square = sub_board.get_king_square(winning_side);
        let losing_side_king_square = sub_board.get_king_square(!winning_side);
        let mut least_distant_corner = Square::D4;
        if is_bishop_knight_endgame {
            let is_light_squared_bishop =
                !(sub_board.get_piece_mask(Bishop) & BB_LIGHT_SQUARES).is_empty();
            let least_distant_corners = if is_light_squared_bishop {
                [Square::A8, Square::H1]
            } else {
                [Square::A1, Square::H8]
            };
            least_distant_corner = *least_distant_corners
                .iter()
                .min_by_key(|&&corner_square| corner_square.distance(losing_side_king_square))
                .unwrap();
        } else {
            for (bb, &corner_square) in BOARD_QUARTER_MASKS
                .iter()
                .zip([Square::A8, Square::H8, Square::A1, Square::H1].iter())
            {
                if bb.contains(losing_side_king_square) {
                    least_distant_corner = corner_square;
                    break;
                }
            }
        }
        let king_distance_score =
            7 - winning_side_king_square.distance(losing_side_king_square) as Score;
        let losing_king_rank_distance_score = 15
            - losing_side_king_square
                .get_rank()
                .to_index()
                .abs_diff(least_distant_corner.get_rank().to_index()) as Score;
        let losing_king_file_distance_score = 15
            - losing_side_king_square
                .get_file()
                .to_index()
                .abs_diff(least_distant_corner.get_file().to_index()) as Score;
        let losing_king_corner_score =
            losing_king_rank_distance_score.pow(2) + losing_king_file_distance_score.pow(2);
        let losing_king_opponent_pieces_score = sub_board
            .occupied_co(winning_side)
            .map(|square| 7 - square.distance(losing_side_king_square))
            .sum::<u8>() as Score;
        10 * (10 * king_distance_score + 2 * losing_king_corner_score)
            + losing_king_opponent_pieces_score
    }

    fn force_passed_pawn_push(&self, sub_board: &SubBoard) -> Score {
        todo!("force_passed_pawn_push {}", sub_board)
    }

    fn king_corner_forcing_evaluation(&self, sub_board: &SubBoard, material_score: Score) -> Score {
        let is_bishop_knight_endgame = sub_board.get_num_pieces() == 4
            && material_score.abs() == Knight.evaluate() + Bishop.evaluate();
        let winning_side = if material_score.is_positive() {
            White
        } else {
            Black
        };
        let signum = material_score.signum();
        let king_forcing_score =
            self.force_opponent_king_to_corner(sub_board, winning_side, is_bishop_knight_endgame);
        (50 * PAWN_VALUE + king_forcing_score / 2) * signum + 2 * material_score
    }

    pub fn is_easily_winning_position(sub_board: &SubBoard, material_score: Score) -> bool {
        if material_score.abs() > PAWN_VALUE + Bishop.evaluate() {
            let white_occupied = sub_board.occupied_co(White);
            let black_occupied = sub_board.occupied_co(Black);
            let num_white_pieces = white_occupied.popcnt();
            let num_black_pieces = black_occupied.popcnt();
            let num_pieces = num_white_pieces + num_black_pieces;
            if num_pieces < 5 {
                if num_white_pieces == 2 && num_black_pieces == 2 {
                    let non_king_white_piece = sub_board
                        .piece_type_at(
                            (white_occupied & !sub_board.get_piece_mask(King))
                                .next()
                                .unwrap(),
                        )
                        .unwrap();
                    let non_king_black_piece = sub_board
                        .piece_type_at(
                            (black_occupied & !sub_board.get_piece_mask(King))
                                .next()
                                .unwrap(),
                        )
                        .unwrap();
                    let mut non_king_pieces = [non_king_white_piece, non_king_black_piece];
                    non_king_pieces.sort();
                    if non_king_pieces == [Pawn, Rook] {
                        return false;
                    }
                }
                for (&bb, &num_pieces) in [white_occupied, black_occupied]
                    .iter()
                    .zip([num_white_pieces, num_black_pieces].iter())
                {
                    if num_pieces == 3 {
                        let non_king_pieces: (PieceType, PieceType) = (bb
                            & !sub_board.get_piece_mask(King))
                        .map(|s| sub_board.piece_type_at(s).unwrap())
                        .collect_tuple()
                        .unwrap();
                        if non_king_pieces == (Knight, Knight) {
                            return false;
                        }
                    }
                }
                return true;
            }
            if num_white_pieces == 1 || num_black_pieces == 1 {
                return true;
            }
        }
        false
    }

    fn evaluate_raw(&mut self, sub_board: &SubBoard) -> Score {
        let knights_mask = sub_board.get_piece_mask(Knight);
        if sub_board.get_non_king_pieces_mask() == knights_mask && knights_mask.popcnt() < 3 {
            return 0;
        }
        let material_score = sub_board.get_material_score();
        if Self::is_easily_winning_position(sub_board, material_score) {
            return self.king_corner_forcing_evaluation(sub_board, material_score);
        }
        let mut nnue_eval = self.model.update_model_and_evaluate(sub_board);
        if nnue_eval.abs() > WINNING_SCORE_THRESHOLD {
            let multiplier = match_interpolate!(
                0,
                1,
                WINNING_SCORE_THRESHOLD,
                35 * PAWN_VALUE,
                nnue_eval.abs()
            );
            nnue_eval += (multiplier * (material_score as f64)).round() as Score;
            let losing_side = if nnue_eval.is_positive() {
                Black
            } else {
                White
            };
            nnue_eval += nnue_eval.signum()
                * match_interpolate!(
                    0,
                    5.0 * multiplier,
                    MAX_MATERIAL_SCORE,
                    0,
                    sub_board.get_masked_material_score_abs(sub_board.occupied_co(losing_side))
                )
                .round() as Score
                * PAWN_VALUE;
        }
        nnue_eval
    }

    fn hashed_evaluate(&mut self, sub_board: &SubBoard) -> Score {
        let hash = sub_board.get_hash();
        if let Some(score) = self.score_cache.get(hash) {
            return score;
        }
        let score = self.evaluate_raw(sub_board);
        self.score_cache.add(hash, score);
        score
    }

    pub(crate) fn evaluate(&mut self, sub_board: &SubBoard) -> Score {
        self.hashed_evaluate(sub_board)
    }

    pub fn slow_evaluate(sub_board: &SubBoard) -> Score {
        HALFKP_MODEL_READER
            .to_model(sub_board)
            .evaluate_current_state(sub_board.turn())
    }

    pub fn reset_variables(&self) {
        self.score_cache.reset_variables();
    }

    pub fn clear(&self) {
        self.score_cache.clear();
    }

    pub fn set_size(&self, size: CacheTableSize) {
        self.score_cache.set_size(size);
    }
}

impl Default for Evaluator {
    fn default() -> Self {
        Self::new(&SubBoard::default())
    }
}
