// jja: swiss army knife for chess file formats
// src/abkbook.rs: Interface to Arena book files (aka `abk')
//
// Copyright (c) 2023 Ali Polatel <alip@chesswob.org>
// Based in part upon ChessX's src/database/*.cpp which is:
//     Copyright (C) 2015, 2017 by Jens Nissen jens-chessx@gmx.net
//
// SPDX-License-Identifier: GPL-3.0-or-later

use std::{
    collections::{BTreeMap, HashSet, VecDeque},
    fs::File,
    io::{self, BufWriter, Error, ErrorKind, Seek, SeekFrom, Write},
    path::Path,
};

use console::style;
use indicatif::ProgressBar;
use memmap::Mmap;
use shakmaty::{
    fen::{Epd, Fen},
    san::San,
    uci::Uci,
    Chess, Color, EnPassantMode, Position,
};
use termtree::Tree;

use crate::{
    abk::{CompactSBookMoveEntry, SBookMoveEntry, ABK_ENTRY_LENGTH, ABK_INDEX_OFFSET},
    hash::{zobrist_hash, ZobristHashSet, ZobristHasherBuilder},
    tr,
};

/// `AbkBook` is a reader for the ABK format used for opening books in chess engines.
///
/// The struct contains the author, a comment, the depth of the book, the number of moves in the
/// book, the raw book data, and a list of move entries.
#[derive(Debug)]
pub struct AbkBook {
    /// The author of the opening book.
    pub author: String,
    /// A comment about the opening book.
    pub comment: String,
    /// The depth of the book, measured in ply.
    pub book_depth: u32,
    /// The total number of moves in the book.
    pub book_moves: u32,
    /// The memory-mapped data of the opening book.
    /// Note, this is `Some(Mmap)` when the `AbkBook` instance is created with `AbkBook::open`, and
    /// `None` when the `AbkBook` instance is created with `AbkBook::new` or `AbkBook::default`.
    book: Option<Mmap>,
    /// A list of move entries in the book.
    pub moves: Vec<SBookMoveEntry>,
    /// The minimum number of games for moves in the opening book.
    pub min_games: u32,
    /// The minimum number of wins for moves in the opening book.
    pub min_wins: u32,
    /// The win percentage for white in the opening book.
    pub win_percent_white: u32,
    /// The win percentage for black in the opening book.
    pub win_percent_black: u32,
    /// The priority probability used for move selection in the opening book.
    pub probability_priority: u32,
    /// The number of games probability used for move selection in the opening book.
    pub probability_games: u32,
    /// The win percentage probability used for move selection in the opening book.
    pub probability_win_percent: u32,
    /// The maximum half move depth the book can be used for.
    pub use_book_half_move: u32,
}

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

impl AbkBook {
    /// Creates a new `AbkBook` instance.
    ///
    /// Initializes an `AbkBook` with default values, such as empty author
    /// and comment strings, zero book depth, zero book moves, empty
    /// book and moves vectors, zero minimum games, zero minimum wins,
    /// and zero win percentages for both white and black players.
    ///
    /// # Returns
    ///
    /// * `AbkBook` - A new `AbkBook` instance with default values.
    pub fn new() -> Self {
        AbkBook {
            author: String::new(),
            comment: String::new(),
            book_depth: 0,
            book_moves: 0,
            book: None,
            moves: Vec::new(),
            min_games: 0,
            min_wins: 0,
            win_percent_white: 0,
            win_percent_black: 0,
            probability_priority: 0,
            probability_games: 0,
            probability_win_percent: 0,
            use_book_half_move: 0,
        }
    }

