/* Noah Himed
 *
 * Implement the Board type.
 *
 * Licensed under MIT License. Terms and conditions enclosed in "LICENSE.txt".
 */

#include "board.h"

#include <boost/multiprecision/cpp_int.hpp>
#include <cctype>
#include <chrono>
#include <cstdint>
#include <random>
#include <stdexcept>
#include <string>
#include <unordered_map>

#include "bad_move.h"
#include "move.h"

namespace omegazero {

using std::invalid_argument;
using std::string;

using std::cout;
using std::endl;

typedef boost::multiprecision::uint128_t U128;

Board::Board(const string& init_pos) {
  for (S8 piece_type = kPawn; piece_type <= kKing; ++piece_type) {
    pieces_[piece_type] = 0ULL;
  }
  for (S8 player = kWhite; player <= kBlack; ++player) {
    player_pieces_[player] = 0ULL;
    // Initialize all castling rights to false before parsing the FEN string to
    // set the board, which may reset some castling rights to true.
    for (S8 board_side = kQueenSide; board_side <= kKingSide; ++board_side) {
      castling_rights_[player][board_side] = false;
    }
  }
  ep_target_sq_ = kNA;

  // Set the piece positions, castling rights, and player to move.
  InitBoardPos(init_pos);
  InitBoardHash();
}

auto Board::GetAttackMap(S8 attacking_player, S8 sq, S8 attacking_piece) const
    -> Bitboard {
  if (!SqOnBoard(sq)) {
    throw invalid_argument("sq in Board::GetAttackMap()");
  }

  Bitboard attack_map = 0X0;
  S8 attacked_player = GetOtherPlayer(attacking_player);
  switch (attacking_piece) {
    case kPawn: {
      Bitboard capture_attacks;
      Bitboard push_attacks;
      if (attacking_player == kWhite) {
        capture_attacks = kNonSliderAttackMaps[kWhitePawnCapture][sq];
        push_attacks = kNonSliderAttackMaps[kWhitePawnPush][sq];
      } else {
        capture_attacks = kNonSliderAttackMaps[kBlackPawnCapture][sq];
        push_attacks = kNonSliderAttackMaps[kBlackPawnPush][sq];
      }
      // Include captures that attack occupied squares and push moves that move
      // onto empty squares only. Note that the resulting attack map may include
      // squares in front of the pawn occupied by other pieces. This is
      // necessary due to how the function is used by other parts of the engine.
      attack_map =
          (capture_attacks & player_pieces_[attacked_player]) | push_attacks;
      break;
    }
    case kKnight:
      attack_map = kNonSliderAttackMaps[kKnightAttack][sq];
      break;
    // Use the magic bitboard method to get possible moves for bishops and
    // rooks. The Boost library's 128 bit unsigned int data type "U128"
    // is used here to avoid integer overflow.
    case kBishop: {
      Bitboard all_pieces = player_pieces_[kWhite] | player_pieces_[kBlack];
      Bitboard blockers = kSliderPieceMaps[kBishopMoves][sq] & all_pieces;
      if (blockers == 0X0) {
        attack_map = kUnblockedSliderAttackMaps[kBishopMoves][sq];
      } else {
        U128 magic = kMagics[kBishopMoves][sq];
        U128 index = (blockers * magic) >> (kNumSq - kBishopMagicLengths[sq]);
        U64 index_U64 = static_cast<U64>(index);
        attack_map = kMagicIndexToAttackMap.at(index_U64);
      }
      break;
    }
    case kRook: {
      Bitboard all_pieces = player_pieces_[kWhite] | player_pieces_[kBlack];
      Bitboard blockers = kSliderPieceMaps[kRookMoves][sq] & all_pieces;
      if (blockers == 0X0) {
        attack_map = kUnblockedSliderAttackMaps[kRookMoves][sq];
      } else {
        U128 magic = kMagics[kRookMoves][sq];
        U128 index = (blockers * magic) >> (kNumSq - kRookMagicLengths[sq]);
        U64 index_U64 = static_cast<U64>(index);
        attack_map = kMagicIndexToAttackMap.at(index_U64);
      }
      break;
    }
    // Combine the attack maps of a rook and bishop to get a queen's attack.
    case kQueen: {
      Bitboard bishop_attack = GetAttackMap(attacking_player, sq, kBishop);
      Bitboard rook_attack = GetAttackMap(attacking_player, sq, kRook);
      return bishop_attack | rook_attack;
    }
    case kKing:
      attack_map = kNonSliderAttackMaps[kKingAttack][sq];
      break;
    default:
      throw invalid_argument("attacking_piece in Board::GetAttackMap()");
  }

  return attack_map;
}

auto Board::GetPiecesByType(S8 piece_type, S8 player) const -> Bitboard {
  if (piece_type == kNA) {
    if (player == kWhite || player == kBlack) {
      return player_pieces_[player];
    }
    throw invalid_argument("player in Board::GetPiecesByType()");
  }
  if (player == kNA) {
    if (piece_type >= kPawn && piece_type <= kKing) {
      return pieces_[piece_type];
    }
    throw invalid_argument("piece_type in Board::GetPiecesByType()");
  }
  if ((piece_type >= kPawn && piece_type <= kKing) &&
      (player == kWhite || player == kBlack)) {
    return pieces_[piece_type] & player_pieces_[player];
  }

  throw invalid_argument("player, piece_type in Board::GetPiecesByType");
}

auto Board::CastlingLegal(S8 board_side) const -> bool {
  // For castling moves, check that the following hold:
  //   * Neither the king nor the chosen rook has previously moved.
  //   * There are no pieces between the king and the chosen rook.
  //   * The king is not currently in check.
  //   * The king does not pass through a square that is attacked by an enemy
  //     piece.
  if (board_side == kQueenSide) {
    return castling_rights_[player_to_move_][kQueenSide] && !KingInCheck() &&
           ((player_to_move_ == kWhite && piece_layout_[kSqB1] == kNA &&
             piece_layout_[kSqC1] == kNA && piece_layout_[kSqD1] == kNA &&
             GetAttackersToSq(kSqD1, kWhite) == 0X0) ||
            (player_to_move_ == kBlack && GetPieceOnSq(kSqB8) == kNA &&
             piece_layout_[kSqC8] == kNA && piece_layout_[kSqD8] == kNA &&
             GetAttackersToSq(kSqD8, kBlack) == 0X0));
  }
  if (board_side == kKingSide) {
    return castling_rights_[player_to_move_][kKingSide] && !KingInCheck() &&
           ((player_to_move_ == kWhite && piece_layout_[kSqF1] == kNA &&
             GetAttackersToSq(kSqF1, kWhite) == 0X0 &&
             piece_layout_[kSqG1] == kNA) ||
            (player_to_move_ == kBlack && GetPieceOnSq(kSqF8) == kNA &&
             GetAttackersToSq(kSqF8, kBlack) == 0X0 &&
             piece_layout_[kSqG8] == kNA));
  }

  throw invalid_argument("board_side in Board::CastlingLegal()");
}

auto Board::DoublePawnPushLegal(S8 file) const -> bool {
  if (!FileOnBoard(file)) {
    throw invalid_argument("file in Board::DoublePawnPushLegal()");
  }

  if (player_to_move_ == kWhite) {
    S8 rank3_double_pawn_push_sq = GetSqFromRankFile(kRank3, file);
    S8 rank2_double_pawn_push_sq = GetSqFromRankFile(kRank2, file);
    return piece_layout_[rank3_double_pawn_push_sq] == kNA &&
           piece_layout_[rank2_double_pawn_push_sq] == kPawn &&
           player_layout_[rank2_double_pawn_push_sq] == kWhite;
  }
  // Handle the case of evaluating if a double pawn push from black is legal.
  S8 rank6_double_pawn_push_sq = GetSqFromRankFile(kRank6, file);
  S8 rank7_double_pawn_push_sq = GetSqFromRankFile(kRank7, file);
  return piece_layout_[rank6_double_pawn_push_sq] == kNA &&
         piece_layout_[rank7_double_pawn_push_sq] == kPawn &&
         player_layout_[rank7_double_pawn_push_sq] == kBlack;
}

auto Board::Evaluate() const -> int {
  Bitboard white_pieces;
  Bitboard black_pieces;
  int board_score = 0;
  for (S8 piece_type = kPawn; piece_type <= kKing; ++piece_type) {
    white_pieces = pieces_[piece_type] & player_pieces_[kWhite];
    black_pieces = pieces_[piece_type] & player_pieces_[kBlack];
    board_score += kPieceVals[piece_type] *
                   (GetNumSetSq(white_pieces) - GetNumSetSq(black_pieces));
  }
  S8 moving_side = (player_to_move_ == kWhite) ? 1 : -1;
  return board_score * moving_side;
}

auto Board::MakeMove(const Move& move) -> void {
  if (move.castling_type == kNA) {
    MakeNonCastlingMove(move);
  } else if (move.castling_type == kQueenSide) {
    // Make queenside castling moves.
    if (player_to_move_ == kWhite) {
      MovePiece(kRook, kSqA1, kSqD1);
      MovePiece(kKing, kSqE1, kSqC1);
    } else if (player_to_move_ == kBlack) {
      MovePiece(kRook, kSqA8, kSqD8);
      MovePiece(kKing, kSqE8, kSqC8);
    }
  } else if (move.castling_type == kKingSide) {
    // Make kingside castling moves.
    if (player_to_move_ == kWhite) {
      MovePiece(kRook, kSqH1, kSqF1);
      MovePiece(kKing, kSqE1, kSqG1);
    } else if (player_to_move_ == kBlack) {
      MovePiece(kRook, kSqH8, kSqF8);
      MovePiece(kKing, kSqE8, kSqG8);
    }
  }

  // Update the en passent target square and the board hash to reflect a change
  // in the file of the en passent target square.
  ep_target_sq_history_.push(ep_target_sq_);
  S8 prev_ep_target_file =
      (ep_target_sq_ == kNA) ? kNA : GetFileFromSq(ep_target_sq_);
  ep_target_sq_ = move.new_ep_target_sq;
  S8 curr_ep_target_file =
      (ep_target_sq_ == kNA) ? kNA : GetFileFromSq(ep_target_sq_);
  if (prev_ep_target_file != curr_ep_target_file) {
    if (prev_ep_target_file != kNA) {
      board_hash_ ^= ep_file_rand_nums_[prev_ep_target_file];
    }
    if (curr_ep_target_file != kNA) {
      board_hash_ ^= ep_file_rand_nums_[curr_ep_target_file];
    }
  }

  // Reset the halfmove clock if a pawn was moved or if a move resulted in a
  // capture.
  halfmove_clock_history_.push(halfmove_clock_);
  if (move.captured_piece != kNA || move.moving_piece == kPawn) {
    halfmove_clock_ = 0;
  } else {
    ++halfmove_clock_;
  }

  UpdateCastlingRights(move);

  // Undo the move if it puts the king in check.
  if (KingInCheck()) {
    SwitchPlayer();
    UnmakeMove(move);
    throw BadMove("move leaves king in check");
  }

  SwitchPlayer();
  // Update the board hash to reflect player turnover.
  board_hash_ ^= black_to_move_rand_num_;
}

// Assume the passed move has been made use MakeMove(). Calling UnmakeMove()
// on a move that wasn't already made will result in undefined behavior.
auto Board::UnmakeMove(const Move& move) -> void {
  // Revert back to the previous player.
  SwitchPlayer();
  // Update the board hash to reflect player turnover.
  board_hash_ ^= black_to_move_rand_num_;

  if (move.castling_type == kNA) {
    // Undo all non-castling moves.
    UnmakeNonCastlingMove(move);
  } else if (move.castling_type == kQueenSide) {
    // Undo queenside castling moves.
    if (player_to_move_ == kWhite) {
      MovePiece(kRook, kSqD1, kSqA1);
      MovePiece(kKing, kSqC1, kSqE1);
    } else if (player_to_move_ == kBlack) {
      MovePiece(kRook, kSqD8, kSqA8);
      MovePiece(kKing, kSqC8, kSqE8);
    }
  } else if (move.castling_type == kKingSide) {
    // Undo kingside castling moves.
    if (player_to_move_ == kWhite) {
      MovePiece(kRook, kSqF1, kSqH1);
      MovePiece(kKing, kSqG1, kSqE1);
    } else if (player_to_move_ == kBlack) {
      MovePiece(kRook, kSqF8, kSqH8);
      MovePiece(kKing, kSqG8, kSqE8);
    }
  }

  // Revert the halfmove clock.
  halfmove_clock_ = halfmove_clock_history_.top();
  halfmove_clock_history_.pop();

  // Revert the en passent target square and update the board hash.
  if (ep_target_sq_ != kNA) {
    S8 ep_file = GetFileFromSq(ep_target_sq_);
    board_hash_ ^= ep_file_rand_nums_[ep_file];
  }
  ep_target_sq_ = ep_target_sq_history_.top();
  ep_target_sq_history_.pop();
  if (ep_target_sq_ != kNA) {
    S8 ep_file = GetFileFromSq(ep_target_sq_);
    board_hash_ ^= ep_file_rand_nums_[ep_file];
  }

  // Revert all castling rights and update the board hash.
  if (castling_rights_[kWhite][kQueenSide] !=
      white_queenside_castling_rights_history_.top()) {
    board_hash_ ^= castling_rights_rand_nums_[kWhite][kQueenSide];
    castling_rights_[kWhite][kQueenSide] =
        white_queenside_castling_rights_history_.top();
  }
  white_queenside_castling_rights_history_.pop();
  if (castling_rights_[kWhite][kKingSide] !=
      white_kingside_castling_rights_history_.top()) {
    board_hash_ ^= castling_rights_rand_nums_[kWhite][kKingSide];
    castling_rights_[kWhite][kKingSide] =
        white_kingside_castling_rights_history_.top();
  }
  white_kingside_castling_rights_history_.pop();
  if (castling_rights_[kBlack][kQueenSide] !=
      black_queenside_castling_rights_history_.top()) {
    board_hash_ ^= castling_rights_rand_nums_[kBlack][kQueenSide];
    castling_rights_[kBlack][kQueenSide] =
        black_queenside_castling_rights_history_.top();
  }
  black_queenside_castling_rights_history_.pop();
  if (castling_rights_[kBlack][kKingSide] !=
      black_kingside_castling_rights_history_.top()) {
    board_hash_ ^= castling_rights_rand_nums_[kBlack][kKingSide];
    castling_rights_[kBlack][kKingSide] =
        black_kingside_castling_rights_history_.top();
  }
  black_kingside_castling_rights_history_.pop();
}

// Implemement private member functions.

auto Board::GetAttackersToSq(S8 sq, S8 attacked_player) const -> Bitboard {
  if (!SqOnBoard(sq)) {
    throw invalid_argument("sq in Board::GetAttackersToSq()");
  }

  Bitboard potential_pawn_attacks;
  S8 attacking_player = GetOtherPlayer(attacked_player);
  // Capture only diagonal squares to sq in the direction of movement.
  if (attacked_player == kWhite) {
    potential_pawn_attacks = kNonSliderAttackMaps[kWhitePawnCapture][sq];
  } else {
    potential_pawn_attacks = kNonSliderAttackMaps[kBlackPawnCapture][sq];
  }
  // Compute the union (bitwise OR) of all pieces of each type that could
  // capture the square "sq"; each of these bitboards are computed by
  // finding the intersection (bitwise AND) between all spots a piece of a
  // given type could move to from sq, and all the positions that pieces of
  // this type from the opposing player are located.
  return (potential_pawn_attacks & GetPiecesByType(kPawn, attacking_player)) |
         (GetAttackMap(attacked_player, sq, kKnight) &
          GetPiecesByType(kKnight, attacking_player)) |
         (GetAttackMap(attacked_player, sq, kBishop) &
          GetPiecesByType(kBishop, attacking_player)) |
         (GetAttackMap(attacked_player, sq, kRook) &
          GetPiecesByType(kRook, attacking_player)) |
         (GetAttackMap(attacked_player, sq, kQueen) &
          GetPiecesByType(kQueen, attacking_player)) |
         (GetAttackMap(attacked_player, sq, kKing) &
          GetPiecesByType(kKing, attacking_player));
}

auto Board::AddPiece(S8 piece_type, S8 player, S8 sq) -> void {
  if (!SqOnBoard(sq)) {
    throw invalid_argument("sq in Board::AddPiece()");
  }

  if (piece_type == kNA && player == kNA) {
    piece_layout_[sq] = kNA;
    player_layout_[sq] = kNA;
  } else if (piece_type >= kPawn && piece_type <= kKing &&
             (player == kWhite || player == kBlack)) {
    Bitboard piece_mask = 1ULL << sq;
    pieces_[piece_type] |= piece_mask;
    player_pieces_[player] |= piece_mask;
    piece_layout_[sq] = piece_type;
    player_layout_[sq] = player;
  } else {
    throw invalid_argument("piece_type, player in Board::AddPiece()");
  }
}

auto Board::InitBoardHash() -> void {
  board_hash_ = 0X0;
  // Initialize the Mersenne Twister 64 bit pseudo-random number generator.
  U64 seed = std::chrono::system_clock::now().time_since_epoch().count();
  std::mt19937_64 rand_num_gen(seed);
  // Generate a set of random numbers for Zobrist Hashing.
  for (S8 player = kWhite; player < kNumPlayers; ++player) {
    for (S8 board_side = kQueenSide; board_side <= kKingSide; ++board_side) {
      castling_rights_rand_nums_[player][board_side] = rand_num_gen();
      if (castling_rights_[player][board_side]) {
        board_hash_ ^= castling_rights_rand_nums_[player][board_side];
      }
    }
  }
  for (S8 file = kFileA; file <= kFileH; ++file) {
    ep_file_rand_nums_[file] = rand_num_gen();
  }
  // Update the hash using the current en passent target square.
  if (ep_target_sq_ != kNA) {
    S8 ep_target_file = GetFileFromSq(ep_target_sq_);
    board_hash_ ^= ep_file_rand_nums_[ep_target_file];
  }
  S8 piece_type;
  for (S8 piece = kPawn; piece <= kKing; ++piece) {
    for (S8 sq = kSqA1; sq <= kSqH8; ++sq) {
      piece_rand_nums_[piece][sq] = rand_num_gen();
      // Update the hash using the current piece placement.
      piece_type = piece_layout_[sq];
      if (piece_type != kNA) {
        board_hash_ ^= piece_rand_nums_[piece_type][sq];
      }
    }
  }
  black_to_move_rand_num_ = rand_num_gen();
  // Update the hash using the side to move.
  if (player_to_move_ == kBlack) {
    board_hash_ ^= black_to_move_rand_num_;
  }
}

auto Board::InitBoardPos(const std::string& init_pos) -> void {
  S8 FEN_field = 0;
  S8 current_sq = kSqA8;
  for (char ch : init_pos) {
    // Keep track of which of the six fields is currently being parsed.
    if (ch == ' ') {
      ++FEN_field;
      continue;
    }

    if (FEN_field == 0) {
      // Add pieces to the board.
      if (isalpha(ch)) {
        switch (ch) {
          // Check for white pieces.
          case 'P':
            AddPiece(kPawn, kWhite, current_sq);
            break;
          case 'N':
            AddPiece(kKnight, kWhite, current_sq);
            break;
          case 'B':
            AddPiece(kBishop, kWhite, current_sq);
            break;
          case 'R':
            AddPiece(kRook, kWhite, current_sq);
            break;
          case 'Q':
            AddPiece(kQueen, kWhite, current_sq);
            break;
          case 'K':
            AddPiece(kKing, kWhite, current_sq);
            break;
          // Check for black pieces.
          case 'p':
            AddPiece(kPawn, kBlack, current_sq);
            break;
          case 'n':
            AddPiece(kKnight, kBlack, current_sq);
            break;
          case 'b':
            AddPiece(kBishop, kBlack, current_sq);
            break;
          case 'r':
            AddPiece(kRook, kBlack, current_sq);
            break;
          case 'q':
            AddPiece(kQueen, kBlack, current_sq);
            break;
          case 'k':
            AddPiece(kKing, kBlack, current_sq);
            break;
          default:
            throw invalid_argument("init_pos in Board::InitBoardPos()");
        }
        ++current_sq;
      } else if (ch - '0' >= 0 && ch - '0' <= 8) {
        S8 empty_sq_count = static_cast<S8>(ch - '0');
        S8 empty_sq = current_sq;
        for (; empty_sq < current_sq + empty_sq_count; ++empty_sq) {
          AddPiece(kNA, kNA, empty_sq);
        }
        current_sq = empty_sq;
      } else if (ch == '/') {
        // Set the current square to the rank below the current position.
        current_sq = static_cast<S8>(current_sq - 2 * kNumFiles);
      } else {
        throw invalid_argument("init_pos in Board::InitBoardPos()");
      }
    } else if (FEN_field == 1) {
      // Record the player to move.
      if (ch == 'w') {
        player_to_move_ = kWhite;
      } else if (ch == 'b') {
        player_to_move_ = kBlack;
      } else {
        throw invalid_argument("init_pos in Board::InitBoardPos()");
      }
    } else if (FEN_field == 2) {
      // Assign castling rights for each player and board side.
      switch (ch) {
        case 'Q':
          castling_rights_[kWhite][kQueenSide] = true;
          break;
        case 'K':
          castling_rights_[kWhite][kKingSide] = true;
          break;
        case 'q':
          castling_rights_[kBlack][kQueenSide] = true;
          break;
        case 'k':
          castling_rights_[kBlack][kKingSide] = true;
          break;
        case '-':
          break;
        default:
          throw invalid_argument("init_pos in Board::InitBoardPos()");
      }
    } else if (FEN_field == 3) {
      // Assign the en passent target square.
      if (ep_target_sq_ == kNA) {
        S8 ep_target_sq_file = static_cast<S8>(ch - 'a');
        if (FileOnBoard(ep_target_sq_file)) {
          ep_target_sq_ = ep_target_sq_file;
        } else if (ch != '-') {
          throw invalid_argument("init_pos in Board::InitBoardPos()");
        }
      } else {
        // Assume ep_target_sq_ is initialized to the target
        // square's file.
        S8 ep_target_sq_rank = static_cast<S8>(ch - '1');
        if (RankOnBoard(ep_target_sq_rank)) {
          ep_target_sq_ = GetSqFromRankFile(ep_target_sq_rank, ep_target_sq_);
        } else {
          throw invalid_argument("init_pos in Board::InitBoardPos()");
        }
      }
    } else if (FEN_field == 4) {
      // Initialize the halfmove clock.
      if (isdigit(ch)) {
        S8 next_digit = static_cast<S8>(ch - '0');
        if (halfmove_clock_ == kNA) {
          halfmove_clock_ = next_digit;
        } else {
          halfmove_clock_ = static_cast<S8>(10 * halfmove_clock_ + next_digit);
        }
      } else {
        throw invalid_argument("init_pos in Board::InitBoardPos()");
      }
    } else if (FEN_field == 5) {
      // Ignore the fullmove counter.
      ;
    } else {
      throw invalid_argument("init_pos in Board::InitBoardPos()");
    }
  }
}

auto Board::MakeNonCastlingMove(const Move& move) -> void {
  // Remove a captured piece from the board.
  if (move.captured_piece != kNA) {
    S8 other_player = GetOtherPlayer(player_to_move_);
    if (move.is_ep) {
      S8 ep_capture_sq = 0X0;
      S8 target_file = GetFileFromSq(move.target_sq);
      // Compute the position for a captured pawn in an en passent,
      // and then remove it from the board.
      if (player_to_move_ == kWhite) {
        ep_capture_sq = GetSqFromRankFile(kRank5, target_file);
        piece_layout_[ep_capture_sq] = kNA;
        player_layout_[ep_capture_sq] = kNA;
      } else if (player_to_move_ == kBlack) {
        ep_capture_sq = GetSqFromRankFile(kRank4, target_file);
        piece_layout_[ep_capture_sq] = kNA;
        player_layout_[ep_capture_sq] = kNA;
      }
      Bitboard ep_capture_mask = ~(1ULL << ep_capture_sq);
      pieces_[kPawn] &= ep_capture_mask;
      player_pieces_[other_player] &= ep_capture_mask;
      // Update the board hash to reflect piece removal.
      board_hash_ ^= piece_rand_nums_[kPawn][ep_capture_sq];
    } else {
      // Remove the captured piece from the board.
      Bitboard piece_capture_mask = ~(1ULL << move.target_sq);
      pieces_[move.captured_piece] &= piece_capture_mask;
      player_pieces_[other_player] &= piece_capture_mask;
      // Update the board hash to reflect piece removal.
      board_hash_ ^= piece_rand_nums_[move.captured_piece][move.target_sq];
    }
  }

  MovePiece(move.moving_piece, move.start_sq, move.target_sq,
            move.promoted_to_piece);
}

auto Board::MovePiece(S8 piece, S8 start_sq, S8 target_sq, S8 promoted_to_piece)
    -> void {
  if (piece < kPawn || piece > kKing) {
    throw invalid_argument("piece in Board::MovePiece()");
  } else if (!SqOnBoard(start_sq)) {
    throw invalid_argument("start_sq in Board::MovePiece()");
  } else if (!SqOnBoard(target_sq)) {
    throw invalid_argument("target_sq in Board::MovePiece()");
  } else if (promoted_to_piece != kNA &&
             (promoted_to_piece <= kPawn || promoted_to_piece >= kKing)) {
    throw invalid_argument("promoted_to_piece in Board::MovePiece()");
  }

  // Remove the selected piece from its start position on the board.
  piece_layout_[start_sq] = kNA;
  player_layout_[start_sq] = kNA;
  Bitboard rm_piece_mask = ~(1ULL << start_sq);
  pieces_[piece] &= rm_piece_mask;
  player_pieces_[player_to_move_] &= rm_piece_mask;
  // Update the board hash to reflect piece removal.
  board_hash_ ^= piece_rand_nums_[piece][start_sq];

  // Add the selected piece back at its target position on the board and update
  // the board hash to reflect piece addition.
  Bitboard new_piece_pos_mask = 1ULL << target_sq;
  if (promoted_to_piece == kNA) {
    pieces_[piece] |= new_piece_pos_mask;
    piece_layout_[target_sq] = piece;
    board_hash_ ^= piece_rand_nums_[piece][target_sq];
  } else {
    // Add a piece back as the type it promotes to if move is a pawn promotion.
    pieces_[promoted_to_piece] |= new_piece_pos_mask;
    piece_layout_[target_sq] = promoted_to_piece;
    board_hash_ ^= piece_rand_nums_[promoted_to_piece][target_sq];
  }

  player_layout_[target_sq] = player_to_move_;
  player_pieces_[player_to_move_] |= new_piece_pos_mask;
}

auto Board::UnmakeNonCastlingMove(const Move& move) -> void {
  // Move the moving piece back to its original position and undo
  // any pawn promotion.
  if (move.promoted_to_piece == kNA) {
    MovePiece(move.moving_piece, move.target_sq, move.start_sq);
  } else {
    // Remove the promoted-to piece from the board.
    Bitboard piece_promotion_rm_mask = ~(1ULL << move.target_sq);
    pieces_[move.promoted_to_piece] &= piece_promotion_rm_mask;
    player_pieces_[player_to_move_] &= piece_promotion_rm_mask;
    piece_layout_[move.target_sq] = kNA;
    player_layout_[move.target_sq] = kNA;
    // Update the board hash to reflect piece removal.
    board_hash_ ^= piece_rand_nums_[move.promoted_to_piece][move.target_sq];

    // Add the original pawn back to its start position.
    Bitboard og_piece_pos_mask = 1ULL << move.start_sq;
    pieces_[kPawn] |= og_piece_pos_mask;
    player_pieces_[player_to_move_] |= og_piece_pos_mask;
    piece_layout_[move.start_sq] = kPawn;
    player_layout_[move.start_sq] = player_to_move_;
    // Update the board hash to reflect piece addition.
    board_hash_ ^= piece_rand_nums_[kPawn][move.start_sq];
  }

  // Place a captured piece back onto the board.
  if (move.captured_piece != kNA) {
    S8 other_player = GetOtherPlayer(player_to_move_);
    if (move.is_ep) {
      S8 ep_capture_sq = 0X0;
      S8 target_file = GetFileFromSq(move.target_sq);
      // Place a captured pawn back onto the board after an en passent.
      if (player_to_move_ == kWhite) {
        ep_capture_sq = GetSqFromRankFile(kRank5, target_file);
        piece_layout_[ep_capture_sq] = kPawn;
        player_layout_[ep_capture_sq] = kBlack;
      } else if (player_to_move_ == kBlack) {
        ep_capture_sq = GetSqFromRankFile(kRank4, target_file);
        piece_layout_[ep_capture_sq] = kPawn;
        player_layout_[ep_capture_sq] = kWhite;
      }
      Bitboard undo_ep_capture_mask = 1ULL << ep_capture_sq;
      pieces_[kPawn] |= undo_ep_capture_mask;
      player_pieces_[other_player] |= undo_ep_capture_mask;
      // Update the board hash to reflect piece addition.
      board_hash_ ^= piece_rand_nums_[kPawn][ep_capture_sq];
    } else {
      Bitboard undo_capture_mask = 1ULL << move.target_sq;
      // Add the captured piece back to its original position.
      pieces_[move.captured_piece] |= undo_capture_mask;
      player_pieces_[other_player] |= undo_capture_mask;
      piece_layout_[move.target_sq] = move.captured_piece;
      player_layout_[move.target_sq] = other_player;
      // Update the board hash to reflect piece addition.
      board_hash_ ^= piece_rand_nums_[move.captured_piece][move.target_sq];
    }
  }
}

auto Board::UpdateCastlingRights(const Move& move) -> void {
  // Record the current castling rights before updating them.
  white_queenside_castling_rights_history_.push(
      castling_rights_[kWhite][kQueenSide]);
  white_kingside_castling_rights_history_.push(
      castling_rights_[kWhite][kKingSide]);
  black_queenside_castling_rights_history_.push(
      castling_rights_[kBlack][kQueenSide]);
  black_kingside_castling_rights_history_.push(
      castling_rights_[kBlack][kKingSide]);

  if (move.castling_type != kNA || move.moving_piece == kKing) {
    // Revoke all castling rights for a player after moving the king.
    if (castling_rights_[player_to_move_][kQueenSide]) {
      castling_rights_[player_to_move_][kQueenSide] = false;
      board_hash_ ^= castling_rights_rand_nums_[player_to_move_][kQueenSide];
    }
    if (castling_rights_[player_to_move_][kKingSide]) {
      castling_rights_[player_to_move_][kKingSide] = false;
      board_hash_ ^= castling_rights_rand_nums_[player_to_move_][kKingSide];
    }
  } else if (move.moving_piece == kRook) {
    S8 start_rank = GetRankFromSq(move.start_sq);
    S8 start_file = GetFileFromSq(move.start_sq);
    // Check that a rook is moving from its original starting position
    // and that the player still has castling rights on that side before
    // revoking castling rights.
    if ((player_to_move_ == kWhite && start_rank == kRank1) ||
        (player_to_move_ == kBlack && start_rank == kRank8)) {
      if (start_file == kFileA &&
          castling_rights_[player_to_move_][kQueenSide]) {
        castling_rights_[player_to_move_][kQueenSide] = false;
        board_hash_ ^= castling_rights_rand_nums_[player_to_move_][kQueenSide];
      } else if (start_file == kFileH &&
                 castling_rights_[player_to_move_][kKingSide]) {
        castling_rights_[player_to_move_][kKingSide] = false;
        board_hash_ ^= castling_rights_rand_nums_[player_to_move_][kKingSide];
      }
    }
  }

  if (move.captured_piece == kRook) {
    // Revoke the other player's castling rights if a player's rook is captured.
    if (player_to_move_ == kWhite) {
      if (move.target_sq == kSqA8 && castling_rights_[kBlack][kQueenSide]) {
        castling_rights_[kBlack][kQueenSide] = false;
        board_hash_ ^= castling_rights_rand_nums_[kBlack][kQueenSide];
      } else if (move.target_sq == kSqH8 &&
                 castling_rights_[kBlack][kKingSide]) {
        castling_rights_[kBlack][kKingSide] = false;
        board_hash_ ^= castling_rights_rand_nums_[kBlack][kKingSide];
      }
    } else if (player_to_move_ == kBlack) {
      if (move.target_sq == kSqA1 && castling_rights_[kWhite][kQueenSide]) {
        castling_rights_[kWhite][kQueenSide] = false;
        board_hash_ ^= castling_rights_rand_nums_[kWhite][kQueenSide];
      } else if (move.target_sq == kSqH1 &&
                 castling_rights_[kWhite][kKingSide]) {
        castling_rights_[kWhite][kKingSide] = false;
        board_hash_ ^= castling_rights_rand_nums_[kWhite][kKingSide];
      }
    }
  }
}

}  // namespace omegazero


