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

#include "GameStateHelpers.hpp"

#include <flatbuffers/flatbuffers.h>

#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/InvalidProtoException.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"

namespace shardok::fb {

using std::unique_ptr;
using std::unordered_map;
using namespace net::eagle0::shardok::storage::fb;
using GameStatusProto = net::eagle0::shardok::common::GameStatus;

const std::vector<PlayerId> kWinningPids{-1, -1, -1, -1, -1, -1, -1, -1, -1, -1};

auto ConvertVictoryCondition(const net::eagle0::shardok::common::VictoryCondition vcProto)
        -> VictoryCondition {
    switch (vcProto) {
        case net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES:
            return VictoryCondition_VICTORY_CONDITION_HOLDS_CRITICAL_TILES;
        case net::eagle0::shardok::common::VICTORY_CONDITION_LAST_ALLIANCE_STANDING:
            return VictoryCondition_VICTORY_CONDITION_LAST_ALLIANCE_STANDING;
        case net::eagle0::shardok::common::VICTORY_CONDITION_LAST_PLAYER_STANDING:
            return VictoryCondition_VICTORY_CONDITION_LAST_PLAYER_STANDING;
        case net::eagle0::shardok::common::VICTORY_CONDITION_MOST_TROOPS_STANDING:
            return VictoryCondition_VICTORY_CONDITION_MOST_TROOPS_STANDING;
        case net::eagle0::shardok::common::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS:
            return VictoryCondition_VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS;

        case net::eagle0::shardok::common::VictoryCondition_INT_MAX_SENTINEL_DO_NOT_USE_:
        case net::eagle0::shardok::common::VictoryCondition_INT_MIN_SENTINEL_DO_NOT_USE_:
        case net::eagle0::shardok::common::UNKNOWN_VICTORY_CONDITION:
            throw InvalidProtoException("Invalid VictoryCondition");
    }
    throw InvalidProtoException("Invalid VictoryCondition");
}

auto FromPlayerInfoProto(flatbuffers::FlatBufferBuilder& fbb, const PlayerInfoProto& piProto)
        -> flatbuffers::Offset<net::eagle0::shardok::storage::fb::PlayerInfo> {
    auto alliesVec = vector<AlliedPlayer>();
    for (const auto& apProto : piProto.allies()) { alliesVec.emplace_back(apProto.player_id()); }
    const auto alliesOffset = fbb.CreateVectorOfStructs(alliesVec);

    auto vcVec = vector<int8_t>();
    for (int i = 0; i < piProto.victory_conditions_size(); i++) {
        vcVec.push_back(ConvertVictoryCondition(piProto.victory_conditions(i)));
    }
    const auto vcOffset = fbb.CreateVector(vcVec);

    PlayerInfoBuilder pib(fbb);
    pib.add_is_defender(piProto.is_defender());
    pib.add_player_id(piProto.player_id());
    pib.add_eagle_faction_id(piProto.eagle_faction_id());
    pib.add_is_ai(piProto.is_ai());
    pib.add_allies(alliesOffset);
    pib.add_victory_conditions(vcOffset);
    pib.add_starting_food(piProto.starting_food());

    return pib.Finish();
}

auto RequiredMonthlyFood(const Unit& unit, const SettingsGetter& settingsGetter) -> double {
    return settingsGetter.GetBattalionType(unit.battalion().type())->monthlyFoodCostPerTroop *
           unit.battalion().size();
}

auto FoodPercentageByPid(
        const vector<PlayerInfoProto>& playerInfoProtos,
        const vector<Unit>& units,
        const SettingsGetter& settings) -> unordered_map<int, double> {
    unordered_map<int, double> foodRequirementsByPid;
    for (const auto& unit : units) {
        const auto cost = RequiredMonthlyFood(unit, settings);
        if (foodRequirementsByPid.count(unit.player_id()) > 0) {
            foodRequirementsByPid[unit.player_id()] += cost;
        } else {
            foodRequirementsByPid[unit.player_id()] = cost;
        }
    }

    std::unordered_map<int, double> foodPercentByPid;
    for (const auto& pi : playerInfoProtos) {
        const double required = foodRequirementsByPid[pi.player_id()];
        const double available = pi.starting_food();
        foodPercentByPid[pi.player_id()] =
                required == 0 ? 1.0 : std::max(1.0, available / required);
    }

    return foodPercentByPid;
}

auto BaseMorale(const Unit& unit) -> double {
    if (!unit.has_attached_hero()) { return 50.0; }

    const double statFactors =
            (unit.attached_hero().charisma() + unit.battalion().training() + 100.0) / 4.0;

    return statFactors;
}

auto SetupInitialGameState(
        const GameId& gameId,
        const HexMapProto& hexMapProto,
        const vector<PlayerInfoProto>& playerInfoProtos,
        vector<Unit>& units,
        int month,
        bool isWinter,
        const SettingsGetter& settings) -> GameStateW {
    auto foodPercentByPid = FoodPercentageByPid(playerInfoProtos, units, settings);

    auto fbb = FlatBufferBuilder();
    fbb.ForceDefaults(true);

    auto mapOffset = ConvertHexMapProto(
            fbb,
            hexMapProto,
            isWinter,
            settings.Backing().initial_winter_ice_integrity());

    auto weather = net::eagle0::shardok::storage::fb::Weather(
            WeatherConditions_CLOUDS,
            Wind(HexMapDirection_NORTHEAST, 0));

    EndGameCondition endGameCondition;
    auto gameStatusOffset = CreateGameStatusDirect(
            fbb,
            GameStatus_::State_SET_UP,
            nullptr,
            &kWinningPids,
            &endGameCondition);
    auto gameIdOffset = fbb.CreateString(gameId);

    auto playerInfoVec = vector<Offset<PlayerInfo>>();
    for (const auto& piProto : playerInfoProtos) {
        playerInfoVec.push_back(FromPlayerInfoProto(fbb, piProto));
    }
    auto playerInfoOffsets = fbb.CreateVectorOfSortedTables(&playerInfoVec);

    // Count necromancers to determine slack space needed
    int necromancerCount = 0;
    for (const auto& unit : units) {
        if (unit.has_attached_hero() &&
            unit.attached_hero().profession_info().profession() ==
                    net::eagle0::shardok::storage::fb::Profession_NECROMANCER) {
            necromancerCount++;
        }
    }

    // Add slack units: 3 per necromancer, minimum 5 for general use
    int slackUnits = std::max(5, necromancerCount * 3);

    auto unitsVec = vector<Unit>();
    unitsVec.reserve(units.size() + slackUnits);
    for (const auto& unit : units) {
        Unit modifiedUnit = unit;
        // FIXME: this stuff should be set up in first action result
        modifiedUnit.mutable_attached_hero().mutable_profession_info().mutate_meteor_cast_state(
                net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE);
        if (modifiedUnit.can_archery()) {
            modifiedUnit.mutate_volleys_remaining(
                    int32_t(modifiedUnit.battalion().armament() *
                            settings.Backing().armament_to_volleys()) +
                    settings.GetBattalionType(modifiedUnit.battalion().type())->bonusVolleys);
        }
        modifiedUnit.mutate_food_remaining(
                RequiredMonthlyFood(modifiedUnit, settings) *
                foodPercentByPid[modifiedUnit.player_id()]);
        auto baseMorale = BaseMorale(modifiedUnit);
        modifiedUnit.mutable_battalion().mutate_base_morale(baseMorale);
        modifiedUnit.mutable_battalion().mutate_morale(baseMorale);

        modifiedUnit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT);

        unitsVec.push_back(modifiedUnit);
    }

