//
//  MeteorCastAction.cpp
//  eagle0_engine
//
//  Created by Dan Crosby on 1/12/19.
//  Copyright © 2019 none. All rights reserved.
//

#include "MeteorCastAction.hpp"

#include <algorithm>
#include <utility>

#include "FallIntoWaterAction.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/FireUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/ActionResultApplier.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/TileModifierHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"

namespace shardok {
using net::eagle0::shardok::common::TileModifierWithCoords;

auto IsResolved(const net::eagle0::shardok::storage::fb::UnitStatus status) -> bool {
    switch (status) {
        case net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT:
        case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
        case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
        case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
        case net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT: return true;

        case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
        case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
        case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
        case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
        case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT: return false;
    }

    throw IllegalActionException("Invalid unit status");
}

class MeteorUnitDamageAction : public ShardokAction {
private:
    [[nodiscard]] auto InternalExecute(
            const GameStateW &currentState,
            const std::shared_ptr<RandomGenerator> &generator) const
            -> vector<ActionResultProto> override;

    const SettingsGetter settings;
    const double attackerIntelligence;
    const Unit *targetUnit;
    const Terrain terrain;
    const double baseDamage;
    const double damageMultiplier;

public:
    MeteorUnitDamageAction(
            const SettingsGetter &settingsGetter,
            const Unit *targetUnit,
            const double &attackerIntelligence,
            const Terrain &terrain,
            const double &baseDamage,
            const double &damageMultiplier = 1.0)
        : settings(settingsGetter),
          attackerIntelligence(attackerIntelligence),
          targetUnit(targetUnit),
          terrain(terrain),
          baseDamage(baseDamage),
          damageMultiplier(damageMultiplier) {}
};

auto MeteorUnitDamageAction::InternalExecute(
        const GameStateW &currentState,
        const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
    CombatDamage attackerDamage =
            CombatDamage::Builder()
                    .SetFire(attackerIntelligence * baseDamage * damageMultiplier)
                    .Build();

    auto directTargetAfter = WithTakenDamageFb(
            targetUnit,
            attackerDamage,
            terrain,
            AttackOrientation::front,
            false /* isAmbush */,
            true /* reduce for fortification */,
            settings);

    directTargetAfter.mutate_fortified(false);

    ActionResultProto result{};
    result.set_type(net::eagle0::shardok::common::METEOR_UNIT_DAMAGE);

    AddChangedUnit(result, directTargetAfter);

    return {result};
}

class MeteorTileDamageAction : public ShardokAction {
private:
    auto InternalExecute(
            const GameStateW &currentState,
            const std::shared_ptr<RandomGenerator> &generator) const
            -> vector<ActionResultProto> override;