//---------------------------------------------------------------------------//
// Copyright (c) 2018-2020 Mikhail Komarov <nemo@nil.foundation>
//
// MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//---------------------------------------------------------------------------//

#ifndef CRYPTO3_BLOCK_MODE_CIPHER_FEEDBACK_HPP
#define CRYPTO3_BLOCK_MODE_CIPHER_FEEDBACK_HPP

#include <memory>
#include <limits>

#include <boost/static_assert.hpp>

#include <nil/crypto3/modes/mode.hpp>
#include <nil/crypto3/modes/cts.hpp>

namespace nil {
    namespace crypto3 {
        namespace block {
            namespace modes {
                namespace detail {
                    template<typename Cipher, typename Padding, template<typename> class Allocator = std::allocator>
                    struct cfb_policy {
                        typedef std::size_t size_type;

                        typedef Cipher cipher_type;
                        typedef Padding padding_type;

                        constexpr static const size_type block_bits = cipher_type::block_bits;
                        constexpr static const size_type block_words = cipher_type::block_words;
                        typedef typename cipher_type::block_type block_type;

                        static void shift_register(cipher_type &cipher) {
                            const size_t shift = feedback();
                            const size_t carryover = block_size() - shift;

                            if (carryover > 0) {
                                copy_mem(m_state.data(), &m_state[shift], carryover);
                            }
                            copy_mem(&m_state[carryover], m_keystream.data(), shift);
                            cipher().encrypt(m_state, m_keystream);
                            m_keystream_pos = 0;
                        }