    /// Reads an ABK file and populates the fields of the `AbkBook` instance.
    ///
    /// # Arguments
    ///
    /// * `file_name` - A file path reference to the ABK file to be read.
    ///
    /// # Errors
    ///
    /// Returns an error if the file cannot be opened or read.
    pub fn open<P: Copy + std::fmt::Display + AsRef<Path>>(file_name: P) -> Result<Self, Error> {
        let file = File::open(file_name)?;
        // SAFETY: Mmap::map is unsafe because it involves file I/O which might lead to data races
        // if the underlying file is modified while the memory map is active. Here, it's safe
        // because we assume that the ABK files are not concurrently modified while they're
        // memory-mapped.
        let book = unsafe { Mmap::map(&file)? };

        let header = &book[0..254];

        // Read the magic number
        let magic_number = &header[0..4];
        if magic_number != b"\x03ABK" && magic_number != b"\x03AB2" {
            eprintln!("{}", tr!("Invalid magic number `{}' in ABK header in file `{}', expected `[0x03, 0x41, 0x42, 0x4B]'.",
                format!("{:?}", magic_number), file_name));
            return Err(Error::from(ErrorKind::InvalidInput));
        }

        let comment_len = header[12] as usize;
        let author_len = header[133] as usize;

        let comment_end = std::cmp::min(13usize.saturating_add(comment_len), header.len());
        let author_end = std::cmp::min(134usize.saturating_add(author_len), header.len());

        let comment = String::from_utf8_lossy(&header[13..comment_end]).to_string();
        let author = String::from_utf8_lossy(&header[134..author_end]).to_string();

        let book_depth = u32::from_le_bytes(header[214..218].try_into().unwrap());
        let book_moves = u32::from_le_bytes(header[218..222].try_into().unwrap());

        let min_games = u32::from_le_bytes(header[222..226].try_into().unwrap());
        let min_wins = u32::from_le_bytes(header[226..230].try_into().unwrap());
        let win_percent_white = u32::from_le_bytes(header[230..234].try_into().unwrap());
        let win_percent_black = u32::from_le_bytes(header[234..238].try_into().unwrap());
        let probability_priority = u32::from_le_bytes(header[238..242].try_into().unwrap());
        let probability_games = u32::from_le_bytes(header[242..246].try_into().unwrap());
        let probability_win_percent = u32::from_le_bytes(header[246..250].try_into().unwrap());
        let use_book_half_move = u32::from_le_bytes(header[250..254].try_into().unwrap());

        let moves = Vec::new();

        Ok(Self {
            moves,
            book: Some(book),
            comment,
            author,
            book_depth,
            book_moves,
            min_games,
            min_wins,
            win_percent_white,
            win_percent_black,
            probability_priority,
            probability_games,
            probability_win_percent,
            use_book_half_move,
        })
    }

    /// Writes the `AbkBook` instance to an ABK file.
    ///
    /// # Arguments
    ///
    /// * `file` - A mutable reference to a `BufWriter` instance.
    /// * `progress_bar` - An optional progress bar to report progress.
    ///
    /// # Errors
    ///
    /// Returns an error if the file cannot be created or written to.
    pub fn write_file<W: Seek + Write>(
        &mut self,
        file: &mut BufWriter<W>,
        progress_bar: Option<&ProgressBar>,
    ) -> io::Result<()> {
        self.write_headers(file)?;

        // Seek to the root position of the moves.
        file.seek(SeekFrom::Start(
            (ABK_INDEX_OFFSET as u64).saturating_mul(ABK_ENTRY_LENGTH as u64),
        ))?;

        // Write the move entries
        for entry in &self.moves {
            file.write_all(&entry.from.to_le_bytes())?;
            file.write_all(&entry.to.to_le_bytes())?;
            file.write_all(&entry.promotion.to_le_bytes())?;
            file.write_all(&entry.priority.to_le_bytes())?;

            // Convert each u32 into little-endian bytes and write them to the file
            file.write_all(&entry.ngames.to_le_bytes())?;
            file.write_all(&entry.nwon.to_le_bytes())?;
            file.write_all(&entry.nlost.to_le_bytes())?;
            file.write_all(&entry.flags.to_le_bytes())?;
            file.write_all(&entry.next_move.to_le_bytes())?;
            file.write_all(&entry.next_sibling.to_le_bytes())?;

            if let Some(progress_bar) = progress_bar {
                progress_bar.inc(1);
            }
        }

        Ok(())
    }

    /// Writes the header of an ABK file.
    ///
    /// The header contains metadata about the opening book, such as the author, comment, and various
    /// statistical information.
    ///
    /// # Arguments
    ///
    /// * `file` - A mutable reference to a `BufWriter` instance.
    ///
    /// # Errors
    ///
    /// Returns an error if the header cannot be written to the file.
    fn write_headers<W: Write>(&self, file: &mut BufWriter<W>) -> io::Result<()> {
        let mut header = vec![0u8; 254];

        let author_bytes = self.author.as_bytes();
        let comment_bytes = self.comment.as_bytes();

        // Write the magic number
        header[0..4].copy_from_slice(b"\x03ABK");

        header[12] = comment_bytes.len().try_into().unwrap_or(u8::MAX);
        header[133] = author_bytes.len().try_into().unwrap_or(u8::MAX);

        let comment_end = std::cmp::min(13usize.saturating_add(comment_bytes.len()), header.len());
        let author_end = std::cmp::min(134usize.saturating_add(author_bytes.len()), header.len());

        header[13..comment_end].copy_from_slice(comment_bytes);
        header[134..author_end].copy_from_slice(author_bytes);

        header[214..218].copy_from_slice(&self.book_depth.to_le_bytes());
        header[218..222].copy_from_slice(&self.book_moves.to_le_bytes());
        header[222..226].copy_from_slice(&self.min_games.to_le_bytes());
        header[226..230].copy_from_slice(&self.min_wins.to_le_bytes());
        header[230..234].copy_from_slice(&self.win_percent_white.to_le_bytes());
        header[234..238].copy_from_slice(&self.win_percent_black.to_le_bytes());
        header[238..242].copy_from_slice(&self.probability_priority.to_le_bytes());
        header[242..246].copy_from_slice(&self.probability_games.to_le_bytes());
        header[246..250].copy_from_slice(&self.probability_win_percent.to_le_bytes());
        header[250..254].copy_from_slice(&self.use_book_half_move.to_le_bytes());

        file.write_all(&header)?;
        Ok(())
    }

