//
//  ShardokGameController.cpp
//  eagle0_engine
//
//  Created by Dan Crosby on 7/9/17.
//  Copyright © 2017 none. All rights reserved.
//

#include "ShardokGameController.hpp"

#include <thread>

#include "src/main/cpp/net/eagle0/common/ContainerUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"

namespace shardok {
using std::condition_variable;
using std::mutex;
using std::scoped_lock;
using std::string;
using std::unique_lock;
using std::weak_ptr;
using std::chrono::duration;

static constexpr duration kWaitForUpdatesDuration = std::chrono::milliseconds(5000);

using net::eagle0::shardok::common::GameStatus;

class InvalidTokenException : public std::exception {};

class MismatchedPlayerIdException : public std::exception {
private:
    PlayerId shardokPlayerId;
    int eagleFactionId;

    string message;

public:
    MismatchedPlayerIdException(const PlayerId shardokPid, const int eagleFid)
        : shardokPlayerId(shardokPid),
          eagleFactionId(eagleFid) {
        message = "Mismatched playerId " + std::to_string(shardokPlayerId) +
                  " with eagle factionId " + std::to_string(eagleFactionId);
    }

    [[nodiscard]] auto what() const noexcept -> const char * override { return message.c_str(); }
};

class MissingPlayerIdException : public std::exception {
private:
    PlayerId shardokPlayerId;

    string message;

public:
    MissingPlayerIdException(const PlayerId shardokPid) : shardokPlayerId(shardokPid) {
        message = "Missing playerId " + std::to_string(shardokPlayerId);
    }

