use std::{time::{Instant, Duration}, f32::INFINITY, cmp::Ordering};
use crate::{bitboard::pop_count, board::*};

use super::ttable::{NodeType, TranspositionTableItem};

static A_FILE:     Bitboard = 0x0101010101010101;

const VALUE_WIN: f32 = 20_000.0;

#[derive(Debug, Default, PartialEq)]
pub struct PerftResult {
    leaf_nodes: u64,
    captures: u64,
    en_passants: u64,
    castles: u64,
    checks: u64,
}

impl Board {
    pub fn perft(&mut self, depth: u8, print: bool) -> PerftResult {
        let mut result = PerftResult::default();

        if depth == 0 {
            result.leaf_nodes = 1;
            return result;
        }
        let color = self.color();

        let moves = self.generate_pseudolegal_moves(color);

        if print {
            println!("Running perft for depth {}. Color to move is {:?}\n{} moves available", depth, color, moves.len());
            println!("{} moves available", moves.len());
        }

        for mov in moves {
            let ep_target_before = self.ep_target.clone();
            let castling_rights_before = self.castling_rights.clone();
            let hash_before = self.hash.clone();
            let captured_piece = self.make_move(mov);
            // King can not be in check after our own move
            if !self.is_king_in_check(color) {
                if depth == 1 {
                    match mov.kind {
                        MoveKind::Capture => {
                            result.captures += 1;
                        }
                        MoveKind::EnPassant => {
                            result.en_passants += 1;
                            result.captures += 1;
                        }
                        MoveKind::Castle => {
                            result.castles += 1;
                        }
                        _ => {}
                    }
                    if self.is_king_in_check(color.flip()) {
                        result.checks += 1;
                    }
                }

                if print {
                    println!("{:?}", mov);
                    self.print();
                }
                let subtree_result = self.perft(depth - 1, print);

                result.leaf_nodes += subtree_result.leaf_nodes;
                result.captures += subtree_result.captures;
                result.checks += subtree_result.checks;
                result.castles += subtree_result.castles;
                result.en_passants += subtree_result.en_passants;

            }
            self.unmake_move(mov, captured_piece, ep_target_before, castling_rights_before, hash_before);
        }

        if print {
            println!("Found {} leaf nodes in this subtree (depth {})", result.leaf_nodes, depth);
        }

        result
    }

    /// Compute material advantage relative to the current player
    pub fn material(&self, color: Color) -> f32 {
        let mut material = 0f32;
        for (piece_index, bitboard) in self.pieces_by_color(color).iter().enumerate() {
            let piece_type = PieceType::from(piece_index);
            material += match piece_type {
                PieceType::Pawn => {
                    match color {
                        Color::White => {
                            serialize_bitboard(*bitboard).iter().fold(0., |acc, square| {
                                acc + match (*square).rank() {
                                    6 => 3.,
                                    5 => 2.,
                                    _ => 1.,
                                }
                            })
                        },
                        Color::Black => {
                            serialize_bitboard(*bitboard).iter().fold(0., |acc, square| {
                                acc + match (*square).rank() {
                                    1 => 3.,
                                    2 => 2.,
                                    _ => 1.,
                                }
                            })
                        }
                    }
                }
                _ => {
                    piece_type.static_eval() * pop_count(*bitboard) as f32
                }
            };
        }
        material
    }

    /// Returns sum of the doubled, blocked and isolated pawns
    /// The greater result is, the worse is the pawn structure
    pub fn pawn_structure_penalty(&self, color: Color) -> f32 {
        let mut result = 0.0;

        let pawns = match color {
            Color::White => self.pieces[PieceType::Pawn as usize],
            Color::Black => self.pieces[PieceType::PawnBlack as usize],
        };

        for file in 0..8 {
            let file_mask = A_FILE << file;
            let pawns_on_file = pop_count(pawns & file_mask) as f32;

            // Doubled pawns (-1 because one pawn on a file is ok)
            result += (pawns_on_file - 1.).max(0.0);

            // Isolated pawns (no pawns on neighbor files)
            if [
                A_FILE << (file - 1).max(0), // File to the left (if any)
                A_FILE << (file + 1).min(7), // File to the right (if any)
            ].iter().all(|file| file & pawns == 0) {
                result += pawns_on_file;
            }
        }

        // Blocked pawns
        let blocked_mask = match color {
            Color::White => self.occupancy >> 8,
            Color::Black => self.occupancy << 8,
        };
        result += pop_count(pawns & blocked_mask) as f32;

        result
    }

