//
//  ShardokGamesManager.cpp
//  Server
//
//  Created by Dan Crosby on 10/29/17.
//  Copyright © 2017 none. All rights reserved.
//

#include "ShardokGamesManager.hpp"

#include <iostream>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <utility>

#include "ServerConfiguration.hpp"
#include "src/main/cpp/net/eagle0/common/UnitConversions.hpp"
#include "src/main/cpp/net/eagle0/shardok/controller/ShardokGameController.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings_loader/SettingsLoader.hpp"
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"

namespace shardok {

auto SetUpController(
        const GameSettingsSPtr &gameSettings,
        const GameId &gameId,
        const std::string &mapName,
        const std::string &customMapBytes,
        int month,
        const std::vector<Unit> &units,
        const std::vector<PlayerInfoProto> &playerSetupInfos,
        const TutorialBattleConfigProto &tutorialConfig,
        const std::string &serializedRequest,
        const std::vector<std::pair<int32_t, std::vector<PlayerId>>> &watcherAllies = {})
        -> std::shared_ptr<ShardokGameController>;

ShardokGamesManager::ShardokGamesManager(const std::vector<std::string> &extraSettings) {
    gameSettings = SettingsLoader::LoadSettings();

    for (const std::string &str : extraSettings) {
        std::stringstream stream(str);
        std::string key;
        std::string value;
        std::getline(stream, key, '=');
        std::getline(stream, value);
        std::cerr << key << "   <<    " << value << '\n';
        std::cerr << "NOT SETTING" << '\n';
        //        setter.SetRaw(key, value);
    }
}

auto TutorialReinforcementUnitCount(const TutorialBattleConfigProto &tutorialConfig) -> size_t {
    if (!tutorialConfig.enabled()) { return 0; }

    size_t unitCount = 0;
    for (const auto &event : tutorialConfig.events()) {
        if (!event.has_action() || event.action().action_type_case() !=
                                           net::eagle0::common::TutorialAction::kReinforcements) {
            continue;
        }
        unitCount += event.action().reinforcements().units_size();
    }

    return unitCount;
}

auto ShardokGamesManager::GetController(const GameId &gameId)
        -> std::shared_ptr<ShardokGameController> {
    std::shared_lock<std::shared_mutex> lk(runningControllersLock);
    const auto existing = runningControllers.find(gameId);
    if (existing == runningControllers.end()) return nullptr;

    return existing->second;
}

void ShardokGamesManager::RemoveController(const GameId &gameId) {
    std::unique_lock<std::shared_mutex> lk(runningControllersLock);
    runningControllers.erase(gameId);
}

auto ShardokGamesManager::UnlockedCreateFromGameStateBytes(
        const byte_vector &gameStateBytes,
        int32_t startingHistoryCount,
        const std::string &mapName) -> GameId {
    std::unique_lock<std::shared_mutex> lk(runningControllersLock);

    auto gs = GameStateW::FromByteVector(gameStateBytes);

    const auto controller = std::make_shared<ShardokGameController>(
            std::make_unique<ShardokEngine>(gameSettings, gs, startingHistoryCount),
            mapName);

    runningControllers[gs->game_id()->str()] = controller;
    return gs->game_id()->str();
}

auto ShardokGamesManager::UnlockedCreateSpecifiedGame(
        const std::string &gameId,
        const std::vector<PlayerInfoWithUnits> &playerSetupInfos,
        const std::string &mapName,
        const std::string &customMapBytes,
        const int month,
        const TutorialBattleConfigProto &tutorialConfig,
        const std::string &serializedRequest,
        const std::vector<std::pair<int32_t, std::vector<PlayerId>>> &watcherAllies) -> GameId {
    // Check for existing game with this ID and early-exit
    std::unique_lock<std::shared_mutex> lk(runningControllersLock);
    const auto existingGame = runningControllers.find(gameId);

    // If it exists, register new handlers and early-exit
    if (existingGame != runningControllers.end()) {
        if (serializedRequest != existingGame->second->SerializedRequest()) {
            std::cout << "Got a new game request that does not match the existing one" << '\n';
        }
        std::cout << "Found a game with game_id " << gameId << '\n';
        return gameId;
    }

    std::vector<PlayerInfoProto> playerInfos{};
    playerInfos.reserve(playerSetupInfos.size());
    std::transform(
            std::begin(playerSetupInfos),
            std::end(playerSetupInfos),
            std::back_inserter(playerInfos),
            [](const PlayerInfoWithUnits &psi) { return psi.playerInfo; });

    std::vector<Unit> units{};
    size_t unitCount = 0;
    for (const auto &info : playerSetupInfos) { unitCount += info.units.size(); }
    units.reserve(unitCount);
    for (const auto &info : playerSetupInfos) {
        units.insert(std::end(units), std::begin(info.units), std::end(info.units));
    }

    const std::shared_ptr<ShardokGameController> controller = SetUpController(
            gameSettings,
            gameId,
            mapName,
            customMapBytes,
            month,
            units,
            playerInfos,
            tutorialConfig,
            serializedRequest,
            watcherAllies);

    return LockedCreateGame(controller, playerSetupInfos, serializedRequest);
}

auto ShardokGamesManager::LockedCreateGame(
        const std::shared_ptr<ShardokGameController> &controller,
        const std::vector<PlayerInfoWithUnits> & /*playerInfos*/,
        const std::string & /*serializedRequest*/) -> GameId {
    GameId newGameId = controller->GetGameId();

    runningControllers[newGameId] = controller;

    return newGameId;
}

auto SetUpController(
        const GameSettingsSPtr &gameSettings,
        const GameId &gameId,
        const std::string &mapName,
        const std::string &customMapBytes,
        const int month,
        const std::vector<Unit> &units,
        const std::vector<PlayerInfoProto> &playerInfos,
        const TutorialBattleConfigProto &tutorialConfig,
        const std::string &serializedRequest,
        const std::vector<std::pair<int32_t, std::vector<PlayerId>>> &watcherAllies)
        -> std::shared_ptr<ShardokGameController> {
    std::vector<Unit> unplacedUnits;
    unplacedUnits.reserve(units.size() + TutorialReinforcementUnitCount(tutorialConfig));

    UnitId nextUnitId = 0;
    for (auto up : units) {
        up.mutate_unit_id(nextUnitId++);
        up.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT);

        // Apply initial visibility settings from tutorial config
        if (tutorialConfig.enabled() && up.has_attached_hero()) {
            const int heroId = up.attached_hero().eagle_hero_id();
            for (const auto &visibility : tutorialConfig.initial_visibilities()) {
                if (visibility.eagle_hero_id() == heroId) {
                    for (const int playerId : visibility.visible_to_player_ids()) {
                        if (playerId >= 0 && playerId < 10) {
                            up.mutable_opponent_knowledge()->Mutate(
                                    static_cast<flatbuffers::uoffset_t>(playerId),
                                    100);
                        }
                    }
                    break;
                }
            }
        }

        unplacedUnits.push_back(std::move(up));
    }

