//
// jja: swiss army knife for chess file formats
// src/ctgdatabase.rs: CTG book file interface
//
// Copyright (c) 2023 Ali Polatel <alip@chesswob.org>
// Based in part upon remoteglot which is:
//     Copyright 2007 Steinar H. Gunderson <steinar+remoteglot@gunderson.no>
//     Licensed under the GNU General Public License, version 2.
//
// SPDX-License-Identifier: GPL-3.0-or-later

use std::{
    cmp::Ordering,
    collections::{btree_map::Entry, BTreeMap, HashSet},
    io::{Error, Read},
    path::Path,
};

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

use crate::{
    abk::{ColorPriority, CompactSBookMoveEntry, NagPriority},
    ctg::*,
    hash::{zobrist_hash, ZobristHashSet, ZobristHasherBuilder},
    polyglot::{
        from_uci, is_king_on_start_square, to_move, ColorWeight, CompactBookEntry, NagWeight,
    },
    tr,
};

/// A structure representing a Chessbase CTG entry with data on the move and game statistics.
#[derive(Debug, Clone)]
pub struct CtgEntry {
    /// The move in UCI notation.
    pub uci: Uci,
    /// The number of games won.
    pub win: u32,
    /// The number of games drawn.
    pub draw: u32,
    /// The number of games lost.
    pub loss: u32,
    /// NAG (Numeric Annotation Glyph) for the move, if available.
    pub nag: Option<Nag>,
    // A comment for the move, if available.
    // pub comment: Option<String>,
    /// The recommendation value for the move.
    pub recommendation: u8,
    /// The number of games used to calculate the average rating.
    pub avg_rating_games: u32,
    /// The total score used to calculate the average rating.
    pub avg_rating_score: u32,
    /// The number of games used to calculate the performance rating.
    pub perf_rating_games: u32,
    /// The total score used to calculate the performance rating.
    pub perf_rating_score: u32,
}

/// A structure representing a Chessbase CTG opening book with associated files and metadata.
pub struct CtgBook {
    /* ctg */
    ctg_file: Mmap,
    cto_file: Mmap,
    page_bounds: PageBounds,
    /* position encoder */
    position: [i8; 32],
    pos_len: u8,
    bits_left: u8,
}

impl CtgEntry {
    /// Calculate the average rating for the move.
    ///
    /// Returns a f64 representing the average rating, or 0.0 if there are no games.
    pub fn avg(&self) -> f64 {
        if self.avg_rating_games == 0 {
            0.0
        } else {
            f64::from(self.avg_rating_score) / f64::from(self.avg_rating_games)
        }
    }

    /// Calculate the performance rating for the move.
    ///
    /// Returns a f64 representing the performance rating, or 0.0 if there are no games.
    pub fn perf(&self) -> f64 {
        if self.perf_rating_games == 0 {
            0.0
        } else {
            f64::from(self.perf_rating_score) / f64::from(self.perf_rating_games)
        }
    }

    /// Calculate the colored recommendation of the move.
    ///
    /// Returns `0` for no-color, `1` for green, `2` for blue, `3` for red.
    pub fn color(&self) -> u8 {
        match self.recommendation {
            0xa4 => 1, /* green */
            0x80 => {
                if self.nag.map(|nag| nag.is_question_mark()).unwrap_or(false) {
                    2 /* blue */
                } else {
                    1 /* green */
                }
            }
            0x40 => 3, /* red */
            _ => 0,    /* no color */
        }
    }
}

/// Generate a display string for the move including annotations and colored recommendations.
///
/// `position`: A reference to the current `Chess` position.
/// `entry`: A reference to the `CtgEntry` with move information.
/// `move_`: A reference to the `Move` being displayed.
///
/// Returns a `String` with the move in SAN notation, including NAGs and colored recommendations.
pub fn display_move(position: &Chess, entry: &CtgEntry, move_: &Move) -> String {
    let mut san = San::from_move(position, move_).to_string();
    if let Some(nag) = &entry.nag {
        san.push_str(&nag.to_string());
    }

    match entry.color() {
        0 /* no colour */ => san,
        1 /* green */ => style(san).green().to_string(),
        2 /* blue */ => style(san).blue().to_string(),
        3 /* red */ => style(san).red().to_string(),
        _ => unreachable!()
    }
}

/// Sorts a list of `CtgEntry` moves according to the following criteria:
///
/// 1. Moves with NAG "!!" are prioritized.
/// 2. Moves with recommendation 0xa4 or 0x80 (without "?") are prioritized.
/// 3. Moves with recommendation 0x80 are prioritized.
/// 4. If no NAGs, no colors, and points are equal, prioritize moves with higher performance.
///    Points are calculated as (2*wins)+draws.
///
/// # Arguments
///
/// * `moves` - A mutable slice of `CtgEntry` that needs to be sorted.
///
pub fn sort_ctg_moves(moves: &mut [CtgEntry]) {
    moves.sort_by(|a, b| {
        let a_is_hard = a.nag.map(|nag| nag == Nag::Hard).unwrap_or(false);
        let b_is_hard = b.nag.map(|nag| nag == Nag::Hard).unwrap_or(false);

        let a_color = if a.recommendation == 0xa4
            || (a.recommendation == 0x80
                && !a.nag.map(|nag| nag.is_question_mark()).unwrap_or(false))
        {
            2
        } else if a_is_hard {
            1
        } else if a.recommendation == 0x80 {
            0
        } else {
            -1
        };

        let b_color = if b.recommendation == 0xa4
            || (b.recommendation == 0x80
                && !b.nag.map(|nag| nag.is_question_mark()).unwrap_or(false))
        {
            2
        } else if b_is_hard {
            1
        } else if b.recommendation == 0x80 {
            0
        } else {
            -1
        };

        let a_points = 2 * a.win + a.draw;
        let b_points = 2 * b.win + b.draw;

        let a_priority = (a_color, a_is_hard, a_points, a.perf());
        let b_priority = (b_color, b_is_hard, b_points, b.perf());

        b_priority
            .partial_cmp(&a_priority)
            .unwrap_or(std::cmp::Ordering::Equal)
    });
}