    /// Returns the weighted sum of distances from attacking pieces to a king
    /// The higher this value, the safer is the king
    pub fn king_tropism(&self, color: Color) -> f32 {
        let mut result = 0.0;

        let king_square = bitscan(match color {
            Color::White => self.pieces[PieceType::King as usize],
            Color::Black => self.pieces[PieceType::KingBlack as usize],
        });

        for (piece_type, bitboard) in self.pieces_by_color(color.flip()).iter().enumerate() {
            if piece_type != PieceType::King as usize && piece_type != PieceType::Pawn as usize {
                for square in serialize_bitboard(*bitboard) {
                    let distance =
                        (king_square.rank() as f32 - square.rank() as f32).abs() +
                        (king_square.file() as f32 - square.file() as f32).abs();

                    result += distance / PieceType::from(piece_type).static_eval();
                }
            }
        }
        result
    }

    /// Evaluate a position relative to the current player
    pub fn evaluate(&self, precomputed_mobility: Option<f32>) -> f32 {
        let color = self.color();
        let opponent_color = color.flip();

        let opponent_mobility = self.mobility(opponent_color);
        let player_mobility = match precomputed_mobility {
            Some(m) => m,
            None => self.mobility(color),
        };
        let mobility_advantage = player_mobility - opponent_mobility as f32;

        let opponent_material = self.material(opponent_color);
        let material_advantage = self.material(color) - opponent_material;

        let pawn_structure_penalty = self.pawn_structure_penalty(color) - self.pawn_structure_penalty(opponent_color);

        let king_tropism_penalty = self.king_tropism(color) - self.king_tropism(opponent_color);

        material_advantage + 0.1 * mobility_advantage - 0.5 * pawn_structure_penalty + king_tropism_penalty * opponent_material / 150.0
    }

    /// Evaluate move for move ordering, prioritizing efficient captures
    /// where low-value pieces capture high-value pieces
    fn eval_move(&self, m: Move) -> f32 {
        if m.is_tactical() {
            let [source_eval, target_eval] = [m.source, m.target]
                .map(|sq| self.piece_by_square(sq))
                .map(|p| {
                    match p {
                        Some(p) => p.static_eval(),
                        None => 0.,
                    }
                });
            return 2. * target_eval - source_eval
        }
        0.0
    }

    pub fn hash_move(&self) -> Option<Move> {
        match self.transposition_table[(self.hash % TTABLE_SIZE) as usize] {
            Some(item) => {
                if item.hash == self.hash {
                    return Some(item.best_move)
                }
                None
            }
            None => None
        }
    }

    pub fn order_moves(&self, moves: Vec<Move>) -> Vec<Move> {
        let mut moves_with_eval: Vec<(Move, f32)> = moves
            .iter()
            .map(|m| (*m, self.eval_move(*m)))
            .collect();

        moves_with_eval.sort_unstable_by(|(a, a_eval), (b, b_eval)| {
            if *a_eval == 0.0 && *b_eval == 0.0 {
                // Prioritize equal captures over non-captures
                if a.is_tactical() && !b.is_tactical() {
                    return Ordering::Less
                }
                if b.is_tactical() && !a.is_tactical() {
                    return Ordering::Greater
                }
            }
            a_eval.total_cmp(b_eval).reverse()
        });

        moves_with_eval.iter_mut().map(|(m, _)| *m).collect()
    }