                        static inline void xor_copy(uint8_t buf[], uint8_t key_buf[], size_t len) {
                            for (size_t i = 0; i != len; ++i) {
                                uint8_t k = key_buf[i];
                                key_buf[i] = buf[i];
                                buf[i] ^= k;
                            }
                        }
                    };

                    template<typename Cipher, std::size_t FeedbackBits, typename Padding,
                             typename CiphertextStealingMode>
                    struct cfb_encryption_policy : public cfb_policy<Cipher, Padding> {};

                    template<typename Cipher, std::size_t FeedbackBits, typename Padding>
                    struct cfb_encryption_policy<Cipher, FeedbackBits, Padding, cts<0, Cipher, Padding>>
                        : public cfb_policy<Cipher, Padding> {
                        typedef typename cfb_policy<Cipher, Padding>::size_type size_type;

                        typedef typename cfb_policy<Cipher, Padding>::cipher_type cipher_type;
                        typedef typename cfb_policy<Cipher, Padding>::padding_type padding_type;

                        constexpr static const size_type block_bits = cfb_policy<Cipher, Padding>::block_bits;
                        constexpr static const size_type block_words = cfb_policy<Cipher, Padding>::block_words;
                        typedef typename cfb_policy<Cipher, Padding>::block_type block_type;

                        inline static block_type begin_message(const cipher_type &cipher, const block_type &plaintext) {
                            if (!valid_nonce_length(nonce_len)) {
                                throw invalid_iv_length(name(), nonce_len);
                            }

                            if (nonce_len == 0) {
                                if (m_state.empty()) {
                                    throw invalid_state("CFB requires a non-empty initial nonce");
                                }
                                // No reason to encipher state->keystream_buf, because no change
                            } else {
                                m_state.assign(nonce, nonce + nonce_len);
                                m_keystream.resize(m_state.size());
                                cipher().encrypt(m_state, m_keystream);
                                m_keystream_pos = 0;
                            }
                        }

                        inline static block_type process_block(const cipher_type &cipher, const block_type &plaintext) {
                            const size_t shift = feedback();

                            size_t left = sz;

                            if (m_keystream_pos != 0) {
                                const size_t take = std::min<size_t>(left, shift - m_keystream_pos);

                                xor_buf(m_keystream.data() + m_keystream_pos, buf, take);
                                copy_mem(buf, m_keystream.data() + m_keystream_pos, take);

                                m_keystream_pos += take;
                                left -= take;
                                buf += take;

                                if (m_keystream_pos == shift) {
                                    shift_register();
                                }
                            }

                            while (left >= shift) {
                                xor_buf(m_keystream.data(), buf, shift);
                                copy_mem(buf, m_keystream.data(), shift);

                                left -= shift;
                                buf += shift;
                                shift_register();
                            }

                            if (left > 0) {
                                xor_buf(m_keystream.data(), buf, left);
                                copy_mem(buf, m_keystream.data(), left);
                                m_keystream_pos += left;
                            }

                            return sz;
                        }

                        inline static block_type end_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type result = {0};

                            return result;
                        }
                    };

