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

#include "HolyWaveCommand.hpp"

#include <algorithm>
#include <utility>

#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/ActionResultApplier.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/util/PlayerUtils.hpp"

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

    const PlayerId playerId;
    const UnitId actingUnitId;
    const UnitId damagedUnitId;
    const double damagePercentage;

public:
    HolyWaveDamageAction(
            const PlayerId playerId,
            const UnitId actingUnitId,
            const UnitId damagedUnitId,
            const double &damagePercentage)
        : playerId(playerId),
          actingUnitId(actingUnitId),
          damagedUnitId(damagedUnitId),
          damagePercentage(damagePercentage) {}

    static auto MakeDamageActions(
            PlayerId playerId,
            UnitId actingUnitId,
            const vector<UnitId> &damagedUnitIds,
            double damagePercentage) -> ActionList;
};

auto HolyWaveDamageAction::InternalExecute(
        const GameStateW &currentState,
        const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResult> {
    ActionResult resultProto;
    resultProto.mutable_player()->set_value(playerId);
    resultProto.mutable_actor()->set_value(actingUnitId);

    resultProto.set_type(net::eagle0::shardok::common::HOLY_WAVE_DAMAGE);

    const Unit *damagedUnit = currentState->units()->Get(damagedUnitId);
    auto newSize = int32_t(damagedUnit->battalion().size() * (1.0 - damagePercentage));

    auto damagedUnitAfter = *damagedUnit;
    damagedUnitAfter.mutable_battalion().mutate_size(newSize);
    damagedUnitAfter.mutate_hidden(false);
    damagedUnitAfter.mutate_targeted_unit(actingUnitId);

    AddChangedUnit(resultProto, damagedUnitAfter);

    return {resultProto};
}

auto HolyWaveDamageAction::MakeDamageActions(
        PlayerId playerId,
        UnitId actingUnitId,
        const vector<UnitId> &damagedUnitIds,
        double damagePercentage) -> ActionList {
    ActionList actions;
    std::transform(
            std::begin(damagedUnitIds),
            std::end(damagedUnitIds),
            std::back_inserter(actions),
            [&](UnitId unitToDamageId) {
                return std::make_shared<HolyWaveDamageAction>(
                        playerId,
                        actingUnitId,
                        unitToDamageId,
                        damagePercentage);
            });

    return actions;
}

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

    const PlayerId playerId;
    const UnitId actingUnitId;
    const UnitId inspiredUnitId;
    const int inspireBuf;
    const int maxInspireOverBase;
    const float vigorBuf;

public:
    HolyWaveInspireAction(
            const PlayerId playerId,
            const UnitId actingUnitId,
            const UnitId inspiredUnitId,
            const int inspireBuf,
            const int maxInspireOverBase,
            const float vigorBuf)
        : playerId(playerId),
          actingUnitId(actingUnitId),
          inspiredUnitId(inspiredUnitId),
          inspireBuf(inspireBuf),
          maxInspireOverBase(maxInspireOverBase),
          vigorBuf(vigorBuf) {}

    static auto MakeInspireActions(
            UnitId playerId,
            UnitId actorId,
            const vector<UnitId> &friendlyUnitIds,
            double inspireBuf,
            double maxInspireOverBase,
            double vigorBuf) -> ActionList;
};