    /// Returns the total number of entries in the ABK opening book.
    ///
    /// # Returns
    ///
    /// A `usize` containing the number of entries.
    /// Note, this function returns zero if the ABK opening book file is not memory mapped.
    pub fn total_entries(&self) -> usize {
        if let Some(book) = &self.book {
            let len = book.len();
            if len <= ABK_INDEX_OFFSET {
                0
            } else {
                (len - ABK_INDEX_OFFSET) / ABK_ENTRY_LENGTH
            }
        } else {
            0
        }
    }

    /// Looks up moves from the book for a given position.
    ///
    /// # Arguments
    ///
    /// * `position` - A reference to a `Chess` position.
    /// * `progress_bar` - An optional progress bar to report progress.
    ///
    /// # Returns
    ///
    /// Returns an `Option` containing a `Vec` of `SBookMoveEntry` for the given position, or
    /// `None` if no moves are found.
    pub fn lookup_moves(
        &self,
        position: &Chess,
        progress_bar: Option<&ProgressBar>,
    ) -> Option<Vec<SBookMoveEntry>> {
        let book = self
            .book
            .as_ref()
            .expect(&tr!("Opening book file is not memory mapped."));
        let key = zobrist_hash(position);
        let blen = book.len();
        let mut stack = vec![(Chess::default(), ABK_INDEX_OFFSET)];
        let mut ret: Vec<SBookMoveEntry> = Vec::new();

        while let Some((board, current_index)) = stack.pop() {
            // Iterate through each move and traverse its next moves
            let moves = self.get_moves(Some(current_index), progress_bar);
            let mut new_moves = Vec::new();

            for book_move in &moves {
                let mut new_board = board.clone();
                let uci = Uci::from(*book_move);
                match uci.to_move(&new_board) {
                    Ok(move_) => {
                        new_board.play_unchecked(&move_);
                        new_moves.push(*book_move);
                    }
                    Err(_ /*err*/) => {
                        /*
                        let epd = format!(
                            "{}",
                            shakmaty::fen::Epd::from_position(
                                new_board,
                                shakmaty::EnPassantMode::PseudoLegal
                            )
                        );
                        eprintln!(
                            "Error making move key:{:#x} uci:{} in epd:`{}', ent:{:?}, moves:{:?}: {}",
                            key, uci, epd, book_move, self.moves, err
                        );
                        */
                        continue;
                    }
                };

                if book_move.next_move != 0
                    && (book_move.next_move as usize)
                        .saturating_mul(ABK_ENTRY_LENGTH)
                        .saturating_add(ABK_ENTRY_LENGTH)
                        <= blen
                {
                    let next_move_pointer = book_move.next_move as usize;
                    stack.push((new_board, next_move_pointer));
                }
            }

            if key == zobrist_hash(&board) {
                for move_ in new_moves {
                    if move_.is_deleted() {
                        continue;
                    }
                    if let Some(existing_move) = ret.iter_mut().find(|e| e == &&move_) {
                        existing_move.merge(move_);
                    } else {
                        ret.push(move_);
                    }
                }
                break;
            }
        }

        if ret.is_empty() {
            None
        } else {
            Some(ret)
        }
    }