                    template<typename Cipher, std::size_t FeedbackBits, typename Padding>
                    struct cfb_encryption_policy<Cipher, FeedbackBits, Padding, cts<1, Cipher, Padding>>
                        : public cfb_policy<Cipher, Padding> {
                        typedef typename cfb_policy<Cipher, Padding>::size_type size_type;

                        typedef typename cfb_policy<Cipher, Padding>::cipher_type cipher_type;
                        typedef typename cfb_policy<Cipher, Padding>::padding_type padding_type;

                        constexpr static const size_type block_bits = cfb_policy<Cipher, Padding>::block_bits;
                        constexpr static const size_type block_words = cfb_policy<Cipher, Padding>::block_words;
                        typedef typename cfb_policy<Cipher, Padding>::block_type block_type;

                        inline static block_type begin_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type block = {0};

                            return cipher.encrypt(block);
                        }

                        inline static block_type process_block(const cipher_type &cipher, const block_type &plaintext) {
                            block_type block = {0};

                            return cipher.encrypt(block);
                        }

                        inline static block_type end_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type result = {0};

                            return result;
                        }
                    };

                    template<typename Cipher, std::size_t FeedbackBits, typename Padding>
                    struct cfb_encryption_policy<Cipher, FeedbackBits, Padding, cts<2, Cipher, Padding>>
                        : public cfb_policy<Cipher, Padding> {
                        typedef typename cfb_policy<Cipher, Padding>::size_type size_type;

                        typedef typename cfb_policy<Cipher, Padding>::cipher_type cipher_type;
                        typedef typename cfb_policy<Cipher, Padding>::padding_type padding_type;

                        constexpr static const size_type block_bits = cfb_policy<Cipher, Padding>::block_bits;
                        constexpr static const size_type block_words = cfb_policy<Cipher, Padding>::block_words;
                        typedef typename cfb_policy<Cipher, Padding>::block_type block_type;

                        inline static block_type begin_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type block = {0};

                            return cipher.encrypt(block);
                        }