/*
 * Database Interface Implementation
 */
impl CtgBook {
    /// Opens a CTG database with the given filename.
    ///
    /// # Arguments
    ///
    /// * `file_name` - A string slice containing the filename of the CTG database.
    ///
    /// # Returns
    ///
    /// A `Result` containing a `CtgBook` instance if successful, or an `Error` otherwise.
    pub fn open<P: AsRef<Path>>(file_name: P) -> Result<Self, Error> {
        let path: &Path = file_name.as_ref();

        let filename_cto = path.with_extension("cto");
        let filename_ctb = path.with_extension("ctb");

        let file = std::fs::File::open(path)?;
        let file_cto = std::fs::File::open(filename_cto)?;
        let mut file_ctb = std::fs::File::open(filename_ctb)?;

        // 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 CTG and CTO files are not concurrently modified while they're
        // memory-mapped.
        let (ctg_file, cto_file) = unsafe { (Mmap::map(&file)?, Mmap::map(&file_cto)?) };

        let mut db = CtgBook {
            ctg_file,
            cto_file,
            page_bounds: PageBounds {
                pad: 0,
                low: 0,
                high: 0,
            },
            position: [0; 32],
            pos_len: 0,
            bits_left: 0,
        };

        // We only need the CTB file to calculate the page bounds,
        // so we don't keep it open after this function.
        let mut buf = [0; 12];
        file_ctb.read_exact(&mut buf)?;

        // Read out upper and lower page limits.
        db.page_bounds.low = i32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
        db.page_bounds.high = i32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]);

        Ok(db)
    }

    /// Returns the total number of pages in the CTG opening book.
    pub fn total_pages(&self) -> usize {
        (self.page_bounds.high - self.page_bounds.low + 1) as usize
    }

    /// Returns the total number of positions in the CTG opening book.
    ///
    /// # Returns
    ///
    /// A `Result` containing the number of positions as `usize` if successful, or an `Error` otherwise.
    pub fn total_positions(&mut self) -> Result<usize, Error> {
        let mut total_positions: usize = 0;

        for page_index in self.page_bounds.low..=self.page_bounds.high {
            // Calculate the offset to the beginning of the page.
            let offset: usize = 4096usize * (page_index as usize + 1);

            // Ensure we don't read past the end of the file
            if offset >= self.ctg_file.len() {
                break;
            }

            // Extract the number of entries from the first two bytes of the page header.
            let buf = &self.ctg_file[offset..offset + 2];
            total_positions += u16::from_be_bytes([buf[0], buf[1]]) as usize;
        }

        Ok(total_positions)
    }

    /// Extracts all positions from the CTG database, starting from the given EPD string as a
    /// `BTreeMap<u64, Vec<CompactSBookMoveEntry>>`.
    ///
    /// # Arguments
    ///
    /// * `epd` - A string slice containing the EPD of the initial position.
    /// * `edit_learn` - If true, preserve CTG nags in the learn field.
    /// * `no_colors: bool` - Avoid assigning weights/priorities based on CTG move colors
    /// * `no_nags: bool` - Avoid assigning weights/priorities based on CTG move NAGs
    /// * `color_weights: ColorWeight` - Convert CTG colour recommendations to weights.
    /// * `nag_weights: NagWeight` - Convert CTG nags to weights.
    /// * `progress_bar` - An optional progress bar to report progress.
    ///
    /// # Returns
    ///
    /// A reference to a `BTreeMap` with move entries mapped by Zobrist hash.
    pub fn extract_abk(
        &mut self,
        epd: &str,
        no_colors: bool,
        no_nags: bool,
        color_priorities: ColorPriority,
        nag_priorities: NagPriority,
        progress_bar: Option<&ProgressBar>,
    ) -> BTreeMap<u64, Vec<CompactSBookMoveEntry>> {
        let epd = Epd::from_ascii(epd.as_bytes()).expect("malformed EPD");
        let root: Chess = epd
            .into_position(CastlingMode::Standard)
            .expect("invalid EPD");

        self.extract_abk2(
            root,
            no_colors,
            no_nags,
            color_priorities,
            nag_priorities,
            progress_bar,
        )
    }

    /// Extracts all positions from the CTG database, starting from the given `root` position as a
    /// `BTreeMap<u64, Vec<PackedSbookMoveEntry>>`.
    ///
    /// # Arguments
    ///
    /// * `root` - A `Chess` instance representing the initial position.
    /// * `no_colors: bool` - Avoid assigning weights/priorities based on CTG move colors
    /// * `no_nags: bool` - Avoid assigning weights/priorities based on CTG move NAGs
    /// * `color_priorities` - CTG colour to ABK priority map
    /// * `nag_priorities` - CTG NAG to ABK priority map
    /// * `progress_bar` - An optional progress bar to report progress.
    ///
    /// # Returns
    ///
    /// A reference to a `BTreeMap` with move entries mapped by Zobrist hash.
    pub fn extract_abk2(
        &mut self,
        root: Chess,
        no_colors: bool,
        no_nags: bool,
        color_priorities: ColorPriority,
        nag_priorities: NagPriority,
        progress_bar: Option<&ProgressBar>,
    ) -> BTreeMap<u64, Vec<CompactSBookMoveEntry>> {
        let mut map = BTreeMap::new();
        let mut stack = vec![root.clone()];

        while let Some(pos) = stack.pop() {
            let hash = zobrist_hash(&pos);
            if let Entry::Vacant(entry) = map.entry(hash) {
                if let Some(moves) = self.lookup_moves(&pos) {
                    let mut entries = Vec::with_capacity(moves.len());
                    for mov in moves {
                        let priority = match (no_colors, mov.color(), no_nags, mov.nag) {
                            (false, 3 /* red */, _, _) => color_priorities.red,
                            (false, 2 /* blue */, _, _) => color_priorities.blue,
                            (false, 1 /* green */, _, _) => color_priorities.green,
                            (_, _, false, Some(Nag::Good)) => nag_priorities.good,
                            (_, _, false, Some(Nag::Mistake)) => nag_priorities.mistake,
                            (_, _, false, Some(Nag::Hard)) => nag_priorities.hard,
                            (_, _, false, Some(Nag::Blunder)) => nag_priorities.blunder,
                            (_, _, false, Some(Nag::Interesting)) => nag_priorities.interesting,
                            (_, _, false, Some(Nag::Dubious)) => nag_priorities.dubious,
                            (_, _, false, Some(Nag::Forced)) => nag_priorities.forced,
                            (_, _, false, Some(Nag::Only)) => nag_priorities.only,
                            _ => 0,
                        };

                        let (from, to, promotion) = match mov.uci {
                            Uci::Normal {
                                from,
                                to,
                                promotion,
                            } => (
                                from as u8,
                                to as u8,
                                match promotion {
                                    Some(Role::Rook) => 1,
                                    Some(Role::Knight) => 2,
                                    Some(Role::Bishop) => 3,
                                    Some(Role::Queen) => 4,
                                    _ => 0,
                                },
                            ),
                            _ => panic!("{}", tr!("Unexpected UCI `{}' in CTG entry", mov.uci)),
                        };

                        let new_entry = CompactSBookMoveEntry {
                            from,
                            to,
                            promotion,
                            priority,
                            ngames: mov.win + mov.draw + mov.loss,
                            nwon: mov.win,
                            nlost: mov.loss,
                        };

                        // Perform a binary search to find the right position for
                        // the new entry based on its priority. We use
                        // `std::cmp::Reverse` because we want higher priorities to
                        // come first, but `binary_search_by_key` finds positions
                        // for ascending order by default.
                        match entries.binary_search_by_key(
                            &std::cmp::Reverse(new_entry.priority),
                            |mov: &CompactSBookMoveEntry| {
                                // Extract and reverse the priority of the existing
                                // move for comparison.
                                std::cmp::Reverse(mov.priority)
                            },
                        ) {
                            // If the exact priority is found, we get its position
                            // with Ok(pos), otherwise, Err(pos) gives the position
                            // where it should be inserted to maintain sort order.
                            Ok(pos) | Err(pos) => {
                                // Insert the new entry at the determined position.
                                entries.insert(pos, new_entry)
                            }
                        }
                    }

                    // Iterate directly on the current_moves which is a mutable reference to the moves
                    for entry in &entries {
                        let uci = Uci::from(*entry);
                        match uci.to_move(&pos) {
                            Ok(mov) => {
                                let mut child_pos = pos.clone();
                                child_pos.play_unchecked(&mov);
                                stack.push(child_pos);
                            }
                            Err(err) => {
                                let epd = format!(
                                    "{}",
                                    Epd::from_position(pos.clone(), EnPassantMode::PseudoLegal)
                                );
                                if let Some(progress_bar) = progress_bar {
                                    progress_bar.println(tr!(
                                        "Skipping illegal move with fen:{} uci:{} err:{}",
                                        style(epd.to_string()).bold().green(),
                                        style(uci.to_string()).bold().magenta(),
                                        style(err).bold().red()
                                    ));
                                } else {
                                    eprintln!(
                                        "{}",
                                        tr!(
                                            "Skipping illegal move with fen:{} uci:{} err:{:?}",
                                            epd,
                                            uci,
                                            err
                                        )
                                    );
                                }
                            }
                        };
                    }

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

        map
    }

    /// Extracts all positions from the CTG database, starting from the given EPD string as a
    /// `BTreeMap<u64, Vec<BookEntry>>`.
    ///
    /// # Arguments
    ///
    /// * `epd` - A string slice containing the EPD of the initial position.
    /// * `edit_learn` - If true, preserve CTG nags in the learn field.
    /// * `no_colors: bool` - Avoid assigning weights/priorities based on CTG move colors
    /// * `no_nags: bool` - Avoid assigning weights/priorities based on CTG move NAGs
    /// * `color_weights: ColorWeight` - Convert CTG colour recommendations to weights.
    /// * `nag_weights: NagWeight` - Convert CTG nags to weights.
    /// * `progress_bar` - An optional progress bar to report progress.
    ///
    /// # Returns
    ///
    /// A reference to a `BTreeMap` with book entries mapped by Zobrist hash.
    #[allow(clippy::too_many_arguments)]
    pub fn extract_bin(
        &mut self,
        epd: &str,
        edit_learn: bool,
        no_colors: bool,
        no_nags: bool,
        color_weights: ColorWeight,
        nag_weights: NagWeight,
        progress_bar: Option<&ProgressBar>,
    ) -> BTreeMap<u64, Vec<CompactBookEntry>> {
        let epd = Epd::from_ascii(epd.as_bytes()).expect("malformed EPD");
        let root: Chess = epd
            .into_position(CastlingMode::Standard)
            .expect("invalid EPD");

        self.extract_bin2(
            root,
            edit_learn,
            no_colors,
            no_nags,
            color_weights,
            nag_weights,
            progress_bar,
        )
    }

    /// Extracts all positions from the CTG database, starting from the given `root` position as a
    /// `BTreeMap<u64, Vec<BookEntry>>`.
    ///
    /// # Arguments
    ///
    /// * `root` - A `Chess` instance representing the initial position.
    /// * `edit_learn` - If true, preserve CTG nags in the learn field.
    /// * `no_colors: bool` - Avoid assigning weights/priorities based on CTG move colors
    /// * `no_nags: bool` - Avoid assigning weights/priorities based on CTG move NAGs
    /// * `color_weights: ColorWeight` - Convert CTG colour recommendations to weights.
    /// * `nag_weights: NagWeight` - Convert CTG nags to weights.
    /// * `progress_bar` - An optional progress bar to report progress.
    ///
    /// # Returns
    ///
    /// A reference to a `BTreeMap` with book entries mapped by Zobrist hash.
    #[allow(clippy::too_many_arguments)]
    pub fn extract_bin2(
        &mut self,
        root: Chess,
        edit_learn: bool,
        no_colors: bool,
        no_nags: bool,
        color_weights: ColorWeight,
        nag_weights: NagWeight,
        progress_bar: Option<&ProgressBar>,
    ) -> BTreeMap<u64, Vec<CompactBookEntry>> {
        let mut map = BTreeMap::new();
        let mut stack = vec![root.clone()];

        while let Some(pos) = stack.pop() {
            let hash = zobrist_hash(&pos);
            if let Entry::Vacant(entry) = map.entry(hash) {
                if let Some(moves) = self.lookup_moves(&pos) {
                    let mut entries = Vec::with_capacity(moves.len());
                    let king_on_start_square = is_king_on_start_square(&pos);
                    for mov in moves {
                        // Use the average of the three potential weights:
                        // 1. Color weight (unless --no-colors)
                        // 2. NAG weight (unless --no-nags)
                        // 3. Performance weight / Average weight or Win/Draw/Loss information.
                        let weights = [
                            u64::from(match (no_colors, mov.color()) {
                                (false, 1) => color_weights.green,
                                (false, 2) => color_weights.blue,
                                (false, 3) => color_weights.red,
                                _ => 0,
                            }),
                            u64::from(match (no_nags, mov.nag) {
                                (false, Some(Nag::Good)) => nag_weights.good,
                                (false, Some(Nag::Mistake)) => nag_weights.mistake,
                                (false, Some(Nag::Hard)) => nag_weights.hard,
                                (false, Some(Nag::Blunder)) => nag_weights.blunder,
                                (false, Some(Nag::Interesting)) => nag_weights.interesting,
                                (false, Some(Nag::Dubious)) => nag_weights.dubious,
                                (false, Some(Nag::Forced)) => nag_weights.forced,
                                (false, Some(Nag::Only)) => nag_weights.only,
                                _ => 0,
                            }),
                            /*
                             * It is common for a CTG book to have no performance rating information.
                             * It is less common but still possible for a CTG book to have no average
                             * rating information as well.
                             * To figure out a weight for CTG entries without NAGs, we use the
                             * following information in order:
                             * 1. Win / Draw / Loss information
                             * 2. Performance rating score
                             * 3. Average rating score
                             * 4. Weight assigned to blunders (--nag-weight-blunder=n, defaults to 1).
                             * This way we avoid skipping entries with zero weights at all costs,
                             * unless the user explicitly specifies `0' as the blunder NAG weight.
                             */
                            u64::from(
                                match mov {
                                    CtgEntry {
                                        win, draw, loss, ..
                                    } if win + draw + loss > 0 => {
                                        // TODO: Make this configurable with --{win,draw,loss}-factor={2,1,0}
                                        win * 2 + draw
                                    }
                                    CtgEntry {
                                        perf_rating_games,
                                        perf_rating_score,
                                        ..
                                    } if perf_rating_games > 0 => {
                                        perf_rating_score / perf_rating_games
                                    }
                                    CtgEntry {
                                        avg_rating_games,
                                        avg_rating_score,
                                        ..
                                    } if avg_rating_games > 0 => {
                                        avg_rating_score / avg_rating_games
                                    }
                                    _ => u32::from(nag_weights.blunder),
                                }
                                .try_into()
                                .unwrap_or(u16::MAX),
                            ), // SAFETY: CTG entries are unsigned, can only overflow positively.
                        ];

                        /* Calculate average weight */
                        let weight_items: u64 = weights.len().try_into().expect("CTG weight count");
                        let weight_total: u64 = weights.into_iter().sum();
                        let weight = (weight_total / weight_items) as u16;

                        /* Map NAG field to Learn field if requested. */
                        let learn = if !edit_learn {
                            0
                        } else {
                            match mov.nag {
                                Some(Nag::Good) => 1,
                                Some(Nag::Mistake) => 2,
                                Some(Nag::Hard) => 3,
                                Some(Nag::Blunder) => 4,
                                Some(Nag::Interesting) => 5,
                                Some(Nag::Dubious) => 6,
                                Some(Nag::Forced) => 7,
                                Some(Nag::Only) => 8,
                                _ => 0,
                            }
                        };

                        let new_entry = CompactBookEntry {
                            learn,
                            weight,
                            mov: from_uci(mov.uci, king_on_start_square),
                        };

                        // Perform a binary search to find the right position for
                        // the new entry based on its priority. We use
                        // `std::cmp::Reverse` because we want higher priorities to
                        // come first, but `binary_search_by_key` finds positions
                        // for ascending order by default.
                        match entries.binary_search_by_key(
                            &std::cmp::Reverse(new_entry.weight),
                            |mov: &CompactBookEntry| {
                                // Extract and reverse the priority of the existing
                                // move for comparison.
                                std::cmp::Reverse(mov.weight)
                            },
                        ) {
                            // If the exact priority is found, we get its position
                            // with Ok(pos), otherwise, Err(pos) gives the position
                            // where it should be inserted to maintain sort order.
                            Ok(pos) | Err(pos) => {
                                // Insert the new entry at the determined position.
                                entries.insert(pos, new_entry);
                            }
                        };
                    }

                    for entry in &entries {
                        match to_move(&pos, entry.mov) {
                            Some(mov) => {
                                let mut child_pos = pos.clone();
                                child_pos.play_unchecked(&mov);
                                stack.push(child_pos);
                            }
                            None => {
                                let mov = entry.mov;
                                let epd = format!(
                                    "{}",
                                    Epd::from_position(pos.clone(), EnPassantMode::PseudoLegal)
                                );
                                if let Some(progress_bar) = progress_bar {
                                    progress_bar.println(tr!(
                                        "Skipping illegal move with fen:{} move:{}",
                                        style(epd.to_string()).bold().green(),
                                        style(mov.to_string()).bold().magenta()
                                    ));
                                } else {
                                    eprintln!(
                                        "{}",
                                        tr!("Skipping illegal move with fen:{} move:{}", epd, mov)
                                    );
                                }
                            }
                        };
                    }

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

        map
    }

    /// Writes all possible games contained in a CTG book to a PGN file.
    ///
    /// This function traverses the CTG 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 ce = console::colors_enabled();
        console::set_colors_enabled(false); // Avoid CTG move colours in PGN

        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)
        };

        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),
            &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."));
        }

        console::set_colors_enabled(ce);
    }

    #[allow(clippy::too_many_arguments)]
    fn _write_pgn(
        &mut self,
        output: &mut dyn std::io::Write,
        position: &Chess,
        position_set: &ZobristHashSet,
        move_history: &mut Vec<String>,
        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(mut entries) = self.lookup_moves(position) {
            sort_ctg_moves(&mut entries);

            for entry in entries {
                let mov = match entry.uci.to_move(position) {
                    Ok(mov) => mov,
                    Err(_) => continue, // TODO: warn about illegal move?
                };
                let san = display_move(position, &entry, &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_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);
                }
            }
        }
    }

    /// Recursively build a tree of chess moves from the given position up to the maximum ply.
    ///
    /// `position`: A reference to the current `Chess` position.
    /// `max_ply`: The maximum ply depth for the tree.
    ///
    /// A `Tree<String>` of moves and variations.
    pub fn tree(&mut self, position: &Chess, max_ply: u16) -> Tree<String> {
        fn build_tree(
            book: &mut CtgBook,
            position: &Chess,
            parent: &mut Tree<String>,
            ply: u16,
            max_ply: u16,
            visited_keys: &HashSet<u64>,
        ) {
            if ply >= max_ply {
                return;
            }

            if let Some(mut book_entries) = book.lookup_moves(position) {
                sort_ctg_moves(book_entries.as_mut_slice());
                for entry in book_entries {
                    let m = entry.uci.to_move(position).expect("invalid UCI move");
                    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(display_move(position, &entry, &m));
                    build_tree(
                        book,
                        &new_position,
                        &mut new_tree,
                        ply + 1,
                        max_ply,
                        &new_visited_keys,
                    );

                    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);

        root_tree
    }

    /// Searches for a position in the CTG database.
    ///
    /// # Arguments
    ///
    /// * `c` - A `u32` value representing the position index.
    /// * `result` - A mutable byte slice to store the result.
    ///
    /// # Returns
    ///
    /// A boolean value indicating whether the position is found.
    fn search_position(&mut self, c: u32, result: &mut [u8]) -> bool {
        // Calculate the offset in the file.
        let offset = (c * 4 + 16) as usize;

        // Check we don't read past the end of the file.
        if offset >= self.cto_file.len() {
            return false; // UnexpectedEof
        }

        // Extract the four bytes.
        let page = i32::from_be_bytes(
            self.cto_file[offset..offset + 4]
                .try_into()
                .expect("cto file offset"),
        );
        if page < 0 {
            return false;
        }

        self.read_page(page, result)
    }

    /// Reads a page from the CTG database.
    ///
    /// # Arguments
    ///
    /// * `page` - An `i32` value representing the page index.
    /// * `result` - A mutable byte slice to store the result.
    ///
    /// # Returns
    ///
    /// A boolean value indicating whether the position is found.
    fn read_page(&mut self, page: i32, result: &mut [u8]) -> bool {
        let page_start = page as usize * 4096 + 4096;
        let page_end = page_start + 4096;

        if page_end > self.ctg_file.len() {
            return false; // UnexpectedEof
        }

        let pagebuf = &self.ctg_file[page_start..page_end];

        // SAFETY: We are creating a slice from a raw pointer. This is safe because we ensure that
        // 'self.position' is valid and that 'self.pos_len' is within the bounds of
        // 'self.position'. We are not modifying the original 'self.position' data and no data
        // races can occur as we only have a const reference to the original data.
        let position_u8 = unsafe {
            std::slice::from_raw_parts(self.position.as_ptr() as *const u8, self.pos_len as usize)
        };

        let mut pos = 4;
        while let Some(&byte) = pagebuf.get(pos) {
            let offset = (byte & 0x1f) as usize;

            if pagebuf.get(pos) != Some(&position_u8[0])
                || pagebuf.get(pos..pos + self.pos_len as usize) != Some(position_u8)
            {
                pos += offset;
                if pos >= pagebuf.len() {
                    break;
                }

                // SAFETY: At this point, we have checked that 'pos' is a valid index for 'pagebuf'.
                pos += unsafe { *pagebuf.get_unchecked(pos) as usize };
                pos += 33;

                if pos >= pagebuf.len() {
                    break;
                }

                continue;
            }

            pos += offset;

            if let Some(&len_byte) = pagebuf.get(pos) {
                let len = len_byte as usize + 33;

                if pos + len > pagebuf.len() {
                    break;
                }

                result[..len].copy_from_slice(&pagebuf[pos..pos + len]);
                return true;
            } else {
                break;
            }
        }

        false
    }

    /// Looks up the position in the CTG database.
    ///
    /// # Arguments
    ///
    /// * `result` - A mutable byte slice to store the result.
    ///
    /// # Returns
    ///
    /// A boolean value indicating whether the position is found.
    fn lookup_position(&mut self, result: &mut [u8]) -> bool {
        let hash = gen_hash(&self.position, self.pos_len as usize);
        let lower_bound = self.page_bounds.low as u32;
        let upper_bound = self.page_bounds.high as u32;

        let mut mask = 0;
        while mask != 0x7fffffff {
            let key = ((hash as u32) & mask) + mask;

            if key >= lower_bound && self.search_position(key, result) {
                return true;
            }

            if key >= upper_bound {
                break;
            }

            mask = 2 * mask + 1;
        }

        false
    }

    /// Puts a bit into the position array.
    ///
    /// # Arguments
    ///
    /// * `x` - A boolean value representing the bit to put.
    #[inline(always)]
    fn put_bit(&mut self, x: bool) {
        // Fetch the current byte once to reduce array accesses.
        let byte = &mut self.position[self.pos_len as usize];

        // Shift the byte left and add the new bit.
        *byte = (*byte << 1) | i8::from(x);
        self.bits_left -= 1;

        // Move to the next byte if the current one is filled.
        if self.bits_left == 0 {
            self.pos_len += 1;
            self.bits_left = 8;
        }
    }

    /// Encodes the position into a CTG format.
    ///
    /// This method encodes a chess position into the CTG format used by the
    /// Chessbase game database. The position is represented using a `Board` object,
    /// and additional information such as castling rights and en passant column
    /// is provided through separate arguments.
    ///
    /// # Arguments
    ///
    /// * `board` - A reference to a `Board` object representing the chess position.
    /// * `invert` - A boolean value indicating whether the position should be inverted.
    /// * `castling_rights` - A `shakmaty::Castles` representing the castling rights.
    /// * `ep_square` - An optional `shakmakty::Square` representing the en-passant square.
    fn encode_position(
        &mut self,
        board: &ByteBoard,
        invert: bool,
        castling_rights: &Castles,
        ep_square: &Option<Square>,
    ) {
        // clear out
        self.position = [0; 32];

        // leave some room for the header byte, which will be filled last
        self.pos_len = 1;
        self.bits_left = 8;

        let board_bytes = board.as_bytes();

        // slightly unusual ordering
        for x in 0..8 {
            for y in 0..8 {
                match board_bytes[(7 - y) * 8 + x] {
                    b' ' => self.put_bit(false),
                    b'p' => {
                        self.put_bit(true);
                        self.put_bit(true);
                        self.put_bit(true);
                    }
                    b'P' => {
                        self.put_bit(true);
                        self.put_bit(true);
                        self.put_bit(false);
                    }
                    b'r' => {
                        self.put_bit(true);
                        self.put_bit(false);
                        self.put_bit(true);
                        self.put_bit(true);
                        self.put_bit(true);
                    }
                    b'R' => {
                        self.put_bit(true);
                        self.put_bit(false);
                        self.put_bit(true);
                        self.put_bit(true);
                        self.put_bit(false);
                    }
                    b'b' => {
                        self.put_bit(true);
                        self.put_bit(false);
                        self.put_bit(true);
                        self.put_bit(false);
                        self.put_bit(true);
                    }
                    b'B' => {
                        self.put_bit(true);
                        self.put_bit(false);
                        self.put_bit(true);
                        self.put_bit(false);
                        self.put_bit(false);
                    }
                    b'n' => {
                        self.put_bit(true);
                        self.put_bit(false);
                        self.put_bit(false);
                        self.put_bit(true);
                        self.put_bit(true);
                    }
                    b'N' => {
                        self.put_bit(true);
                        self.put_bit(false);
                        self.put_bit(false);
                        self.put_bit(true);
                        self.put_bit(false);
                    }
                    b'q' => {
                        self.put_bit(true);
                        self.put_bit(false);
                        self.put_bit(false);
                        self.put_bit(false);
                        self.put_bit(true);
                        self.put_bit(true);
                    }
                    b'Q' => {
                        self.put_bit(true);
                        self.put_bit(false);
                        self.put_bit(false);
                        self.put_bit(false);
                        self.put_bit(true);
                        self.put_bit(false);
                    }
                    b'k' => {
                        self.put_bit(true);
                        self.put_bit(false);
                        self.put_bit(false);
                        self.put_bit(false);
                        self.put_bit(false);
                        self.put_bit(true);
                    }
                    b'K' => {
                        self.put_bit(true);
                        self.put_bit(false);
                        self.put_bit(false);
                        self.put_bit(false);
                        self.put_bit(false);
                        self.put_bit(false);
                    }
                    _ => unreachable!(),
                };
            }
        }

        // en passant
        let mut ep_any = false;
        if let Some(ep_square) = ep_square {
            let epcn: usize = ep_square.file() as usize;
            ep_any = (epcn > 0 && board_bytes[3 * 8 + epcn - 1] == b'P')
                || (epcn < 7 && board_bytes[3 * 8 + epcn + 1] == b'P');
        }

        // really odd padding
        // find the right number of bits
        let mut right = if ep_any { 3 } else { 8 };

        // castling needs four more
        if !castling_rights.is_empty() {
            right += 4;
            if right > 8 {
                right %= 8;
            }
        }

        let mut nb = match self.bits_left.cmp(&right) {
            Ordering::Greater => self.bits_left - right,
            Ordering::Less => self.bits_left + 8 - right,
            Ordering::Equal => 0,
        };

        if self.bits_left == 8 && castling_rights.is_empty() && !ep_any {
            nb = 8;
        }

        for _ in 0..nb {
            self.put_bit(false);
        }
        ///////////////////////

        // en passant
        if ep_any {
            let epcn = ep_square.unwrap().file() as u8;
            self.put_bit(epcn & 0x04 != 0);
            self.put_bit(epcn & 0x02 != 0);
            self.put_bit(epcn & 0x01 != 0);
        }

        // castling rights
        if !castling_rights.is_empty() {
            if invert {
                self.put_bit(castling_rights.has(Color::White, CastlingSide::KingSide));
                self.put_bit(castling_rights.has(Color::White, CastlingSide::QueenSide));
                self.put_bit(castling_rights.has(Color::Black, CastlingSide::KingSide));
                self.put_bit(castling_rights.has(Color::Black, CastlingSide::QueenSide));
            } else {
                self.put_bit(castling_rights.has(Color::Black, CastlingSide::KingSide));
                self.put_bit(castling_rights.has(Color::Black, CastlingSide::QueenSide));
                self.put_bit(castling_rights.has(Color::White, CastlingSide::KingSide));
                self.put_bit(castling_rights.has(Color::White, CastlingSide::QueenSide));
            }
        }

        // padding stuff
        if self.bits_left == 8 {
            // self.pos_len += 1;
        } else {
            // self.pos_len += 1;
            let nd = 8 - self.bits_left;
            for _ in 0..nd {
                self.put_bit(false);
            }
        }

        // and the header byte
        self.position[0] = self.pos_len as i8;

        if !castling_rights.is_empty() {
            self.position[0] |= 0x40;
        }
        if ep_any {
            self.position[0] |= 0x20;
        }
    }

    /// Executes a move on the board.
    ///
    /// # Arguments
    ///
    /// * `board` - A string representing the board position.
    /// * `castling_rights` - A `shakmaty::Castles` representing the castling rights.
    /// * `inverted` - A boolean value indicating whether the position is inverted.
    /// * `from_square` - A usize value representing the from square index.
    /// * `to_square` - A usize value representing the to square index.
    ///
    /// # Returns
    ///
    /// A tuple containing the new board position, castling rights, and en passant square.
    fn execute_move(
        &mut self,
        board: &ByteBoard,
        castling_rights: &Castles,
        inverted: bool,
        from_square: usize,
        to_square: usize,
    ) -> (ByteBoard, Castles, Option<Square>) {
        // fudge
        let from_square = (7 - (from_square / 8)) * 8 + (from_square % 8);
        let to_square = (7 - (to_square / 8)) * 8 + (to_square % 8);

        // compute the new castling rights
        let mut black_ks = castling_rights.has(Color::Black, CastlingSide::KingSide);
        let mut black_qs = castling_rights.has(Color::Black, CastlingSide::QueenSide);
        let mut white_ks = castling_rights.has(Color::White, CastlingSide::KingSide);
        let mut white_qs = castling_rights.has(Color::White, CastlingSide::QueenSide);

        let mut board = board.bytes;
        if board[from_square] == b'K' {
            if inverted {
                black_ks = false;
                black_qs = false;
            } else {
                white_ks = false;
                white_qs = false;
            }
        }
        if board[to_square] == b'R' {
            if inverted {
                if to_square == 56 {
                    // h1
                    black_qs = false;
                } else if to_square == 63 {
                    // h8
                    black_ks = false;
                }
            } else if to_square == 56 {
                // a1
                white_qs = false;
            } else if to_square == 63 {
                // a8
                white_ks = false;
            }
        }
        if board[to_square] == b'r' {
            if inverted {
                if to_square == 0 {
                    // h1
                    white_qs = false;
                } else if to_square == 7 {
                    // h8
                    white_ks = false;
                }
            } else if to_square == 0 {
                // a1
                black_qs = false;
            } else if to_square == 7 {
                // a8
                black_ks = false;
            }
        }

        let castling_rights = if !(black_ks || black_qs || white_ks || white_qs) {
            Castles::empty(CastlingMode::Standard)
        } else {
            let mut new_rights = Castles::default();
            if !white_ks {
                new_rights.discard_rook(Square::H1);
            }
            if !white_qs {
                new_rights.discard_rook(Square::A1);
            }
            if !black_ks {
                new_rights.discard_rook(Square::H8);
            }
            if !black_qs {
                new_rights.discard_rook(Square::A8);
            }
            new_rights
        };

        // now the ep square
        let ep_square = if board[from_square] == b'P'
            && to_square as isize - from_square as isize == -16
        {
            // SAFETY: The `from_square` is always in the range 0..=63 because it represents a square on a chess board.
            Some(unsafe { Square::new_unchecked((from_square / 8 * 8 + from_square % 8) as u32) })
        } else {
            None
        };

        // is this move an en passant capture?
        if board[from_square] == b'P'
            && board[to_square] == b' '
            && (to_square as isize - from_square as isize == -9
                || to_square as isize - from_square as isize == -7)
        {
            board[to_square + 8] = b' ';
        }

        // make the move
        board[to_square] = board[from_square];
        board[from_square] = b' ';

        // promotion
        if board[to_square] == b'P' && to_square < 8 {
            board[to_square] = b'Q';
        }

        if board[to_square] == b'K' && to_square.saturating_sub(from_square) == 2 {
            // short castling
            board[to_square - 1] = b'R';
            board[to_square + 1] = b' ';
        } else if board[to_square] == b'K' && to_square as isize - from_square as isize == -2 {
            // long castling
            board[to_square + 1] = b'R';
            board[to_square - 2] = b' ';
        }

        (ByteBoard::from_bytes(&board), castling_rights, ep_square)
    }

    /// `lookup_moves` takes a mutable reference to `CtgBook`, a `shakmaty::Position`, and returns
    /// an `Option` of a vector of `CtgEntry`. This function is responsible for encoding the
    /// position, processing the moves, and returning a vector of `CtgEntry` structures
    /// corresponding to the available moves in the position.
    pub fn lookup_moves(&mut self, position: &dyn Position) -> Option<Vec<CtgEntry>> {
        // encode the position
        let mut board = ByteBoard::from_board(position.board());
        let mut result = [0; 256];
        let mut invert = false;

        // always from white's position
        if position.turn() == Color::Black {
            invert = true;
            board.invert();
        }

        // and the white king is always in the right half
        // (never flip if either side can castle)
        let flip = position.castles().is_empty() && board.needs_flipping();
        let mut ep_square = position.maybe_ep_square();
        if flip {
            ep_square = board.flip(ep_square)
        };

        self.encode_position(&board, invert, position.castles(), &ep_square);
        let found = self.lookup_position(&mut result);
        if !found {
            //eprintln!("Not found in book.");
            return None;
        }

        let book_moves = (result[0] >> 1) as usize;
        let mut book = Vec::with_capacity(book_moves);

        for i in 0..book_moves {
            if let Some(entry) = self.process_move(
                &board,
                position.castles(),
                invert,
                flip,
                result[i * 2 + 1],
                result[i * 2 + 2],
            ) {
                book.push(entry);
            }
        }

        if !book.is_empty() {
            // Shrink the capacity as much as possible.
            book.shrink_to_fit();

            Some(book)
        } else {
            None
        }
    }

    /// `process_move` takes a mutable reference to `CtgBook`, a `ByteBoard`, a `shakmaty::Castles`
    /// representing castling rights, two booleans representing whether the board should be
    /// inverted or flipped, and two u8 values for the move and annotation. It returns an `Option`
    /// of a `CtgEntry` structure. This function is responsible for processing the move and
    /// creating a `CtgEntry` based on the given parameters.
    pub fn process_move(
        &mut self,
        board: &ByteBoard,
        castling_rights: &Castles,
        invert: bool,
        flip: bool,
        move_: u8,
        annotation: u8,
    ) -> Option<CtgEntry> {
        if let Ok(index) = MOVETABLE.binary_search_by(|entry| entry.encoding.cmp(&move_)) {
            let movetable = &MOVETABLE[index];
            let from_square = board.find_piece(movetable.piece, movetable.num)?;
            let mut from_row = from_square / 8;
            let mut from_col = from_square % 8;

            let mut to_row = (from_row + 8 + movetable.forward) % 8;
            let mut to_col = (from_col + 8 + movetable.right) % 8;
            let to_square = to_row * 8 + to_col;

            // output the move
            if invert {
                from_row = 7 - from_row;
                to_row = 7 - to_row;
            }
            if flip {
                from_col = 7 - from_col;
                to_col = 7 - to_col;
            }

            // Maximum size required is 5 chars (e.g., e2e4q)
            let mut uci = [0u8; 5];
            uci[0] = b'a' + from_col as u8;
            uci[1] = from_row as u8 + b'1';
            uci[2] = b'a' + to_col as u8;
            uci[3] = to_row as u8 + b'1';

            /*
             * Check for promotion.
             * Note: CTG does not support underpromotion.
             */
            let len = if movetable.piece == 'P' && (to_row == 0 || to_row == 7) {
                uci[4] = b'q';
                5
            } else {
                4
            };
            let uci = Uci::from_ascii(&uci[..len]).expect("invalid UCI move");

            /* Numeric Annotation Glyphs */
            let nag = match annotation & 31 {
                0 => None,
                n => Some(Nag::from(n)),
            };

            let mut entry = CtgEntry {
                uci,
                win: 0,
                draw: 0,
                loss: 0,
                nag,
                // comment: None,
                recommendation: 0,
                avg_rating_games: 0,
                avg_rating_score: 0,
                perf_rating_games: 0,
                perf_rating_score: 0,
            };

            // do the move, and look up the new position
            let mut result = [0; 256];
            let (mut newboard, newcastling_rights, mut new_epsquare) = self.execute_move(
                board,
                castling_rights,
                invert,
                from_square as usize,
                to_square as usize,
            );
            newboard.invert();
            if newcastling_rights.is_empty() && newboard.needs_flipping() {
                new_epsquare = newboard.flip(new_epsquare);
                // flip = !flip;
            }
            self.encode_position(&newboard, !invert, &newcastling_rights, &new_epsquare);
            self.lookup_position(&mut result);

            let mut offset = result[0] as usize + 3;
            let win_offset = if invert { offset + 3 } else { offset };
            let loss_offset = if invert { offset } else { offset + 3 };

            entry.draw = (u32::from(result[offset + 6]) << 16)
                | (u32::from(result[offset + 7]) << 8)
                | u32::from(result[offset + 8]);
            let win = (u32::from(result[win_offset]) << 16)
                | (u32::from(result[win_offset + 1]) << 8)
                | u32::from(result[win_offset + 2]);
            let loss = (u32::from(result[loss_offset]) << 16)
                | (u32::from(result[loss_offset + 1]) << 8)
                | u32::from(result[loss_offset + 2]);
            entry.win = if invert { loss } else { win };
            entry.loss = if invert { win } else { loss };

            offset += 9; /* win, draw, loss */
            offset += 4; /* 4 bytes of unknown data */

            entry.avg_rating_games = (u32::from(result[offset]) << 16)
                | (u32::from(result[offset + 1]) << 8)
                | u32::from(result[offset + 2]);
            offset += 3;

            entry.avg_rating_score = (u32::from(result[offset]) << 24)
                | (u32::from(result[offset + 1]) << 16)
                | (u32::from(result[offset + 2]) << 8)
                | u32::from(result[offset + 3]);
            offset += 4;

            entry.perf_rating_games = (u32::from(result[offset]) << 16)
                | (u32::from(result[offset + 1]) << 8)
                | u32::from(result[offset + 2]);
            offset += 3;

            entry.perf_rating_score = (u32::from(result[offset]) << 24)
                | (u32::from(result[offset + 1]) << 16)
                | (u32::from(result[offset + 2]) << 8)
                | u32::from(result[offset + 3]);
            offset += 4;

            entry.recommendation = result[offset];
            /*
            offset += 1;

            offset += 1; /* another unknown byte */
            entry.comment = match result[offset] {
                0 => None,
                n => Some(char::from(n).to_string()),
            };
            */

            Some(entry)
        } else {
            None
        }
    }
}