    /// Writes all possible games contained in an Arena opening book to a PGN file.
    ///
    /// This function traverses the Arena book, which is a type of opening book, and writes all
    /// possible games to the output file in PGN format. A game is considered "possible" if it
    /// follows a path of moves in the book from the given starting position to a position with no
    /// more book moves. Each game is written as a separate round, and the rounds are numbered
    /// consecutively starting from 1.
    ///
    /// The `output` argument is a mutable reference to a `Write` trait object where the generated PGN will be written.
    /// The `event`, `site`, `date`, `white`, `black`, and `result` arguments are used to fill in the corresponding PGN tags for each game.
    /// The `max_ply` argument determines the limit of variation depth in plies.
    /// The `progress_bar` is an optional reference to a progress bar to report progress.
    ///
    /// # Errors
    ///
    /// This function will panic if writing to the output file fails.
    ///
    /// # Panics
    ///
    /// Panics if the disk is full or the file isn't writable.
    #[allow(clippy::too_many_arguments)]
    pub fn write_pgn(
        &mut self,
        output: &mut dyn std::io::Write,
        position: &Chess,
        event: &str,
        site: &str,
        date: &str,
        white: &str,
        black: &str,
        result: &str,
        max_ply: usize,
        progress_bar: Option<&ProgressBar>,
    ) {
        let fen_header: String;
        let fen = if *position == Chess::default() {
            None
        } else {
            fen_header = Fen::from_position(position.clone(), EnPassantMode::Legal).to_string();
            Some(&fen_header)
        };
        let move_map = self.traverse_book(progress_bar);

        if let Some(progress_bar) = progress_bar {
            progress_bar.set_message(tr!("Writing:"));
            progress_bar.set_length(0);
            progress_bar.set_position(0);
        }
        Self::_write_pgn(
            output,
            position,
            &HashSet::with_hasher(ZobristHasherBuilder),
            &move_map,
            &mut Vec::new(),
            fen,
            &mut 1,
            event,
            site,
            date,
            white,
            black,
            result,
            max_ply,
            position.turn(),
            progress_bar,
        );
        if let Some(progress_bar) = progress_bar {
            progress_bar.set_message(tr!("Writing done."));
        }
    }

    #[allow(clippy::too_many_arguments)]
    fn _write_pgn(
        output: &mut dyn std::io::Write,
        position: &Chess,
        position_set: &ZobristHashSet,
        move_map: &BTreeMap<u64, Vec<SBookMoveEntry>>,
        move_history: &mut Vec<San>,
        fen: Option<&String>,
        round: &mut usize,
        event: &str,
        site: &str,
        date: &str,
        white: &str,
        black: &str,
        result: &str,
        max_ply: usize,
        initial_color: Color,
        progress_bar: Option<&ProgressBar>,
    ) {
        // Return if the maximum ply is reached
        if move_history.len() >= max_ply {
            return;
        }

        // Each recursive call gets a localized copy of visited positions, preventing global skips.
        // TODO: This is a relatively memory-intensive operation but does the right thing.
        let mut position_set = position_set.clone();

        if let Some(entries) = move_map.get(&zobrist_hash(position)) {
            let mut entries = entries.clone();
            entries.sort_unstable_by_key(|entry| {
                std::cmp::Reverse((entry.priority, entry.nwon, entry.ngames))
            });

            for entry in entries {
                let mov = match Uci::from(entry).to_move(position) {
                    Ok(mov) => mov,
                    Err(_) => continue, // TODO: warn about illegal move?
                };
                let san = San::from_move(position, &mov);
                move_history.push(san);
                let mut new_position = position.clone();
                new_position.play_unchecked(&mov);

                // If the new position has been seen before, skip it to avoid infinite recursion.
                let hash = zobrist_hash(&new_position);
                if !position_set.insert(hash) {
                    // Insert returned false, the set already contained this value.
                    move_history.pop();
                    continue;
                }

                // Recursively generate all games starting from the new position.
                Self::_write_pgn(
                    output,
                    &new_position,
                    &position_set,
                    move_map,
                    move_history,
                    fen,
                    round,
                    event,
                    site,
                    date,
                    white,
                    black,
                    result,
                    max_ply,
                    initial_color,
                    progress_bar,
                );

                // Undo the move and remove it from the move history.
                move_history.pop();
            }
        } else {
            // This is a leaf node.
            if !move_history.is_empty() {
                let opening = move_history
                    .iter()
                    .enumerate()
                    .map(|(i, san)| {
                        let move_number = i / 2 + 1;
                        let move_text = san.to_string();
                        match (initial_color, i, i % 2) {
                            (Color::White, _, 0) => format!("{}. {} ", move_number, move_text),
                            (Color::Black, 0, 0) => format!("{}... {} ", move_number, move_text),
                            (Color::Black, _, 1) => format!("{}. {} ", move_number + 1, move_text),
                            _ => format!("{} ", move_text),
                        }
                    })
                    .collect::<String>();

                let fen_header = if let Some(fen) = fen {
                    format!("[FEN \"{}\"]\n[Setup \"1\"]\n", fen)
                } else {
                    String::new()
                };

                writeln!(
                    output,
                    "[Event \"{}\"]\n\
                    [Site \"{}\"]\n\
                    [Date \"{}\"]\n\
                    [Round \"{}\"]\n\
                    [White \"{}\"]\n\
                    [Black \"{}\"]\n\
                    [Result \"{}\"]\n{}\
                    [Annotator \"{} v{}\"]",
                    event,
                    site,
                    date,
                    round,
                    white,
                    black,
                    result,
                    fen_header,
                    crate::built_info::PKG_NAME,
                    crate::built_info::PKG_VERSION,
                )
                .expect("write output PGN");

                writeln!(output, "\n{} {}\n", opening.trim(), result).expect("write output PGN");
                *round += 1;
                if let Some(progress_bar) = progress_bar {
                    progress_bar.inc(1);
                }
            }
        }
    }