                        inline static block_type process_block(const cipher_type &cipher, const block_type &plaintext) {
                            block_type block = {0};

                            return cipher.encrypt(block);
                        }

                        inline static block_type end_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type result = {0};

                            return result;
                        }
                    };

                    template<typename Cipher, std::size_t FeedbackBits, typename Padding>
                    struct cfb_encryption_policy<Cipher, FeedbackBits, Padding, cts<3, Cipher, Padding>>
                        : public cfb_policy<Cipher, Padding> {
                        typedef typename cfb_policy<Cipher, Padding>::size_type size_type;

                        typedef typename cfb_policy<Cipher, Padding>::cipher_type cipher_type;
                        typedef typename cfb_policy<Cipher, Padding>::padding_type padding_type;

                        constexpr static const size_type block_bits = cfb_policy<Cipher, Padding>::block_bits;
                        constexpr static const size_type block_words = cfb_policy<Cipher, Padding>::block_words;
                        typedef typename cfb_policy<Cipher, Padding>::block_type block_type;

                        inline static block_type begin_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type block = {0};

                            return cipher.encrypt(block);
                        }

                        inline static block_type process_block(const cipher_type &cipher, const block_type &plaintext) {
                            block_type block = {0};

                            return cipher.encrypt(block);
                        }

                        inline static block_type end_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type result = {0};

                            return result;
                        }
                    };

                    template<typename Cipher, std::size_t FeedbackBits, typename Padding,
                             typename CiphertextStealingMode>
                    struct cfb_decryption_policy : public cfb_policy<Cipher, Padding> {};

                    template<typename Cipher, std::size_t FeedbackBits, typename Padding>
                    struct cfb_decryption_policy<Cipher, FeedbackBits, Padding, cts<0, Cipher, Padding>>
                        : public cfb_policy<Cipher, Padding> {
                        typedef typename cfb_policy<Cipher, Padding>::size_type size_type;

                        typedef typename cfb_policy<Cipher, Padding>::cipher_type cipher_type;
                        typedef typename cfb_policy<Cipher, Padding>::padding_type padding_type;

                        constexpr static const std::size_t block_bits = cfb_policy<Cipher, Padding>::block_bits;
                        constexpr static const std::size_t block_words = cfb_policy<Cipher, Padding>::block_words;
                        typedef typename cfb_policy<Cipher, Padding>::block_type block_type;

                        inline static block_type begin_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type block = cipher.decrypt(plaintext);

                            //                        codec::encode<codec::encoder::logic_xor>(block, iv);

                            return block;
                        }

                        inline static block_type process_block(const cipher_type &cipher, const block_type &plaintext) {
                            const size_t shift = feedback();

                            size_t left = sz;

                            if (m_keystream_pos != 0) {
                                const size_t take = std::min<size_t>(left, shift - m_keystream_pos);

                                xor_copy(buf, m_keystream.data() + m_keystream_pos, take);

                                m_keystream_pos += take;
                                left -= take;
                                buf += take;

                                if (m_keystream_pos == shift) {
                                    shift_register();
                                }
                            }

                            while (left >= shift) {
                                xor_copy(buf, m_keystream.data(), shift);
                                left -= shift;
                                buf += shift;
                                shift_register();
                            }

                            if (left > 0) {
                                xor_copy(buf, m_keystream.data(), left);
                                m_keystream_pos += left;
                            }

                            return sz;
                        }

                        inline static block_type end_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type result = cipher.decrypt(plaintext);

                            //                        codec::encode<codec::encoder::logic_xor>(result, iv);

                            return result;
                        }
                    };

                    template<typename Cipher, std::size_t FeedbackBits, typename Padding>
                    struct cfb_decryption_policy<Cipher, FeedbackBits, Padding, cts<1, Cipher, Padding>>
                        : public cfb_policy<Cipher, Padding> {
                        typedef typename cfb_policy<Cipher, Padding>::size_type size_type;

                        typedef typename cfb_policy<Cipher, Padding>::cipher_type cipher_type;
                        typedef typename cfb_policy<Cipher, Padding>::padding_type padding_type;

                        constexpr static const std::size_t block_bits = cfb_policy<Cipher, Padding>::block_bits;
                        constexpr static const std::size_t block_words = cfb_policy<Cipher, Padding>::block_words;
                        typedef typename cfb_policy<Cipher, Padding>::block_type block_type;

                        inline static block_type begin_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type block = cipher.decrypt(plaintext);

                            //                        codec::encode<codec::encoder::logic_xor>(block, iv);

                            return block;
                        }

                        inline static block_type process_block(const cipher_type &cipher, const block_type &plaintext) {
                            block_type result = cipher.decrypt(plaintext);

                            //                        codec::encode<codec::encoder::logic_xor>(result, previous);

                            return result;
                        }

                        inline static block_type end_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type result = cipher.decrypt(plaintext);

                            //                        codec::encode<codec::encoder::logic_xor>(result, iv);

                            return result;
                        }
                    };

                    template<typename Cipher, std::size_t FeedbackBits, typename Padding>
                    struct cfb_decryption_policy<Cipher, FeedbackBits, Padding, cts<2, Cipher, Padding>>
                        : public cfb_policy<Cipher, Padding> {
                        typedef typename cfb_policy<Cipher, Padding>::size_type size_type;

                        typedef typename cfb_policy<Cipher, Padding>::cipher_type cipher_type;
                        typedef typename cfb_policy<Cipher, Padding>::padding_type padding_type;

                        constexpr static const std::size_t block_bits = cfb_policy<Cipher, Padding>::block_bits;
                        constexpr static const std::size_t block_words = cfb_policy<Cipher, Padding>::block_words;
                        typedef typename cfb_policy<Cipher, Padding>::block_type block_type;

                        inline static block_type begin_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type block = cipher.decrypt(plaintext);

                            //                        codec::encode<codec::encoder::logic_xor>(block, iv);

                            return block;
                        }

                        inline static block_type process_block(const cipher_type &cipher, const block_type &plaintext) {
                            block_type result = cipher.decrypt(plaintext);

                            //                        codec::encode<codec::encoder::logic_xor>(result, previous);

                            return result;
                        }

                        inline static block_type end_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type result = cipher.decrypt(plaintext);

                            //                        codec::encode<codec::encoder::logic_xor>(result, iv);

                            return result;
                        }
                    };

                    template<typename Cipher, std::size_t FeedbackBits, typename Padding>
                    struct cfb_decryption_policy<Cipher, FeedbackBits, Padding, cts<3, Cipher, Padding>>
                        : public cfb_policy<Cipher, Padding> {
                        typedef typename cfb_policy<Cipher, Padding>::size_type size_type;

                        typedef typename cfb_policy<Cipher, Padding>::cipher_type cipher_type;
                        typedef typename cfb_policy<Cipher, Padding>::padding_type padding_type;

                        constexpr static const std::size_t block_bits = cfb_policy<Cipher, Padding>::block_bits;
                        constexpr static const std::size_t block_words = cfb_policy<Cipher, Padding>::block_words;
                        typedef typename cfb_policy<Cipher, Padding>::block_type block_type;

                        inline static block_type begin_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type block = cipher.decrypt(plaintext);

                            //                        codec::encode<codec::encoder::logic_xor>(block, iv);

                            return block;
                        }

                        inline static block_type process_block(const cipher_type &cipher, const block_type &plaintext) {
                            block_type result = cipher.decrypt(plaintext);

                            //                        codec::encode<codec::encoder::logic_xor>(result, previous);

                            return result;
                        }

                        inline static block_type end_message(const cipher_type &cipher, const block_type &plaintext) {
                            block_type result = cipher.decrypt(plaintext);

                            //                        codec::encode<codec::encoder::logic_xor>(result, iv);

                            return result;
                        }
                    };

                    template<typename Policy>
                    class cipher_feedback {
                        typedef Policy policy_type;

                    public:
                        typedef typename policy_type::cipher_type cipher_type;
                        typedef typename policy_type::padding_type padding_type;

                        typedef typename policy_type::size_type size_type;

                        typedef typename cipher_type::key_type key_type;
                        typedef typename policy_type::iv_type iv_type;

                        constexpr static const size_type block_bits = policy_type::block_bits;
                        constexpr static const size_type block_words = policy_type::block_words;
                        typedef typename cipher_type::block_type block_type;

                        cipher_feedback(const cipher_type &cipher) : cipher(cipher) {
                        }

                        block_type begin_message(const block_type &plaintext) {
                            previous = policy_type::begin_message(cipher, plaintext);
                            return previous;
                        }

                        block_type process_block(const block_type &plaintext) {
                            previous = policy_type::process_block(cipher, plaintext, previous);
                            return previous;
                        }

                        block_type end_message(const block_type &plaintext) {
                            return policy_type::end_message(cipher, plaintext);
                        }

                        inline static size_type required_output_size(size_type inputlen) {
                            return padding_type::required_output_size(inputlen);
                        }

                    protected:
                        block_type previous;
                        cipher_type cipher;
                    };
                }    // namespace detail

                /*!
                 * @brief Cipher Feedback Mode (CBC).
                 *
                 * @tparam Cipher
                 * @tparam FeedbackBits
                 * @tparam Padding
                 * @tparam CiphertextStealingMode
                 *
                 * @ingroup block_modes
                 */
                template<typename Cipher, std::size_t FeedbackBits, template<typename> class Padding,
                         template<typename, typename> class CiphertextStealingMode = cts0>
                struct cipher_feedback {
                    typedef Cipher cipher_type;
                    typedef Padding<Cipher> padding_type;
                    typedef CiphertextStealingMode<Cipher, Padding<Cipher>> ciphertext_stealing_type;

                    constexpr static const std::size_t feedback_bits = FeedbackBits;
                    BOOST_STATIC_ASSERT(feedback_bits <= cipher_type::block_bits);
                    BOOST_STATIC_ASSERT(feedback_bits % CHAR_BIT == 0);

                    typedef detail::cfb_encryption_policy<cipher_type, feedback_bits, padding_type,
                                                          ciphertext_stealing_type>
                        encryption_policy;
                    typedef detail::cfb_decryption_policy<cipher_type, feedback_bits, padding_type,
                                                          ciphertext_stealing_type>
                        decryption_policy;

                    template<template<typename, typename> class Policy>
                    struct bind {
                        typedef detail::cipher_feedback<Policy<cipher_type, padding_type>> type;
                    };
                };

                /*!
                 * @brief
                 * @tparam Cipher
                 * @tparam FeedbackBits
                 * @tparam Padding
                 * @tparam CiphertextStealingMode
                 *
                 * @ingroup block_modes
                 */
                template<typename Cipher, std::size_t FeedbackBits, template<typename> class Padding,
                         template<typename, typename> class CiphertextStealingMode = cts0>
                using cfb = cipher_feedback<Cipher, FeedbackBits, Padding, CiphertextStealingMode>;
            }    // namespace modes
        }        // namespace block
    }            // namespace crypto3
}    // namespace nil