    [[nodiscard]] auto what() const noexcept -> const char * override { return message.c_str(); }
};

vector<shared_ptr<ShardokAIClient>> ShardokGameController::MakeAIClients(
        const unique_ptr<ShardokEngine> &e) {
    vector<shared_ptr<ShardokAIClient>> aic;

    for (const auto &pi : e->GetPlayerInfos()) {
        if (pi.is_ai()) {
            auto newClient = std::make_shared<ShardokAIClient>(
                    pi.player_id(),
                    pi.is_defender(),
                    e->GetCurrentGameState()->hex_map(),
                    e->GetGameSettings()->GetGetter());

            aic.push_back(newClient);
        }
    }

    return aic;
}

void ShardokGameController::LockedNotifyClients() const { updateCondition.notify_all(); }

auto ShardokGameController::LockedAIClientForPid(PlayerId pid) const
        -> shared_ptr<ShardokAIClient> {
    return common::FindIf(
                   aiClients,
                   [pid](const auto &client) { return client->GetPlayerId() == pid; })
            .value_or(nullptr);
}

void ShardokGameController::DoAIThread() {
    printf("Entering AI thread.\n");
    if (aiClients.empty()) { printf("No AI players, exiting AI thread.\n"); }

    while (aiThreadKeepGoing) {
        while (incomingRegistrations > 0) {
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
        }

        unique_lock lk(masterLock);
        if (LockedCheckOneAICommand()) {
            LockedNotifyClients();
            aiThreadKeepGoing = aiThreadKeepGoing && !engine->GameIsOver();
        } else {
            aiCondition.wait(lk);
            aiThreadKeepGoing = aiThreadKeepGoing && !engine->GameIsOver();
        }
    }
    printf("Exiting AI thread.\n");
}

ShardokGameController::~ShardokGameController() {
    {
        unique_lock lk(masterLock);
        aiThreadKeepGoing = false;
        aiCondition.notify_one();
    }
    aiThread.join();
}

auto ShardokGameController::LockedCheckOneAICommand() -> bool {
    if (engine->GameIsOver()) {
        printf("Game is over!\n");
        return false;
    }

    const PlayerId currentPid = engine->GetCurrentPlayerId();
    if (const shared_ptr<ShardokAIClient> currentPlayerClient = LockedAIClientForPid(currentPid)) {
        const int index = currentPlayerClient->ChooseCommandIndex(*engine).chosenIndex;

        engine->PostCommand(currentPid, index);
        LockedNotifyClients();
        return true;
    }

    return false;
}

void CheckFactionId(
        const unique_ptr<ShardokEngine> &engine,
        const PlayerId shardokPlayerId,
        const int eagleFactionId) {
    bool found = false;
    for (const auto &pi : *engine->GetCurrentGameState()->player_infos()) {
        if (pi->player_id() != shardokPlayerId) continue;
        found = true;
        if (pi->eagle_faction_id() != eagleFactionId) {
            throw MismatchedPlayerIdException(shardokPlayerId, eagleFactionId);
        }
    }

    if (!found) throw MissingPlayerIdException(shardokPlayerId);
}

void ShardokGameController::PostCommand(
        const PlayerId shardokPlayerId,
        const int eagleFactionId,
        const int64_t token,
        const int64_t commandIndex,
        const optional<int32_t> roll) {
    scoped_lock<mutex> guard(masterLock);

    CheckFactionId(engine, shardokPlayerId, eagleFactionId);

    const auto expectedToken = engine->GetUnfilteredHistoryCount();
    if (token < expectedToken) {
        printf("Double token in postCommand\n");
        // The client is missing some updates; probably it's a double-submit
        // Let's just ignore and see how it goes
    } else if (token > expectedToken) {
        // This shouldn't be possible
        throw InvalidTokenException();
    } else {
        engine->PostCommand(shardokPlayerId, commandIndex, nullptr, roll);
        LockedNotifyClients();
        engine->PostWhileCurrentPlayerHasOnlyOneOption(nullptr);
    }

    LockedNotifyClients();
    aiCondition.notify_one();
}

void ShardokGameController::PostPlacementCommands(
        const PlayerId shardokPlayerId,
        const int eagleFactionId,
        const int64_t token,
        const vector<UnitPlacementInfo> &infos) {
    {
        scoped_lock<mutex> guard(masterLock);

        CheckFactionId(engine, shardokPlayerId, eagleFactionId);

        const auto expectedToken = engine->GetUnfilteredHistoryCount();
        if (token < expectedToken) {
            printf("Double token in postPlacementCommands\n");
            // The client is missing some updates; probably it's a double-submit
            // Let's just ignore and see how it goes
        } else if (token > expectedToken) {
            // This shouldn't be possible
            throw InvalidTokenException();
        } else {
            // The token matches, execute the commands
            engine->PostPlacementCommands(shardokPlayerId, infos);
            engine->PostFinishedPlacementCommand(shardokPlayerId);
            engine->PostWhileCurrentPlayerHasOnlyOneOption(nullptr);
        }

        LockedNotifyClients();
        aiCondition.notify_one();
    }
}

auto ShardokGameController::GetCurrentGameStateBytes() -> byte_vector {
    return engine->GetCurrentGameStateBytes();
}

auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpdates {
    AllUpdates updates{};

    incomingRegistrations++;
    vector<net::eagle0::shardok::storage::ShardokActionWithResultingState> awrs;
    {
        unique_lock<mutex> guard(masterLock);

        updates.newUnfilteredCount = engine->GetUnfilteredHistoryCount();

        awrs = engine->GetGameHistory(startingActionId);
        incomingRegistrations--;

        // If the current player is an AI, wait for some results to post. Otherwise go ahead and
        // return, we might be telling the caller about available commands.
        if (awrs.empty() && !engine->GameIsOver() &&
            engine->GetPlayerInfos()[engine->GetCurrentPlayerId()].is_ai()) {
            updateCondition.wait_for(guard, kWaitForUpdatesDuration);
            incomingRegistrations++;
            awrs = engine->GetGameHistory(startingActionId);
            incomingRegistrations--;
        }

        updates.mainResults = common::Map(awrs, [](const ShardokActionWithResultingState &a) {
            return a.action_result();
        });

        const auto playerInfos = engine->GetPlayerInfos();
        updates.filteredResults.reserve(playerInfos.size() + 1);
        for (const auto &pi : playerInfos) {
            if (pi.is_ai()) continue;

            auto fid = pi.eagle_faction_id();

            auto actionResultViews = engine->FilterNewResults(pi.player_id(), startingActionId);

            shared_ptr<AvailableCommands> acs = nullptr;

            const auto availableCommands = engine->GetAvailableCommandProtos(pi.player_id(), true);
            const auto previewCommands = engine->GetPreviewCommands(pi.player_id());

            if (!availableCommands.empty() || !previewCommands.empty()) {
                acs = std::make_shared<AvailableCommands>();
                *acs->mutable_current_command() = {
                        std::begin(availableCommands),
                        std::end(availableCommands)};
                *acs->mutable_preview_command() = {
                        std::begin(previewCommands),
                        std::end(previewCommands)};
            }

            updates.filteredResults.emplace_back(fid, actionResultViews, acs);
        }

        updates.filteredResults.emplace_back(
                -1,
                engine->FilterNewResults(-1, startingActionId),
                nullptr);
    }

    return updates;
}

auto ShardokGameController::GameIsOver() -> bool {
    scoped_lock<mutex> guard(masterLock);

    const auto &gameStatus = engine->GetGameStatus();
    return ShardokEngine::GameIsOver(gameStatus);
}

auto ShardokGameController::GameOverStatus() -> net::eagle0::shardok::common::GameStatus {
    scoped_lock<mutex> guard(masterLock);

    return fb::ToProto(engine->GetGameStatus());
}

auto ShardokGameController::EndGameUnits() -> vector<net::eagle0::shardok::storage::ResolvedUnit> {
    scoped_lock<mutex> guard(masterLock);
    return engine->EndGameUnits();
}

auto ShardokGameController::ResolvedPlayerInfos()
        -> vector<net::eagle0::shardok::common::PlayerInfo> {
    scoped_lock<mutex> guard(masterLock);
    return engine->GetPlayerInfos();
}

}  // namespace shardok