    /// Constructs a tree from the `AbkBook` instance, starting from the
    /// given position and up to the specified `max_ply`.
    ///
    /// # Arguments
    ///
    /// * `position: &Chess` - The starting position for the tree.
    /// * `max_ply: u16` - The maximum depth of the tree.
    /// * `progress_bar: Option<&ProgressBar>` - An optional progress bar to report progress.
    ///
    /// # Returns
    ///
    /// * A `Tree<String>` containing a tree of moves.
    pub fn tree(
        &self,
        position: &Chess,
        max_ply: u16,
        progress_bar: Option<&ProgressBar>,
    ) -> Tree<String> {
        fn build_tree(
            book: &AbkBook,
            position: &Chess,
            parent: &mut Tree<String>,
            ply: u16,
            max_ply: u16,
            visited_keys: &HashSet<u64>,
            progress_bar: Option<&ProgressBar>,
        ) {
            if ply >= max_ply {
                return;
            }

            let mut book_entries = match book.lookup_moves(position, progress_bar) {
                Some(moves) => moves,
                None => return,
            };
            book_entries.sort_unstable_by_key(|mov| {
                std::cmp::Reverse((mov.priority, mov.nwon, mov.ngames))
            });

            for entry in book_entries {
                //TODO: Better error handling, too many unwrap.
                let m = Uci::from(entry).to_move(position).unwrap();
                let mut new_position = position.clone();
                new_position.play_unchecked(&m);

                let key = zobrist_hash(&new_position);
                if visited_keys.contains(&key) {
                    continue;
                }

                let mut new_visited_keys = visited_keys.clone(); // Clone visited_keys
                new_visited_keys.insert(key);

                let mut new_tree = Tree::new(San::from_move(position, &m).to_string());
                build_tree(
                    book,
                    &new_position,
                    &mut new_tree,
                    ply + 1,
                    max_ply,
                    &new_visited_keys,
                    progress_bar,
                );

                parent.push(new_tree);
            }
        }

        let epd = format!(
            "{}",
            Epd::from_position(position.clone(), EnPassantMode::PseudoLegal)
        );
        let mut root_tree = Tree::new(epd);

        let key = zobrist_hash(position);
        let mut visited_keys = HashSet::new();
        visited_keys.insert(key);

        build_tree(
            self,
            position,
            &mut root_tree,
            0,
            max_ply,
            &visited_keys,
            progress_bar,
        );

        root_tree
    }

    /// Traverses the book and merges the moves in the `AbkBook` instance.
    ///
    /// # Arguments
    ///
    /// * `pos: &Chess` - The starting position for the traversal.
    /// * `moves: &mut BTreeMap<u64, Vec<CompactSBookMoveEntry>>` - The btree map of moves to merge.
    /// * `progress_bar: Option<&ProgressBar>` - An optional progress bar to report progress.
    pub fn traverse_book_and_merge(
        &mut self,
        pos: &Chess,
        moves: &mut BTreeMap<u64, Vec<CompactSBookMoveEntry>>,
        progress_bar: Option<&ProgressBar>,
    ) {
        self.book_depth = 0;
        // Clear, and reserve the total space once to avoid frequent reallocations.
        self.moves.clear();
        self.moves
            .reserve(moves.values().map(|vec| vec.len()).sum());
        let initial_zobrist = zobrist_hash(pos);
        self.traverse_book_and_merge_helper(pos, 0, moves, initial_zobrist, progress_bar);
        self.book_moves = self.moves.len().try_into().unwrap_or(u32::MAX);
    }

