//
// Created by Dan Crosby on 5/30/20.
//

#include "RaiseDeadCommand.hpp"

#include "src/main/cpp/net/eagle0/shardok/library/actions/ActionRequiresHeroException.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"
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/OddsFilter.hpp"

namespace shardok {

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

static constexpr float BASE_MORALE = 50.0;
static constexpr float BASE_TRAINING = 0.0;
static constexpr float BASE_ARMAMENT = 0.0;

static auto GetNewUnit(
        const SettingsGetter &settings,
        PlayerId playerId,
        UnitId commandingUnitId,
        const Coords &location,
        const Terrain &terrain) -> Unit;

auto RaiseDeadCommand::InternalExecute(
        const GameStateW &currentState,
        const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResult> {
    const auto roll = generator->OpenEndedPercentile();
    return ExecuteWithRoll(currentState, roll);
}

[[nodiscard]] auto RaiseDeadCommand::ExecuteWithRoll(const GameStateW &currentState, double roll)
        const -> vector<ActionResult> {
    const auto *actor = currentState->units()->Get(actorId);
    if (!actor->has_attached_hero()) { throw ActionRequiresHeroException("raise dead"); }

    ActionResult result{};
    result.mutable_player()->set_value(GetPlayerId());
    result.mutable_actor()->set_value(actorId);
    *result.mutable_target_coords() = ToCoordsProto(target);

    auto actorAfter = *actor;
    MutatingSpendActionPoints(&actorAfter, cost);
    MutatingBumpWisdomXp(&actorAfter, wisdomXp);
    MutatingBumpCharismaXp(&actorAfter, charismaXp);
    MutatingBumpAllKnowledgeToMinimum(&actorAfter, minimumKnowledge);

    auto &mutableActingHero = actorAfter.mutable_attached_hero();

    const auto *occupant = currentState.GetOccupant(target);
    if (!occupant && PercentileRollSucceeds(odds, roll)) {
        result.set_type(ActionType::RAISED_UNDEAD);

        auto newUnit = GetNewUnit(
                settings,
                GetPlayerId(),
                actorId,
                target,
                *GetTerrain(currentState->hex_map(), target));

        UnitId nextUnitId = currentState->units()->size();
        // Find the next available unit ID that is a reserved slot
        while (nextUnitId > 0 &&
               currentState->units()->Get(nextUnitId - 1)->status() ==
                       net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
            nextUnitId--;
        }
        newUnit.mutate_unit_id(nextUnitId);
        AddChangedUnit(result, newUnit);
        mutableActingHero.mutable_control_info().mutate_controlled_unit_id(nextUnitId);
        mutableActingHero.mutable_control_info().mutate_controlled_this_round(true);

        AddChangedUnit(result, actorAfter);
    } else {
        AddChangedUnit(result, actorAfter);
        result.set_type(ActionType::FAILED_RAISE_UNDEAD);
    }

    return {result};
}

auto RaiseDeadCommand::GetCommandProto() const -> CommandProto {
    CommandProto proto{};

    proto.set_player(GetPlayerId());
    proto.set_type(net::eagle0::shardok::common::RAISE_DEAD_COMMAND);
    proto.mutable_actor()->set_value(actorId);
    *proto.mutable_target() = ToCoordsProto(target);
    *proto.mutable_odds() = OddsFilteredForPlayer(odds, GetPlayerId());

    return proto;
}

auto GetNewUnit(
        const SettingsGetter &settings,
        const PlayerId playerId,
        const UnitId commandingUnitId,
        const Coords &location,
        const Terrain &terrain) -> Unit {
    Unit unit;

    unit.mutate_player_id(playerId);
    unit.mutate_commanding_unit_id(commandingUnitId);

    for (int i = 0; i < unit.opponent_knowledge()->size(); i++) {
        unit.mutable_opponent_knowledge()->Mutate(i, 0);
    }

    unit.mutable_battalion().mutate_size(settings.Backing().raise_dead_starting_size());
    unit.mutable_battalion().mutate_type(net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD);
    unit.mutable_battalion().mutate_morale(BASE_MORALE);
    unit.mutable_battalion().mutate_training(BASE_TRAINING);
    unit.mutable_battalion().mutate_armament(BASE_ARMAMENT);

    unit.mutable_location() = location;

    unit.mutate_hidden(false);

    if (IsWater(terrain.type()) && !terrain.modifier().ice().present() &&
        !terrain.modifier().bridge().present()) {
        unit.mutate_hidden(true);
    }

    unit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);

    return unit;
}

auto RaiseDeadCommand::GetOddsPercentile() const -> int32_t {
    return OddsFilteredForPlayer(odds, GetPlayerId()).success_chance();
}

}  // namespace shardok