auto HolyWaveInspireAction::InternalExecute(
        const GameStateW &currentState,
        const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResult> {
    ActionResult resultProto;
    resultProto.mutable_player()->set_value(playerId);
    resultProto.mutable_actor()->set_value(actingUnitId);

    const auto *inspiredUnit = currentState->units()->Get(inspiredUnitId);
    float moraleBefore = inspiredUnit->battalion().morale();

    resultProto.set_type(net::eagle0::shardok::common::INSPIRED_TROOPS);

    float newMorale = moraleBefore + float(inspireBuf);
    float moraleCap = inspiredUnit->battalion().base_morale() + float(maxInspireOverBase);
    if (moraleCap < newMorale) newMorale = moraleCap;
    if (newMorale > 100) newMorale = 100;

    float vigorBefore = inspiredUnit->attached_hero().vigor();
    float vigorAfter =
            std::min(vigorBefore + vigorBuf, inspiredUnit->attached_hero().starting_vigor());

    auto unitAfter = *inspiredUnit;
    unitAfter.mutable_battalion().mutate_morale(newMorale);

    unitAfter.mutate_stun_rounds_remaining(0);
    unitAfter.mutable_attached_hero().mutate_vigor(vigorAfter);
    AddChangedUnit(resultProto, unitAfter);

    return {resultProto};
}

auto HolyWaveInspireAction::MakeInspireActions(
        UnitId playerId,
        UnitId actorId,
        const vector<UnitId> &friendlyUnitIds,
        const double inspireBuf,
        const double maxInspireOverBase,
        const double vigorBuf) -> ActionList {
    ActionList actions;
    std::transform(
            std::begin(friendlyUnitIds),
            std::end(friendlyUnitIds),
            std::back_inserter(actions),
            [&](const UnitId friendlyUnitId) {
                return std::make_shared<HolyWaveInspireAction>(
                        playerId,
                        actorId,
                        friendlyUnitId,
                        inspireBuf,
                        maxInspireOverBase,
                        vigorBuf);
            });

    return actions;
}

HolyWaveCommand::HolyWaveCommand(
        const ActionCost &cost,
        const PlayerId playerId,
        const UnitId actorId,
        const SettingsGetter &settingsGetter)
    : ShardokCommand(playerId),
      cost(cost),
      actorId(actorId),
      settings(settingsGetter) {}

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

    const Unit *actorBefore = currentState->units()->Get(actorId);

    auto actorAfter = *actorBefore;
    MutatingSpendActionPoints(&actorAfter, cost);
    MutatingBumpCharismaXp(&actorAfter, settings.Backing().holy_wave_charisma_xp());
    MutatingBumpAllKnowledgeToMinimum(
            &actorAfter,
            settings.Backing().minimum_knowledge_for_profession());
    // Start vigor from the *before* state, so we're not taking away both the setting and the normal
    // spend Take away the vigor buf too because they're about to get it back
    float vigorAfter = actorBefore->attached_hero().vigor() -
                       settings.Backing().holy_wave_vigor_cost() -
                       settings.Backing().holy_wave_vigor_buf();
    if (vigorAfter < 1) vigorAfter = 1;
    actorAfter.mutable_attached_hero().mutate_vigor(vigorAfter);

    ActionResult mainResult;
    mainResult.set_type(net::eagle0::shardok::common::HOLY_WAVE);
    mainResult.mutable_player()->set_value(actorBefore->player_id());
    mainResult.mutable_actor()->set_value(actorBefore->unit_id());
    AddChangedUnit(mainResult, actorAfter);
    results.push_back(mainResult);

    auto runningState = ApplyResult(currentState, mainResult, settings);

    vector<UnitId> undeadIds{};
    vector<UnitId> allFriendlyUnitIds{};
    auto allyPids = AlliedPids(runningState, GetPlayerId());
    for (const auto &coords :
         HexMapUtils::GetAdjacentCoords(runningState->hex_map(), actorBefore->location())) {
        const auto *occupant = runningState.GetOccupant(coords);
        if (occupant) {
            if (occupant->battalion().type() ==
                net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
                undeadIds.push_back(occupant->unit_id());
            } else if (
                    occupant->player_id() == GetPlayerId() ||
                    common::Contains(allyPids, GetPlayerId())) {
                allFriendlyUnitIds.push_back(occupant->unit_id());
            }
        }
    }

    allFriendlyUnitIds.push_back(actorId);

    ActionList inspireActions = HolyWaveInspireAction::MakeInspireActions(
            GetPlayerId(),
            actorId,
            allFriendlyUnitIds,
            settings.Backing().holy_wave_inspire_bonus(),
            settings.Backing().max_inspire_over_base(),
            settings.Backing().holy_wave_vigor_buf());

    for (const ActionSPtr &action : inspireActions) {
        const vector<ActionResult> subResults = action->Execute(runningState, nullptr);
        runningState = ApplyResults(runningState, subResults, settings);

        results.insert(results.end(), subResults.begin(), subResults.end());
    }

    ActionList damageActions = HolyWaveDamageAction::MakeDamageActions(
            GetPlayerId(),
            actorId,
            undeadIds,
            settings.Backing().holy_wave_damage_percentage());

    for (const ActionSPtr &action : damageActions) {
        const vector<ActionResult> subResults = action->Execute(runningState, nullptr);
        runningState = ApplyResults(runningState, subResults, settings);

        results.insert(results.end(), subResults.begin(), subResults.end());
    }

    return results;
}

auto HolyWaveCommand::GetCommandProto() const -> CommandProto {
    CommandProto commandProto;

    commandProto.set_type(net::eagle0::shardok::common::HOLY_WAVE_COMMAND);
    commandProto.set_player(GetPlayerId());
    commandProto.mutable_actor()->set_value(actorId);

    return commandProto;
}
}  // namespace shardok