    fn traverse_book_and_merge_helper(
        &mut self,
        pos: &Chess,
        parent_idx: usize,
        moves: &mut BTreeMap<u64, Vec<CompactSBookMoveEntry>>,
        key: u64,
        progress_bar: Option<&ProgressBar>,
    ) {
        let mut queue = VecDeque::new();
        queue.push_back((pos.clone(), parent_idx, key, 0));

        /* Calculate statistics for the ABK header */
        self.min_games = u32::MAX;
        self.min_wins = u32::MAX;
        let mut total_white_wins = 0;
        let mut total_black_wins = 0;
        let mut total_games = 0;

        // Here we assume entries are sorted to avoid repetitive sorting.
        while let Some((pos, parent_idx, key, depth)) = queue.pop_front() {
            if let Some(entries) = moves.remove(&key) {
                let first_index = self.moves.len();
                let last_index = first_index + entries.len() - 1;

                for (idx, entry) in entries.into_iter().enumerate() {
                    let current_index = first_index + idx;
                    let uci = Uci::from(entry);
                    let chess_move = match uci.to_move(&pos) {
                        Ok(mov) => mov,
                        Err(_ /*err*/) => {
                            /*
                            let epd = format!(
                                "{}",
                                shakmaty::fen::Epd::from_position(
                                    pos.clone(),
                                    shakmaty::EnPassantMode::PseudoLegal
                                )
                            );
                            eprintln!(
                                "Skipping illegal move with fen:{} uci:{} ent:{:?} err:{:?}",
                                epd, uci, entry, err
                            );
                            */
                            continue; // Illegal move. Skip it.
                        }
                    };

                    let mut entry = SBookMoveEntry {
                        from: entry.from,
                        to: entry.to,
                        promotion: entry.promotion,
                        priority: entry.priority,
                        ngames: entry.ngames,
                        nwon: entry.nwon,
                        nlost: entry.nlost,
                        flags: 0,
                        next_move: 0,
                        next_sibling: 0,
                    };

                    // Set next_sibling
                    entry.next_sibling = if idx < last_index - first_index {
                        (current_index + 1 + ABK_INDEX_OFFSET)
                            .try_into()
                            .unwrap_or(u32::MAX)
                    } else {
                        u32::MAX
                    };

                    // Set next move
                    entry.next_move = u32::MAX;
                    if parent_idx >= ABK_INDEX_OFFSET {
                        self.moves[parent_idx - ABK_INDEX_OFFSET].next_move = (first_index
                            + ABK_INDEX_OFFSET)
                            .try_into()
                            .unwrap_or(u32::MAX)
                    }

                    self.moves.push(entry);
                    if let Some(progress_bar) = progress_bar {
                        progress_bar.inc(1);
                    }

                    // Update move statistics.
                    self.min_games = self.min_games.min(entry.ngames);
                    self.min_wins = self.min_wins.min(entry.nwon);
                    total_white_wins += entry.nwon;
                    total_black_wins += entry.nlost;
                    total_games += entry.ngames;

                    // Update max_depth based on the current depth
                    let current_depth = depth + 1;
                    if current_depth > self.book_depth {
                        self.book_depth = current_depth;
                    }

                    let mut new_pos = pos.clone();
                    new_pos.play_unchecked(&chess_move);
                    let new_key = zobrist_hash(&new_pos);
                    queue.push_back((
                        new_pos,
                        current_index + ABK_INDEX_OFFSET,
                        new_key,
                        current_depth,
                    ));
                }
            }
        }

        /* Calculate and update ABK header statistics */
        self.win_percent_white = if total_games > 0 {
            // SAFETY: Percentage cannot be NaN or infinite, and we ensure the value is within u32 range.
            unsafe {
                ((f64::from(total_white_wins) / f64::from(total_games) * 100.0)
                    .max(0.0)
                    .min(f64::from(u32::MAX)))
                .round()
                .trunc()
                .to_int_unchecked()
            }
        } else {
            0
        };
        self.win_percent_black = if total_games > 0 {
            // SAFETY: Percentage cannot be NaN or infinite, and we ensure the value is within u32 range.
            unsafe {
                ((f64::from(total_black_wins) / f64::from(total_games) * 100.0)
                    .max(0.0)
                    .min(f64::from(u32::MAX)))
                .round()
                .trunc()
                .to_int_unchecked()
            }
        } else {
            0
        };

        if !moves.is_empty() {
            if let Some(progress_bar) = progress_bar {
                progress_bar.println(tr!(
                    "Failed to traverse some positions, {} positions unreachable!",
                    style(moves.len().to_string()).bold().cyan()
                ));
            }
        }
    }