    // Add slack units for future expansion (e.g., necromancer raise dead)
    for (int i = 0; i < slackUnits; i++) {
        Unit slackUnit;
        slackUnit.mutate_unit_id(static_cast<int16_t>(units.size() + i));
        slackUnit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT);
        // Set other required fields to safe defaults
        slackUnit.mutate_player_id(-1);
        slackUnit.mutate_eagle_player_id(-1);
        slackUnit.mutable_location().mutate_row(-1);
        slackUnit.mutable_location().mutate_column(-1);
        unitsVec.push_back(slackUnit);
    }

    auto unitsOffset = fbb.CreateVectorOfSortedStructs(&unitsVec);

    vector<UnitId> possibleChargeeIdsVec = {-1, -1, -1, -1, -1, -1};
    auto possibleChargeeIdsOffset = fbb.CreateVector(possibleChargeeIdsVec);

    GameStateBuilder builder(fbb);
    builder.add_game_id(gameIdOffset);
    builder.add_month(month);
    builder.add_player_infos(playerInfoOffsets);
    builder.add_units(unitsOffset);
    builder.add_hex_map(mapOffset);
    builder.add_eligible_charger_id(-1);
    builder.add_possible_chargee_ids(possibleChargeeIdsOffset);
    builder.add_current_round(0);
    builder.add_current_player(0);
    builder.add_weather(&weather);
    builder.add_status(gameStatusOffset);
    builder.add_dead_count(0);
    builder.add_dead_armament(0.0);

    auto offset = builder.Finish();

    fbb.Finish(offset);
    return GameStateW(fbb);
}

