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

#include "EndTurnCommand.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/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 {

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

auto ApplyAndAdd(
        const GameStateW &state,
        const ActionResultProto &newResult,
        vector<ActionResultProto> &allResults,
        const SettingsGetter &settings) -> GameStateW {
    allResults.push_back(newResult);
    return ApplyResult(state, newResult, settings);
}

auto ApplyAndAdd(
        const GameStateW &state,
        const vector<ActionResultProto> &newResults,
        vector<ActionResultProto> &allResults,
        const SettingsGetter &settings) -> GameStateW {
    GameStateW runningState = state;
    for (const auto &result : newResults) {
        runningState = ApplyAndAdd(runningState, result, allResults, settings);
    }

    return runningState;
}

auto EndTurnCommand::InternalExecute(
        const GameStateW &currentState,
        const std::shared_ptr<RandomGenerator> &generator) const -> vector<ActionResultProto> {
    if (GetPlayerId() != gameState->current_player()) {
        throw ShardokInternalErrorException(
                "Trying to end turn for player " + std::to_string(GetPlayerId()) +
                ", but turn belongs to " + std::to_string(gameState->current_player()));
    }

    GameStateW runningGameState = gameState;
    vector<ActionResultProto> allResults{};

    vector<UnitId> castingUnitIds{};

    for (const auto &unit : *runningGameState->units()) {
        if (unit->player_id() != GetPlayerId()) continue;
        if (unit->stun_rounds_remaining() > 0) continue;
        if (!unit->has_attached_hero()) continue;
        if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;

        const auto &hero = unit->attached_hero();
        if (hero.profession_info().meteor_cast_state() ==
            net::eagle0::shardok::storage::fb::MultiroundMagicState_CAST) {
            castingUnitIds.push_back(unit->unit_id());
        }
    };

    MeteorCastActionFactory factory(settings);
    ActionSPtr meteorCastAction = factory.MakeMeteorCastAction(runningGameState, castingUnitIds);

    auto meteorCastResults = meteorCastAction->Execute(runningGameState, generator);
    runningGameState = ApplyAndAdd(runningGameState, meteorCastResults, allResults, settings);

    // Any heroes in fire get burninated
    // Decrement stun counters, increment cast state
    const auto beforeFireUnitsCount = runningGameState->units()->size();
    for (int i = 0; i < beforeFireUnitsCount; i++) {
        const auto *beforeUnit = runningGameState->units()->Get(i);
        if (beforeUnit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT)
            continue;
        if (beforeUnit->player_id() != GetPlayerId()) continue;

        const Coords &location = beforeUnit->location();
        const Terrain *terrain = GetTerrain(runningGameState->hex_map(), location);
        const auto &battType = settings.GetBattalionType(beforeUnit->battalion().type());
        if (terrain->modifier().fire().present()) {
            Unit unit = *beforeUnit;
            CombatDamage fireDamage = GetFireDamage(
                    unit.battalion().size(),
                    generator->OpenEndedPercentile(),
                    generator->OpenEndedPercentile());
            MutatingApplyEnvironmentalDamage(
                    &unit,
                    fireDamage,
                    *terrain,
                    settings.Backing().morale_shift_per_casualty(),
                    settings.Backing().base_deadliness(),
                    battType);
            unit.mutate_fortified(false);
            unit.mutate_hidden(false);

            ActionResultProto result{};
            result.set_type(net::eagle0::shardok::common::FIRE_DAMAGE);
            AddChangedUnit(result, unit);

            runningGameState = ApplyAndAdd(runningGameState, result, allResults, settings);
        }
    }

    ActionResultProto endTurnResult{};
    endTurnResult.set_type(ActionType::END_TURN);
    endTurnResult.mutable_player()->set_value(GetPlayerId());
    endTurnResult.mutable_next_player()->set_value(NextPlayerId(gameState, GetPlayerId()));

    const auto unitsAtEndOfRoundCount = runningGameState->units()->size();
    for (int i = 0; i < unitsAtEndOfRoundCount; i++) {
        //  Grab the unit fresh because it might have been modified by a previous iteration of the
        //  loop
        const auto *eorUnit = runningGameState->units()->Get(i);
        if (eorUnit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT)
            continue;
        if (eorUnit->player_id() != GetPlayerId()) continue;

        auto unit = *eorUnit;
        if (IsDestroyed(unit)) continue;
        if (unit.has_attached_hero()) {
            MutatingAdjustVigor(
                    &unit.mutable_attached_hero(),
                    unit.remaining_action_points() *
                            settings.Backing().action_cost_to_vigor_cost());
        }
        unit.mutate_remaining_action_points(0);

        // Check for stun and cast state advancement
        if (unit.stun_rounds_remaining() > 0) {
            unit.mutate_stun_rounds_remaining(unit.stun_rounds_remaining() - 1);
        } else if (unit.has_attached_hero()) {
            switch (unit.attached_hero().profession_info().meteor_cast_state()) {
                case net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE: break;

                case net::eagle0::shardok::storage::fb::MultiroundMagicState_START:
                    unit.mutable_attached_hero().mutable_profession_info().mutate_meteor_cast_state(
                            net::eagle0::shardok::storage::fb::MultiroundMagicState_TARGET);
                    break;

                case net::eagle0::shardok::storage::fb::MultiroundMagicState_TARGET:
                    unit.mutable_attached_hero().mutable_profession_info().mutate_meteor_cast_state(
                            net::eagle0::shardok::storage::fb::MultiroundMagicState_CAST);
                    break;

                case net::eagle0::shardok::storage::fb::MultiroundMagicState_CAST:
                    unit.mutable_attached_hero().mutable_profession_info().mutate_meteor_cast_state(
                            net::eagle0::shardok::storage::fb::MultiroundMagicState_NONE);
                    break;

                case net::eagle0::shardok::storage::fb::MultiroundMagicState_STATE_UNKNOWN:
                    throw ShardokInternalErrorException("Unknown magic cast state");
            }
        }

        // Check for undead breaking control
        if (unit.has_attached_hero()) {
            auto &hero = unit.mutable_attached_hero();

            if (hero.control_info().controlled_unit_id() != -1 &&
                (unit.stun_rounds_remaining() > 0 ||
                 !hero.control_info().controlled_this_round())) {
                auto controlledUnit =
                        *runningGameState->units()->Get(hero.control_info().controlled_unit_id());

                hero.mutable_control_info().mutate_controlled_this_round(false);
                hero.mutable_control_info().mutate_controlled_unit_id(-1);
                controlledUnit.mutate_player_id(UNCONTROLLED_PLAYER_ID);
                controlledUnit.mutate_commanding_unit_id(-1);
                controlledUnit.mutate_targeted_unit(unit.unit_id());

                ActionResultProto result{};
                result.set_type(net::eagle0::shardok::common::CONTROL_BROKEN);
                result.mutable_player()->set_value(unit.player_id());
                result.mutable_actor()->set_value(controlledUnit.unit_id());
                result.mutable_target_unit()->set_value(unit.unit_id());
                AddChangedUnit(result, controlledUnit);
                AddChangedUnit(result, unit);

                runningGameState = ApplyAndAdd(runningGameState, result, allResults, settings);
            }
        }

        AddChangedUnit(endTurnResult, unit);
    }
    ApplyAndAdd(runningGameState, endTurnResult, allResults, settings);

    return allResults;
}

auto EndTurnCommand::GetCommandProto() const -> CommandProto {
    CommandProto command{};

    command.set_type(CommandType::END_TURN_COMMAND);
    command.set_player(GetPlayerId());

    return command;
}
}  // namespace shardok
