//
// Created by Dan Crosby on 4/12/20.
//

#include "NewRoundAction.hpp"

#include "src/main/cpp/net/eagle0/shardok/library/action_factories/FireOutActionFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_factories/FireSpreadActionFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/ActionResultApplier.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/TileModifierWithCoords.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/WeatherHelpers.hpp"
#include "src/main/protobuf/net/eagle0/shardok/common/action_type.pb.h"

namespace shardok {

NewRoundAction::NewRoundAction(
        GameStateW gameState,
        const SettingsGetter &settingsGetter,
        const std::shared_ptr<FireOutActionFactory> &fireOutFact,
        const std::shared_ptr<FireSpreadActionFactory> &fireSpreadFact,
        const std::shared_ptr<IceAndSnowAdjustmentActionFactory> &iceAdjustFact,
        const std::shared_ptr<UndeadChangeActionFactory> &undeadGrewFact)
    : startingGameState(std::move(gameState)),
      settings(settingsGetter),
      fireOutActionFactory(fireOutFact ?: std::make_shared<FireOutActionFactory>(settings)),
      fireSpreadActionFactory(
              fireSpreadFact ?: std::make_shared<FireSpreadActionFactory>(settings)),
      iceAdjustmentActionFactory(
              iceAdjustFact ?: std::make_shared<IceAndSnowAdjustmentActionFactory>(settings)),
      undeadGrewActionFactory(
              undeadGrewFact ?: std::make_shared<UndeadChangeActionFactory>(settings)){};

auto ConditionsByMonth(
        const net::eagle0::shardok::storage::fb::MonthlyWeather &monthlyWeather,
        double percentileRoll) -> Weather::Conditions {
    Weather::Conditions newConditions = Weather::Conditions::Weather_Conditions_UNKNOWN;

    if (percentileRoll < monthlyWeather.sun_chance()) {
        newConditions = Weather::Conditions::Weather_Conditions_SUN;
    } else if ((percentileRoll -= monthlyWeather.sun_chance()) < monthlyWeather.clouds_chance()) {
        newConditions = Weather::Conditions::Weather_Conditions_CLOUDS;
    } else if ((percentileRoll -= monthlyWeather.clouds_chance()) < monthlyWeather.rain_chance()) {
        newConditions = Weather::Conditions::Weather_Conditions_RAIN;
    } else if (
            (percentileRoll -= monthlyWeather.rain_chance()) <
            monthlyWeather.thunderstorm_chance()) {
        newConditions = Weather::Conditions::Weather_Conditions_THUNDERSTORM;
    } else if (
            (percentileRoll -= monthlyWeather.thunderstorm_chance()) <
            monthlyWeather.snow_chance()) {
        newConditions = Weather::Conditions::Weather_Conditions_SNOW;
    } else if (
            (percentileRoll -= monthlyWeather.snow_chance()) < monthlyWeather.blizzard_chance()) {
        newConditions = Weather::Conditions::Weather_Conditions_BLIZZARD;
    } else {
        throw ShardokInternalErrorException("Impossible weather condition chance");
    }

    return newConditions;
}

using net::eagle0::shardok::common::ActionType;

auto BurnStructuresResult(const GameStateW &gameState, const SettingsGetter &settings)
        -> std::optional<ActionResultProto> {
    vector<TileModifierWithCoords> burnedTmcs{};
    // Check for burning structures
    for (const Coords &coords : GetAllCoords(gameState->hex_map())) {
        const auto terrain = GetMutableTerrain(gameState->hex_map(), coords);
        if (terrain->modifier().fire().present()) {
            if (terrain->modifier().bridge().present()) {
                ReduceBridgeIntegrity(
                        terrain->mutable_modifier(),
                        settings.Backing().burning_bridge_damage());
                burnedTmcs.push_back(MakeTmc(coords, terrain->modifier()));
            } else if (terrain->modifier().castle().present()) {
                ReduceCastleIntegrity(
                        terrain->mutable_modifier(),
                        settings.Backing().burning_castle_damage());

                burnedTmcs.push_back(MakeTmc(coords, terrain->modifier()));
            }
        }
    }

    if (!burnedTmcs.empty()) {
        ActionResultProto result{};
        result.set_type(ActionType::STRUCTURES_BURNED);
        *result.mutable_changed_tile_modifiers() = {burnedTmcs.begin(), burnedTmcs.end()};
        return result;
    } else {
        return std::optional<ActionResultProto>();
    }
}

auto NewWeather(
        const net::eagle0::shardok::storage::fb::MonthlyWeather &monthlyWeather,
        const WeatherFb &oldWeather,
        const SettingsGetter &settings,
        const std::shared_ptr<RandomGenerator> &randomGenerator) -> Weather {
    const Weather::Conditions newConditions =
            ConditionsByMonth(monthlyWeather, randomGenerator->Percentile());

    const auto currentWindDirection = oldWeather.wind().direction();
    const double windDirectionRoll = randomGenerator->Percentile();
    int newWindDirection = currentWindDirection;
    if (windDirectionRoll <= 40) {
        newWindDirection = currentWindDirection;
    } else if (windDirectionRoll <= 60) {
        newWindDirection = (currentWindDirection + 1);
    } else if (windDirectionRoll <= 80) {
        newWindDirection = (currentWindDirection - 1);
    } else if (windDirectionRoll <= 88) {
        newWindDirection = (currentWindDirection + 2);
    } else if (windDirectionRoll <= 96) {
        newWindDirection = (currentWindDirection - 2);
    } else {
        newWindDirection = (currentWindDirection + 3);
    }

    if (newWindDirection < 0) newWindDirection += 6;
    else if (newWindDirection > 5)
        newWindDirection -= 6;

    const double windSpeedRoll = randomGenerator->OpenEndedHighPercentile();
    const uint8_t newWindSpeed = windSpeedRoll / 100.0 * monthlyWeather.max_wind_mph();

    return MakeWeather(
            newConditions,
            MakeWind(
                    (net::eagle0::shardok::common::HexMapDirection)newWindDirection,
                    newWindSpeed));
}

auto NewRoundAction::InternalExecute(
        const GameStateW &currentState,
        const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
    vector<ActionResultProto> results{};

    GameStateW runningGameState = startingGameState;

    auto maybeBurnedStructuresResult = BurnStructuresResult(runningGameState, settings);
    if (maybeBurnedStructuresResult.has_value()) {
        results.push_back(*maybeBurnedStructuresResult);
        runningGameState = ApplyResult(runningGameState, *maybeBurnedStructuresResult, settings);
    }

    ActionResultProto newRoundResult{};
    newRoundResult.set_type(ActionType::ROUND_START);
    newRoundResult.mutable_next_round()->set_value(runningGameState->current_round() + 1);
    newRoundResult.mutable_next_player()->set_value(0);

    // Check for spreading fire
    ActionList fireSpreadAction = fireSpreadActionFactory->MakeFireSpreadActions(
            runningGameState->hex_map(),
            runningGameState->weather());
    for (const ActionSPtr &action : fireSpreadAction) {
        auto fireSpreadResults = action->Execute(currentState, generator);
        for (const auto &result : fireSpreadResults) {
            results.push_back(result);
            runningGameState = ApplyResult(runningGameState, result, settings);
        }
    }

    // Check for ice adjustments
    ActionList iceAdjustmentAction = iceAdjustmentActionFactory->MakeIceAndSnowAdjustmentActions(
            runningGameState->hex_map(),
            *runningGameState->units(),
            runningGameState->month(),
            runningGameState->weather());
    for (const ActionSPtr &action : iceAdjustmentAction) {
        auto iceAdjustmentResults = action->Execute(currentState, generator);
        for (const auto &result : iceAdjustmentResults) {
            results.push_back(result);
            runningGameState = ApplyResult(runningGameState, result, settings);
        }
    }

    // Check for undead growth
    vector<ActionResultProto> undeadGrewResults =
            undeadGrewActionFactory->MakeUndeadChangeAction(runningGameState)
                    ->Execute(currentState, generator);
    for (const auto &result : undeadGrewResults) {
        runningGameState = ApplyResult(runningGameState, result, settings);
        results.push_back(result);
    }

    *newRoundResult.mutable_next_weather() = NewWeather(
            *runningGameState->hex_map()->monthly_weather()->Get(runningGameState->month() - 1),
            *runningGameState->weather(),
            settings,
            generator);

    runningGameState = ApplyResult(runningGameState, newRoundResult, settings);
    results.push_back(newRoundResult);

    ActionList fireOutActions = fireOutActionFactory->MakeFireOutActions(
            runningGameState->hex_map(),
            runningGameState->weather());
    for (const ActionSPtr &action : fireOutActions) {
        auto fireOutResults = action->Execute(currentState, generator);
        for (const auto &result : fireOutResults) {
            runningGameState = ApplyResult(runningGameState, result, settings);
            results.push_back(result);
        }
    }

    return results;
}
}  // namespace shardok