auto ToProto(const GameStatus* fbStatus) -> GameStatusProto {
    GameStatusProto protoStatus{};
    protoStatus.set_state((net::eagle0::shardok::common::GameStatus_State)fbStatus->state());
    if (fbStatus->description()) { protoStatus.set_description(fbStatus->description()->str()); }
    if (fbStatus->winning_shardok_ids()) {
        for (const auto pid : *fbStatus->winning_shardok_ids()) {
            if (pid != -1) { protoStatus.add_winning_shardok_ids(pid); }
        }
    }
    switch (fbStatus->end_game_condition()->victory_type()) {
        case net::eagle0::shardok::storage::fb::VictoryType_VICTORY:
            protoStatus.mutable_end_game_condition()->set_victory(
                    static_cast<net::eagle0::shardok::common::VictoryCondition>(
                            fbStatus->end_game_condition()->victory_details()));
            break;
        case net::eagle0::shardok::storage::fb::VictoryType_ALLY_VICTORY:
            protoStatus.mutable_end_game_condition()->set_ally_victory(
                    static_cast<net::eagle0::shardok::common::VictoryCondition>(
                            fbStatus->end_game_condition()->victory_details()));
            break;
        case net::eagle0::shardok::storage::fb::VictoryType_DRAW:
            protoStatus.mutable_end_game_condition()->set_draw(
                    static_cast<net::eagle0::shardok::common::DrawType>(
                            fbStatus->end_game_condition()->draw_details()));
            break;
        case net::eagle0::shardok::storage::fb::VictoryType_LOSS:
            protoStatus.mutable_end_game_condition()->set_loss(
                    static_cast<net::eagle0::shardok::common::VictoryCondition>(
                            fbStatus->end_game_condition()->victory_details()));
            break;
        case net::eagle0::shardok::storage::fb::VictoryType_UNKNOWN_VICTORY_TYPE:
            protoStatus.mutable_end_game_condition()->Clear();
            break;
    }

    return protoStatus;
}

auto ToPlayerInfoProto(const PlayerInfo* fbPI) -> PlayerInfoProto {
    PlayerInfoProto piProto{};
    piProto.set_player_id(fbPI->player_id());
    piProto.set_is_defender(fbPI->is_defender());
    piProto.set_starting_food(fbPI->starting_food());

    *piProto.mutable_victory_conditions() = {
            fbPI->victory_conditions()->begin(),
            fbPI->victory_conditions()->end()};

    if (fbPI->allies()) {
        for (const auto& ally : *fbPI->allies()) {
            piProto.add_allies()->set_player_id(ally->player_id());
        }
    }
    piProto.set_is_ai(fbPI->is_ai());
    piProto.set_eagle_faction_id(fbPI->eagle_faction_id());

    return piProto;
}

auto ToWeatherProto(const Weather* fbW) -> net::eagle0::shardok::common::Weather {
    net::eagle0::shardok::common::Weather protoWeather;
    protoWeather.set_conditions(
            (net::eagle0::shardok::common::Weather_Conditions)fbW->conditions());
    protoWeather.mutable_wind()->set_direction(
            (net::eagle0::shardok::common::HexMapDirection)fbW->wind().direction());
    protoWeather.mutable_wind()->set_speed_in_mph(fbW->wind().speed_in_mph());

    return protoWeather;
}

}  // namespace shardok::fb