    // Add reinforcement units from tutorial config as PENDING_REINFORCEMENT.
    // These units exist in the game state from the start but are invisible and inactive
    // until the TutorialBattleController activates them at the triggered round.
    if (tutorialConfig.enabled()) {
        std::vector<PlayerId> allPlayerIds;
        allPlayerIds.reserve(playerInfos.size());
        for (size_t i = 0; i < playerInfos.size(); ++i) {
            allPlayerIds.push_back(static_cast<PlayerId>(i));
        }

        for (const auto &event : tutorialConfig.events()) {
            if (!event.has_action() ||
                event.action().action_type_case() !=
                        net::eagle0::common::TutorialAction::kReinforcements) {
                continue;
            }
            const auto &reinforcements = event.action().reinforcements();
            const PlayerId playerId = reinforcements.player_id();

            for (const auto &commonUnit : reinforcements.units()) {
                Unit unit = ConvertUnit(commonUnit, playerId, allPlayerIds);
                unit.mutate_unit_id(nextUnitId++);
                unit.mutate_status(
                        net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT);
                unplacedUnits.push_back(std::move(unit));
            }
        }
    }

    // For tutorial battles, override settings so duels are always accepted
    auto effectiveSettings = gameSettings;
    if (tutorialConfig.enabled()) {
        effectiveSettings = std::make_shared<GameSettings>(*gameSettings);
        effectiveSettings->GetSetter().SetInt(kSettingsKeys.duelBaseAcceptOdds, 200);
    }

    std::unique_ptr<ShardokEngine> engine = std::make_unique<ShardokEngine>(
            effectiveSettings,
            std::move(unplacedUnits),
            customMapBytes.empty() ? LoadMap(mapName) : LoadMapFromBytes(customMapBytes),
            month,
            gameId,
            playerInfos);

    engine->SetTutorialBattleConfig(tutorialConfig);

    return std::make_shared<ShardokGameController>(
            std::move(engine),
            mapName,
            serializedRequest,
            watcherAllies);
}

}  // namespace shardok
