//////////////////////////////////////////////////////////////////////
//
//  FILE:       game.cpp
//              Game class methods
//
//  Part of:    Scid (Shane's Chess Information Database)
//  Version:    3.5
//
//  Notice:     Copyright (c) 2000-2003  Shane Hudson.  All rights reserved.
//
//  Author:     Shane Hudson (sgh@users.sourceforge.net)
//
//////////////////////////////////////////////////////////////////////

#include "game.h"
#include "bytebuf.h"
#include "common.h"
#include "dstring.h"
#include "nagtext.h"
#include "position.h"
#include "stored.h"
#include "textbuf.h"
#include <algorithm>
#include <cstring>

// Piece letters translation
int language = 0; // default to english
//  0 = en,
//  1 = fr, 2 = es, 3 = de, 4 = it, 5 = ne, 6 = cz
//  7 = hu, 8 = no, 9 = sw, 10 = ca, 11 = fi, 12 = gr, 13 = pt, 14 = he, 15 = swa
//  16 = hi, 17 = uk, 18 = bn, 19 = ko, 20 = ja, 21 = zh, 22 = ro, 23 = bg
//  TODO Piece translations for greek
//  Note: Languages using multi-byte UTF-8 piece chars (gr, he, ko, zh, bg) use empty
//  strings here; translation is handled by Tcl-side string map instead.
const char *langPieces[] = {"",
                            "PPKRQDRTBFNC",
                            "PPKRQDRTBANC",
                            "PBKKQDRTBLNS",
                            "PPKRQDRTBANC",
                            "PpKKQDRTBLNP",
                            "PPKKQDRVBSNJ",
                            "PGKKQVRBBFNH",
                            "PBKKQDRTBLNS",
                            "PBKKQDRTBLNS",
                            "PPKRQDRTBANC",
                            "PSKKQDRTBLNR",
                            "",
                            "PPKRQDRTBBNC", // 13 pt
                            "", // 14 = he (multi-byte UTF-8)
                            "", // 15 = swa
                            "", // 16 = hi
                            "", // 17 = uk
                            "", // 18 = bn
                            "", // 19 = ko
                            "", // 20 = ja
                            "", // 21 = zh
                            "PPKRQDRTBNNC", // 22 = ro
                            ""  // 23 = bg
};

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// transPieces():
// Given a string, will translate pieces from english to another language
void transPieces(char *s) {
  if (language == 0)
    return;
  // Bounds check: verify langPieces[language] has at least 12 characters
  // to avoid OOB reads for languages with empty mappings (he, swa, hi, uk, bn)
  if (strlen(langPieces[language]) < 12)
    return;
  char *ptr = s;
  int i;

  while (*ptr) {
    if (*ptr >= 'A' && *ptr <= 'Z') {
      for (i = 0; i < 12; i += 2) {
        if (*ptr == langPieces[language][i]) {
          *ptr = langPieces[language][i + 1];
          break;
        }
      }
    }
    ptr++;
  }
}

char transPiecesChar(char c) {
  char ret = c;
  if (language == 0)
    return c;
  // Bounds check: verify langPieces[language] has at least 12 characters
  // to avoid OOB reads for languages with empty mappings (he, swa, hi, uk, bn)
  if (strlen(langPieces[language]) < 12)
    return c;
  for (int i = 0; i < 12; i += 2) {
    if (c == langPieces[language][i]) {
      ret = langPieces[language][i + 1];
      break;
    }
  }
  return ret;
}

const char *ratingTypeNames[8] = {"Elo", "Rating", "Rapid", "ICCF", "USCF",
                                  "DWZ", "ECF",
                                  // End of array marker:
                                  NULL};

