//
// Created by Dan Crosby on 3/29/21.
//

#include <algorithm>
#include <array>
#include <climits>
#include <iostream>
#include <memory>
#include <mutex>
#include <random>
#include <stdexcept>
#include <thread>
#include <utility>
#include <vector>

#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
#include "src/main/cpp/net/eagle0/common/UnitConversions.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
#include "src/main/cpp/net/eagle0/shardok/server/ShardokGamesManager.hpp"
#include "src/main/protobuf/net/eagle0/common/random_units.pb.h"

using net::eagle0::common::RandomUnits;

constexpr int THREAD_COUNT = 16;

#define FIXED_SEED true

#if FIXED_SEED
std::mt19937 g;
#else
std::random_device rd;
std::mt19937 g(rd());
#endif

std::mutex randomMutex;
std::vector<std::string> fileNames{};

std::unique_ptr<shardok::ShardokGamesManager> manager;

auto randInt(const int lowInc, const int hiInc) {
    std::lock_guard<std::mutex> lock(randomMutex);
    return std::uniform_int_distribution(lowInc, hiInc)(g);
}

void runBattle(const std::vector<shardok::Unit>& units) {
    if (fileNames.empty()) { throw std::runtime_error("No map files found"); }
    if (units.size() < 2) { throw std::runtime_error("Need at least two units to run a battle"); }

    const auto mapName = fileNames[randInt(0, static_cast<int>(fileNames.size()) - 1)];
    const auto month = randInt(1, 12);

    const auto maxP1UnitCount = std::min<size_t>(20, units.size() - 1);
    const auto p1UnitCount = randInt(1, static_cast<int>(maxP1UnitCount));
    std::vector<shardok::Unit> p1Units{};
    p1Units.reserve(p1UnitCount);
    for (int i = 0; i < p1UnitCount; ++i) {
        auto unit = units[i];
        unit.mutate_player_id(0);
        p1Units.push_back(std::move(unit));
    }

    const auto maxP2UnitCount = std::min<size_t>(20, units.size() - p1UnitCount);
    const auto p2UnitCount = randInt(1, static_cast<int>(maxP2UnitCount));
    std::vector<shardok::Unit> p2Units{};
    p2Units.reserve(p2UnitCount);
    for (int i = p1UnitCount; i < p1UnitCount + p2UnitCount; ++i) {
        auto unit = units[i];
        unit.mutate_player_id(1);
        p2Units.push_back(std::move(unit));
    }

    std::cout << "map name is " << mapName << ", " << p1UnitCount << " attacking " << p2UnitCount
              << '\n';

    std::vector<VictoryConditionProto> attVictoryConditions{
            VictoryConditionProto::VICTORY_CONDITION_HOLDS_CRITICAL_TILES,
            VictoryConditionProto::VICTORY_CONDITION_LAST_PLAYER_STANDING};
    std::vector<VictoryConditionProto> defVictoryConditions{
            VictoryConditionProto::VICTORY_CONDITION_LAST_PLAYER_STANDING,
            VictoryConditionProto::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS};

    std::vector<shardok::PlayerInfoWithUnits> playerInfos;
    playerInfos.reserve(2);
    playerInfos.emplace_back(
            0,
            true,
            5,
            false,
            10000,
            attVictoryConditions,
            std::vector<shardok::PlayerId>{},
            p1Units,
            false);
    playerInfos.emplace_back(
            1,
            true,
            7,
            true,
            10000,
            defVictoryConditions,
            std::vector<shardok::PlayerId>{},
            p2Units,
            false);

    const std::string gameId = std::to_string(randInt(INT_MIN, INT_MAX));

    manager->UnlockedCreateSpecifiedGame(
            gameId,
            playerInfos,
            mapName,
            "",
            month,
            TutorialBattleConfigProto{},
            "");
}

auto LoadUnits() -> std::vector<shardok::Unit> {
    const auto bytes = byte_vector::FromPath("/tmp/random_heroes.bin");

    RandomUnits commonUnits;
    if (!commonUnits.ParseFromArray(bytes.data(), static_cast<int>(bytes.size()))) {
        throw std::runtime_error("Failed to parse /tmp/random_heroes.bin");
    }
    std::cout << "Loaded " << commonUnits.units_size() << " units\n";

    std::vector<shardok::Unit> units{};
    units.reserve(commonUnits.units_size());

    for (const auto& commonUnit : commonUnits.units()) {
        // FIXME: assumes 2 players
        units.push_back(shardok::ConvertUnit(commonUnit, 0, {0, 1}));
    }
    std::shuffle(units.begin(), units.end(), g);

    return units;
}

[[noreturn]] void runBattlesThread(const std::vector<shardok::Unit>& units) {
    while (true) { runBattle(units); }
}

auto main(int /*argc*/, char** argv) -> int {
#if FIXED_SEED
    g.seed(378473);
#endif

    FilesystemUtils::SetExecPath(argv[0]);

    auto randomUnits = LoadUnits();

    std::cout << "Map file directory is " << FilesystemUtils::MapFilesDirectory() << '\n';
    fileNames = FilesystemUtils::FilesInDirectory(FilesystemUtils::MapFilesDirectory(), ".e0m");

    for (auto& fileName : fileNames) { fileName.resize(fileName.size() - 4); }

    manager = std::make_unique<shardok::ShardokGamesManager>(std::vector<std::string>());

    std::array<std::thread, THREAD_COUNT> threads;

    for (int i = 0; i < THREAD_COUNT; ++i) {
        threads[i] = std::thread(runBattlesThread, randomUnits);
    }

    for (auto& thread : threads) { thread.join(); }
}