#endif


/** Boost */
#include <boost/serialization/serialization.hpp>
#include <boost/serialization/unordered_map.hpp>
#include <boost/serialization/unordered_set.hpp>
#include <boost/serialization/utility.hpp>
#include <boost/serialization/vector.hpp>

#include <boost/archive/binary_iarchive.hpp>
#include <boost/archive/binary_oarchive.hpp>

#include <boost/program_options.hpp>
namespace po = boost::program_options;

/** STL */
#include <chrono>
#include <condition_variable>
#include <future>
#include <mutex>
#include <thread>
#include <vector>

/** Ligero */
#include "Common.hpp"

#include "EncryptedClient.hpp"
#include "EncryptedCoordinator.hpp"

#include "ZkArgument.hpp"
#include "protocol/ExpressNPStatement.hpp"

#include "FiniteFields.hpp"
#include "Hash.hpp"
#include "SecretSharingNTT.hpp"

using namespace ligero;
using namespace std::chrono_literals;

/** ===== Thread synchronization ===== */
std::mutex mutex;
std::condition_variable cv;
bool thread_aborted = false;

/** Parameters RSA Ceremony */
/** ======================= */

constexpr auto degree = 1 << 16;
constexpr auto p = 9;
constexpr auto q = 21;
constexpr auto sigma = 8;
constexpr auto lambda = 128;
constexpr auto tau_limit_bit = 175;
constexpr auto tau = 1000;
constexpr auto numCandidates = 2048;

/** throughput parties */
constexpr auto wait_timeout = 300 * 1000; /* wait 5 min when start */
constexpr auto timeout = 25 * 1000;       /* a little longer than test */
constexpr auto data_size = 32 * 1024;     /* 32 KB */
constexpr auto nb_max_send = 1024;
constexpr auto parties = 1;

/** throughtput coordinator */
constexpr auto pbs = 1000;
constexpr auto duration = 20s;
constexpr auto wait_time = 120s;
constexpr auto cleanup_time = 5s;
constexpr auto throughput_cutoff = 100 * 1024; /* 200 KB/s */

constexpr auto receive_timeout = 60min;

/** Parameters Zero-Knowledge */
/** ========================= */

/** Settings Zero-Knowledge */
RegularInteger p_zk(10514644122014433281ULL);
typedef Fpp_Fixed<p_zk> FieldT;

zksnark::InterleavedRSCode irs;        /** Reed-Solomon Interleaved Code */
ligero::MTParameters<FieldT> mtParams; /** Merkle Tree Parameters */
constexpr size_t l = 8192;             // 4096;             /** Bloc size */
constexpr size_t k = 16384;            // 8192;             /** Degree */
constexpr size_t n = 65536;            // 16384;            /** # of Shares */

/** Columns to Open Up (By default half the columns) */
constexpr size_t t = 189;
constexpr auto ptNumberEvaluation = 3;

/** Random Generator */
const hash::HashIntegerSeedGeneratorF<FieldT>& fieldGenerator =
    hash::blake2bIntegerSeedGenerator<FieldT>;
const hash::HashStringSeedGeneratorF& stringGenerator = hash::blake2bStringHash;

/** Threading */
constexpr size_t maxNumberThreads = 50;
/** [m5.metal has 96 threads over 48 cores] */

/** Selecting a port for distributed verifying */
constexpr char* port = "5556";

/** Depositing the data here */
PublicData pdata;
SigmaProtocolPublicData spdata;
std::vector<ligero::zksnark::FullFSTranscript<FieldT>> transcripts;
size_t availableTranscriptsIndex = 0;
bool firstIteration = true;

std::string localIpAddr = "UNKNOWN";

/** Stitching Proofs */
constexpr int nbIterations = 3;
constexpr int nbVarsStitched = 4;
constexpr int nbBlocksPerVar = degree / l;

/** Data */
std::string hashedPublicData;

void resetGlobalVariables() {
  pdata = PublicData{};
  transcripts.clear();
  availableTranscriptsIndex = 0;
  firstIteration = true;
  thread_aborted = false;
  hashedPublicData.clear();
}