    pub fn negamax_search(&mut self, mut alpha: f32, beta: f32, depth_left: u8, deadline: Instant) -> (f32, Vec<Move>) {
        let mut principal_variation = Vec::new();
        let color = self.color();

        let mut moves = self.generate_pseudolegal_moves(color);
        moves = self.order_moves(moves);

        match self.hash_move() {
            Some(mov) => moves.insert(0, mov),
            None => {},
        }

        if depth_left == 0 {
            return (self.quiscence(alpha, beta), principal_variation);
        }

        for mov in moves {
            let ep_target_before = self.ep_target.clone();
            let castling_rights_before = self.castling_rights.clone();
            let hash_before = self.hash.clone();
            let captured_piece = self.make_move(mov);

            if !self.is_king_in_check(color) {
                let (mut score, mut subtree_pv) = self.negamax_search(-beta, -alpha, depth_left - 1, deadline);
                score *= -1.;
                self.unmake_move(mov, captured_piece, ep_target_before, castling_rights_before, hash_before);

                if score >= beta {
                    self.transposition_table[(self.hash % TTABLE_SIZE) as usize] = Some(TranspositionTableItem {
                        hash: self.hash,
                        best_move: mov,
                        depth: depth_left, // TODO: should be actual depth searched
                        node_type: NodeType::Cut,
                        score,
                    });

                    return (beta, principal_variation);
                }
                if score > alpha {
                    alpha = score;
                    principal_variation = Vec::with_capacity(depth_left as usize);
                    principal_variation.push(mov);
                    principal_variation.append(&mut subtree_pv);

                    self.transposition_table[(self.hash % TTABLE_SIZE) as usize] = Some(TranspositionTableItem {
                        hash: self.hash,
                        best_move: mov,
                        depth: depth_left, // TODO: should be actual depth searched
                        node_type: NodeType::PV,
                        score,
                    });
                } else {
                    self.transposition_table[(self.hash % TTABLE_SIZE) as usize] = Some(TranspositionTableItem {
                        hash: self.hash,
                        best_move: mov,
                        depth: depth_left, // TODO: should be actual depth searched
                        node_type: NodeType::All,
                        score,
                    });
                }
            } else {
                self.unmake_move(mov, captured_piece, ep_target_before, castling_rights_before, hash_before);
            }

            // Could not finish in time, return what we have so far
            if Instant::now() > deadline {
                return (alpha, principal_variation)
            }
        }

        if principal_variation.len() == 0 {
            if self.is_king_in_check(color) {
                return (-VALUE_WIN, principal_variation);
            }
        }

        (alpha, principal_variation)
    }

    pub fn quiscence(&mut self, mut alpha: f32, beta: f32) -> f32 {
        let color = self.color();
        let mut moves = self.generate_pseudolegal_moves(color);
        moves = self.order_moves(moves);

        match self.hash_move() {
            Some(mov) => moves.insert(0, mov),
            None => {},
        }

        let stand_pat = self.evaluate(Some(moves.len() as f32));

        if stand_pat >= beta {
            return beta;
        }
        if alpha < stand_pat {
            alpha = stand_pat;
        }

        let tactical_moves = moves.iter().filter(|m| m.is_tactical());

        for mov in tactical_moves {
            let ep_target_before = self.ep_target.clone();
            let castling_rights_before = self.castling_rights.clone();
            let hash_before = self.hash.clone();
            let captured_piece = self.make_move(*mov);

            if !self.is_king_in_check(color) {
                let evaluation = -self.quiscence(-beta, -alpha);
                self.unmake_move(*mov, captured_piece, ep_target_before, castling_rights_before, hash_before);

                if evaluation >= beta {
                    return beta; // Fail-hard beta-cutoff
                }
                if evaluation > alpha {
                    alpha = evaluation;
                }
            } else {
                self.unmake_move(*mov, captured_piece, ep_target_before, castling_rights_before, hash_before);
            }
        }

        alpha
    }

    pub fn iterative_deepening(&mut self, max_depth: u8, duration: Duration) -> (f32, Vec<Move>) {
        let start = Instant::now();
        let deadline = start + duration;
        let mut result = None;
        let mut depth = 1;
        let mut alpha = -INFINITY;
        let mut beta = INFINITY;
        let window_size = 0.25;
        let mut gradual_widening_counter = 0;

        while depth <= max_depth {
            println!("\nSearching depth({}) in the window {:?}", depth, (alpha, beta));
            let search_result = self.negamax_search(alpha, beta, depth, deadline);

            if search_result.0.abs() >= VALUE_WIN {
                return search_result
            }

            if Instant::now() > deadline {
                println!("Aborting...");
                break;
            }

            println!("Finished depth({}) {:?} [{:?} left]", depth, search_result, deadline - Instant::now());

            if search_result.1.len() > 0 {
                depth += 1;
                gradual_widening_counter = 0;
                alpha = search_result.0 - window_size;
                beta = search_result.0 + window_size;
            } else if search_result.0 <= alpha { // Alpha-cutoff
                println!("Alpha cutoff {} <= {:?}", search_result.0, (alpha, beta));
                gradual_widening_counter += 1;
                beta = alpha;
                alpha = search_result.0 - window_size * 4.0f32.powi(gradual_widening_counter);
                continue;
            } else if search_result.0 >= beta { // Beta-cutoff
                println!("Beta cutoff {:?} <= {}", (alpha, beta), search_result.0);
                gradual_widening_counter += 1;
                alpha = beta;
                beta = search_result.0 + window_size * 4.0f32.powi(gradual_widening_counter);
                continue;
            } else {
                panic!("Can this ever be possible? (probably not)");
            }

            result = Some(search_result);
        }

        match result {
            Some(r) => return r,
            None => panic!("Could not find a move in time"),
        }
    }
}