    /// Traverses the book and builds a `HashMap` with the Zobrist hash of the position as the key
    /// and the corresponding moves as the value.
    ///
    /// # Arguments
    ///
    /// - `progress_bar: Option<&ProgressBar>`: An optional progress bar to report progress.
    ///
    /// # Returns
    ///
    /// Returns a `BTreeMap` containing the Zobrist hash of positions and their corresponding moves.
    pub fn traverse_book(
        &mut self,
        progress_bar: Option<&ProgressBar>,
    ) -> BTreeMap<u64, Vec<SBookMoveEntry>> {
        let book = self
            .book
            .as_ref()
            .expect(&tr!("Opening book file is not memory mapped."));
        let mut opening_book: BTreeMap<u64, Vec<SBookMoveEntry>> = BTreeMap::new();

        let initial_position = Chess::default();
        let initial_next_move_pointer = Some(ABK_INDEX_OFFSET); // Start from the first move

        let mut stack = vec![(initial_position, initial_next_move_pointer)];

        if let Some(progress_bar) = progress_bar {
            progress_bar.println(tr!(
                "Traversing the input opening book. This may take a while."
            ));
            progress_bar.set_message(tr!("Traversing:"));
        }

        while let Some((position, next_move_pointer)) = stack.pop() {
            // Get the moves for the current position
            let moves = if let Some(ptr) = next_move_pointer {
                self.get_moves(Some(ptr), progress_bar)
            } else {
                continue;
            };

            let key = zobrist_hash(&position);

            for book_move in moves {
                let mut new_position = position.clone();
                let uci = Uci::from(book_move);
                match uci.to_move(&new_position) {
                    Ok(move_) => {
                        new_position.play_unchecked(&move_);
                    }
                    Err(err) => {
                        let epd = format!(
                            "{}",
                            shakmaty::fen::Epd::from_position(
                                new_position,
                                shakmaty::EnPassantMode::PseudoLegal
                            )
                        );
                        eprintln!(
                            "{}",
                            tr!(
                                "Error making move key:{} uci:{} with entry:{} in epd:{}: {}",
                                format!("{:#x}", key),
                                uci,
                                format!("{:?}", book_move),
                                epd,
                                err
                            )
                        );
                        continue;
                    }
                };
                let next_move_pointer = if book_move.next_move != 0
                    && (book_move.next_move as usize).saturating_mul(ABK_ENTRY_LENGTH) < book.len()
                {
                    Some(book_move.next_move as usize)
                } else {
                    None
                };

                opening_book
                    .entry(key)
                    .and_modify(|book_moves| {
                        if !book_move.is_deleted() {
                            match book_moves
                                .iter_mut()
                                .find(|existing_move| existing_move == &&book_move)
                            {
                                Some(existing_move) => {
                                    existing_move.merge(book_move);
                                }
                                None => {
                                    book_moves.push(book_move);
                                }
                            }
                        }
                    })
                    .or_insert_with(|| vec![book_move]);

                stack.push((new_position, next_move_pointer));
            }
        }

        if let Some(progress_bar) = progress_bar {
            progress_bar.set_message(tr!("Traversal done."));
            progress_bar.println(tr!(
                "Found {} positions in the input opening book.",
                style(opening_book.len()).bold().cyan()
            ));
        }

        opening_book
    }

    fn get_moves(
        &self,
        next_move_pointer: Option<usize>,
        progress_bar: Option<&ProgressBar>,
    ) -> Vec<SBookMoveEntry> {
        // SAFETY: This is an internal function and all the public functions which call this
        // function panic if the book is not memory mapped yet. Hence at this point
        // self.book is guaranteed to be `Some`.
        let book = unsafe { self.book.as_ref().unwrap_unchecked() };
        let pointer = next_move_pointer.unwrap_or(ABK_INDEX_OFFSET);
        let mut moves = Vec::new();

        let mut current_pointer = pointer;
        while current_pointer != 0
            && current_pointer
                .saturating_mul(ABK_ENTRY_LENGTH)
                .saturating_add(ABK_ENTRY_LENGTH)
                <= book.len()
        {
            let move_bytes = &book
                [current_pointer * ABK_ENTRY_LENGTH..(current_pointer + 1) * ABK_ENTRY_LENGTH]
                .to_vec();

            let book_move = self.get_moves_helper(move_bytes);

            current_pointer = book_move.next_sibling.try_into().unwrap_or(usize::MAX);

            moves.push(book_move);
            if let Some(progress_bar) = progress_bar {
                progress_bar.inc(1);
            }
        }

        //self.adjust_book_moves();

        moves
    }