/** This separate function handles the data coming in from the coordinator, as
 * well as sigma protocol verification. The concern is that otherwise we might
 * fall behind on receiving public data & proof because of processing delays.
 * @param transport the transport object handling communication with the
 * coordinator
 */
void collectData(ZeroMQClientTransport& transport) {
  DBG("Verifier " << transport.getSocketId() << " begining data acquisition.");

  /** Tell the gathering coodrinator we are ready */
  if (firstIteration) transport.send(MessageType::VERIFIER_READY, localIpAddr, false);

  /** Receive Sigma Protocol Public Data */
  auto maybe_sigma = transport.awaitReply<SigmaProtocolPublicData>(
      MessageType::GATHER_SIGMA_DATA, true);

  if (hasError(maybe_sigma)) {
    LOG(ERROR) << "Kill/Restart received";
    thread_aborted = true;
    cv.notify_one();
    return;
  } else {
    spdata = getResult(maybe_sigma);
  }

  /** Receive public data */
  auto maybe_pdata =
      transport.awaitReply<PublicData>(MessageType::GATHER_PUBLIC_DATA, true);

  if (hasError(maybe_pdata)) {
    LOG(ERROR) << "Kill/Restart received";
    thread_aborted = true;
    cv.notify_one();
    return;
  } else {
    pdata = getResult(maybe_pdata);
    DBG("Received and deserialized public data.");

    /** Hashing Public Data */
    pdata.modulusIdx = 0;
    pdata.roots.clear();
    pdata.ptNumberEvaluation = ptNumberEvaluation;
    hashedPublicData = hash::hashPublicData(stringGenerator, pdata, spdata);
  }

  /** Verify Sigma Protocol */
  LOG(INFO) << "Verifying Sigma protocol proof";

  auto [alphasGCD, _drop_val] =
      math::fixed_bucket_n_primes(3210, primesGCD, 175);
  std::vector<mpz_class> coefs;
  std::vector<mpz_class> nfl_primes;
  std::vector<mpz_class> nfl_primes_full;
  for (int i = 0; i < 9; i++)
    nfl_primes.emplace_back(mpz_class(nfl::params<uint64_t>::P[i]));
  for (int i = 0; i < NbPrimesQ; i++)
    nfl_primes_full.emplace_back(mpz_class(nfl::params<uint64_t>::P[i]));

  /** Recomputing the final candidate from the CRT factors */
  std::vector<mpz_class> finalModuli_GCD;
  finalModuli_GCD.resize(alphasGCD.size());
  for (int alphaGCDidx = 0; alphaGCDidx < alphasGCD.size(); alphaGCDidx++) {
    std::vector<mpz_class> prime_decomp_f;
    for (int i = 0; i < NbPrimesQ; i++) {
      prime_decomp_f.emplace_back(
          mpz_class(pdata.finalModuli_GCD[alphaGCDidx * NbPrimesQ + i]));
    }
    finalModuli_GCD[alphaGCDidx] =
        math::crt_reconstruct(prime_decomp_f, coefs, nfl_primes_full);
  }
  std::vector<mpz_class> candidates;
  candidates.emplace_back(
      math::crt_reconstruct(finalModuli_GCD, coefs, alphasGCD));

  /** Recomputing the gamma values */
  mpz_class gammaSeed = pdata.gammaSeed;
  std::vector<mpz_class> gammaValues;
  gammaValues.resize(candidates.size() * 128);
  std::vector<mpz_class> engine =
      math::generateRandomVector(gammaSeed, kJacobiNumberOfRandomValues, 2048);
  mpz_t one;
  mpz_init(one);
  mpz_set_ui(one, 1);
  size_t engineCount = 0;

  {
    mpz_t gcdResult;
    mpz_init(gcdResult);

    for (int i = 0; i < candidates.size(); ++i) {
      const mpz_class& N = candidates[i];

      DBG(" N % 2 = 1; N = " << N);
      /** We expect that N is 3 (mod 4), and therefore must be odd. */
      assert(N % 2 == 1);

      int foundGamma = 0;
      while (foundGamma < 128) {
        mpz_class g = engine[engineCount];
        ++engineCount;
        if (engineCount >= engine.size()) {
          engineCount = 0;
          DBG("Regenerating random values for Jacobi (GCD) gamma values");
          engine = math::generateRandomVector(
              engine[0], kJacobiNumberOfRandomValues, 2048);
        }
        g = g % N;
        assert(g != 0);

        DBG("g = " << g);
        if (mpz_jacobi(g.get_mpz_t(), N.get_mpz_t()) == 1) {
          gammaValues[i * 128 + foundGamma] = g;
          foundGamma++;
        }
      }

      /** Sanity check, assert if gcd(gammaValues[i], N) == 1 */
      for (int j = 0; j < 128; ++j) {
        mpz_gcd(gcdResult, gammaValues[i * 128 + j].get_mpz_t(), N.get_mpz_t());
        assert(mpz_cmp(gcdResult, one) == 0);
      }
    }

    mpz_clear(one);
    mpz_clear(gcdResult);
  }
  for (int j = 0; j < 128; j++) {
    std::vector<mpz_class> sigma_a_GCD;
    sigma_a_GCD.resize(alphasGCD.size());
    std::vector<mpz_class> sigma_e_GCD;
    sigma_e_GCD.resize(alphasGCD.size());
    std::vector<mpz_class> sigma_z_GCD;
    sigma_z_GCD.resize(alphasGCD.size());
    std::vector<mpz_class> sigma_g_GCD;
    sigma_g_GCD.resize(alphasGCD.size());
    for (int alphaGCDidx = 0; alphaGCDidx < alphasGCD.size(); alphaGCDidx++) {
      std::vector<mpz_class> prime_decomp_a;
      std::vector<mpz_class> prime_decomp_e;
      std::vector<mpz_class> prime_decomp_z;
      std::vector<mpz_class> prime_decomp_g;
      for (int i = 0; i < 9; i++) {
        prime_decomp_a.emplace_back(
            mpz_class(spdata.sigmaaGCD[i * 128 * alphasGCD.size() +
                                       j * alphasGCD.size() + alphaGCDidx]));
        prime_decomp_e.emplace_back(
            mpz_class(spdata.sigmaeGCD[i * 128 * alphasGCD.size() +
                                       j * alphasGCD.size() + alphaGCDidx]));
        prime_decomp_z.emplace_back(
            mpz_class(spdata.sigmazGCD[i * 128 * alphasGCD.size() +
                                       j * alphasGCD.size() + alphaGCDidx]));
        prime_decomp_g.emplace_back(
            mpz_class(spdata.sigmagGCD[i * 128 * alphasGCD.size() +
                                       j * alphasGCD.size() + alphaGCDidx]));
      }
      sigma_a_GCD[alphaGCDidx] =
          math::crt_reconstruct(prime_decomp_a, coefs, nfl_primes);
      sigma_e_GCD[alphaGCDidx] =
          math::crt_reconstruct(prime_decomp_e, coefs, nfl_primes);
      sigma_z_GCD[alphaGCDidx] =
          math::crt_reconstruct(prime_decomp_z, coefs, nfl_primes);
      sigma_g_GCD[alphaGCDidx] =
          math::crt_reconstruct(prime_decomp_g, coefs, nfl_primes);
    }
    mpz_class sigmaa = math::crt_reconstruct(sigma_a_GCD, coefs, alphasGCD);
    mpz_class sigmae = math::crt_reconstruct(sigma_e_GCD, coefs, alphasGCD);
    mpz_class sigmaz = math::crt_reconstruct(sigma_z_GCD, coefs, alphasGCD);
    mpz_class sigmag = math::crt_reconstruct(sigma_g_GCD, coefs, alphasGCD);
    mpz_class sigma_lhs = math::powm(gammaValues[j], sigmaz, candidates[0]);
    mpz_class sigma_rhs = math::mod(
        sigmaa * math::powm(sigmag, sigmae, candidates[0]), candidates[0]);

    assert(sigma_lhs == sigma_rhs);
  }

  LOG(INFO) << "Verified Sigma protocol proof";

  for (size_t modulusIdx = 0; modulusIdx < q; modulusIdx++) {
    /** Receive proofs */
    auto maybe_proof =
        transport.awaitReply<ligero::zksnark::FullFSTranscript<FieldT>>(
            MessageType::GATHER_PROOFS, true);

    if (hasError(maybe_proof)) {
      LOG(ERROR) << "Kill/Restart received";
      thread_aborted = true;
      cv.notify_one();
      return;
    } else {
      /** get lock and notify verifier */
      std::unique_lock<std::mutex> lock(mutex);

      transcripts.push_back(getResult(maybe_proof));
      availableTranscriptsIndex++;

      lock.unlock();
      cv.notify_one();
    }
  }

  LOG(INFO) << "Got all proofs";

  return;
}

/** This function verifies the proofs as they are gathered as well as
 * cross-moduli integrity constraints ("stitching")
 * @param transport the transport object handling communication with the
 * coordinator
 * @param myTimers reference to the timers to use for this run
 * @return Unit if successful
 */
