// Copyright 2019 DeepMind Technologies Ltd. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "open_spiel/games/chess/pgn_parser.h"
#include "open_spiel/games/chess/pgn.h"

#include <iostream>
#include <string>
#include <utility>
#include <vector>

#include "open_spiel/abseil-cpp/absl/strings/match.h"
#include "open_spiel/abseil-cpp/absl/strings/str_cat.h"
#include "open_spiel/abseil-cpp/absl/strings/str_join.h"
#include "open_spiel/spiel_utils.h"

namespace open_spiel {
namespace pgn {

namespace {

void skipBlanks(std::string::const_iterator &itr1, const std::string::const_iterator &end)
{
  while ((itr1 != end) && (isspace(*itr1)))
  {
    itr1++;
  }
}

} // namespace


bool Parser::GetGames(std::string::const_iterator &itr1, const std::string::const_iterator &itr2, std::vector<ChessPGN> &out)
{
  std::string::const_iterator local_itr = itr1;
  int a = 1;
  while (true)
  {
    std::cout << a++ << std::endl;
    absl::optional<ChessPGN> game = GetGame(local_itr, itr2);
    if (!game) {
      break;
    }
    itr1 = local_itr;
    out.push_back(*game);
    skipBlanks(itr1, itr2);
    if (itr1 == itr2) {
      break;
    }
  }
  return true;
}

absl::optional<ChessPGN> Parser::GetGame(std::string::const_iterator &itr1, const std::string::const_iterator &itr2)
{
  std::string::const_iterator local_itr = itr1;

  std::vector<std::pair<std::string, std::string>> tags;
  if (!GetTags(local_itr, itr2, tags))
    return absl::nullopt;

  itr1 = local_itr;

  skipBlanks(local_itr, itr2);
  if (absl::optional<std::string> comment = GetComment(local_itr, itr2)) {
    itr1 = local_itr;
    // before game comment not supported
  }

  skipBlanks(local_itr, itr2);
  std::vector<Turn> turns;
  if (!GetTurns(local_itr, itr2, turns))
    SpielFatalError("Error parsing moves");

  itr1 = local_itr;

  absl::optional<std::vector<double>> result;
  if (!(result = GetGameResult(local_itr, itr2))) {
    return absl::nullopt;
  }

  itr1 = local_itr;

  skipBlanks(local_itr, itr2);
  if (absl::optional<std::string> comment = GetComment(local_itr, itr2)) {
    itr1 = local_itr;
    // after game comments not supported
  }

  // parse the tags and the chess state
  std::string event;
  std::string site;
  std::string date;
  int round;
  std::string white;
  std::string black;
  for (auto &[key, value] : tags) {
    if (key == "Event") event = value;
    else if (key == "Site") site = value; // todo use site or is it even needed
    else if (key == "Date") date = value; // todo use date or is it even needed
    else if (key == "Round") round = std::atoi(value.c_str());
    else if (key == "White") white = value;
    else if (key == "Black") black = value;
    // todo kriegspiel and the rest
  }

//  int a = event.find("w16", 0);
//  if (event.find("w16", 0) >= event.size()) {
//    auto b = event.c_str();
//    return absl::nullopt;
//  }

  std::unique_ptr<State> state = LoadGame("chess")->NewInitialState();
  auto chess_state = down_cast<chess::ChessState*>(state.get());

  int prev_turn = 0;
  bool should_be_over = false;
  for (const Turn &turn : turns) {
    if (should_be_over) {
      SpielFatalError("Only last turn can have only one ply");
    }
    if (turn.turn_number != ++prev_turn) {
      SpielFatalError("Invalid order of turns in PGN file");
    }
    std::optional<chess::Move>
        white_move = chess_state->Board().ParseSANMove(turn.white_move);
    if (!white_move) {
      std::cout << turn.white_move << std::endl;
      std::cout << chess_state->Board().DebugString() << std::endl;
      SpielFatalError("Invalid move parsed from PGN file");
    }
    Action white_action =
        chess::MoveToAction(*white_move, chess_state->BoardSize());
    chess_state->ApplyAction(white_action);

    if (turn.black_move) {
      std::optional<chess::Move>
          black_move = chess_state->Board().ParseSANMove(*turn.black_move);
      if (!black_move) {
        SpielFatalError("Invalid move parsed from PGN file");
      }
      Action black_action =
          chess::MoveToAction(*black_move, chess_state->BoardSize());
      chess_state->ApplyAction(black_action);
    } else {
      should_be_over = true;
    }
  }

  ChessPGN pgn = ChessPGN(*chess_state, event, round, white, black);
  return pgn;
}

bool pgn::Parser::GetTags(std::string::const_iterator &itr1, const std::string::const_iterator &itr2, std::vector<std::pair<std::string, std::string>> &out)
{
  std::string::const_iterator local_itr = itr1;

  std::vector<std::pair<std::string, std::string>> tags;
  while (std::optional<std::pair<std::string, std::string>> tag = GetTag(local_itr, itr2))
  {
    itr1 = local_itr;
    tags.push_back(*tag);
  }
  if (tags.empty())  {
    return false;
  }

  out = tags;
  return true;
}

absl::optional<std::pair<std::string, std::string>> Parser::GetTag(std::string::const_iterator &itr1, const std::string::const_iterator &itr2)
{
  std::string::const_iterator local_itr = itr1;

  skipBlanks(local_itr, itr2);
  if (*local_itr != '[')
  {
    return absl::nullopt;
  }
  local_itr++;

  skipBlanks(local_itr, itr2);
  std::string key;

  while ((local_itr != itr2) && (isalnum(*local_itr)))
  {
    key += *local_itr++;
  }
  itr1 = local_itr;

  skipBlanks(local_itr, itr2);
  std::string value;
  while ((local_itr != itr2) && (*local_itr != ']'))
  {
    value += *local_itr++;
  }

  if (local_itr == itr2 || value[0] != '"' || value[value.size() - 1] != '"') // missing ']'
  {
    SpielFatalError("Invalid tag in PGN file");
  }

  value = value.substr(1, value.size() - 2);

  local_itr++;
  itr1 = local_itr;
  return std::make_pair(key, value);
}

bool Parser::GetTurns(std::string::const_iterator &itr1, const std::string::const_iterator &itr2, std::vector<Turn> &out)
{
  std::string::const_iterator local_itr = itr1;

  std::vector<Turn> turns;

  // It's possible that a move list (i.e. a variation) starts with a comment
  if (absl::optional<std::string> comment = GetComment(local_itr, itr2))
  {
    skipBlanks(local_itr, itr2);
    itr1 = local_itr;
    // turn comments not supported
  }

  skipBlanks(local_itr, itr2);

  while (absl::optional<Turn> turn = GetTurn(local_itr, itr2))
  {
    itr1 = local_itr;
    turns.push_back(*turn);
  }
  itr1 = local_itr;
  out = turns;

  return true;
}

absl::optional<Turn> Parser::GetTurn(std::string::const_iterator &itr1, const std::string::const_iterator &itr2)
{
  std::string::const_iterator local_itr = itr1;

  // looking for move number
  absl::optional<int> move_number;
  if (!(move_number = GetMoveNumber(local_itr, itr2)))
    return absl::nullopt;
  itr1 = local_itr;

  // looking for first ply (mandatory)
  skipBlanks(local_itr, itr2);
  absl::optional<std::string> first_ply;
  if (!(first_ply = GetPly(local_itr, itr2))) {
    SpielFatalError("Error parsing move");
  }

  skipBlanks(local_itr, itr2);

  // eof as in
  // ss << "1.Nf3"; ss >> move;
  if (local_itr == itr2)
  {
    return Turn{*move_number, *first_ply, absl::nullopt};
  }

  // second first_ply, game result or another move number?
  // a ply always begins with an uppercase or lowercase alphabetic char.
  // digits and '*' have lesser ascii values.
  // if we meet a "[" that's owned by the next game, so game result is
  // missing
  absl::optional<std::string> second_ply = absl::nullopt;
  if (*local_itr > '1' && *local_itr != '[')
  {
    // looking for second ply (optional)
    second_ply = GetPly(local_itr, itr2);
    skipBlanks(local_itr, itr2);
  }
  itr1 = local_itr;

  return Turn{*move_number, *first_ply, second_ply};
}

absl::optional<std::vector<double>> pgn::Parser::GetGameResult(std::string::const_iterator &itr1, const std::string::const_iterator &itr2)
{
  std::string::const_iterator local_itr = itr1;

  std::string result;
  while (!isspace(*local_itr) && local_itr != itr2)
  {
    result += *local_itr++;
  }

  if (result[0] == '[')
  {
    SpielFatalError("Missing result in PGN file");
  }

  int white_result;
  int black_result;
  if (result == "1-0") {
    white_result = chess::WinUtility();
    black_result = chess::LossUtility();
  } else if (result == "0-1") {
    white_result = chess::LossUtility();
    black_result = chess::WinUtility();
  } else if (result == "1/2-1/2" || result == "*") {
    white_result = chess::DrawUtility();
    black_result = chess::DrawUtility();
  } else {
    SpielFatalError("Invalid result in PGN file");
  }
  std::vector<double> returns(chess::NumPlayers());
  returns[chess::ColorToPlayer(chess::Color::kWhite)] = white_result;
  returns[chess::ColorToPlayer(chess::Color::kBlack)] = black_result;
  itr1 = local_itr;
  return returns;
}

absl::optional<int> Parser::GetMoveNumber(std::string::const_iterator &itr1, const std::string::const_iterator &itr2)
{
  std::string::const_iterator local_itr = itr1;

  if (!isdigit(*local_itr))
    return absl::nullopt;

  // looking for number
  std::string move_number;
  while (isdigit(*local_itr) && local_itr != itr2)
  {
    move_number += *local_itr++;
  }
  skipBlanks(local_itr, itr2);

  if (*local_itr != '.')
  {
    // maybe reading game result '1-0'.
    return absl::nullopt;
  }

  // skipping and counting dots
  while ((*local_itr == '.') && (local_itr != itr2))
  {
    local_itr++;
  }
  itr1 = local_itr;
  return std::atoi(move_number.c_str());
}

absl::optional<std::string> pgn::Parser::GetPly(std::string::const_iterator &itr1, const std::string::const_iterator &itr2)
{
  std::string::const_iterator local_itr = itr1;

  skipBlanks(local_itr, itr2);
  while ((local_itr != itr2) && (!isspace(*local_itr)))
  {
    if (*local_itr != ')')
      local_itr++;
    else
      break;
  }
  std::string move_str = std::string(itr1, local_itr);

  skipBlanks(local_itr, itr2);
  itr1 = local_itr;

  int glyphval = GetGlyph(local_itr, itr2);
  if (glyphval >= 0)
  {
    skipBlanks(local_itr, itr2);
    itr1 = local_itr;
    // glyphs not supported
  }

  itr1 = local_itr;

  if (absl::optional<std::string> comment = GetComment(local_itr, itr2))
  {
    skipBlanks(local_itr, itr2);
    itr1 = local_itr;
    // todo in kriegspiel use it
  }

  itr1 = local_itr;

  std::vector<Turn> variation;
  while (GetVariation(local_itr, itr2, variation))
  {
    skipBlanks(local_itr, itr2);
    itr1 = local_itr;
    // variations not supported
  }

  itr1 = local_itr;

  return move_str;
}

absl::optional<std::string> Parser::GetComment(std::string::const_iterator &itr1, const std::string::const_iterator &itr2)
{
  std::string::const_iterator local_itr = itr1;
  std::string comment;
  if (*local_itr != '{')
    return absl::nullopt;
  local_itr++; // skipping '{'

  while ((*local_itr != '}') && (local_itr != itr2))
  {
    comment += *local_itr++;
  }
  local_itr++; // skipping '}'
  skipBlanks(local_itr, itr2);
  itr1 = local_itr;
  return comment;
}

bool Parser::GetVariation(std::string::const_iterator &itr1, const std::string::const_iterator &itr2, std::vector<Turn> &out)
{
  std::string::const_iterator local_itr = itr1;
  if (*local_itr != '(')
    return false;
  local_itr++; // skipping '('

  if (!GetTurns(local_itr, itr2, out))
    throw std::runtime_error("Error parsing movelist");

  local_itr++; // skipping ')'
  skipBlanks(local_itr, itr2);
  itr1 = local_itr;

  return true;
}

int Parser::GetGlyph(std::string::const_iterator &itr1, const std::string::const_iterator &itr2)
{
  if (*itr1 == '$')
  {
    int val = -1;
    itr1++;
    if (isdigit(*itr1))
      val = *itr1 - '0';
    itr1++;
    if (isdigit(*itr1))
      val = val*10 + (*itr1 - '0');
    return val;
  }
  else
    return -1;
}

}  // namespace pgn
}  // namespace open_spiel