    fn get_moves_helper(&self, move_bytes: &[u8]) -> SBookMoveEntry {
        let priority = move_bytes[3];
        let ngames = u32::from_le_bytes(move_bytes[4..8].try_into().unwrap());
        let nwon = u32::from_le_bytes(move_bytes[8..12].try_into().unwrap());
        let nlost = u32::from_le_bytes(move_bytes[12..16].try_into().unwrap());
        let flags = u32::from_le_bytes(move_bytes[16..20].try_into().unwrap());
        let next_move = u32::from_le_bytes(move_bytes[20..24].try_into().unwrap());
        let next_sibling = u32::from_le_bytes(move_bytes[24..28].try_into().unwrap());
        SBookMoveEntry {
            from: move_bytes[0],
            to: move_bytes[1],
            // 0 none, +-1 rook, +-2 knight, +-3 bishop, +-4 queen
            promotion: match i16::from(move_bytes[2]) {
                128..=132 | 380..=383 => (i16::from(move_bytes[2]) - 128).try_into().unwrap_or(0),
                _ => 0,
            },
            priority,
            ngames,
            nwon,
            nlost,
            flags,
            next_move,
            next_sibling,
        }
    }

    /*
    fn adjust_book_moves(&mut self) {
        let max = self.moves.iter().map(|m| m.ngames).max().unwrap_or(0);

        for book_move in &mut self.moves {
            if book_move.priority > 0 && book_move.ngames > 0 {
                let factor = (book_move.ngames as f64) / (max as f64);
                let factor = if factor <= 0.01 && factor > 0.001 {
                    0.05
                } else {
                    factor
                };
                book_move.priority = (book_move.priority as f64 * factor).round() as u8;
            }
        }
    }
    */

    /// Sorts the given slice of `SBookMoveEntry` instances based on their
    /// priority and the weighted score.
    ///
    /// # Arguments
    ///
    /// * `entries: &mut [SBookMoveEntry]` - The mutable slice of
    /// `SBookMoveEntry` instances to be sorted.
    pub fn sort_entries(&self, entries: &mut [CompactSBookMoveEntry]) {
        entries.sort_unstable_by_key(|entry| {
            std::cmp::Reverse((
                entry.priority,
                entry.nwon * 2
                    + entry
                        .ngames
                        .saturating_sub(entry.nwon)
                        .saturating_sub(entry.nlost),
            ))
        });
    }

    /*
        let mut sum_priority = 0;
        let mut sum_ngames = 0;
        let mut sum_scores = 0.0;
        let mut weight_scores = Vec::new();
        let mut weights = Vec::new();

        for m in moves.iter() {
            sum_priority += m.priority;
            sum_ngames += m.ngames;
            let ndraw = m.ngames - m.nwon - m.nlost;
            let score = if m.ngames == 0 {
                0.0
            } else {
                (m.nwon as f64 + ndraw as f64 * 0.5) / m.ngames as f64
            };
            sum_scores += score;
            weight_scores.push(score);
        }

        for (idx, m) in moves.iter().enumerate() {
            let wp: f64 = if sum_priority > 0 {
                m.priority as f64 / sum_priority as f64
            } else {
                0.0
            };
            let wn: f64 = if sum_ngames > 0 {
                m.ngames as f64 / sum_ngames as f64
            } else {
                0.0
            };
            let ws: f64 = if sum_scores > 0.0 {
                weight_scores[idx] / sum_scores
            } else {
                0.0
            };
            let a: f64 = 0.624;
            let w: f64 = wp * (a * self.probability_priority as f64).exp()
                + wn * (a * self.probability_games as f64).exp()
                + ws * (a * self.probability_win_percent as f64) * 1.4;
            weights.push(self.scale_weight(w));
        }

        let mut moves_with_indices: Vec<(usize, &SBookMoveEntry)> =
            moves.iter().enumerate().collect();
        moves_with_indices.sort_unstable_by(|(a_idx, _a), (b_idx, _b)| {
            weights[*b_idx]
                .partial_cmp(&weights[*a_idx])
                .unwrap_or(std::cmp::Ordering::Equal)
        });

        let sorted_moves: Vec<SBookMoveEntry> = moves_with_indices
            .into_iter()
            .map(|(_, move_)| *move_)
            .collect();
        *moves = sorted_moves;
    }

    fn scale_weight(&self, weight: f64) -> f64 {
        let e = match self.probability_win_percent {
            0 => return 0.0,
            1 => 0.66,
            2 => 0.86,
            _ => 1.0 + (self.probability_win_percent as f64 - 3.0) / 6.0,
        };
        weight.powf(e)
    }
    */
}