    const Terrain *terrain;
    const Coords coords;
    const PercentileRollOdds fireOdds;
    const double roll;
    const double integrityAdjustment;
    const double frozenAdjustment;
    const double snowAdjustment;

public:
    MeteorTileDamageAction(
            const Terrain *terrain,
            const Coords coords,
            PercentileRollOdds fireOdds,
            const double &roll,
            const double integrityAdjustment,
            const double frozenAdjustment,
            const double snowAdjustment)
        : terrain(terrain),
          coords(coords),
          fireOdds(std::move(fireOdds)),
          roll(roll),
          integrityAdjustment(integrityAdjustment),
          frozenAdjustment(frozenAdjustment),
          snowAdjustment(snowAdjustment) {}
};

auto MeteorTileDamageAction::InternalExecute(
        const GameStateW &currentState,
        const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
    auto tm = fb::ToTileModifierProto(terrain->modifier());

    MutatingAdjustBridgeIntegrity(&tm, integrityAdjustment);
    MutatingAdjustCastleIntegrity(&tm, integrityAdjustment);
    MutatingAdjustIceIntegrity(&tm, frozenAdjustment);
    MutatingAdjustSnowIntegrity(&tm, snowAdjustment);

    if (PercentileRollSucceeds(fireOdds, roll)) { MutatingSetFire(tm, true); }

    ActionResultProto result{};
    result.set_type(net::eagle0::shardok::common::STRUCTURES_DAMAGED);
    *result.mutable_target_coords() = ToCoordsProto(coords);

    TileModifierWithCoords *tmc = result.add_changed_tile_modifiers();
    *tmc->mutable_modifiers() = tm;
    *tmc->mutable_coords() = ToCoordsProto(coords);

    return {result};
}

auto MeteorCastAction::InternalExecute(
        const GameStateW &currentState,
        const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
    vector<ActionResultProto> allResults{};
    auto runningGameState = startingGameState;
    for (const UnitId &actorId : actorIds) {
        const Unit *actorBefore = startingGameState->units()->Get(actorId);
        runningGameState =
                PerformOneActorCast(allResults, actorBefore, runningGameState, generator);
    }

    return allResults;
}

auto MeteorCastAction::PerformOneActorCast(
        vector<ActionResultProto> &results,
        const Unit *actorBefore,
        GameStateW &runningGameState,
        const std::shared_ptr<RandomGenerator> &generator) const -> GameStateW {
    const double actorIntelligence = actorBefore->attached_hero().wisdom();

    ActionResultProto mainResult{};
    mainResult.set_type(net::eagle0::shardok::common::METEOR_CAST);
    mainResult.mutable_player()->set_value(actorBefore->player_id());
    mainResult.mutable_actor()->set_value(actorBefore->unit_id());
    *mainResult.mutable_target_coords() =
            ToCoordsProto(actorBefore->attached_hero().profession_info().cast_target());
    results.push_back(mainResult);

    const Coords target = actorBefore->attached_hero().profession_info().cast_target();
    const Unit *possibleOccupant = runningGameState.GetOccupant(target);
    const Terrain *targetTerrain = GetTerrain(startingGameState->hex_map(), target);
    if (possibleOccupant) {
        // Direct damage action
        MeteorUnitDamageAction unitDamageAction(
                settings,
                possibleOccupant,
                actorIntelligence,
                *targetTerrain,
                MeteorBaseDamage(),
                1.0);

        vector<ActionResultProto> directDamageResults =
                unitDamageAction.Execute(runningGameState, generator);

        runningGameState = ApplyResults(runningGameState, directDamageResults, settings);
        results.insert(
                std::end(results),
                std::begin(directDamageResults),
                std::end(directDamageResults));
    }

    CoordsSet destroyedBridgeOrIceTiles(runningGameState->hex_map());
    auto weatherPropensity = settings.GetFirePropensityByWeatherConditions(
            runningGameState->weather()->conditions());

    {
        PercentileRollOdds directFireOdds = MakeOdds(
                MeteorDirectFireBaseChance(),
                PropensityByTerrain(targetTerrain, settings),
                weatherPropensity,
                0,
                0);

        MeteorTileDamageAction directTileDamageAction(
                targetTerrain,
                target,
                directFireOdds,
                generator->Percentile(),
                -MeteorStructureDamage(),
                -MeteorFrozenReduction(),
                -MeteorSnowReduction());
        vector<ActionResultProto> directTileResults =
                directTileDamageAction.Execute(runningGameState, generator);

        const auto &modsAfter = directTileResults[0].changed_tile_modifiers(0);
        if ((targetTerrain->modifier().bridge().present() ||
             targetTerrain->modifier().ice().present()) &&
            !(modsAfter.modifiers().has_bridge() || modsAfter.modifiers().has_ice())) {
            destroyedBridgeOrIceTiles.Add(target);
        }

        runningGameState = ApplyResults(runningGameState, directTileResults, settings);
        results.insert(
                std::end(results),
                std::begin(directTileResults),
                std::end(directTileResults));
    }

    // Splash damage actions
    const CoordsSet adjacentCoords =
            HexMapUtils::GetAdjacentCoords(runningGameState->hex_map(), target);
    for (const Coords &splashCoords : adjacentCoords) {
        const auto &splashTerrain = GetTerrain(runningGameState->hex_map(), splashCoords);

        const Unit *splashOccupant = runningGameState.GetOccupant(splashCoords);
        if (splashOccupant) {
            MeteorUnitDamageAction splashUnitDamageAction(
                    settings,
                    splashOccupant,
                    actorIntelligence,
                    *splashTerrain,
                    MeteorBaseDamage(),
                    MeteorSplashFactor());

            vector<ActionResultProto> splashDamageResults =
                    splashUnitDamageAction.Execute(runningGameState, generator);

            runningGameState = ApplyResults(runningGameState, splashDamageResults, settings);
            results.insert(
                    std::end(results),
                    std::begin(splashDamageResults),
                    std::end(splashDamageResults));
        }

        PercentileRollOdds splashFireOdds = MakeOdds(
                MeteorSplashFireBaseChance(),
                PropensityByTerrain(splashTerrain, settings),
                weatherPropensity,
                0,
                0);

        MeteorTileDamageAction splashTileDamageAction(
                splashTerrain,
                splashCoords,
                splashFireOdds,
                generator->Percentile(),
                (-MeteorStructureDamage() * MeteorSplashFactor()),
                (-MeteorFrozenReduction() * MeteorSplashFactor()),
                (-MeteorSnowReduction() * MeteorSplashFactor()));
        vector<ActionResultProto> splashTileResults =
                splashTileDamageAction.Execute(runningGameState, generator);

        const auto &modsAfter = splashTileResults[0].changed_tile_modifiers(0);
        if ((splashTerrain->modifier().bridge().present() ||
             splashTerrain->modifier().ice().present()) &&
            !(modsAfter.modifiers().has_bridge() || modsAfter.modifiers().has_ice())) {
            destroyedBridgeOrIceTiles.Add(splashCoords);
        }
        runningGameState = ApplyResults(runningGameState, splashTileResults, settings);
        results.insert(
                std::end(results),
                std::begin(splashTileResults),
                std::end(splashTileResults));
    }

    // Check for fallen heroes
    for (const Coords &coords : destroyedBridgeOrIceTiles) {
        const auto *maybeOccupant = runningGameState.GetOccupant(coords);
        if (maybeOccupant) {
            const BattalionTypeSPtr &battalionType =
                    settings.GetBattalionType(maybeOccupant->battalion().type());
            if (battalionType->GetCostToEnterTerrain(MakeHexMapTerrain(Terrain_Type_STILL_WATER))
                        .type == ActionCost::impossible) {
                auto fallAction = FallIntoWaterAction(
                        maybeOccupant->unit_id(),
                        settings,
                        FallInWaterDamagePerTroop(),
                        FallInWaterBaseEscapeOdds());
                auto fallResults = fallAction.Execute(runningGameState, generator);
                runningGameState = ApplyResults(runningGameState, fallResults, settings);

                results.insert(std::end(results), std::begin(fallResults), std::end(fallResults));
            }
        }
    }

    for (const Unit *resolvedUnit : *runningGameState->units()) {
        if (!IsResolved(resolvedUnit->status())) continue;
        if (resolvedUnit->unit_id() == actorBefore->unit_id()) {
            // The actor was destroyed; we're done
            return runningGameState;
        }
    }

    // The actor is still alive; get her current state and stun her
    auto actorAfter = *runningGameState->units()->Get(actorBefore->unit_id());
    actorAfter.mutate_remaining_action_points(0);
    actorAfter.mutate_stun_rounds_remaining(actorAfter.stun_rounds_remaining() + 2);
    actorAfter.mutable_attached_hero().mutable_profession_info().mutable_cast_target() =
            Coords{-1, -1};
    actorAfter.mutable_attached_hero().mutable_profession_info().mutate_meteor_cast_state(
            net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE);
    float vigorAfter =
            actorAfter.attached_hero().vigor() - settings.Backing().meteor_cast_vigor_cost();
    // can't kill yourself from casting unless the meteor itself does it
    actorAfter.mutable_attached_hero().mutate_vigor(std::max(1.0f, vigorAfter));
    actorAfter.mutable_attached_hero().mutate_wisdom_xp(
            actorAfter.attached_hero().wisdom_xp() + settings.Backing().meteor_cast_wisdom_xp());

    ActionResultProto stunResult{};
    stunResult.mutable_player()->set_value(actorAfter.player_id());
    stunResult.set_type(net::eagle0::shardok::common::METEOR_STUN);
    AddChangedUnit(stunResult, actorAfter);
    results.push_back(stunResult);

    return ApplyResult(runningGameState, stunResult, settings);
}
}  // namespace shardok
