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

#include "MoveCommand.hpp"

#include <utility>

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

MoveCommand::MoveCommand(
        const ActionPoints pointCost,
        const Unit* mover,
        const SettingsGetter& settingsGetter,
        vector<Coords> interimTargets,
        const Units* units,
        vector<PlayerId> allyPids,
        const HexMap* inMap,
        const bool unhide,
        const CoordsSet& enemyZocCoords)
    : ShardokCommand(mover->player_id()),
      pointCost(pointCost),
      moverBefore(mover),
      settings(settingsGetter),
      interimTargets(std::move(interimTargets)),
      allUnits(units),
      allyPids(std::move(allyPids)),
      map(inMap),
      willUnhide(unhide),
      enemyZocCoords(enemyZocCoords) {}

auto shardok::MoveCommand::InternalExecute(
        const GameStateW& currentState,
        const std::shared_ptr<RandomGenerator>& generator) const -> vector<ActionResult> {
    // HANDLE INTERIM TARGETS
    auto mover = *moverBefore;
    mover.mutate_fortified(false);
    const auto& moverBattalionType = settings.GetBattalionType(mover.battalion().type());
    PlayerId player = mover.player_id();

    Coords origin = mover.location();

    vector<ActionResult> results{};
    results.reserve(interimTargets.size());

    for (const Coords& destination : interimTargets) {
        const auto* destinationTerrain = GetTerrain(map, destination);
        bool startedInEnemyZoc = enemyZocCoords.Contains(origin);
        vector<const Unit*> knownAdjacentEnemies =
                KnownAdjacentEnemies(player, allUnits, map, allyPids, origin);

        MutatingSpendActionPoints(
                &mover,
                moverBattalionType->GetCostToEnterTerrain(destinationTerrain));

        vector<UnitId> possibleChargees{};
        bool isEligibleCharger = false;
        bool wasHidden = mover.hidden();

        const auto* occ = currentState.GetOccupant(destination);
        if (occ) {
            // Tile is occupied -- ambush!
            // Reconstruct the current state by applying results generated so far
            GameStateW ambushState = currentState;
            for (const auto& result : results) {
                ambushState = ApplyResult(std::move(ambushState), result, settings);
            }

            auto ambusherRoll = generator->OpenEndedPercentile();
            auto ambushResults =
                    DefensiveAmbushAction(occ->unit_id(), mover.unit_id(), ambusherRoll, settings)
                            .Execute(ambushState, generator);

            results.insert(std::end(results), std::begin(ambushResults), std::end(ambushResults));
            break;
        } else {
            mover.mutable_location() = destination;

            ActionResult moveResult{};
            moveResult.set_type(net::eagle0::shardok::common::MOVE);
            moveResult.mutable_player()->set_value(mover.player_id());
            moveResult.mutable_actor()->set_value(mover.unit_id());
            *moveResult.mutable_target_coords() = ToCoordsProto(destination);

            if (mover.hidden()) {
                if (mover.has_attached_hero()) {
                    mover.mutate_hidden(AllowsHiding(destinationTerrain));
                }

                for (const auto& adj : HexMapUtils::GetAdjacentCoords(map, destination)) {
                    const auto* occupant = currentState.GetOccupant(adj);
                    if (!occupant) continue;
                    if (occupant->player_id() == mover.player_id()) continue;
                    if (common::Contains(allyPids, occupant->player_id())) continue;
                    if (!occupant->has_attached_hero()) continue;
                    if (occupant->attached_hero().profession_info().profession() ==
                        net::eagle0::shardok::storage::fb::Profession_RANGER) {
                        mover.mutate_hidden(false);
                    }
                }
            }

            if (mover.has_attached_hero() &&
                mover.attached_hero().profession_info().profession() ==
                        net::eagle0::shardok::storage::fb::Profession_RANGER) {
                for (const auto& adj : HexMapUtils::GetAdjacentCoords(map, destination)) {
                    auto occupant = currentState.GetOccupant(adj);
                    if (!occupant) continue;
                    if (occupant->player_id() == mover.player_id()) continue;
                    if (common::Contains(allyPids, occupant->player_id())) continue;
                    if (occupant->hidden()) {
                        auto occupantAfter = *occupant;
                        occupantAfter.mutate_hidden(false);
                        AddChangedUnit(moveResult, occupantAfter);
                    }
                }
            }

            if (!mover.has_attached_hero()) {
                mover.mutate_hidden(moverBattalionType->AlwaysHiddenInTerrain(destinationTerrain));
            }

            if (!startedInEnemyZoc) {
                vector<const Unit*> newAdjacentEnemies =
                        KnownAdjacentEnemies(player, allUnits, map, allyPids, destination);

                for (const Unit* possibleChargee : newAdjacentEnemies) {
                    if (std::find_if(
                                knownAdjacentEnemies.begin(),
                                knownAdjacentEnemies.end(),
                                [possibleChargee](const Unit* adj) {
                                    return adj->unit_id() == possibleChargee->unit_id();
                                }) == knownAdjacentEnemies.end()) {
                        const Coords& chargeeLocation = possibleChargee->location();
                        const auto* terrain = GetTerrain(map, chargeeLocation);
                        // Can't charge an undamaged castle
                        if (!terrain->modifier().castle().present() ||
                            settings.Backing().minimum_castle_to_prevent_charge() >
                                    terrain->modifier().castle().integrity()) {
                            ActionCost costForCharge =
                                    moverBattalionType->GetCostToEnterTerrain(terrain);

                            if (costForCharge.IsPossible(mover.remaining_action_points())) {
                                possibleChargees.push_back(possibleChargee->unit_id());
                                isEligibleCharger = true;
                            }
                        }
                    }
                }
            }

            if (enemyZocCoords.Contains(destination)) { mover.mutate_has_moved_in_zoc(true); }

            if (isEligibleCharger)
                moveResult.mutable_eligible_charger()->set_value(mover.unit_id());
            *moveResult.mutable_possible_chargees() = {
                    possibleChargees.begin(),
                    possibleChargees.end()};
            if (!wasHidden && mover.hidden()) moveResult.add_became_hidden_units(mover.unit_id());

            AddChangedUnit(moveResult, mover);
            results.push_back(std::move(moveResult));

            // Update origin for next iteration
            origin = destination;
        }
    }

    return results;
}

void MoveCommand::AddFollowUpCommandTypes(const std::unordered_set<CommandType>& newTypes) {
    followUpCommandTypes.insert(newTypes.begin(), newTypes.end());
}

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

    proto.set_player(GetPlayerId());
    proto.set_type(net::eagle0::shardok::common::MOVE_COMMAND);
    proto.mutable_action_points()->set_value(pointCost);
    proto.mutable_actor()->set_value(moverBefore->unit_id());
    *proto.mutable_target() = ToCoordsProto(interimTargets.back());
    for (const auto& fup : followUpCommandTypes) { proto.add_follow_up_command_types(fup); }
    proto.set_will_unhide(willUnhide);

    return proto;
}
}  // namespace shardok