typedef Game *GamePtr;

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// printNag(): converts a numeric NAG to its string equivalent.
//    The parameter <str> should point to a string at least 10 bytes long.
// TODO
//    replace < and > in NAG codes by <lt> and <gt>
void game_printNag(byte nag, char *str, bool asSymbol, gameFormatT format) {
  ASSERT(str != NULL);

  if (nag == 0) {
    *str = 0;
    return;
  }

  if (nag >= (sizeof evalNagsRegular / sizeof(const char *))) {
    sprintf(str, "$%u", nag);
    return;
  }

  if (asSymbol) {
    strcpy(str, evalNagsRegular[nag]);
    if (nag == NAG_Diagram) {
      if (format == PGN_FORMAT_HTML) {
        strcpy(str, "<i>(D)</i>");
      } else {
        str[0] = 'D';
        str[1] = 0;
      }
    }
    return;
  } else {
    sprintf(str, "$%d", nag);
  }
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// game_parseNag():
//      Parses an annotation symbol into its numeric equivalent.
//      Accepts numeric format ($51) or symbols such as
//      !, ?, +=, -/+, N, etc.
//
byte game_parseNag(std::pair<const char *, const char *> strview) {
  auto slen = std::distance(strview.first, strview.second);
  if (slen == 0 || slen > 7)
    return 0;

  char strbuf[8] = {0};
  std::copy_n(strview.first, slen, strbuf);
  const char *str = strbuf;

  if (*str == '$') {
    str++;
    return (byte)strGetUnsigned(str);
  }
  if ((*str <= '9' && *str >= '0')) {
    return (byte)strGetUnsigned(str);
  }

  if (*str == '!') {
    // Must be "!", "!!", "!?", or invalid:
    str++;
    if (*str == 0) {
      return NAG_GoodMove;
    } // !      $1
    if (*str == '!') {
      return NAG_ExcellentMove;
    } // !!     $3
    if (*str == '?') {
      return NAG_InterestingMove;
    } // !?     $5
    return 0;
  }

  if (*str == '?') {
    // Must be "?", "??", "?!", or invalid:
    str++;
    if (*str == 0) {
      return NAG_PoorMove;
    } // ?      $2
    if (*str == '?') {
      return NAG_Blunder;
    } // ??     $4
    if (*str == '!') {
      return NAG_DubiousMove;
    } // ?!     $6
    return 0;
  }

  if (*str == '+') {
    // Must be "+=", "+/=", "+/-", "+-", "+--", "+>" or invalid:
    str++;
    if (*str == '=') {
      return NAG_WhiteSlight;
    } // +=      $14
    if (*str == '-' && str[1] == 0) { // +-      $18
      return NAG_WhiteDecisive;
    }
    if (*str == '>') {
      return NAG_WithAttack;
    } // +>      $40
    if (*str == '/' && str[1] == '-') { // +/-     $16
      return NAG_WhiteClear;
    }
    if (*str == '/' && str[1] == '=') { // +/=     $14
      return NAG_WhiteSlight;
    }
    if (*str == '-' && str[1] == '-') { // +--     $20
      return NAG_WhiteCrushing;
    }
    return 0;
  }

  if (*str == '=') {
    // Must be "=" (equal), "=+", "=/+", "=/&" or invalid:
    str++;
    if (*str == 0) {
      return NAG_Equal;
    } // =       $10
    if (*str == '+') {
      return NAG_BlackSlight;
    } // =+      $15
    if (*str == '/' && str[1] == '+') { // =/+     $15
      return NAG_BlackSlight;
    }
    if (*str == '/' && str[1] == '&') { // =/&     $44
      return NAG_Compensation;
    }
    return 0;
  }

  if (*str == '-') {
    // Must be "-+", "-/+" or "--+", "->":
    str++;
    if (*str == '+') {
      return NAG_BlackDecisive;
    } // -+     $19
    if (*str == '>') {
      return NAG_WithBlackAttack;
    } // ->     $41
    if (*str == '/' && str[1] == '+') { // -/+    $17
      return NAG_BlackClear;
    }
    if (*str == '-' && str[1] == '+') { // --+    $21
      return NAG_BlackCrushing;
    }
    if (*str == '-' && str[1] == 0) { // --     $210
      return NAG_See;
    }
    return 0;
  }

  if (*str == '/') {
    // Must be "/\" or "/"
    str++;
    if (*str == 0) {
      return NAG_Diagonal;
    } // /      $150
    if (*str == '\\') {
      return NAG_WithIdea;
    } // Tri    $140
    return 0;
  }

  if (*str == 'R') {
    // Must be "R", "RR"
    str++;
    if (*str == 0) {
      return NAG_VariousMoves;
    } // R      $144
    if (*str == 'R') {
      return NAG_Comment;
    } // RR     $145
    return 0;
  }

  if (*str == 'z') {
    // Must be "zz"
    str++;
    if (*str == 'z') {
      return NAG_BlackZugZwang;
    } // zz     $23
    return 0;
  }
  if (*str == 'Z') {
    // Must be "ZZ"
    str++;
    if (*str == 'Z') {
      return NAG_ZugZwang;
    } // ZZ     $22
    return 0;
  }

  if (*str == 'B') {
    // Must be "BB", "Bb"
    str++;
    if (*str == 'B') {
      return NAG_BishopPair;
    } // BB     $151
    if (*str == 'b') {
      return NAG_OppositeBishops;
    } // Bb     $153
    return 0;
  }

  if (*str == 'o') {
    // Must be "BB", "Bb"
    str++;
    if (*str == '-' && str[1] == 'o') { // o-o    $192
      return NAG_SeparatedPawns;
    }
    if (*str == 'o' && str[1] == 0) { // [+]    $193
      return NAG_UnitedPawns;
    }
    if (*str == '^' && str[1] == 0) { // o^     $212
      return NAG_PassedPawn;
    }
    return 0;
  }

  if (*str == '(') {
    // Must be (_)
    str++;
    if (*str == '_' && str[1] == ')') { // (_)     $142
      return NAG_BetterIs;
    }
    return 0;
  }

  if (*str == '[') {
    // Must be (_)
    str++;
    if (*str == ']' && str[1] == 0) { // []     $8
      return NAG_OnlyMove;
    }
    if (*str == '+' && str[1] == ']') { // [+]    $48
      return NAG_SlightCentre;
    }
    if (*str == '+' && str[1] == '+' && str[2] == ']') { // [++]   $50
      return NAG_Centre;
    }
    return 0;
  }

  if (*str == '_') {
    // must be _|_ or _|
    str++;
    if (*str == '|' && str[1] == '_') { // _|_    $148
      return NAG_Ending;
    }
    if (*str == '|' && str[1] == 0) { // _|     $215
      return NAG_Without;
    }
    return 0;
  }

  if (*str == '|') {
    // must be ||, |_
    str++;
    if (*str == '|') {
      return NAG_Etc;
    } // ||      $190
    if (*str == '_') {
      return NAG_With;
    } // |_      $214
    return 0;
  }

  if (*str == '>') {
    // must be >, >>, >>>
    str++;
    if (*str == 0) {
      return NAG_SlightKingSide;
    } // >       $54
    if (*str == '>' && str[1] == 0) { // >>      $56
      return NAG_ModerateKingSide;
    }
    if (*str == '>' && str[1] == '>') { // >>>     $58
      return NAG_KingSide;
    }
    return 0;
  }

  if (*str == '<') {
    // must be <, <<, <<<, <=>
    str++;
    if (*str == 0) {
      return NAG_SlightQueenSide;
    } // <        $60
    if (*str == '<' && str[1] == 0) { // <<       $62
      return NAG_ModerateQueenSide;
    }
    if (*str == '<' && // <<<      $64
        str[1] == '<' && str[2] == 0) {
      return NAG_QueenSide;
    }
    if (*str == '=' && // <=>      $149
        str[1] == '>' && str[2] == 0) {
      return NAG_File;
    }
    if (*str == '+' && // <+>      $130
        str[1] == '>' && str[2] == 0) {
      return NAG_SlightCounterPlay;
    }
    if (*str == '-' && // <->      $131
        str[1] == '>' && str[2] == 0) {
      return NAG_BlackSlightCounterPlay;
    }
    if (*str == '+' && // <++>     $132
        str[1] == '+' && str[2] == '>' && str[3] == 0) {
      return NAG_CounterPlay;
    }
    if (*str == '-' && // <-->     $133
        str[1] == '-' && str[2] == '>' && str[3] == 0) {
      return NAG_BlackCounterPlay;
    }
    if (*str == '+' && // <+++>    $134
        str[1] == '+' && str[2] == '+' && str[3] == '>') {
      return NAG_DecisiveCounterPlay;
    }
    if (*str == '-' && // <--->    $135
        str[1] == '-' && str[2] == '-' && str[3] == '>') {
      return NAG_BlackDecisiveCounterPlay;
    }
    return 0;
  }

  if (*str == '~' && *(str + 1) == '=') { // ~=       $44
                                          // alternative Compensation symbol:
    return NAG_Compensation;
  }

  if (*str == '~') { // ~        $13
                     // Unclear symbol:
    return NAG_Unclear;
  }

  if (*str == 'x') { // x        $147
    return NAG_WeakPoint;
  }

  if (str[0] == 'N' && str[1] == 0) { // N        $146
                                      // Novelty symbol:
    return NAG_Novelty;
  }

  if (str[0] == 'D' && str[1] == 0) { // D        $201
                                      // Diagram symbol:
    return NAG_Diagram;
  }
  return 0;
}

errorT Game::AddNag(byte nag) {
  moveT *m = CurrentMove->prev;
  if (m->nagCount + 1 >= MAX_NAGS) {
    return ERROR_GameFull;
  }
  if (nag == 0) { /* Nags cannot be zero! */
    return OK;
  }
  // If it is a move nag replace an existing
  if (nag >= 1 && nag <= 6)
    for (int i = 0; i < m->nagCount; i++)
      if (m->nags[i] >= 1 && m->nags[i] <= 6) {
        m->nags[i] = nag;
        return OK;
      }
  // If it is a position nag replace an existing
  if (nag >= 10 && nag <= 21)
    for (int i = 0; i < m->nagCount; i++)
      if (m->nags[i] >= 10 && m->nags[i] <= 21) {
        m->nags[i] = nag;
        return OK;
      }
  if (nag >= 1 && nag <= 6) {
    // Put Move Nags at the beginning
    for (int i = m->nagCount; i > 0; i--)
      m->nags[i] = m->nags[i - 1];
    m->nags[0] = nag;
  } else
    m->nags[m->nagCount] = nag;
  m->nagCount += 1;
  m->nags[m->nagCount] = 0;
  return OK;
}

errorT Game::RemoveNag(bool isMoveNag) {
  moveT *m = CurrentMove->prev;
  if (isMoveNag) {
    for (int i = 0; i < m->nagCount; i++)
      if (m->nags[i] >= 1 && m->nags[i] <= 6) {
        m->nagCount -= 1;
        for (int j = i; j < m->nagCount; j++)
          m->nags[j] = m->nags[j + 1];
        m->nags[m->nagCount] = 0;
        return OK;
      }
  } else {
    for (int i = 0; i < m->nagCount; i++)
      if (m->nags[i] >= 10 && m->nags[i] <= 21) {
        m->nagCount -= 1;
        for (int j = i; j < m->nagCount; j++)
          m->nags[j] = m->nags[j + 1];
        m->nags[m->nagCount] = 0;
        return OK;
      }
  }
  return OK;
}

//////////////////////////////////////////////////////////////////////
//  PUBLIC FUNCTIONS
//////////////////////////////////////////////////////////////////////

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Move allocation:
//      moves are allocated in chunks to save memory and for faster
//      performance.
//
constexpr int MOVE_CHUNKSIZE = 128;

moveT *Game::allocMove() {
  if (moveChunkUsed_ == MOVE_CHUNKSIZE) {
    moveChunks_.emplace_front(new moveT[MOVE_CHUNKSIZE]);
    moveChunkUsed_ = 0;
  }
  return moveChunks_.front().get() + moveChunkUsed_++;
}

moveT *Game::NewMove(markerT marker) {
  moveT *res = allocMove();
  res->clear();
  res->marker = marker;
  return res;
}

Game::Game(const Game &obj) {
  extraTags_ = obj.extraTags_;
  WhiteStr = obj.WhiteStr;
  BlackStr = obj.BlackStr;
  EventStr = obj.EventStr;
  SiteStr = obj.SiteStr;
  RoundStr = obj.RoundStr;
  Date = obj.Date;
  EventDate = obj.EventDate;
  EcoCode = obj.EcoCode;
  WhiteElo = obj.WhiteElo;
  BlackElo = obj.BlackElo;
  WhiteRatingType = obj.WhiteRatingType;
  BlackRatingType = obj.BlackRatingType;
  Result = obj.Result;
  std::copy_n(obj.ScidFlags, sizeof(obj.ScidFlags), ScidFlags);

  if (obj.StartPos)
    StartPos = std::make_unique<Position>(*obj.StartPos);

  NumHalfMoves = obj.NumHalfMoves;
  NumMovesPrinted = obj.NumMovesPrinted;
  PgnStyle = obj.PgnStyle;
  PgnFormat = obj.PgnFormat;
  HtmlStyle = obj.HtmlStyle;

  moveChunkUsed_ = MOVE_CHUNKSIZE;
  FirstMove =
      obj.FirstMove->cloneLine(nullptr, [this]() { return allocMove(); });

  MoveToLocationInPGN(obj.GetLocationInPGN());
}
Game *Game::clone() { return new Game(*this); }

void Game::strip(bool variations, bool comments, bool NAGs) {
  while (variations && MoveExitVariation() == OK) { // Go to main line
  }

  for (auto &chunk : moveChunks_) {
    moveT *move = chunk.get();
    moveT *end = (chunk == moveChunks_.front()) ? move + moveChunkUsed_
                                                : move + MOVE_CHUNKSIZE;
    for (; move != end; ++move) {
      if (variations) {
        move->numVariations = 0;
        move->varChild = nullptr;
      }
      if (comments)
        move->comment.clear();

      if (NAGs) {
        move->nagCount = 0;
        std::fill_n(move->nags, sizeof(move->nags), 0);
      }
    }
  }
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::ClearMoves(): clear all moves.
void Game::ClearMoves() {
  // Delete any chunks of moves except the first:
  if (moveChunks_.empty()) {
    moveChunkUsed_ = MOVE_CHUNKSIZE;
  } else {
    moveChunks_.erase_after(moveChunks_.begin(), moveChunks_.end());
    moveChunkUsed_ = 0;
  }
  StartPos = nullptr;
  CurrentPos->StdStart();

  // Initialize FirstMove: start and end of movelist markers
  FirstMove = NewMove(START_MARKER);
  CurrentMove = NewMove(END_MARKER);
  FirstMove->setNext(CurrentMove);

  VarDepth = 0;
  NumHalfMoves = 0;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::Clear():
//      Reset the game to its normal empty state.
//
void Game::Clear() {
  extraTags_.clear();
  WhiteStr.clear();
  BlackStr.clear();
  EventStr.clear();
  SiteStr.clear();
  RoundStr.clear();
  Date = ZERO_DATE;
  EventDate = ZERO_DATE;
  EcoCode = 0;
  WhiteElo = BlackElo = 0;
  WhiteRatingType = BlackRatingType = RATING_Elo;
  Result = RESULT_None;
  ScidFlags[0] = 0;

  NumMovesPrinted = 0;
  PgnStyle = PGN_STYLE_TAGS | PGN_STYLE_VARS | PGN_STYLE_COMMENTS;
  PgnFormat = PGN_FORMAT_Plain;
  HtmlStyle = 0;

  ClearMoves();
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::PgnFormatFromString():
//      Converts a string to a gameFormatT, returning true on success
//      or false on error.
//      The string should be a case-insensitive unique prefix of
//      "plain" (or "pgn"), "HTML", "LaTeX" or "Color".
bool Game::PgnFormatFromString(const char *str, gameFormatT *fmt) {
  if (strIsCasePrefix(str, "Plain")) {
    *fmt = PGN_FORMAT_Plain;
  } else if (strIsCasePrefix(str, "PGN")) {
    *fmt = PGN_FORMAT_Plain;
  } else if (strIsCasePrefix(str, "HTML")) {
    *fmt = PGN_FORMAT_HTML;
  } else if (strIsCasePrefix(str, "Color")) {
    *fmt = PGN_FORMAT_Color;
  } else {
    return false;
  }
  return true;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::SetPgnFormatFromString():
//      Sets the PgnFormat from the provided string.
//      Returns true if the PgnFormat was successfully set.
bool Game::SetPgnFormatFromString(const char *str) {
  return PgnFormatFromString(str, &PgnFormat);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::FindExtraTag():
//   Finds and returns an extra PGN tag if it
//   exists, or NULL if it does not exist.
const char *Game::FindExtraTag(const char *tag) const {
  for (auto &e : extraTags_) {
    if (e.first == tag)
      return e.second.c_str();
  }
  return NULL;
}

std::string_view Game::GetResultStr() const {
  using namespace std::literals;
  static std::string_view res[] = {"*"sv, "1-0"sv, "0-1"sv, "1/2-1/2"sv};
  return res[Result];
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::SetMoveComment():
//      Sets the comment for a move. A comment before the game itself
//      is stored as a comment of FirstMove.
//
void Game::SetMoveComment(const char *comment) {
  ASSERT(CurrentMove != NULL && CurrentMove->prev != NULL);
  moveT *m = CurrentMove->prev;
  if (comment == NULL) {
    m->comment.clear();
  } else {
    m->comment = comment;
    // CommentsFlag = 1;
  }
}

int Game::setRating(colorT col, const char *ratingType, size_t ratingTypeLen,
                    std::pair<const char *, const char *> rating) {
  auto begin = ratingTypeNames;
  const size_t ratingSz = 7;
  auto it = std::find_if(begin, begin + ratingSz, [&](auto rType) {
    return std::equal(ratingType, ratingType + ratingTypeLen, rType,
                      rType + std::strlen(rType));
  });
  byte rType = static_cast<byte>(std::distance(begin, it));
  if (rType >= ratingSz)
    return -1;

  int res = 1;
  auto elo = strGetUnsigned(std::string{rating.first, rating.second}.c_str());
  if (elo > MAX_ELO) {
    elo = 0;
    res = 0;
  }
  if (col == WHITE) {
    SetWhiteElo(static_cast<eloT>(elo));
    SetWhiteRatingType(rType);
  } else {
    SetBlackElo(static_cast<eloT>(elo));
    SetBlackRatingType(rType);
  }
  return res;
}

///////////////////////////////////////////////////////////////////////////
// A "location" in the game is represented by a position (Game::CurrentPos), the
// next move to be played (Game::CurrentMove) and the number of parent
// variations (Game::VarDepth). Since CurrentMove is the next move to be played,
// some invariants must hold: it is never nullptr and it never points to a
// START_MARKER (it will point to a END_MARKER if there are no more moves). This
// also means that CurrentMove->prev is always valid: it will point to a
// previous move or to a START_MARKER.
// The following functions modify ONLY the current location of the game.

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Move current position forward one move.
// Also update all the necessary fields in the simpleMove structure
// (CurrentMove->moveData) so it can be undone.
//
errorT Game::MoveForward(void) {
  if (CurrentMove->endMarker())
    return ERROR_EndOfMoveList;

  CurrentPos->DoSimpleMove(CurrentMove->moveData);
  CurrentMove = CurrentMove->next;

  // Invariants
  ASSERT(CurrentMove && CurrentMove->prev);
  ASSERT(!CurrentMove->startMarker());
  return OK;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::MoveBackup():
//      Backup one move.
//
errorT Game::MoveBackup(void) {
  if (CurrentMove->prev->startMarker())
    return ERROR_StartOfMoveList;

  CurrentMove = CurrentMove->prev;
  CurrentPos->UndoSimpleMove(CurrentMove->moveData);

  // Invariants
  ASSERT(CurrentMove && CurrentMove->prev);
  ASSERT(!CurrentMove->startMarker());
  return OK;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::MoveIntoVariation():
//      Move into a subvariation. Variations are numbered from 0.
errorT Game::MoveIntoVariation(uint varNumber) {
  for (auto subVar = CurrentMove; subVar->varChild; --varNumber) {
    subVar = subVar->varChild;
    if (varNumber == 0) {
      CurrentMove = subVar->next; // skip the START_MARKER
      ++VarDepth;

      // Invariants
      ASSERT(CurrentMove && CurrentMove->prev);
      ASSERT(!CurrentMove->startMarker());
      return OK;
    }
  }
  return ERROR_NoVariation; // there is no such variation
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::MoveExitVariation():
//      Move out of a variation, to the parent.
//
errorT Game::MoveExitVariation(void) {
  if (VarDepth == 0) // not in a variation!
    return ERROR_NoVariation;

  // Algorithm: go back previous moves as far as possible, then
  // go up to the parent of the variation.
  while (MoveBackup() == OK) {
  }
  CurrentMove = CurrentMove->getParent().first;
  --VarDepth;

  // Invariants
  ASSERT(CurrentMove && CurrentMove->prev);
  ASSERT(!CurrentMove->startMarker());
  return OK;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Move to the beginning of the game.
//
void Game::MoveToStart() {
  if (StartPos) {
    *CurrentPos = *StartPos;
  } else {
    CurrentPos->StdStart();
  }
  VarDepth = 0;
  CurrentMove = FirstMove->next;

  // Invariants
  ASSERT(CurrentMove && CurrentMove->prev);
  ASSERT(!CurrentMove->startMarker());
}

void Game::MoveToEnd() {
  MoveToStart();
  while (MoveForward() == OK) {
  }
}

errorT Game::MoveForwardInPGN() {
  if (CurrentMove->prev->varChild && MoveBackup() == OK)
    return MoveIntoVariation(0);

  while (MoveForward() != OK) {
    if (VarDepth == 0)
      return ERROR_EndOfMoveList;

    auto varnum = GetVarNumber();
    MoveExitVariation();
    if (MoveIntoVariation(varnum + 1) == OK)
      return OK;

    MoveForward();
  }
  return OK;
}

errorT Game::MoveToLocationInPGN(unsigned stopLocation) {
  MoveToStart();
  for (unsigned loc = 1; loc < stopLocation; ++loc) {
    errorT err = MoveForwardInPGN();
    if (err != OK)
      return err;
  }
  return OK;
}

unsigned Game::GetLocationInPGN() const {
  unsigned res = 1;
  const moveT *last_move = CurrentMove->prev;
  const moveT *move = FirstMove;
  for (; move != last_move; move = move->nextMoveInPGN()) {
    if (!move->endMarker())
      ++res;
  }
  return res;
}

unsigned Game::GetPgnOffset() const {
  unsigned res = 1;
  const moveT *last_move = CurrentMove->getPrevMove();
  if (last_move) {
    const moveT *move = FirstMove;
    for (; move != last_move; move = move->nextMoveInPGN()) {
      if (!move->endMarker())
        ++res;
    }
  }
  return res;
}

std::string Game::currentPosUCI() const {
  std::string res = "position startpos moves";
  char FEN[256] = {};

  std::vector<const moveT *> moves;
  const moveT *move = CurrentMove;
  while ((move = move->getPrevMove())) {
    if (move->moveData.isNullMove()) {
      Position lastValidPos = *currentPos();
      for (const moveT *m : moves) {
        lastValidPos.UndoSimpleMove(m->moveData);
      }
      lastValidPos.PrintFEN(FEN);
      break;
    }
    moves.emplace_back(move);
  }

  if (*FEN || HasNonStandardStart(FEN)) {
    res.replace(9, 4, "fen ");
    res.replace(13, 4, FEN);
  }

  const auto allocSpeedup = res.size();
  res.resize(allocSpeedup + moves.size() * 6);
  auto it = res.data() + allocSpeedup;
  for (auto m = moves.crbegin(), end = moves.crend(); m != end; ++m) {
    *it++ = ' ';
    it = (*m)->moveData.toLongNotation(it);
  }
  res.resize(std::distance(res.data(), it)); // shrink
  return res;
}

std::vector<std::string> Game::mainLineUCI() const {
  std::vector<std::string> res;
  char buf[256] = {};
  if (HasNonStandardStart(buf)) {
    res.push_back(std::string("position fen ") + buf + " moves");
  } else {
    res.push_back("position startpos moves");
  }
  auto it_pos = std::unique_ptr<Position>{};
  auto it = FirstMove->Next();
  for (auto m = it; !m->endMarker(); m = m->Next()) {
    if (m->isNull()) {
      if (!it_pos) {
        it_pos = std::make_unique<Position>(StartPos ? *StartPos
                                                     : Position::getStdStart());
      }
      for (auto end = m->Next(); it != end; it = it->Next()) {
        it_pos->DoSimpleMove(it->moveData);
      }
      it_pos->PrintFEN(buf);
      res.push_back(std::string("position fen ") + buf + " moves");
    } else {
      auto end = m->moveData.toLongNotation(buf);
      res.emplace_back(buf, end);
    }
  }
  return res;
}

///////////////////////////////////////////////////////////////////////////
// The following functions modify the moves graph
// moves. Promoting variations also modifies the moves graph.

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::AddMove():
//      Add a move at current position and do it.
//
errorT Game::AddMove(simpleMoveT const &sm) {
  // We must be at the end of a game/variation to add a move:
  if (!CurrentMove->endMarker())
    Truncate();

  CurrentMove->setNext(NewMove(END_MARKER));
  CurrentMove->marker = NO_MARKER;
  CurrentMove->moveData = sm;
  if (VarDepth == 0)
    ++NumHalfMoves;

  return MoveForward();
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::AddVariation():
//      Add a variation for the current move.
//      Also moves into the variation.
errorT Game::AddVariation() {
  auto err = MoveBackup();
  if (err != OK)
    return err;

  auto newVar = NewMove(START_MARKER);
  newVar->setNext(NewMove(END_MARKER));
  CurrentMove->appendChild(newVar);

  // Move into variation
  CurrentMove = newVar->next;
  ++VarDepth;

  // Invariants
  ASSERT(CurrentMove && CurrentMove->prev);
  ASSERT(!CurrentMove->startMarker());
  return OK;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::FirstVariation():
// Promotes the current variation to first variation.
errorT Game::FirstVariation() {
  auto parent = CurrentMove->getParent();
  auto root = parent.first;
  if (!root)
    return ERROR_NoVariation;

  root->detachChild(parent.second);
  root->insertChild(parent.second, 0);
  return OK;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::MainVariation():
//    Like FirstVariation, but promotes the variation to the main line,
//    demoting the main line to be the first variation.
errorT Game::MainVariation() {
  auto parent = CurrentMove->getParent();
  auto root = parent.first;
  if (!root)
    return ERROR_NoVariation;
  if (parent.second->next->endMarker()) // Do not promote empty variations
    return OK;

  // Make the current variation the first variation
  root->detachChild(parent.second);
  root->insertChild(parent.second, 0);

  // Swap the mainline with the current variation
  root->swapLine(*parent.second->next);

  ASSERT(VarDepth);
  if (--VarDepth == 0) { // Recalculate NumHalfMoves
    const auto count_moves = [](auto move) {
      int res = 0;
      while (!move->endMarker()) {
        ++res;
        move = move->next;
      }
      return res;
    };
    ASSERT(FirstMove->startMarker() && FirstMove->next);
    NumHalfMoves = count_moves(FirstMove->next);
  }

  return OK;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::DeleteVariation():
//      Deletes a variation. Variations are numbered from 0.
//      Note that for speed and simplicity, freed moves are not
//      added to the free list. This means that repeatedly adding and
//      deleting variations will waste memory until the game is cleared.
//
errorT Game::DeleteVariation() {
  auto parent = CurrentMove->getParent();
  auto root = parent.first;
  if (!root || MoveExitVariation() != OK)
    return ERROR_NoVariation;

  root->detachChild(parent.second);
  return OK;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::Truncate():
//      Truncate game at the current move.
//      For speed and simplicity, moves and comments are not freed.
//      So repeatedly adding moves and truncating a game will waste
//      memory until the game is cleared.
void Game::Truncate() {
  if (CurrentMove->endMarker())
    return;

  auto endMove = NewMove(END_MARKER);
  CurrentMove->prev->setNext(endMove);

  CurrentMove = endMove;
  if (VarDepth == 0)
    NumHalfMoves = GetCurrentPly();

  // Invariants
  ASSERT(CurrentMove && CurrentMove->prev);
  ASSERT(!CurrentMove->startMarker());
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::TruncateStart():
//      Truncate all moves leading to current position.
void Game::TruncateStart() {
  // It is necessary to rebuild the current position using ReadFromFEN()
  // because the order of pieces is important when encoding to SCIDv4 format.
  char tempStr[256];
  CurrentPos->PrintFEN(tempStr);
  auto pos = std::make_unique<Position>();
  if (pos->ReadFromFEN(tempStr) != OK)
    return;

  if (VarDepth != 0 && MainVariation() != OK)
    return;

  NumHalfMoves -= GetCurrentPly();
  StartPos = std::move(pos);
  *CurrentPos = *StartPos;
  FirstMove->setNext(CurrentMove);

  // Do all the moves to update moveData.pieceNum to the new StartPos
  do {
    if (!CurrentMove->startMarker() && !CurrentMove->endMarker()) {
      CurrentPos->fillMove(CurrentMove->moveData);
    }
  } while (MoveForwardInPGN() == OK);
  MoveToStart();
}

namespace {
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// calcHomePawnMask():
//      Computes the homePawn mask for a position.
//
int calcHomePawnMask(pieceT pawn, const pieceT *board) {
  ASSERT(pawn == WP || pawn == BP);
  const pieceT *bd = &(board[(pawn == WP ? H2 : H7)]);
  int result = 0;
  if (*bd == pawn) {
    result |= 128;
  }
  bd--; // H-fyle pawn
  if (*bd == pawn) {
    result |= 64;
  }
  bd--; // G-fyle pawn
  if (*bd == pawn) {
    result |= 32;
  }
  bd--; // F-fyle pawn
  if (*bd == pawn) {
    result |= 16;
  }
  bd--; // E-fyle pawn
  if (*bd == pawn) {
    result |= 8;
  }
  bd--; // D-fyle pawn
  if (*bd == pawn) {
    result |= 4;
  }
  bd--; // C-fyle pawn
  if (*bd == pawn) {
    result |= 2;
  }
  bd--; // B-fyle pawn
  if (*bd == pawn) {
    result |= 1;
  } // A-fyle pawn
  return result;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// patternsMatch():
//      Used by Game::MaterialMatch() to test patterns.
//      Returns 1 if all the patterns in the list match, 0 otherwise.
//
int patternsMatch(const Position *pos, patternT *ptn, size_t ptn_size) {
  const pieceT *board = pos->GetBoard();
  for (auto ptn_end = ptn + ptn_size; ptn != ptn_end; ++ptn) {
    if (ptn->rankMatch == NO_RANK) {

      if (ptn->fyleMatch == NO_FYLE) { // Nothing to test!
      } else {                         // Test this fyle:
        squareT sq = square_Make(ptn->fyleMatch, RANK_1);
        int found = 0;
        for (uint i = 0; i < 8; i++, sq += 8) {
          if (board[sq] == ptn->pieceMatch) {
            found = 1;
            break;
          }
        }
        if (found != ptn->flag) {
          return 0;
        }
      }

    } else { // rankMatch is a rank from 1 to 8:

      if (ptn->fyleMatch == NO_FYLE) { // Test the whole rank:
        int found = 0;
        squareT sq = square_Make(A_FYLE, ptn->rankMatch);
        for (uint i = 0; i < 8; i++, sq++) {
          if (board[sq] == ptn->pieceMatch) {
            found = 1;
            break;
          }
        }
        if (found != ptn->flag) {
          return 0;
        }
      } else { // Just test one square:
        squareT sq = square_Make(ptn->fyleMatch, ptn->rankMatch);
        int found = 0;
        if (board[sq] == ptn->pieceMatch) {
          found = 1;
        }
        if (found != ptn->flag) {
          return 0;
        }
      }
    }
  }

  // If we reach here, all patterns matched:
  return 1;
}
} // end of anonymous namespace

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::MaterialMatch(): Material search test.
//      The parameters min and max should each be an array of 15
//      counts, to specify the maximum and minimum number of counts
//      of each type of piece.
//
bool Game::MaterialMatch(bool PromotionsFlag, ByteBuffer &buf, byte *min,
                         byte *max, patternT *patterns, size_t ptn_size,
                         int minPly, int maxPly, int matchLength,
                         bool oppBishops, bool sameBishops, int minDiff,
                         int maxDiff) {
  ASSERT(matchLength >= 1);

  int matchesNeeded = matchLength;
  int matDiff;
  uint plyCount = 0;
  errorT err = DecodeSkipTags(&buf);
  while (err == OK) {
    bool foundMatch = false;
    byte wMinor, bMinor;

    // If current pos has LESS than the minimum of pawns, this
    // game can never match so return false;
    if (CurrentPos->PieceCount(WP) < min[WP]) {
      return false;
    }
    if (CurrentPos->PieceCount(BP) < min[BP]) {
      return false;
    }

    // If not in the valid move range, go to the next move or return:
    if ((int)plyCount > maxPly) {
      return false;
    }
    if ((int)plyCount < minPly) {
      goto Next_Move;
    }

    // For these comparisons, we really could only do half of them each move,
    // according to which side just moved.
    // For non-pawns, the count could be increased by promotions:
    if (CurrentPos->PieceCount(WQ) < min[WQ]) {
      goto Check_Promotions;
    }
    if (CurrentPos->PieceCount(BQ) < min[BQ]) {
      goto Check_Promotions;
    }
    if (CurrentPos->PieceCount(WR) < min[WR]) {
      goto Check_Promotions;
    }
    if (CurrentPos->PieceCount(BR) < min[BR]) {
      goto Check_Promotions;
    }
    if (CurrentPos->PieceCount(WB) < min[WB]) {
      goto Check_Promotions;
    }
    if (CurrentPos->PieceCount(BB) < min[BB]) {
      goto Check_Promotions;
    }
    if (CurrentPos->PieceCount(WN) < min[WN]) {
      goto Check_Promotions;
    }
    if (CurrentPos->PieceCount(BN) < min[BN]) {
      goto Check_Promotions;
    }
    wMinor = CurrentPos->PieceCount(WB) + CurrentPos->PieceCount(WN);
    bMinor = CurrentPos->PieceCount(BB) + CurrentPos->PieceCount(BN);
    if (wMinor < min[WM]) {
      goto Check_Promotions;
    }
    if (bMinor < min[BM]) {
      goto Check_Promotions;
    }

    // Now test maximum counts:
    if (CurrentPos->PieceCount(WQ) > max[WQ]) {
      goto Next_Move;
    }
    if (CurrentPos->PieceCount(BQ) > max[BQ]) {
      goto Next_Move;
    }
    if (CurrentPos->PieceCount(WR) > max[WR]) {
      goto Next_Move;
    }
    if (CurrentPos->PieceCount(BR) > max[BR]) {
      goto Next_Move;
    }
    if (CurrentPos->PieceCount(WB) > max[WB]) {
      goto Next_Move;
    }
    if (CurrentPos->PieceCount(BB) > max[BB]) {
      goto Next_Move;
    }
    if (CurrentPos->PieceCount(WN) > max[WN]) {
      goto Next_Move;
    }
    if (CurrentPos->PieceCount(BN) > max[BN]) {
      goto Next_Move;
    }
    if (CurrentPos->PieceCount(WP) > max[WP]) {
      goto Next_Move;
    }
    if (CurrentPos->PieceCount(BP) > max[BP]) {
      goto Next_Move;
    }
    if (wMinor > max[WM]) {
      goto Next_Move;
    }
    if (bMinor > max[BM]) {
      goto Next_Move;
    }

    // If both sides have ONE bishop, we need to check if the search
    // was restricted to same-color or opposite-color bishops:
    if (CurrentPos->PieceCount(WB) == 1 && CurrentPos->PieceCount(BB) == 1) {
      if (!oppBishops || !sameBishops) { // Check the restriction:
        colorT whiteBishCol = NOCOLOR;
        colorT blackBishCol = NOCOLOR;

        // Search for the white and black bishop, to find their
        // square color:
        const pieceT *bd = CurrentPos->GetBoard();
        for (squareT sq = A1; sq <= H8; sq++) {
          if (bd[sq] == WB) {
            whiteBishCol = BOARD_SQUARECOLOR[sq];
          } else if (bd[sq] == BB) {
            blackBishCol = BOARD_SQUARECOLOR[sq];
          }
        }
        // They should be valid colors:
        ASSERT(blackBishCol != NOCOLOR && whiteBishCol != NOCOLOR);

        // If the square colors do not match the restriction,
        // then this game cannot match:
        if (oppBishops && blackBishCol == whiteBishCol) {
          return false;
        }
        if (sameBishops && blackBishCol != whiteBishCol) {
          return false;
        }
      }
    }

    // Now check if the material difference is in-range:
    matDiff = (int)CurrentPos->MaterialValue(WHITE) -
              (int)CurrentPos->MaterialValue(BLACK);
    if (matDiff < minDiff || matDiff > maxDiff) {
      goto Next_Move;
    }

    // At this point, the Material matches; do the patterns match?
    if (ptn_size == 0 || patternsMatch(currentPos(), patterns, ptn_size)) {
      foundMatch = true;
      matchesNeeded--;
      if (matchesNeeded <= 0) {
        return true;
      }
    }
    // No? well, keep trying...
    goto Next_Move;

  Check_Promotions:
    // We only continue if this game has promotion moves:
    if (!PromotionsFlag) {
      return false;
    }

  Next_Move: {
    simpleMoveT sm;
    err = DecodeNextMove(&buf, sm);
    if (err == OK) {
      CurrentPos->DoSimpleMove(sm);
    }
  }
    plyCount++;
    if (!foundMatch) {
      matchesNeeded = matchLength;
    }
  }

  // End of game reached, and no match:
  return false;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::ExactMatch():
//      Exact position search test.
//      If sm is not NULL, its from, to, promote etc will be filled with
//      the next move at the matching position, if there is one.
//      If neverMatch is non-NULL, the boolean it points to is set to
//      true if the game could never match even with extra moves.
//
bool Game::ExactMatch(Position *searchPos, ByteBuffer *buf,
                      gameExactMatchT searchType) {
  // If buf is NULL, the game is in memory. Otherwise, Decode only
  // the necessary moves:
  errorT err = OK;

  if (buf == NULL) {
    MoveToStart();
  } else {
    err = DecodeSkipTags(buf);
  }

  uint search_whiteHPawns = 0;
  uint search_blackHPawns = 0;
  bool check_pawnMaskWhite, check_pawnMaskBlack;
  bool doHomePawnChecks = false;

  uint wpawnFyle[8] = {0, 0, 0, 0, 0, 0, 0, 0};
  uint bpawnFyle[8] = {0, 0, 0, 0, 0, 0, 0, 0};
  ;

  if (searchType == GAME_EXACT_MATCH_Fyles) {
    const pieceT *board = searchPos->GetBoard();
    uint fyle = 0;
    for (squareT sq = A1; sq <= H8; sq++, board++) {
      if (*board == WP) {
        wpawnFyle[fyle]++;
      } else if (*board == BP) {
        bpawnFyle[fyle]++;
      }
      fyle = (fyle + 1) & 7;
    }
  }

  if (searchType == GAME_EXACT_MATCH_Exact ||
      searchType == GAME_EXACT_MATCH_Pawns) {
    doHomePawnChecks = true;
    search_whiteHPawns = calcHomePawnMask(WP, searchPos->GetBoard());
    search_blackHPawns = calcHomePawnMask(BP, searchPos->GetBoard());
  }
  check_pawnMaskWhite = check_pawnMaskBlack = false;

  while (err == OK) {
    const pieceT *currentBoard = CurrentPos->GetBoard();
    const pieceT *board = searchPos->GetBoard();
    const pieceT *b1 = currentBoard;
    const pieceT *b2 = board;

    // If NO_SPEEDUPS is defined, a slower search is done without
    // optimisations that detect insufficient material.
#ifndef NO_SPEEDUPS
    // Insufficient material optimisation:
    if (searchPos->GetCount(WHITE) > CurrentPos->GetCount(WHITE) ||
        searchPos->GetCount(BLACK) > CurrentPos->GetCount(BLACK)) {
      return false;
    }
    // Insufficient pawns optimisation:
    if (searchPos->PieceCount(WP) > CurrentPos->PieceCount(WP) ||
        searchPos->PieceCount(BP) > CurrentPos->PieceCount(BP)) {
      return false;
    }

    // HomePawn mask optimisation:
    // If current pos doesn't have a pawn on home rank where
    // the search pos has one, it can never match.
    // This happens when (current_xxHPawns & search_xxHPawns) is
    // not equal to search_xxHPawns.
    // We do not do this optimisation for a pawn files search,
    // because the exact pawn squares are not important there.
    if (check_pawnMaskWhite) {
      auto current_whiteHPawns = calcHomePawnMask(WP, currentBoard);
      if ((current_whiteHPawns & search_whiteHPawns) != search_whiteHPawns) {
        return false;
      }
      check_pawnMaskWhite = false;
    }
    if (check_pawnMaskBlack) {
      auto current_blackHPawns = calcHomePawnMask(BP, currentBoard);
      if ((current_blackHPawns & search_blackHPawns) != search_blackHPawns) {
        return false;
      }
      check_pawnMaskBlack = false;
    }
#endif // #ifndef NO_SPEEDUPS
    bool found = true;

    // Not correct color: skip to next move
    if (searchPos->GetToMove() != CurrentPos->GetToMove()) {
      // skip++;
      goto Move_Forward;
    }

    // Extra material: skip to next move
    if (searchPos->GetCount(WHITE) < CurrentPos->GetCount(WHITE) ||
        searchPos->GetCount(BLACK) < CurrentPos->GetCount(BLACK)) {
      // skip++;
      goto Move_Forward;
    }
    // Extra pawns/pieces: skip to next move
    if (searchPos->PieceCount(WP) != CurrentPos->PieceCount(WP) ||
        searchPos->PieceCount(BP) != CurrentPos->PieceCount(BP) ||
        searchPos->PieceCount(WN) != CurrentPos->PieceCount(WN) ||
        searchPos->PieceCount(BN) != CurrentPos->PieceCount(BN) ||
        searchPos->PieceCount(WB) != CurrentPos->PieceCount(WB) ||
        searchPos->PieceCount(BB) != CurrentPos->PieceCount(BB) ||
        searchPos->PieceCount(WR) != CurrentPos->PieceCount(WR) ||
        searchPos->PieceCount(BR) != CurrentPos->PieceCount(BR) ||
        searchPos->PieceCount(WQ) != CurrentPos->PieceCount(WQ) ||
        searchPos->PieceCount(BQ) != CurrentPos->PieceCount(BQ)) {
      // skip++;
      goto Move_Forward;
    }

    // NOW, compare the actual boards piece-by-piece.
    if (searchType == GAME_EXACT_MATCH_Exact) {
      if (searchPos->HashValue() == CurrentPos->HashValue()) {
        for (squareT sq = A1; sq <= H8; sq++, b1++, b2++) {
          if (*b1 != *b2) {
            found = false;
            break;
          }
        }
      } else {
        found = false;
      }
    } else if (searchType == GAME_EXACT_MATCH_Pawns) {
      if (searchPos->PawnHashValue() == CurrentPos->PawnHashValue()) {
        for (squareT sq = A1; sq <= H8; sq++, b1++, b2++) {
          if (*b1 != *b2 && (*b1 == WP || *b1 == BP)) {
            found = false;
            break;
          }
        }
      } else {
        found = false;
      }
    } else if (searchType == GAME_EXACT_MATCH_Fyles) {
      for (fyleT f = A_FYLE; f <= H_FYLE; f++) {
        if (searchPos->FyleCount(WP, f) != CurrentPos->FyleCount(WP, f) ||
            searchPos->FyleCount(BP, f) != CurrentPos->FyleCount(BP, f)) {
          found = false;
          break;
        }
      }
    } else {
      // searchType == GAME_EXACT_Match_Material, so do nothing.
    }

    if (found) {
      return true;
    }

  Move_Forward:
    if (buf == NULL) {
      err = MoveForward();
    } else {
      simpleMoveT nextMove;
      err = DecodeNextMove(buf, nextMove);
      if (err == OK) {
        CurrentPos->DoSimpleMove(nextMove);
        if (doHomePawnChecks) {
          rankT rTo = square_Rank(nextMove.to);
          rankT rFrom = square_Rank(nextMove.from);
          // We only re-check the home pawn masks when something moves
          // to or from the 2nd/7th rank:
          if (rTo == RANK_2 || rFrom == RANK_2) {
            check_pawnMaskWhite = true;
          }
          if (rTo == RANK_7 || rFrom == RANK_7) {
            check_pawnMaskBlack = true;
          }
        }
      }
    }
  }
  return false;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::VarExactMatch():
//    Like ExactMatch(), but also searches in variations.
//    This is much slower than ExactMatch(), since it will
//    search every position until a match is found.
bool Game::VarExactMatch(Position *searchPos, gameExactMatchT searchType) {
  uint wpawnFyle[8] = {0, 0, 0, 0, 0, 0, 0, 0};
  uint bpawnFyle[8] = {0, 0, 0, 0, 0, 0, 0, 0};
  ;

  if (searchType == GAME_EXACT_MATCH_Fyles) {
    const pieceT *board = searchPos->GetBoard();
    uint fyle = 0;
    for (squareT sq = A1; sq <= H8; sq++, board++) {
      if (*board == WP) {
        wpawnFyle[fyle]++;
      } else if (*board == BP) {
        bpawnFyle[fyle]++;
      }
      fyle = (fyle + 1) & 7;
    }
  }

  errorT err = OK;
  while (err == OK) {
    // Check if this position matches:
    bool match = false;
    if (searchPos->GetToMove() == CurrentPos->GetToMove() &&
        searchPos->GetCount(WHITE) == CurrentPos->GetCount(WHITE) &&
        searchPos->GetCount(BLACK) == CurrentPos->GetCount(BLACK) &&
        searchPos->PieceCount(WP) == CurrentPos->PieceCount(WP) &&
        searchPos->PieceCount(BP) == CurrentPos->PieceCount(BP) &&
        searchPos->PieceCount(WN) == CurrentPos->PieceCount(WN) &&
        searchPos->PieceCount(BN) == CurrentPos->PieceCount(BN) &&
        searchPos->PieceCount(WB) == CurrentPos->PieceCount(WB) &&
        searchPos->PieceCount(BB) == CurrentPos->PieceCount(BB) &&
        searchPos->PieceCount(WR) == CurrentPos->PieceCount(WR) &&
        searchPos->PieceCount(BR) == CurrentPos->PieceCount(BR) &&
        searchPos->PieceCount(WQ) == CurrentPos->PieceCount(WQ) &&
        searchPos->PieceCount(BQ) == CurrentPos->PieceCount(BQ)) {
      match = true;
      const pieceT *b1 = CurrentPos->GetBoard();
      const pieceT *b2 = searchPos->GetBoard();
      if (searchType == GAME_EXACT_MATCH_Pawns) {
        for (squareT sq = A1; sq <= H8; sq++, b1++, b2++) {
          if (*b1 != *b2 && (*b1 == WP || *b1 == BP)) {
            match = false;
            break;
          }
        }
      } else if (searchType == GAME_EXACT_MATCH_Fyles) {
        uint wpf[8] = {0, 0, 0, 0, 0, 0, 0, 0};
        uint bpf[8] = {0, 0, 0, 0, 0, 0, 0, 0};
        uint fyle = 0;
        for (squareT sq = A1; sq <= H8; sq++, b1++) {
          if (*b1 == WP) {
            wpf[fyle]++;
            if (wpf[fyle] > wpawnFyle[fyle]) {
              match = false;
              break;
            }
          } else if (*b1 == BP) {
            bpf[fyle]++;
            if (bpf[fyle] > bpawnFyle[fyle]) {
              match = false;
              break;
            }
          }
          fyle = (fyle + 1) & 7;
        }
      } else if (searchType == GAME_EXACT_MATCH_Exact) {
        if (searchPos->HashValue() == CurrentPos->HashValue()) {
          for (squareT sq = A1; sq <= H8; sq++, b1++, b2++) {
            if (*b1 != *b2) {
              match = false;
              break;
            }
          }
        } else {
          match = false;
        }
      } else {
        // searchType == GAME_EXACT_MATCH_Material, so do nothing.
      }
    }
    if (match) {
      return true;
    }

    // Now try searching each variation in turn:
    for (uint i = 0; i < CurrentMove->numVariations; i++) {
      MoveIntoVariation(i);
      match = VarExactMatch(searchPos, searchType);
      MoveExitVariation();
      if (match) {
        return true;
      }
    }
    // Continue down this variation:
    MoveForward();
    if (CurrentMove->marker == END_MARKER) {
      err = ERROR_EndOfMoveList;
    }
  }
  return false;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::GetPartialMoveList():
//      Write the first few moves of a game.
//
errorT Game::GetPartialMoveList(DString *outStr, uint plyCount) {
  // First, copy the relevant data so we can leave the game state
  // unaltered:
  auto location = currentLocation();

  MoveToStart();
  char temp[80];
  for (uint i = 0; i < plyCount; i++) {
    if (CurrentMove->marker == END_MARKER) {
      break;
    }
    if (i != 0) {
      outStr->Append(" ");
    }
    if (i == 0 || CurrentPos->GetToMove() == WHITE) {
      sprintf(temp, "%d%s", CurrentPos->GetFullMoveCount(),
              (CurrentPos->GetToMove() == WHITE ? "." : "..."));
      outStr->Append(temp);
    }
    moveT *m = CurrentMove;
    if (m->san[0] == 0) {
      CurrentPos->MakeSANString(&(m->moveData), m->san, SAN_CHECKTEST);
    }
    // add one space for indenting to work out right
    outStr->Append(" ");
    outStr->Append(m->san);
    MoveForward();
  }

  // Now reconstruct the original game state:
  restoreLocation(location);
  return OK;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Returns the SAN representation of the next move or an empty string ("") if
// not at a move.
const char *Game::GetNextSAN() {
  ASSERT(!CurrentMove->endMarker() || *CurrentMove->san == '\0');

  if (!CurrentMove->endMarker() && *CurrentMove->san == '\0') {
    CurrentPos->MakeSANString(&CurrentMove->moveData, CurrentMove->san,
                              CurrentMove->next->endMarker() ? SAN_MATETEST
                                                             : SAN_CHECKTEST);
  }
  return CurrentMove->san;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::GetSAN():
//      Print the SAN representation of the current move to a string.
//      Prints an empty string ("") if not at a move.
void Game::GetSAN(char *str) {
  ASSERT(str != NULL);
  strcpy(str, GetNextSAN());
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::GetPrevSAN():
//      Print the SAN representation of the previous move to a string.
//      Prints an empty string ("") if not at a move.
void Game::GetPrevSAN(char *str) {
  ASSERT(str != NULL);
  moveT *m = CurrentMove->prev;
  if (m->startMarker() || m->endMarker()) {
    str[0] = 0;
    return;
  }
  if (m->san[0] == 0) {
    MoveBackup();
    CurrentPos->MakeSANString(&(m->moveData), m->san, SAN_MATETEST);
    MoveForward();
  }
  strcpy(str, m->san);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::GetPrevMoveUCI():
//      Print the UCI representation of the current move to a string.
//      Prints an empty string ("") if not at a move.
void Game::GetPrevMoveUCI(char *str) const {
  ASSERT(str != NULL);
  const auto m = CurrentMove->prev;
  if (!m->startMarker())
    str = m->moveData.toLongNotation(str);

  *str = '\0';
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::GetNextMoveUCI():
//      Print the UCI representation of the next move to a string.
//      Prints an empty string ("") if not at a move.
void Game::GetNextMoveUCI(char *str) {
  ASSERT(str != NULL);
  if (!CurrentMove->endMarker())
    str = CurrentMove->moveData.toLongNotation(str);

  *str = '\0';
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// writeComment:
//    Called by WriteMoveList to write a single comment.
void Game::WriteComment(TextBuffer *tb, const char *preStr, const char *comment,
                        const char *postStr) {
  const char *s = comment;
  if (s[0] != '\0') {

    if (IsColorFormat()) {
      tb->PrintString("<c_");
      tb->PrintInt(NumMovesPrinted);
      tb->PrintChar('>');
    }

    if (IsColorFormat()) {
      // Translate "<", ">" in comments:
      tb->AddTranslation('<', "<lt>");
      tb->AddTranslation('>', "<gt>");
      // S.A any issues ?
      tb->NewlinesToSpaces(0);
      tb->PrintString(s);
      tb->ClearTranslation('<');
      tb->ClearTranslation('>');
    } else {
      tb->PrintString(preStr);
      tb->PrintString(s);
      tb->PrintString(postStr);
    }

    if (IsColorFormat()) {
      tb->PrintString("</c>");
    }
  }
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::WriteMoveList():
//      Write the moves, variations and comments in PGN notation.
//      Recursive; calls itself to write variations.
//
errorT Game::WriteMoveList(TextBuffer *tb, moveT *oldCurrentMove,
                           bool printMoveNum, bool inComment) {
  char tempTrans[10];
  const char *preCommentStr = "{";
  const char *postCommentStr = "}";
  const char *startTable = "\n";
  const char *startColumn = "\t";
  const char *nextColumn = "\t";
  const char *endColumn = "\n";
  const char *endTable = "\n";
  bool printDiagrams = false;

  if (IsHtmlFormat()) {
    preCommentStr = "";
    postCommentStr = "";
    startTable = "<table width=\"50%\">\n";
    startColumn = "<tr align=left>\n  <td width=\"15%\"><b>";
    nextColumn = "</b></td>\n  <td width=\"45%\" align=left><b>";
    endColumn = "</b></td>\n</tr>\n";
    endTable = "</table>\n";
    printDiagrams = true;
  }
  if (IsColorFormat()) {
    startTable = "<br>";
    endColumn = "<br>";
  }

  if (IsHtmlFormat() && VarDepth == 0) {
    tb->PrintString("<b>");
  }
  if ((PgnStyle & PGN_STYLE_COLUMN) && VarDepth == 0) {
    tb->PrintString(startTable);
  }

  if (IsPlainFormat() && inComment) {
    preCommentStr = "";
    postCommentStr = "";
  }
  moveT *m = CurrentMove;

  // Print null moves:
  if ((PgnStyle & PGN_STYLE_NO_NULL_MOVES) && !inComment && IsPlainFormat() &&
      m->isNull()) {
    inComment = true;
    tb->PrintString(preCommentStr);
    preCommentStr = "";
    postCommentStr = "";
  }

  std::string strippedComment;
  // If this is a variation and it starts with a comment, print it:
  if ((VarDepth > 0 || CurrentMove->prev == FirstMove) &&
      (PgnStyle & PGN_STYLE_COMMENTS)) {
    const char *comment = CurrentMove->prev->comment.c_str();
    if (*comment && (PgnStyle & PGN_STYLE_STRIP_MARKS)) {
      strippedComment = comment;
      strTrimMarkCodes(strippedComment.data());
      comment = strippedComment.data();
    }
    if (*comment) {
      WriteComment(tb, preCommentStr, comment, postCommentStr);
      tb->PrintSpace();
      if (!VarDepth) {
        tb->ClearTranslation('\n');
        tb->NewLine();
        if (IsColorFormat()) {
          tb->NewLine();
        }
      }
    }
  }

  while (CurrentMove->marker != END_MARKER) {
    moveT *m = CurrentMove;
    bool commentLine = false;

    if (m->san[0] == 0) {
      // If there is a next move we can skip the SAN_MATETEST
      CurrentPos->MakeSANString(&(m->moveData), m->san,
                                (m->next->marker != END_MARKER) ? SAN_CHECKTEST
                                                                : SAN_MATETEST);
    }

    bool printThisMove = true;
    if (m->isNull()) {
      // Null moves are not printed in LaTeX or HTML:
      if (IsHtmlFormat()) {
        printThisMove = false;
        printMoveNum = true;
      }
      // If Plain PGN format, check whether to convert the
      // null move and remainder of the line to a comment:
      if ((PgnStyle & PGN_STYLE_NO_NULL_MOVES) && IsPlainFormat()) {
        if (!inComment) {
          // Enter inComment mode to convert rest of line
          // to a comment:
          inComment = true;
          tb->PrintString(preCommentStr);
          preCommentStr = "";
          postCommentStr = "";
        }
        printThisMove = false;
        printMoveNum = true;
      }
    }
    int colWidth = 6;
    NumMovesPrinted++;

    if (printThisMove) {
      // Print the move number and following dots if necessary:
      if (IsColorFormat()) {
        tb->PrintString("<m_");
        tb->PrintInt(NumMovesPrinted);
        tb->PrintChar('>');
      }
      if (printMoveNum || (CurrentPos->GetToMove() == WHITE)) {
        if ((PgnStyle & PGN_STYLE_COLUMN) && VarDepth == 0) {
          tb->PrintString(startColumn);
          char temp[10];
          sprintf(temp, "%4u.", CurrentPos->GetFullMoveCount());
          tb->PrintString(temp);
          if (CurrentPos->GetToMove() == BLACK) {
            tb->PauseTranslations();
            tb->PrintString(nextColumn);
            tb->PrintString("...");
            if (IsPlainFormat() || IsColorFormat()) {
              tb->PrintString("        ");
            }
            tb->ResumeTranslations();
          }
        } else {
          if (PgnStyle & PGN_STYLE_MOVENUM_SPACE) {
            tb->PrintInt(CurrentPos->GetFullMoveCount(),
                         (CurrentPos->GetToMove() == WHITE ? "." : ". ..."));
          } else {
            tb->PrintInt(CurrentPos->GetFullMoveCount(),
                         (CurrentPos->GetToMove() == WHITE ? "." : "..."));
          }
          if (PgnStyle & PGN_STYLE_MOVENUM_SPACE) {
            if (PgnStyle & PGN_STYLE_MOVENUM_SPACE) {
              tb->PrintChar(' ');
            }
          }
        }
        printMoveNum = false;
      }

      // Now print the move: only regenerate the SAN string if necessary.

      if ((PgnStyle & PGN_STYLE_COLUMN) && VarDepth == 0) {
        tb->PauseTranslations();
        tb->PrintString(nextColumn);
        tb->ResumeTranslations();
      }
      if (IsColorFormat() && (PgnStyle & PGN_STYLE_UNICODE)) {
        char buf[100];
        char *q = buf;

        for (char const *p = m->san; *p; ++p) {
          ASSERT(q - buf < static_cast<std::ptrdiff_t>(sizeof(buf) - 4));

          switch (*p) {
          case 'K':
            q = std::copy_n("\xe2\x99\x94", 3, q);
            break;
          case 'Q':
            q = std::copy_n("\xe2\x99\x95", 3, q);
            break;
          case 'R':
            q = std::copy_n("\xe2\x99\x96", 3, q);
            break;
          case 'B':
            q = std::copy_n("\xe2\x99\x97", 3, q);
            break;
          case 'N':
            q = std::copy_n("\xe2\x99\x98", 3, q);
            break;
          case 'P':
            q = std::copy_n("\xe2\x99\x99", 3, q);
            break;
          default:
            *q++ = *p;
            break;
          }
        }
        *q = '\0';
        tb->PrintWord(buf);
      } else {
        // translate pieces
        strcpy(tempTrans, m->san);
        transPieces(tempTrans);
        // tb->PrintWord (m->san);
        tb->PrintWord(tempTrans);
      }
      colWidth -= (int)std::strlen(m->san);
      if (IsColorFormat()) {
        tb->PrintString("</m>");
      }
    }

    bool endedColumn = false;

    // Print NAGs and comments if the style indicates:

    if (PgnStyle & PGN_STYLE_COMMENTS) {
      bool printDiagramHere = false;
      if (IsColorFormat() && m->nagCount > 0) {
        tb->PrintString("<nag>");
      }
      for (uint i = 0; i < (uint)m->nagCount; i++) {
        char temp[20];
        game_printNag(m->nags[i], temp, PgnStyle & PGN_STYLE_SYMBOLS,
                      PgnFormat);

        // Do not print a space before the Nag if it is the
        // first nag and starts with "!" or "?" -- those symbols
        // look better printed next to the move:

        if (i > 0 || (temp[0] != '!' && temp[0] != '?')) {
          tb->PrintSpace();
          colWidth--;
        }
        if (printDiagrams && m->nags[i] == NAG_Diagram) {
          printDiagramHere = true;
        }
        tb->PrintWord(temp);
        colWidth -= (int)std::strlen(temp);
      }
      if (IsColorFormat() && m->nagCount > 0) {
        tb->PrintString("</nag>");
      }
      tb->PrintSpace();
      colWidth--;
      if ((PgnStyle & PGN_STYLE_COLUMN) && VarDepth == 0) {
        if (IsPlainFormat() || IsColorFormat()) {
          while (colWidth-- > 0) {
            tb->PrintSpace();
          }
        }
      }

      if (printDiagramHere) {
        if ((PgnStyle & PGN_STYLE_COLUMN) && VarDepth == 0) {
          if (!endedColumn) {
            if (CurrentPos->GetToMove() == WHITE) {
              tb->PauseTranslations();
              tb->PrintString(nextColumn);
              tb->ResumeTranslations();
            }
            tb->PrintString(endColumn);
            tb->PrintString(endTable);
            endedColumn = true;
          }
        }
        if (IsHtmlFormat() && VarDepth == 0) {
          tb->PrintString("</b>");
        }
        if (IsHtmlFormat() && VarDepth == 0) {
          tb->PrintString("</b>");
        }
        MoveForward();
        DString *dstr = new DString;
        if (IsHtmlFormat()) {
          CurrentPos->DumpHtmlBoard(dstr, HtmlStyle, NULL);
        }
        MoveBackup();
        tb->PrintString(dstr->Data());
        delete dstr;
        if (IsHtmlFormat() && VarDepth == 0) {
          tb->PrintString("<b>");
        }
        printMoveNum = true;
      }

      const char *comment = m->comment.c_str();
      if (*comment && (PgnStyle & PGN_STYLE_STRIP_MARKS)) {
        strippedComment = m->comment;
        strTrimMarkCodes(strippedComment.data());
        comment = strippedComment.data();
      }
      if (*comment) {
        if (!inComment && IsPlainFormat() &&
            (PgnStyle & PGN_STYLE_NO_NULL_MOVES)) {
          // If this move has no variations, but the next move
          // is a null move, enter inComment mode:
          if (m->next->isNull() && ((!(PgnStyle & PGN_STYLE_VARS)) ||
                                    (CurrentMove->next->numVariations == 0))) {
            inComment = true;
            tb->PrintString(preCommentStr);
            preCommentStr = "";
            postCommentStr = "";
          }
        }

        /* Code commented to remove extra lines
                        if ((PgnStyle & PGN_STYLE_COLUMN)  &&  VarDepth == 0) {
                               if (! endedColumn) {
                                   if (CurrentPos->GetToMove() == WHITE) {
                                       tb->PauseTranslations ();
                                       tb->PrintString (nextColumn);
                                       tb->ResumeTranslations ();
                                   }
                                   tb->PrintString (endColumn);
                                   tb->PrintString (endTable);
                                   endedColumn = true;
                               }
                        }
        */
        if (IsHtmlFormat() && VarDepth == 0) {
          tb->PrintString("</b><dl><dd>");
        }
        if ((PgnStyle & PGN_STYLE_INDENT_COMMENTS) && VarDepth == 0) {
          if (IsColorFormat()) {
            tb->PrintString("<br><ip1>");
          } else {
            tb->SetIndent(tb->GetIndent() + 4);
            tb->Indent();
          }
        }

        WriteComment(tb, preCommentStr, comment, postCommentStr);

        if ((PgnStyle & PGN_STYLE_INDENT_COMMENTS) && VarDepth == 0) {
          if (IsColorFormat()) {
            tb->PrintString("</ip1><br>");
            commentLine = true;
          } else {
            tb->SetIndent(tb->GetIndent() - 4);
            tb->Indent();
          }
        } else {
          tb->PrintSpace();
        }
        if (printDiagrams && strIsPrefix("#", comment)) {
          MoveForward();
          DString *dstr = new DString;
          if (IsHtmlFormat()) {
            CurrentPos->DumpHtmlBoard(dstr, HtmlStyle, NULL);
          }
          MoveBackup();
          tb->PrintString(dstr->Data());
          delete dstr;
        }
        if (IsHtmlFormat() && VarDepth == 0) {
          tb->PrintString("</dl><b>");
        }
        printMoveNum = true;
      }
    } else {
      tb->PrintSpace();
    }

    // Print any variations if the style indicates:
    if ((PgnStyle & PGN_STYLE_VARS) && (m->numVariations > 0)) {
      if ((PgnStyle & PGN_STYLE_COLUMN) && VarDepth == 0) {
        if (!endedColumn) {
          if (CurrentPos->GetToMove() == WHITE) {
            tb->PauseTranslations();
            tb->PrintString(nextColumn);
            tb->ResumeTranslations();
          }
          // Doesn't seem wanted!! S.A (see a few lines below)
          // tb->PrintString (endColumn);
          tb->PrintString(endTable);
          endedColumn = true;
        }
      }
      if (IsColorFormat() && VarDepth == 0) {
        tb->PrintString("<var>");
      }
      // Doesn't indent first var in column mode properly
      // if including !(PgnStyle & PGN_STYLE_COLUMN) here.
      // But as-is, depth 3 vars don't indent in COLUMN mode (bug)
      if ((PgnStyle & PGN_STYLE_INDENT_VARS) && IsColorFormat()) {
        if (!commentLine) {
          tb->PrintString("<br>");
        }
      }
      for (uint i = 0; i < m->numVariations; i++) {
        if (PgnStyle & PGN_STYLE_INDENT_VARS) {
          if (IsColorFormat()) {
            if (VarDepth < 19) {
              char tmp_str[16];
              std::snprintf(tmp_str, sizeof(tmp_str), "<ip%u>", VarDepth + 1);
              tb->PrintString(tmp_str);
            }
          } else {
            tb->SetIndent(tb->GetIndent() + 4);
            tb->Indent();
          }
        }
        if (IsHtmlFormat()) {
          if (VarDepth == 0) {
            tb->PrintString("</b><dl><dd>");
          }
        }
        if (IsColorFormat()) {
          tb->PrintString("<blue>");
        }

        // Note tabs in column mode don't work after this VarDepth>1 for some
        // reason this VarDepth check is redundant i think
        tb->PrintChar('(');

        MoveIntoVariation(i);
        NumMovesPrinted++;
        tb->PrintSpace();

        // Recursively print the variation:
        WriteMoveList(tb, oldCurrentMove, true, inComment);

        MoveExitVariation();
        tb->PrintChar(')');
        if (IsColorFormat()) {
          tb->PrintString("<blue>");
        }
        if (IsHtmlFormat()) {
          if (VarDepth == 0) {
            tb->PrintString("</dl><b>");
          }
        }
        if (PgnStyle & PGN_STYLE_INDENT_VARS) {
          if (IsColorFormat()) {
            if (VarDepth < 19) {
              char tmp_str[16];
              std::snprintf(tmp_str, sizeof(tmp_str), "</ip%u><br>",
                            VarDepth + 1);
              tb->PrintString(tmp_str);
            }
          } else {
            tb->SetIndent(tb->GetIndent() - 4);
            tb->Indent();
          }
        } else {
          tb->PrintSpace();
        }
        printMoveNum = true;
      }
      if (IsColorFormat() && VarDepth == 0) {
        tb->PrintString("</var>");
      }
    }
    if ((PgnStyle & PGN_STYLE_COLUMN) && VarDepth == 0) {
      if (endedColumn) {
        tb->PrintString(startTable);
      }
      if (!endedColumn && CurrentPos->GetToMove() == BLACK) {
        tb->PrintString(endColumn);
        endedColumn = true;
      }
    }
    MoveForward();
  }
  if (inComment) {
    tb->PrintString("}");
  }
  if (IsHtmlFormat() && VarDepth == 0) {
    tb->PrintString("</b>");
  }
  if ((PgnStyle & PGN_STYLE_COLUMN) && VarDepth == 0) {
    tb->PrintString(endTable);
  }
  return OK;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::WritePGN():
//      Write a game in PGN to a textbuffer.
//
errorT Game::WritePGN(TextBuffer *tb) {
  char temp[256];
  char dateStr[20];
  const char *newline = "\n";
  tb->NewlinesToSpaces(false);
  if (IsHtmlFormat()) {
    newline = "<br>\n";
  }
  if (IsColorFormat()) {
    newline = "<br>";
  }

  if (PgnStyle & PGN_STYLE_COLUMN) {
    PgnStyle |= PGN_STYLE_INDENT_COMMENTS;
    PgnStyle |= PGN_STYLE_INDENT_VARS;
  }

  // First: is there a pre-game comment? If so, print it:
  //    if (FirstMove->comment != NULL && (PgnStyle & PGN_STYLE_COMMENTS)
  //        &&  ! strIsAllWhitespace (FirstMove->comment)) {
  //        tb->AddTranslation ('\n', newline);
  //        char * s = FirstMove->comment;
  //        if (PgnStyle & PGN_STYLE_STRIP_MARKS) {
  //            s = strDuplicate (FirstMove->comment);
  //            strTrimMarkCodes (s);
  //        }
  //        if (IsColorFormat()) {
  //            sprintf (temp, "<c_%u>", NumMovesPrinted);
  //            tb->PrintString (temp);
  //            tb->AddTranslation ('<', "<lt>");
  //            tb->AddTranslation ('>', "<gt>");
  //            tb->PrintString (s);
  //            tb->ClearTranslation ('<');
  //            tb->ClearTranslation ('>');
  //            tb->PrintLine ("</c>");
  //        } else {
  //            tb->PrintLine (s);
  //        }
  //        if (PgnStyle & PGN_STYLE_STRIP_MARKS) { delete[] s; }
  //        tb->ClearTranslation ('\n');
  //        tb->NewLine();
  //    }

  date_DecodeToString(Date, dateStr);
  if (IsHtmlFormat()) {
    tb->PrintLine("<p><b>");
  }

  //    if (IsColorFormat()) {
  //        tb->AddTranslation ('<', "<lt>");
  //        tb->AddTranslation ('>', "<gt>");
  //    }

  if (PgnStyle & PGN_STYLE_SHORT_HEADER) {
    // Print tags in short, 3-line format:

    // if (IsHtmlFormat()) { tb->PrintString ("<font size=+1>"); }
    if (PgnFormat == PGN_FORMAT_Color) {
      tb->PrintString("<tag>");
    }
    tb->PrintString(GetWhiteStr());
    if (WhiteElo > 0) {
      std::snprintf(temp, sizeof(temp), "  (%u)", WhiteElo);
      tb->PrintString(temp);
    }
    switch (PgnFormat) {
    case PGN_FORMAT_HTML:
      tb->PrintString(" &nbsp;&nbsp; -- &nbsp;&nbsp; ");
      break;
    default:
      tb->PrintString("   --   ");
      break;
    }
    tb->PrintString(GetBlackStr());
    if (BlackElo > 0) {
      std::snprintf(temp, sizeof(temp), "  (%u)", BlackElo);
      tb->PrintString(temp);
    }
    // if (IsHtmlFormat()) { tb->PrintString ("</font>"); }
    tb->PrintString(newline);

    tb->PrintString(GetEventStr());
    if (!RoundStr.empty() && RoundStr != "?") {
      tb->PrintString(IsHtmlFormat() ? " &nbsp;(" : " (");
      tb->PrintString(GetRoundStr());
      tb->PrintString(")");
    }
    if (!SiteStr.empty() && SiteStr != "?") {
      tb->PrintString(GetSiteStr());
      tb->PrintString(newline);
    }

    // Remove ".??" or ".??.??" from end of dateStr, then print it:
    if (dateStr[4] == '.' && dateStr[5] == '?') {
      dateStr[4] = 0;
    }
    if (dateStr[7] == '.' && dateStr[8] == '?') {
      dateStr[7] = 0;
    }
    tb->PrintString(dateStr);

    // Print ECO code:
    tb->PrintString(IsHtmlFormat() ? " &nbsp; &nbsp; " : "  ");
    tb->PrintString(RESULT_LONGSTR[Result]);
    if (EcoCode != 0) {
      tb->PrintString(IsHtmlFormat() ? " &nbsp; &nbsp; " : "  ");
      ecoStringT ecoStr;
      eco_ToExtendedString(EcoCode, ecoStr);
      tb->PrintString(ecoStr);
    }
    auto annotator = FindExtraTag("Annotator");
    if (annotator != NULL) {
      std::snprintf(temp, sizeof(temp), " (%s)", annotator);
      tb->PrintString(temp);
    }

    tb->PrintString(newline);
    if (PgnFormat == PGN_FORMAT_Color) {
      tb->PrintString("</tag>");
    }

    // Print FEN if non-standard start:
    if (StartPos) {
      if (IsHtmlFormat()) {
        DString dstr;
        StartPos->DumpHtmlBoard(&dstr, HtmlStyle, NULL);
        tb->PrintString(dstr.Data());
      } else {
        StartPos->PrintFEN(std::copy_n("Position: ", 10, temp));
        std::strcat(temp, newline);
        tb->PrintString(temp);
      }
    }
  } else {
    // Print tags in standard PGN format, one per line:
    // Note: we want no line-wrapping when printing PGN tags
    // so set it to a huge value for now:
    uint wrapColumn = tb->GetWrapColumn();
    tb->SetWrapColumn(99999);
    if (IsColorFormat()) {
      tb->PrintString("<tag>");
    }
    std::snprintf(temp, sizeof(temp), "[Event \"%s\"]%s", GetEventStr(),
                  newline);
    tb->PrintString(temp);
    std::snprintf(temp, sizeof(temp), "[Site \"%s\"]%s", GetSiteStr(), newline);
    tb->PrintString(temp);
    std::snprintf(temp, sizeof(temp), "[Date \"%s\"]%s", dateStr, newline);
    tb->PrintString(temp);
    std::snprintf(temp, sizeof(temp), "[Round \"%s\"]%s", GetRoundStr(),
                  newline);
    tb->PrintString(temp);
    std::snprintf(temp, sizeof(temp), "[White \"%s\"]%s", GetWhiteStr(),
                  newline);
    tb->PrintString(temp);
    std::snprintf(temp, sizeof(temp), "[Black \"%s\"]%s", GetBlackStr(),
                  newline);
    tb->PrintString(temp);
    std::snprintf(temp, sizeof(temp), "[Result \"%s\"]%s",
                  RESULT_LONGSTR[Result], newline);
    tb->PrintString(temp);

    // Print all tags, not just the standard seven, if applicable:
    if (PgnStyle & PGN_STYLE_TAGS) {
      if (WhiteElo > 0) {
        std::snprintf(temp, sizeof(temp), "[White%s \"%u\"]%s",
                      ratingTypeNames[WhiteRatingType], WhiteElo, newline);
        tb->PrintString(temp);
      }
      if (BlackElo > 0) {
        std::snprintf(temp, sizeof(temp), "[Black%s \"%u\"]%s",
                      ratingTypeNames[BlackRatingType], BlackElo, newline);
        tb->PrintString(temp);
      }
      if (EcoCode != 0) {
        ecoStringT ecoStr;
        eco_ToExtendedString(EcoCode, ecoStr);
        std::snprintf(temp, sizeof(temp), "[ECO \"%s\"]%s", ecoStr, newline);
        tb->PrintString(temp);
      }
      if (EventDate != ZERO_DATE) {
        char edateStr[20];
        date_DecodeToString(EventDate, edateStr);
        std::snprintf(temp, sizeof(temp), "[EventDate \"%s\"]%s", edateStr,
                      newline);
        tb->PrintString(temp);
      }

      if (PgnStyle & PGN_STYLE_SCIDFLAGS && *ScidFlags != 0) {
        std::snprintf(temp, sizeof(temp), "[ScidFlags \"%s\"]%s", ScidFlags,
                      newline);
        tb->PrintString(temp);
      }

      // Now print other tags
      for (auto &e : extraTags_) {
        std::snprintf(temp, sizeof(temp), "[%s \"%s\"]%s", e.first.c_str(),
                      e.second.c_str(), newline);
        tb->PrintString(temp);
      }
    }
    // Finally, write the FEN tag if necessary:
    if (StartPos) {
      StartPos->PrintFEN(std::copy_n("[FEN \"", 6, temp));
      auto it_end = std::copy_n("\"]", 2, temp + std::strlen(temp));
      std::strcpy(it_end, newline);
      tb->PrintString(temp);
    }
    if (IsColorFormat()) {
      tb->PrintString("</tag>");
    }
    // Now restore the linewrap column:
    tb->SetWrapColumn(wrapColumn);
  }

  //    if (IsColorFormat()) {
  //        tb->ClearTranslation ('<');
  //        tb->ClearTranslation ('>');
  //    }

  if (IsHtmlFormat()) {
    tb->PrintLine("</b></p>");
  }
  tb->PrintString(newline);

  // Move list printing - executes for ALL PGN formats
  MoveToStart();

  if (IsHtmlFormat()) {
    tb->PrintString("<p>");
  }
  NumMovesPrinted = 1;
  WriteMoveList(tb, CurrentMove, true, false);
  if (IsHtmlFormat()) {
    tb->PrintString("<b>");
  }
  if (IsColorFormat()) {
    tb->PrintString("<tag>");
  }
  tb->PrintWord(RESULT_LONGSTR[Result]);
  if (IsHtmlFormat()) {
    tb->PrintString("</b><hr></p>");
  }
  if (IsColorFormat()) {
    tb->PrintString("</tag>");
  }
  tb->NewLine();

  tb->NewLine();

  return OK;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::WriteToPGN():
//      Print the entire game.
//
std::pair<const char *, unsigned>
Game::WriteToPGN(uint lineWidth, bool NewLineAtEnd, bool newLineToSpaces) {
  static TextBuffer tbuf;

  auto location = currentLocation();
  tbuf.Empty();
  tbuf.SetWrapColumn(lineWidth ? lineWidth : tbuf.GetBufferSize());
  tbuf.NewlinesToSpaces(newLineToSpaces);
  WritePGN(&tbuf);
  if (NewLineAtEnd)
    tbuf.NewLine();
  restoreLocation(location);
  return std::make_pair(tbuf.GetBuffer(), tbuf.GetByteCount());
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::LoadStandardTags():
//      Sets the standard tag values for this game, given an
//      index file entry and a namebase that stores the
//      player/site/event/round names.
//
void Game::LoadStandardTags(IndexEntry const &ie, TagRoster const &tags) {
  SetEventStr(tags.event);
  SetSiteStr(tags.site);
  SetWhiteStr(tags.white);
  SetBlackStr(tags.black);
  SetRoundStr(tags.round);
  SetDate(ie.GetDate());
  SetEventDate(ie.GetEventDate());
  SetWhiteElo(ie.GetWhiteElo());
  SetBlackElo(ie.GetBlackElo());
  SetWhiteRatingType(ie.GetWhiteRatingType());
  SetBlackRatingType(ie.GetBlackRatingType());
  SetResult(ie.GetResult());
  SetEco(ie.GetEcoCode());
  ie.GetFlagStr(ScidFlags, NULL);
  if (!ie.isChessStd())
    assignTagValue("Variant", "Chess960");
}

eloT Game::GetAverageElo() {
  auto white = WhiteElo;
  auto black = BlackElo;
  return (white == 0 || black == 0) ? 0 : (white + black) / 2;
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// encodeKing(): encoding of King moves.
//
static byte encodeKing(squareT from, int to) {
  // Valid King difference-from-old-square values are:
  // -9, -8, -7, -1, 1, 7, 8, 9, and -2 and 2 for castling.
  // To convert this to a val in the range [1-10], we add 9 and
  // then look up the val[] table.
  // Coded values 1-8 are one-square moves; 9 and 10 are Castling.
  static const byte val[] = {
      /* -9 -8 -7 -6 -5 -4 -3 -2 -1  0  1   2  3  4  5  6  7  8  9 */
      1, 2, 3, 0, 0, 0, 0, 9, 4, 0, 5, 10, 0, 0, 0, 0, 6, 7, 8};
  auto diff = to - from;
  static_assert(std::is_same_v<decltype(diff), int>);

  // If target square is the from square, it is the null move, which
  // is represented as a king move to its own square and is encoded
  // as the byte value zero.
  ASSERT((to == from && val[diff + 9] == 0) || val[diff + 9] != 0);

  // Verify we have a valid King move:
  ASSERT(diff >= -9 && diff <= 9);
  return val[diff + 9];
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// encodeKnight(): encoding Knight moves.
//
static byte encodeKnight(squareT from, squareT to) {
  // Valid Knight difference-from-old-square values are:
  // -17, -15, -10, -6, 6, 10, 15, 17.
  // To convert this to a value in the range [1-8], we add 17 to
  // the difference and then look up the val[] table.

  auto diff = to - from;
  static_assert(std::is_same_v<decltype(diff), int>);
  static const byte val[] = {
      /* -17 -16 -15 -14 -13 -12 -11 -10 -9 -8 -7 -6 -5 -4 -3 -2 -1  0 */
      1, 0, 2, 0, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0,

      /*  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 */
      0, 0, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 0, 7, 0, 8};

  // Verify we have a valid knight move:
  ASSERT(diff >= -17 && diff <= 17 && val[diff + 17] != 0);
  return val[diff + 17];
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// encodeRook(): encoding rook moves.
//
static byte encodeRook(squareT from, squareT to) {
  // Valid Rook moves are to same rank, OR to same fyle.
  // We encode the 8 squares on the same rank 0-8, and the 8
  // squares on the same fyle 9-15. This means that for any particular
  // rook move, two of the values in the range [0-15] will be
  // meaningless, as they will represent the from-square.

  // Check if the two squares share the same rank:
  if (square_Rank(from) == square_Rank(to)) {
    return square_Fyle(to);
  }
  return 8 + square_Rank(to);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// encodeBishop(): encoding Bishop moves.
//
static byte encodeBishop(squareT from, squareT to) {
  // We encode a Bishop move as the Fyle moved to, plus
  // a one-bit flag to indicate if the direction was
  // up-right/down-left or vice versa.

  auto rankdiff = square_Rank(to) - square_Rank(from);
  auto fylediff = square_Fyle(to) - square_Fyle(from);
  static_assert(std::is_same_v<decltype(rankdiff), int>);
  static_assert(std::is_same_v<decltype(fylediff), int>);

  // If (rankdiff * fylediff) is negative, it's up-left/down-right:
  if (rankdiff * fylediff < 0)
    return square_Fyle(to) + 8;

  return square_Fyle(to);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// encodeQueen(): encoding Queen moves.
//
static byte encodeQueen(squareT from, squareT to, byte &multibyte) {
  // We cannot fit all Queen moves in one byte, so Rooklike moves
  // are in one byte (encoded the same way as Rook moves),
  // while diagonal moves are in two bytes.
  if (square_Rank(from) == square_Rank(to)) // Rook-horizontal move:
    return square_Fyle(to);

  if (square_Fyle(from) == square_Fyle(to)) // Rook-vertical move:
    return 8 + square_Rank(to);

  // Diagonal move:
  ASSERT(std::abs(to / 8 - from / 8) == std::abs(to % 8 - from % 8));
  // First, we put a rook-horizontal move to the from square (which
  // is illegal of course) to indicate it is NOT a rooklike move:
  // Now we put the to-square in the next byte. We add a 64 to it
  // to make sure that it cannot clash with the Special tokens (which
  // are in the range 0 to 15, since they are special King moves).
  multibyte = to + 64;
  return square_Fyle(from);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// encodePawn(): encoding Pawn moves.
//
static byte encodePawn(squareT from, squareT to, pieceT promo) {
  // Pawn moves require a promotion encoding.
  // The pawn moves are:
  // 0 = capture-left,
  // 1 = forward,
  // 2 = capture-right (all no promotion);
  //    3/4/5 = 0/1/2 with Queen promo;
  //    6/7/8 = 0/1/2 with Rook promo;
  //  9/10/11 = 0/1/2 with Bishop promo;
  // 12/13/14 = 0/1/2 with Knight promo;
  // 15 = forward TWO squares.

  byte val;
  auto diff = to - from;
  static_assert(std::is_same_v<decltype(diff), int>);

  if (diff < 0) {
    diff = -diff;
  }
  if (diff == 16) { // Move forward two squares
    val = 15;
    ASSERT(promo == EMPTY);

  } else {
    if (diff == 7) {
      val = 0;
    } else if (diff == 8) {
      val = 1;
    } else { // diff is 9:
      ASSERT(diff == 9);
      val = 2;
    }
    if (promo != EMPTY) {
      // Handle promotions.
      // sm->promote must be Queen=2,Rook=3, Bishop=4 or Knight=5.
      // We add 3 for Queen, 6 for Rook, 9 for Bishop, 12 for Knight.

      ASSERT(promo >= QUEEN && promo <= KNIGHT);
      val += 3 * ((promo)-1);
    }
  }
  return val;
}

// Special-move tokens:
// Since king-move values 1-10 are taken for actual King moves, only
// 11-15 (and zero) are available for non-move information.

#define ENCODE_NAG 11
#define ENCODE_COMMENT 12
#define ENCODE_START_MARKER 13
#define ENCODE_END_MARKER 14
#define ENCODE_END_GAME 15

#define ENCODE_FIRST 11
#define ENCODE_LAST 15

// The end-game and end-variation tokens could be the same single token,
// but having two different tokens allows for detecting corruption, since
// a game must end with the end-game token.

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// decodeMove():
//      Decode a move from a bytebuffer. Assumes the byte val is an
//      actual move, not the value of a "special" (non-move) token.
//      This function needs to be passed the bytebuffer because some
//      moves (only Queen diagonal moves) are encoded in two bytes, so
//      it may be necessary to read the next byte as well.
//
static errorT decodeMove(ByteBuffer *buf, simpleMoveT *sm, byte val,
                         const Position *pos) {
  const colorT toMove = pos->GetToMove();
  const squareT from = pos->GetList(toMove)[val >> 4];
  if (from > H8)
    return ERROR_Decode;

  const auto ptype = piece_Type(pos->GetPiece(from));
  const auto [to, promo] = buf->decodeMove(toMove, ptype, from, val);
  if (to < 0 || to > 63)
    return ERROR_Decode;

  if (to == from) {
    if (promo == INVALID_PIECE)
      return ERROR_Decode;

    if (promo != PAWN && !pos->canCastle<false>(promo == KING))
      return ERROR_Decode;
  } else {
    if (to == pos->GetKingSquare(WHITE) || to == pos->GetKingSquare(BLACK))
      return ERROR_Decode;
  }
  pos->makeMove(from, to, promo, *sm);
  return OK;
}

template <typename DestT> void encodeMove(const simpleMoveT &sm, DestT &dest) {
  byte multibyte = 0;
  byte val;
  switch (piece_Type(sm.movingPiece)) {
  case KING:
    ASSERT(sm.pieceNum == 0); // Kings MUST be piece Number zero.
    val = encodeKing(sm.from, sm.isCastle() ? sm.from + sm.isCastle() : sm.to);
    break;
  case QUEEN:
    val = encodeQueen(sm.from, sm.to, multibyte);
    break;
  case ROOK:
    val = encodeRook(sm.from, sm.to);
    break;
  case BISHOP:
    val = encodeBishop(sm.from, sm.to);
    break;
  case KNIGHT:
    val = encodeKnight(sm.from, sm.to);
    break;
  default:
    ASSERT(PAWN == piece_Type(sm.movingPiece));
    val = encodePawn(sm.from, sm.to, sm.promote);
  }
  ASSERT(sm.pieceNum <= 15 && val <= 15);
  const auto encoded = static_cast<byte>(val | (sm.pieceNum << 4));
  dest.emplace_back(encoded);
  if (multibyte) { // Diagonal Queen moves are stored using two bytes.
    dest.emplace_back(multibyte);
  }
}

/// Encode the moves, the nags, the comment mark and the variations.
template <typename MoveT, typename DestT>
std::pair<unsigned, unsigned> encodeMovelist(bool mark_comments, const MoveT *m,
                                             DestT &dest) {
  ASSERT(m && m->startMarker());

  // Check if there is a pre-game comment
  if (mark_comments && !m->comment.empty())
    dest.emplace_back(ENCODE_COMMENT);

  unsigned n_vars = 0;
  unsigned n_nags = 0;
  while ((m = m->nextMoveInPGN())) {
    if (m->startMarker()) {
      ++n_vars;
      dest.emplace_back(ENCODE_START_MARKER);
      if (mark_comments && !m->comment.empty())
        dest.emplace_back(ENCODE_COMMENT);

    } else if (m->endMarker()) {
      if (m->nextMoveInPGN())
        dest.emplace_back(ENCODE_END_MARKER);

    } else {
      encodeMove(m->moveData, dest);

      for (int i = 0, n = m->nagCount; i < n; ++i) {
        dest.emplace_back(ENCODE_NAG);
        dest.emplace_back(m->nags[i]);
        ++n_nags;
      }
      if (mark_comments && !m->comment.empty())
        dest.emplace_back(ENCODE_COMMENT);
    }
  }
  dest.emplace_back(ENCODE_END_GAME);
  return {n_vars, n_nags};
}

/// Decodes the game moves
errorT Game::DecodeVariation(ByteBuffer &buf,
                             std::vector<moveT *> &comment_marks) {
  simpleMoveT sm;
  for (;;) {
    auto [err, val] = buf.nextMove(
        this->VarDepth, [&](auto) { return true; },
        [&] {
          // Mark this comment as needing to be read
          comment_marks.push_back(this->CurrentMove->prev);
        },
        [&](auto newVariation) {
          if (newVariation)
            return AddVariation() == OK;

          return (MoveExitVariation() == OK && MoveForward() == OK);
        },
        [&](auto nag) { return this->AddNag(nag) == OK; });
    if (err)
      return (err == ERROR_EndOfMoveList) ? OK : err;

    auto errMove = decodeMove(&buf, &sm, val, currentPos());
    if (!errMove)
      errMove = AddMove(sm);
    if (errMove)
      return errMove;
  }
}

// Return the number of comments and true if comment marks are useful
template <typename MoveT> auto countComments(const MoveT *m) {
  unsigned n_comments = 0;
  unsigned n_empty = 0;
  for (; m; m = m->nextMoveInPGN()) {
    if (m->endMarker())
      continue;

    if (m->comment.empty()) {
      ++n_empty;
    } else {
      ++n_comments;
    }
  }
  return std::make_pair(n_comments, n_comments < n_empty);
}

/**
 * The Comments section is composed by null-terminated strings. The comments
 * are stored in the order in which they will appear in the PGN notation:
 * {C1} 1.d4 {C2} (1.b4 {C3} 1...e5 {C4} (1...Na6 {C5}) 2.e4 {C6})
 * ({C7} 1.g4 {C8}) 1...d5 {C9}
 */
template <typename MoveT, typename DestT>
void encodeComments(bool mark_comments, const MoveT *m, DestT &dest) {
  for (; m; m = m->nextMoveInPGN()) {
    if (m->endMarker())
      continue;

    if (!m->comment.empty() || !mark_comments) {
      const auto len = m->comment.size() + 1; // Include the null char
      const auto data = m->comment.c_str();
      dest.insert(dest.end(), data, data + len);
    }
  }
}

// Decodes the comments from @e buf and stores them into the marked moves.
// If the number of comments is greater than the number of marked moves, they
// are added sequentially after the last move that has a comment.
// This way it is possible to not add encode marks for games that are almost
// fully commented.
template <typename SourceT, typename MoveT>
static errorT decodeComments(SourceT &buf, MoveT *first_move,
                             std::vector<MoveT *> &comment_marks) {
  if (!comment_marks.empty()) {
    for (auto m : comment_marks) {
      if (auto str = buf.GetTerminatedString())
        m->comment = str;
      else
        return ERROR_Decode;
    }
    first_move = comment_marks.back()->nextMoveInPGN();
  }
  while (auto str = buf.GetTerminatedString()) {
    if (!first_move)
      return ERROR_Decode;

    first_move->comment = str;
    first_move = first_move->nextMoveInPGN();
  }
  return OK;
}

/// Calculate the game's main line information:
/// - home pawn delta information
/// - promotions flags
/// - number of half moves
/// - final material signature
/// - stored line code
template <typename MoveT>
std::pair<bool, bool> mainlineInfo(const Position *customStart,
                                   const MoveT *firstMove, IndexEntry &dest) {
  ushort nHalfMoves = 0;
  bool PromoFlag = false;
  bool UnderPromosFlag = false;
  unsigned hpCount = 0;
  byte hpVal[8] = {};
  Position pos = customStart ? *customStart : Position::getStdStart();

  auto hpOld = HPSIG_StdStart; // All 16 pawns are on their home squares.
  for (auto move = firstMove; !move->endMarker(); move = move->next) {
    ++nHalfMoves;

    if (move->moveData.promote != EMPTY) {
      PromoFlag = true;
      if (piece_Type(move->moveData.promote) != QUEEN) {
        UnderPromosFlag = true;
      }
    }

    pos.DoSimpleMove(move->moveData);
    if (!customStart) {
      const auto hpNew = pos.GetHPSig();
      if (unsigned changed = hpOld - hpNew) {
        hpOld = hpNew;
        byte idxMovedPawn = 0; // __builtin_ctz(changed)
        while (changed >>= 1) {
          ++idxMovedPawn;
        }
        assert(idxMovedPawn <= 0x0F);
        if ((hpCount & 1) == 0) // There are only 16 pawns, so we can
          idxMovedPawn <<= 4;   // store two pawn moves in every byte
        hpVal[hpCount++ / 2] |= idxMovedPawn;
      }
    }
  }

  byte storedCode = 0;
  if (!customStart) {
    storedCode = StoredLine::classify([&](auto begin, auto end) {
      if (std::distance(begin, end) > nHalfMoves)
        return false;

      const moveT *gameMove = firstMove;
      for (; begin != end; ++begin) {
        if (begin->isCastle()) {
          auto side = begin->getTo() > begin->getFrom() ? 2 : -2;
          if (gameMove->moveData.isCastle() != side)
            return false;

        } else if (gameMove->moveData.from != begin->getFrom() ||
                   gameMove->moveData.to != begin->getTo()) {
          return false;
        }

        gameMove = gameMove->next;
      }
      return true;
    });
  }

  dest.SetHomePawnData(static_cast<byte>(hpCount), hpVal);
  dest.SetPromotionsFlag(PromoFlag);
  dest.SetUnderPromoFlag(UnderPromosFlag);
  dest.SetNumHalfMoves(nHalfMoves);
  dest.SetFinalMatSig(matsig_Make(pos.GetMaterial()));
  dest.SetStoredLineCode(storedCode);

  return {PromoFlag, UnderPromosFlag};
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::Encode(): Encode the game to a buffer for disk storage.
//      If passed a NON-null IndexEntry pointer, it will fill in the
//      following fields of that index entry, which are computed as
//      the game is encoded:
//       -  result, ecoCode, whiteElo, blackElo
//       -  promotion flag
//       -  nMoves: the number of halfmoves
//       -  finalMatSig: the material signature of the final position.
//       -  homePawnData: the home pawn change list.
//
std::pair<IndexEntry, TagRoster> Game::Encode(std::vector<byte> &dest) const {
  auto tags = TagRoster();
  tags.event = GetEventStr();
  tags.site = GetSiteStr();
  tags.white = GetWhiteStr();
  tags.black = GetBlackStr();
  tags.round = GetRoundStr();

  auto ie = IndexEntry();
  // Set the fields in the IndexEntry:
  ie.SetDate(Date);
  ie.SetEventDate(EventDate);
  ie.SetResult(Result);
  ie.SetEcoCode(EcoCode);
  ie.SetWhiteElo(WhiteElo);
  ie.SetBlackElo(BlackElo);
  ie.SetWhiteRatingType(WhiteRatingType);
  ie.SetBlackRatingType(BlackRatingType);
  if (HasNonStandardStart()) {
    ie.SetStartFlag(true);
    if (StartPos->isChess960()) {
      ie.setChess960();
    }
  } else {
    ie.SetStartFlag(false);
  }
  ie.SetFlag(IndexEntry::StrToFlagMask(ScidFlags), true);

  const auto [promo, underPromo] =
      mainlineInfo(StartPos.get(), FirstMove->next, ie);

  // First, encode info not already stored in the index
  // This will be the non-STR (non-"seven tag roster") PGN tags.
  encodeTags(GetExtraTags(), dest);

  // Encode the promotion flags and the start position
  char FEN[256];
  encodeStartBoard(promo, underPromo, HasNonStandardStart(FEN) ? FEN : nullptr,
                   dest);

  auto [commentCount, markComments] = countComments(FirstMove);

  // Compatibility: SCID4 requires the markers
  markComments = true;

  // Now the movelist:
  auto [varCount, nagCount] = encodeMovelist(markComments, FirstMove, dest);

  // Now do the comments
  encodeComments(markComments, FirstMove, dest);

  ie.SetCommentCount(commentCount);
  ie.SetVariationCount(varCount);
  ie.SetNagCount(nagCount);

  return {ie, tags};
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::DecodeNextMove():
//      Decodes one more mainline move of the game from the bytebuffer.
//      Used in searches for speed, since it is usually possible to
//      determine if a game matches the search criteria without decoding
//      all of it.
//      If the game flag KeepDecodedMoves is true, the move decodes is
//      added normally. If it is false, only the current position is
//      updated and the list of moves is not updated -- this is done
//      in searches for speed.
//      Returns OK if a move was found, or ERROR_EndOfMoveList if all the
//      moves have been decoded. Returns ERROR_Game if some corruption was
//      detected.
//
errorT Game::DecodeNextMove(ByteBuffer *buf, simpleMoveT &sm) {
  ASSERT(buf != NULL);

  auto [err, val] = buf->nextLineMove();
  if (err)
    return err;

  return decodeMove(buf, &sm, val, currentPos());
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::DecodeStart():
//      Decodes the starting information from the game's on-disk
//      representation in the bytebuffer. After this is called,
//      DecodeNextMove() can be called to decode each successive
//      mainline move.
//
errorT Game::DecodeSkipTags(ByteBuffer *buf) {
  ASSERT(buf != NULL);

  Clear();
  errorT err = buf->decodeTags([](auto, auto) {});
  if (err != OK)
    return err;

  const auto [err_startpos, fen] = buf->decodeStartBoard();
  if (err_startpos)
    return err_startpos;

  if (fen)
    return SetStartFen(fen);

  return OK;
}

errorT Game::DecodeMovesOnly(ByteBuffer &buf) {
  if (errorT err = DecodeSkipTags(&buf))
    return err;

  std::vector<moveT *> comment_marks;
  return DecodeVariation(buf, comment_marks);
}

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Game::Decode():
//      Decodes a game from its on-disk representation in a bytebuffer.
//      Decodes all the information: comments, variations, non-standard
//      tags, etc.
//
errorT Game::Decode(IndexEntry const &ie, TagRoster const &tags,
                    ByteBuffer buf) {
  Clear();
  LoadStandardTags(ie, tags);

  errorT err = buf.decodeTags(
      [&](const auto &tag, const auto &value) { assignTagValue(tag, value); });
  if (err)
    return err;

  const auto [err_startpos, fen] = buf.decodeStartBoard();
  if (err_startpos)
    return err_startpos;

  if (fen)
    err = SetStartFen(fen);

  std::vector<moveT *> comment_marks;
  if (err == OK)
    err = DecodeVariation(buf, comment_marks);

  if (err == OK)
    err = decodeComments(buf, FirstMove, comment_marks);

  return err;
}

//////////////////////////////////////////////////////////////////////
//  EOF:    game.cpp
//////////////////////////////////////////////////////////////////////