#[cfg(test)]
mod tests {
    use std::time::Duration;
    use crate::{board::{Board, engine::PerftResult, Color}, square::Square, moves::{Move, MoveKind}};
    use super::VALUE_WIN;

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

        assert_eq!(board.perft(0, false), PerftResult { leaf_nodes: 1, captures: 0, en_passants: 0, castles: 0 , checks: 0 });
        assert_eq!(board.perft(1, false), PerftResult { leaf_nodes: 20, captures: 0, en_passants: 0, castles: 0 , checks: 0 });
        assert_eq!(board.perft(2, false), PerftResult { leaf_nodes: 400, captures: 0, en_passants: 0, castles: 0 , checks: 0 });
        assert_eq!(board.perft(3, false), PerftResult { leaf_nodes: 8902, captures: 34, en_passants: 0, castles: 0 , checks: 12 });
        assert_eq!(board.perft(4, false), PerftResult { leaf_nodes: 197281, captures: 1576, en_passants: 0, castles: 0 , checks: 469 });
        // assert_eq!(board.perft(5, false), PerftResult { leaf_nodes: 4865609, captures: 82719, en_passants: 258, castles: 0, checks: 27351 });
    }

    #[test]
    fn position_perft() {
        let fen = String::from("r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - ");
        let mut board = Board::from_FEN(fen);
        assert_eq!(board.perft(0, false), PerftResult { leaf_nodes: 1, captures: 0, en_passants: 0, castles: 0 , checks: 0 });
        assert_eq!(board.perft(1, false), PerftResult { leaf_nodes: 48, captures: 8, en_passants: 0, castles: 2 , checks: 0 });
        assert_eq!(board.perft(2, false), PerftResult { leaf_nodes: 2039, captures: 351, en_passants: 1, castles: 91 , checks: 3 });
        assert_eq!(board.perft(3, false), PerftResult { leaf_nodes: 97862, captures: 17102, en_passants: 45, castles: 3162, checks: 993 });
        // assert_eq!(board.perft(4, false), PerftResult { leaf_nodes: 4085603, captures: 757163, en_passants: 1929, castles: 128013, checks: 25523 });
    }

    #[test]
    fn endgame_perft() {
        let fen = String::from("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - ");
        let mut board = Board::from_FEN(fen);

        assert_eq!(board.perft(1, false), PerftResult { leaf_nodes: 14, captures: 1, en_passants: 0, castles: 0 , checks: 2 });
        assert_eq!(board.perft(2, false), PerftResult { leaf_nodes: 191, captures: 14, en_passants: 0, castles: 0 , checks: 10 });
        // assert_eq!(board.perft(3, false), PerftResult { leaf_nodes: 2182, captures: 209, en_passants: 2, castles: 0 , checks: 267 });
    }

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

    }

    #[test]
    fn checkmate() {
        let fen = String::from("2kr1b1r/pp1npppp/2p1bn2/7q/5B2/2NB1Q1P/PPP1N1P1/2KR3R w - - 0 1");
        let mut board = Board::from_FEN(fen);
        let (score, pv) = board.iterative_deepening(8, Duration::from_secs(15));

        assert_eq!(score, VALUE_WIN);
        assert_eq!(pv, vec![
            Move { source: Square::F3, target: Square::C6, kind: MoveKind::Capture },
            Move { source: Square::B7, target: Square::C6, kind: MoveKind::Capture },
            Move { source: Square::D3, target: Square::A6, kind: MoveKind::Quiet },
        ]);
    }

    mod evaluation {
        use crate::{moves::{Move, MoveKind}, square::Square};

        use super::*;

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

        #[test]
        fn king_tropism() {
            let mut board = Board::new();
            board.make_move(Move { source: Square::D1, target: Square::F5, kind: MoveKind::Quiet });
            let score = board.evaluate(None);
            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 b - - 1 27");
            let board = Board::from_FEN(fen);
            let score = board.evaluate(None);
            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 b - - 1 38");
            let board = Board::from_FEN(fen);
            let score = board.evaluate(None);
            board.print();
            println!("Score {}", score);

            assert!(score < -4.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 score = board.evaluate(None);
                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 score = board.evaluate(None);
                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 score = board.evaluate(None);
                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 score = board.evaluate(None);
                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 score = board.evaluate(None);
                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 mut board = Board::from_FEN(fen);
                board.ply += 1; // TODO: remove me when FEN parsing includes side to move
                let score = board.evaluate(None);
                board.print();
                println!("Score {}", score);
                score
            };

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

}