expected<Unit> verify(ZeroMQClientTransport& transport, timers& myTimers) {
  /** Reset all global variables */
  resetGlobalVariables();

  /** Verifying */
  /** ================================================================ */
  FieldT::modulusIdx_ = 0;
  size_t aggregateSuccess = 0;

  Statements statement = Statements::RSACeremony;
  Statements localStatement = statement;

  /** Primitive Roots of Unity for All Moduli */
  nfl::poly_p<uint64_t, degree, q> roots({0});
  for (size_t idx = 0; idx < q; idx++) {
    roots(idx, 1) = 1ull;
  }
  roots.ntt_pow_phi();

  ligero::zksnark::FullFSTranscript<FieldT> transcript;

  SecretData sdata;

  /** Dimensioning ai scalars */
  sdata.ai.resize(nbIterations);
  for (size_t iter = 0; iter < nbIterations; iter++) {
    sdata.ai[iter].resize(nbVarsStitched);
    for (size_t var = 0; var < nbVarsStitched; var++) {
      sdata.ai[iter][var].resize(nbBlocksPerVar);
      for (size_t block = 0; block < nbBlocksPerVar; block++) {
        sdata.ai[iter][var][block].resize(l);
      }
    }
  }

  /** Storing the results of the verification per modulus */
  std::vector<int> report(q, 0);

  /** Import Public Data */
  std::thread* dataCollection =
      new std::thread(collectData, std::ref(transport));

  /** CRT representation to check integrity for the proofs */
  std::vector<mpz_class> stitchingCRT;
  mpz_class stitching0;

  for (size_t modulusIdx = 0; modulusIdx < q; modulusIdx++) {
    std::string timerVerificationIdle =
        std::string("6.a. verification, idle, modulus ") +
        std::to_string(modulusIdx);
    myTimers.begin(1, timerVerificationIdle.c_str(), 0);

    {
      /** get lock and wait for complete */
      std::unique_lock<std::mutex> lock(mutex);
      auto received = cv.wait_for(lock, receive_timeout, [&] {
        return thread_aborted || (availableTranscriptsIndex > modulusIdx);
      });

      /** check if thread failed or timeout */
      /** we won't restart this verifier because the corresponding party will be
       */
      /** kicked out */
      if (thread_aborted || !received) {
        LOG(INFO) << "Working thread failed or timed out, quit verifier";
        dataCollection->join();
        delete dataCollection;
        return Error::TIMED_OUT;
      }

      lock.unlock();
    }

    myTimers.end(1, timerVerificationIdle.c_str(), 0, true);

    std::string timerVerificationCompute =
        std::string("6.a. verification, compute, modulus ") +
        std::to_string(modulusIdx);
    myTimers.begin(1, timerVerificationCompute.c_str(), 0);

    auto& transcript = transcripts[modulusIdx];
    bool success = true;

    LOG(INFO) << "Verifying modulus idx:" << modulusIdx;

    /** Zero-Knowledge */
    p_zk = nfl::params<uint64_t>::P[modulusIdx];
    FieldT::modulusIdx_ = modulusIdx;
    FieldT::preprocessing(log2(n) + 1);

    pdata.modulusIdx = modulusIdx;

    /** For portions 7 to 12, only run the first 9 moduli */
    if ((localStatement == Statements::RSACeremony) && (pdata.modulusIdx > 8))
      localStatement = Statements::RSACeremony_Rounds3to6;

    /** Roots of Unity */
    pdata.roots.assign(
        roots.poly_obj().data() + pdata.modulusIdx * (size_t)roots.degree,
        roots.poly_obj().data() +
            (pdata.modulusIdx + 1) * (size_t)roots.degree);
    pdata.ptNumberEvaluation = ptNumberEvaluation;

    zksnark::Verifier<FieldT> verifier(
        localStatement, irs, t, l, fieldGenerator, stringGenerator, mtParams,
        pdata, spdata, sdata, permut<k>::compute, permut<n>::compute,
        hashedPublicData);

    try {
      verifier.verifyProof(transcript);

      LOG(INFO) << "Verifier mod " << modulusIdx << ":" << verifier._stitching;
    } catch (failedTest& e) {
      DBG("Failed Test: " << e.what());
      success = false;
    }

    if (modulusIdx == 0) {
      stitching0 = verifier._stitching;
    } else {
      stitchingCRT.push_back(verifier._stitching);
    }

    if (success) {
      LOG(INFO) << "Verification for modulus idx:" << modulusIdx
                << " succeeded";
      report[modulusIdx] = 1;
      aggregateSuccess++;
    } else {
      LOG(INFO) << "Verification for modulus idx:" << modulusIdx << " failed";
      report[modulusIdx] = 0;
    }

    myTimers.end(1, timerVerificationCompute.c_str(), 0, true);
  }

  /** Cross-Moduli Verification */
  {
    bool success = false;
    std::vector<mpz_class> mods, coefs;
    for (size_t i = 1; i < q; i++) {
      mods.emplace_back(params<uint64_t>::P[i]);
    }
    success = (math::mod(stitching0 -
                             math::crt_reconstruct(stitchingCRT, coefs, mods),
                         mpz_class(params<uint64_t>::P[0])) == mpz_class(0));

    if (success) {
      LOG(INFO) << "Verification for cross-moduli integrity succeeded";
    } else {
      LOG(INFO) << "Verification for cross-moduli integrity failed";
      for (size_t idx = 0; idx < q; idx++) {
        report[idx] = 0;
      }
      aggregateSuccess = 0;
    }
  }

  dataCollection->join();
  delete dataCollection;

  /** Wait for the signal from the gathering coordinator */
  auto maybe_response_reports = transport.awaitReply();

  if (hasError(maybe_response_reports)) {
    if (getError(maybe_response_reports) == Error::RESTART) {
      LOG(INFO) << "Restart verifier";
      myTimers.reset();
      return verify(transport, myTimers);
    } else {
      LOG(INFO) << "Timed out";
      return Unit{};
    }
  } else {
    if (getResult(maybe_response_reports) != MessageType::GATHER_REPORTS) {
      LOG(ERROR) << "Out of sequence message received when expecting gathering "
                    "reports, aborting.";
      return Error::OUT_OF_SYNC;
    }
  }

  if (aggregateSuccess == q) {
    LOG(INFO) << "All proofs successfully verified.";
  } else {
    LOG(INFO) << "Failed to verify some of the proofs.";
  }

  /** Send the verifier report to the gathering coordinator */
  transport.send<std::vector<int>>(MessageType::VERIFIER_REPORT, report, false);

  /** Finally wait for restart message if any verification failed */
  auto maybeRestart = transport.awaitReply();

  if (hasError(maybeRestart) && getError(maybeRestart) == Error::RESTART) {
    LOG(INFO) << "Restart message received, restarting verifier";
    myTimers.reset();
    return verify(transport, myTimers);
  } else if (getResult(maybeRestart) == MessageType::END) {
    LOG(INFO) << "All done.";
    return Unit{};
  } else {
    LOG(INFO) << "Unknown error happened!";
    return Error::UNKNOWN_ERROR;
  }
}

int main(int argc, char** argv) {
  std::string coordinatorIP("127.0.0.1");

  po::options_description desc("Allowed options");
  desc.add_options()("help", "produce help message")(
      "localIPaddress", po::value<std::string>(), "sets the local IP address")(
      "ip", po::value<std::string>(), "coordinator IP address");

  po::variables_map vm;
  po::positional_options_description p;
  p.add("coordinator-ip", -1);

  po::store(
      po::command_line_parser(argc, argv).options(desc).positional(p).run(),
      vm);

  po::notify(vm);

  if (vm.count("help")) {
    LOG(INFO) << desc << "\n";
    LOG(INFO)
        << "USAGE: --localIPaddress <IP Distributed Verifier> <IP Coordinator>";
    return 0;
  }

  if (vm.count("localIPaddress")) {
    localIpAddr = vm["localIPaddress"].as<std::string>();
    LOG(INFO) << "Local IP Address " << localIpAddr;
  }

  if (vm.count("ip")) {
    coordinatorIP = vm["ip"].as<std::string>();
    LOG(INFO) << "Gathering Coordinator IP " << coordinatorIP;
  }

  SocketId socketId = boost::uuids::random_generator()();
  ZeroMQClientTransport transport(std::string("tcp://") + coordinatorIP +
                                      std::string(":") + std::string(port),
                                  socketId, receive_timeout);

  /** Set Zero-Knowledge Parameters */
  irs.k = k; /** Degree of the Polynomial */
  irs.n = n; /** Number of Shares */

  mtParams.digestLength = 32;
  mtParams.zkByteSize = 8;
  mtParams.hashInnerNodeF = hash::blake2bTwoToOneHash;
  mtParams.hashLeafContentF = hash::blake2bFieldElementHash<FieldT>;
  mtParams.hashZKRandomnessF = hash::blake2bZKElementHash;
  mtParams.leavesNumber = irs.n;

  /** Configure easylogging */
  configureEasyLogging("distributed_verifier");

  /** Add timers */
  timers myTimers;
  myTimers.initialize_timer();

  auto result = verify(transport, myTimers);

  if (hasError(result)) {
    return EXIT_FAILURE;
  }

  return 0;
}