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

#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/ActionResultApplier.hpp"

#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/GameStateCopier.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/UnitHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapHasher.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"

namespace shardok {

using std::unique_ptr;

using ResolvedUnitProto = net::eagle0::shardok::storage::ResolvedUnit;
using GameState = net::eagle0::shardok::storage::fb::GameState;
using GameStateT = net::eagle0::shardok::storage::fb::GameStateT;
using net::eagle0::shardok::storage::fb::DrawType;
using net::eagle0::shardok::storage::fb::VictoryCondition;
using net::eagle0::shardok::storage::fb::VictoryType;

void ApplyResolvedUnit(
        GameState *inoutState,
        const Unit &unit,
        const net::eagle0::shardok::storage::fb::UnitStatus status,
        const double vipCapturedMoraleAdjustment,
        const SettingsGetter &settingsGetter) {
    const UnitId unitId = unit.unit_id();

    if (unit.has_attached_hero() &&
        unit.attached_hero().control_info().controlled_unit_id() != -1) {
        const UnitId controlledUnitId = unit.attached_hero().control_info().controlled_unit_id();
        auto *controlledUnit = inoutState->mutable_units()->GetMutableObject(controlledUnitId);
        internalAssert(controlledUnit->unit_id() == controlledUnitId);
        internalAssert(controlledUnit->commanding_unit_id() == unitId);
        controlledUnit->mutate_commanding_unit_id(-1);
        controlledUnit->mutate_player_id(UNCONTROLLED_PLAYER_ID);
        controlledUnit->mutate_remaining_action_points(0);
    }
    if (unit.commanding_unit_id() != -1) {
        const UnitId commandingUnitId = unit.commanding_unit_id();

        auto *commandingUnit = inoutState->mutable_units()->GetMutableObject(commandingUnitId);
        internalAssert(commandingUnit->unit_id() == commandingUnitId);
        internalAssert(
                commandingUnit->attached_hero().control_info().controlled_unit_id() == unitId);
        commandingUnit->mutable_attached_hero().mutable_control_info().mutate_controlled_unit_id(
                -1);
    }
    // If a VIP was captured, alter the morale of all this player's units
    if (status == net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT &&
        unit.has_attached_hero() && unit.attached_hero().is_vip()) {
        for (uint32_t i = 0; i < inoutState->units()->size(); i++) {
            auto *playerUnit = inoutState->mutable_units()->GetMutableObject(i);
            if (playerUnit->player_id() != unit.player_id()) continue;
            if (playerUnit->unit_id() == unit.unit_id()) continue;

            MutatingMaybeAdjustMorale(
                    &playerUnit->mutable_battalion(),
                    vipCapturedMoraleAdjustment,
                    0.0,
                    settingsGetter.GetBattalionType(playerUnit->battalion().type()));
        }
    }

    inoutState->mutable_units()->GetMutableObject(unitId)->mutate_status(status);
}

void ApplyResolvedUnit(
        GameState *inoutState,
        const ResolvedUnitProto &resolvedUnit,
        const double vipCapturedMoraleAdjustment,
        const SettingsGetter &settingsGetter) {
    ApplyResolvedUnit(
            inoutState,
            *((Unit *)resolvedUnit.unit_bytes().data()),
            static_cast<net::eagle0::shardok::storage::fb::UnitStatus>(resolvedUnit.status()),
            vipCapturedMoraleAdjustment,
            settingsGetter);
}

void ApplyResolvedUnit(
        GameStateT &inoutState,
        const ResolvedUnitProto &resolvedUnit,
        const double vipCapturedMoraleAdjustment) {
    const Unit &unit = *((Unit *)resolvedUnit.unit_bytes().data());
    UnitId unitId = unit.unit_id();

    if (unit.has_attached_hero() &&
        unit.attached_hero().control_info().controlled_unit_id() != -1) {
        const UnitId controlledUnitId = unit.attached_hero().control_info().controlled_unit_id();

        for (auto &innerUnit : inoutState.units) {
            if (innerUnit.unit_id() == controlledUnitId) {
                auto &controlledUnit = innerUnit;
                controlledUnit.mutate_commanding_unit_id(-1);
                controlledUnit.mutate_player_id(UNCONTROLLED_PLAYER_ID);
                controlledUnit.mutate_remaining_action_points(0);
                break;
            }
        }
    }
    if (unit.commanding_unit_id() != -1) {
        const UnitId commandingUnitId = unit.commanding_unit_id();
        for (auto &innerUnit : inoutState.units) {
            if (innerUnit.unit_id() == commandingUnitId) {
                auto &commandingUnit = innerUnit;
                commandingUnit.mutable_attached_hero()
                        .mutable_control_info()
                        .mutate_controlled_unit_id(-1);
            }
        }
    }
    // If a VIP was captured, alter the morale of all this player's units
    if (resolvedUnit.status() ==
                net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_CAPTURED_UNIT &&
        unit.has_attached_hero() && unit.attached_hero().is_vip()) {
        for (auto &playerUnit : inoutState.units) {
            if (playerUnit.player_id() != unit.player_id()) continue;
            if (playerUnit.unit_id() == unit.unit_id()) continue;

            MutatingAdjustMorale(playerUnit.mutable_battalion(), vipCapturedMoraleAdjustment, 0.0);
        }
    }

    common::FilterInPlace(inoutState.units, [unitId](const auto &unit) {
        return unit.unit_id() != unitId;
    });
    inoutState.units[unitId] = *((Unit *)resolvedUnit.unit_bytes().data());
    inoutState.units[unitId].mutate_status(
            static_cast<net::eagle0::shardok::storage::fb::UnitStatus>(resolvedUnit.status()));
}

auto ApplyResults(
        const GameStateW &startingState,
        const std::vector<ActionResultProto> &actionResults,
        const SettingsGetter &settings) -> GameStateW {
    GameStateW stateAfter = startingState;

    for (const ActionResultProto &result : actionResults) {
        stateAfter = ApplyResult(stateAfter, result, settings);
    }

    return stateAfter;
}

void MutatingAddUnits(GameStateW &mutatingState, const ActionResultProto &result) {
    UnitId maxChangedUnitId = 0;
    bool needsVectorExpansion = false;
    bool needsReservedSlotConversion = false;

    // First pass: check what kind of modifications we need
    for (const auto &unitBytes : result.changed_units_fb()) {
        const auto *unit = (Unit *)unitBytes.data();
        maxChangedUnitId = std::max(maxChangedUnitId, unit->unit_id());

        if (unit->unit_id() >= mutatingState->units()->size()) {
            // Unit ID beyond vector size - must expand
            needsVectorExpansion = true;
            break;  // No point checking further
        } else if (
                mutatingState->units()->Get(unit->unit_id())->status() ==
                net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
            // Unit wants to use a reserved slot
            needsReservedSlotConversion = true;
        }
    }

    // Early return if no modifications needed
    if (!needsVectorExpansion && !needsReservedSlotConversion) { return; }

    // If we need to expand the vector, go straight to slow path
    if (needsVectorExpansion) {
        int unitsNeeded = 1 + maxChangedUnitId - mutatingState->units()->size();
        mutatingState = CopyWithExtraUnits(mutatingState, unitsNeeded);
        return;
    }

    // Otherwise, we just need to convert reserved slots (fast path)
    if (needsReservedSlotConversion) {
        // Convert reserved slots to real units in place
        // We only need to process the units that are being changed
        for (const auto &unitBytes : result.changed_units_fb()) {
            const auto *unit = (Unit *)unitBytes.data();
            auto *mutableUnit = mutatingState->mutable_units()->GetMutableObject(unit->unit_id());
            if (mutableUnit->status() ==
                net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
                // Convert this reserved slot to a real unit
                mutableUnit->mutate_status(
                        net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
                // The calling code will set the specific values it needs
            }
        }
    }
}

auto ApplyResult(
        GameStateW startingState,
        const ActionResultProto &result,
        const SettingsGetter &settings) -> GameStateW {
    MutatingApplyResult(startingState, result, settings);
    return startingState;
}

void MutatingApplyResult(
        GameStateW &mutatingGameState,
        const ActionResultProto &result,
        const SettingsGetter &settings) {
    const auto initialUnitCount = mutatingGameState->units()->size();

    MutatingAddUnits(mutatingGameState, result);

    if (result.has_next_player())
        internalAssert(mutatingGameState->mutate_current_player(result.next_player().value()));
    if (result.has_next_round())
        internalAssert(mutatingGameState->mutate_current_round(result.next_round().value()));
    if (result.has_next_weather()) {
        mutatingGameState->mutable_weather()->mutable_wind().mutate_speed_in_mph(
                result.next_weather().wind().speed_in_mph());
        mutatingGameState->mutable_weather()->mutable_wind().mutate_direction(
                static_cast<net::eagle0::shardok::storage::fb::HexMapDirection>(
                        result.next_weather().wind().direction()));
        mutatingGameState->mutable_weather()->mutate_conditions(
                static_cast<net::eagle0::shardok::storage::fb::WeatherConditions>(
                        result.next_weather().conditions()));
    }

    if (result.has_game_status()) {
        internalAssert(mutatingGameState->mutable_status()->mutate_state(
                static_cast<net::eagle0::shardok::storage::fb::GameStatus_::State>(
                        result.game_status().state())));
        for (int i = 0; i < result.game_status().winning_shardok_ids_size(); i++) {
            mutatingGameState->mutable_status()->mutable_winning_shardok_ids()->Mutate(
                    i,
                    result.game_status().winning_shardok_ids(i));
        }
        for (int i = result.game_status().winning_shardok_ids_size(); i < 10; i++) {
            mutatingGameState->mutable_status()->mutable_winning_shardok_ids()->Mutate(i, -1);
        }

        if (result.game_status().has_end_game_condition()) {
            switch (result.game_status().end_game_condition().condition_case()) {
                case net::eagle0::shardok::common::EndGameCondition::CONDITION_NOT_SET:
                    mutatingGameState->mutable_status()
                            ->mutable_end_game_condition()
                            ->mutate_victory_type(VictoryType::VictoryType_UNKNOWN_VICTORY_TYPE);
                    break;
                case net::eagle0::shardok::common::EndGameCondition::kAllyVictory:
                    mutatingGameState->mutable_status()
                            ->mutable_end_game_condition()
                            ->mutate_victory_type(VictoryType::VictoryType_ALLY_VICTORY);
                    mutatingGameState->mutable_status()
                            ->mutable_end_game_condition()
                            ->mutate_victory_details(static_cast<VictoryCondition>(
                                    result.game_status().end_game_condition().ally_victory()));
                    break;
                case net::eagle0::shardok::common::EndGameCondition::kDraw:
                    mutatingGameState->mutable_status()
                            ->mutable_end_game_condition()
                            ->mutate_victory_type(VictoryType::VictoryType_DRAW);
                    mutatingGameState->mutable_status()
                            ->mutable_end_game_condition()
                            ->mutate_draw_details(static_cast<DrawType>(
                                    result.game_status().end_game_condition().draw()));
                case net::eagle0::shardok::common::EndGameCondition::kLoss:
                    mutatingGameState->mutable_status()
                            ->mutable_end_game_condition()
                            ->mutate_victory_type(VictoryType::VictoryType_LOSS);
                    mutatingGameState->mutable_status()
                            ->mutable_end_game_condition()
                            ->mutate_victory_details(static_cast<VictoryCondition>(
                                    result.game_status().end_game_condition().loss()));
                    break;
                case net::eagle0::shardok::common::EndGameCondition::kVictory:
                    mutatingGameState->mutable_status()
                            ->mutable_end_game_condition()
                            ->mutate_victory_type(VictoryType::VictoryType_VICTORY);
                    mutatingGameState->mutable_status()
                            ->mutable_end_game_condition()
                            ->mutate_victory_details(static_cast<VictoryCondition>(
                                    result.game_status().end_game_condition().victory()));
                    break;
            }
        }
    }
    for (const auto &mod : result.changed_tile_modifiers()) {
        MutatingSetTileModifier(
                mutatingGameState->mutable_hex_map(),
                mod.coords(),
                mod.modifiers());
    }
    if (!result.changed_tile_modifiers().empty()) {
        mutatingGameState->mutable_hex_map()->mutate_modifier_hash(
                GetModifierHash(mutatingGameState->hex_map()));
    }

    const double vipCapturedMoraleAdjustment = settings.Backing().vip_captured_morale_adjustment();

    for (const auto &changedUnitBytes : result.changed_units_fb()) {
        const Unit *changedUnit = (Unit *)changedUnitBytes.data();
        if (changedUnit->battalion().type() ==
                    net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD &&
            changedUnit->battalion().morale() != 50.0) {
            printf("bad morale\n");
        }

        internalAssert(
                changedUnit->unit_id() < (int32_t)mutatingGameState->units()->size(),
                "Got a changed unit outside acceptable range");
        const auto battalionSizeAfter = changedUnit->battalion().size();

        const int battalionSizeBefore = (changedUnit->unit_id() < (int32_t)initialUnitCount)
                                                ? mutatingGameState->units()
                                                          ->Get(changedUnit->unit_id())
                                                          ->battalion()
                                                          .size()
                                                : changedUnit->battalion().size();

        auto status = net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT;
        if (IsDestroyed(*changedUnit)) {
            status =
                    changedUnit->has_attached_hero()
                            ? net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT
                            : net::eagle0::shardok::storage::fb::UnitStatus_DESTROYED_SUMMONED_UNIT;

            ApplyResolvedUnit(
                    mutatingGameState.Get(),
                    *changedUnit,
                    status,
                    vipCapturedMoraleAdjustment,
                    settings);
        }

        // Capture old position before applying changes
        auto *mutableUnit = mutatingGameState->units()->GetMutableObject(changedUnit->unit_id());
        const auto oldLocation = mutableUnit->location();

        fb::ApplyUnit(mutableUnit, changedUnit, status);

        // Update occupied tiles bitfield if position changed
        const auto &newLocation = changedUnit->location();
        if (oldLocation.row() != newLocation.row() ||
            oldLocation.column() != newLocation.column()) {
            mutatingGameState.UpdateOccupiedTile(oldLocation, newLocation);
        }

        if (battalionSizeBefore != battalionSizeAfter) {
            if (changedUnit->battalion().type() ==
                net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
                if (battalionSizeAfter > battalionSizeBefore) {
                    // Undead consumed some bodies
                    const int consumed = battalionSizeAfter - battalionSizeBefore;
                    const int newCount = std::max(0, mutatingGameState->dead_count() - consumed);
                    mutatingGameState->mutate_dead_count(newCount);
                }
            } else {
                if (battalionSizeAfter > battalionSizeBefore) {
                    std::ostringstream stringStream;
                    stringStream << "Battalion got larger! Was " << battalionSizeBefore << ", now "
                                 << changedUnit << " result was " << result.DebugString();
                    throw ShardokInternalErrorException(stringStream.str());
                }

                const int casualties = battalionSizeBefore - battalionSizeAfter;
                const int newCount = mutatingGameState->dead_count() + casualties;
                double newArmament = 0;
                if (newCount != 0) {
                    newArmament =
                            (mutatingGameState->dead_count() * mutatingGameState->dead_armament() +
                             casualties * changedUnit->battalion().armament()) /
                            newCount;
                }

                mutatingGameState->mutate_dead_count(newCount);
                mutatingGameState->mutate_dead_armament(newArmament);
            }
        }
    }

    for (const auto &resolvedUnit : result.resolved_units()) {
        ApplyResolvedUnit(mutatingGameState, resolvedUnit, vipCapturedMoraleAdjustment, settings);
    }
    if (result.has_eligible_charger()) {
        internalAssert(
                mutatingGameState->mutate_eligible_charger_id(result.eligible_charger().value()));
    } else {
        internalAssert(mutatingGameState->mutate_eligible_charger_id(-1));
    }
    for (int i = 0; i < result.possible_chargees_size(); i++) {
        mutatingGameState->mutable_possible_chargee_ids()->Mutate(i, result.possible_chargees(i));
    }
    for (int i = result.possible_chargees_size(); i < 6; i++) {
        mutatingGameState->mutable_possible_chargee_ids()->Mutate(i, -1);
    }

    return;
}

}  // namespace shardok
