//
//  AvailableCommandsFactory.cpp
//  eagle0_engine
//
//  Created by Dan Crosby on 12/31/18.
//  Copyright © 2018 none. All rights reserved.
//

#include "AvailableCommandsFactory.hpp"

#include "src/main/cpp/net/eagle0/shardok/library/FireUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_factories/PlayerSetupCommandFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/action_result_applier/ActionResultApplier.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/command_factories/CommandFactoriesList.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/commands/EndTurnCommand.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/PlayerUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateFilter.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateGuesser.hpp"

using std::make_shared;

namespace shardok {

class AvailableCommandsFactoryImpl : public AvailableCommandsFactory {
private:
    const SettingsGetter settings;

    static auto IsAttacker(const GameStateW &gameState, PlayerId pid) -> bool {
        if (pid == UNCONTROLLED_PLAYER_ID) return false;

        const auto &player = std::find_if(
                begin(*gameState->player_infos()),
                end(*gameState->player_infos()),
                [pid](const PlayerInfoFb *pi) { return pi->player_id() == pid; });

        return (player == end(*gameState->player_infos()) || !player->is_defender());
    }

    const vector<shared_ptr<const CommandFactory>> commandFactories;
    const PlayerSetupCommandFactory playerSetupCommandFactory;

    // AvailableCommands helpers for a single unit
    void AddAvailableCommandsForOneUnit(
            const GameStateW &gameState,
            CommandList &commands,
            const Unit *unit,
            bool onlyFollowUps) const;

public:
    explicit AvailableCommandsFactoryImpl(const SettingsGetter &settingsGetter)
        : settings(settingsGetter),
          commandFactories(CommandFactoriesList(settings).GetFactories()),
          playerSetupCommandFactory(settings) {}

    [[nodiscard]] auto GetPlayerSetupCommands(const GameStateW &gameState, PlayerId playerId) const
            -> CommandListSPtr override;

    [[nodiscard]] auto GetAvailableCommands(
            const GameStateW &gameState,
            PlayerId player,
            bool includeFollowUps) const -> CommandListSPtr override;
};

auto AvailableCommandsFactory::MakeAvailableCommandsFactory(const SettingsGetter &settings)
        -> std::unique_ptr<AvailableCommandsFactory> {
    return std::make_unique<AvailableCommandsFactoryImpl>(settings);
}

auto AvailableCommandsFactoryImpl::GetPlayerSetupCommands(
        const GameStateW &gameState,
        const PlayerId playerId) const -> CommandListSPtr {
    CommandList previewCommands{};
    playerSetupCommandFactory.AddAvailablePlayerSetupCommands(previewCommands, playerId, gameState);

    return make_shared<CommandList>(previewCommands);
}

void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
        const GameStateW &gameState,
        CommandList &commands,
        const Unit *unit,
        const bool onlyFollowUps) const {
    const auto &battType = settings.GetBattalionType(unit->battalion().type());
    const bool hasHero = unit->has_attached_hero();
    const auto allyPids = AlliedPids(gameState, unit->player_id());
    const bool isAttacker = IsAttacker(gameState, unit->player_id());

    const bool unitMovedIntoZoc = unit->has_moved_in_zoc();
    const ActionPoints remainingActionPoints = unit->remaining_action_points();
    const Coords &currentCoords = unit->location();

    const bool isEligibleCharger = unit->unit_id() == gameState->eligible_charger_id();
    vector<UnitId> possibleChargees{};
    for (const UnitId cid : *gameState->possible_chargee_ids()) {
        if (cid != -1) { possibleChargees.push_back(cid); }
    }

    // Undead units cannot act until the commanding hero has controlled them
    if (unit->commanding_unit_id() != -1) {
        const auto *commandingUnit = gameState->units()->Get(unit->commanding_unit_id());
        if (!commandingUnit->attached_hero().control_info().controlled_this_round()) { return; }
    }

    // Don't attach any commands if the unit is casting. Not ideal...
    if (hasHero && unit->attached_hero().profession_info().meteor_cast_state() ==
                           net::eagle0::shardok::storage::fb::MultiroundMagicState_CAST) {
        return;
    }

    // New style factory commands
    CommandList oneUnitCommands{};
    const CommandFactory::CommandParams commandParams{
            .unit = unit,
            .remainingActionPoints = remainingActionPoints,
            .position = currentCoords,
            .weatherFb = gameState->weather(),
            .isEligibleCharger = isEligibleCharger,
            .possibleChargees = possibleChargees,
            .hexMap = gameState->hex_map(),
            .units = gameState->units(),
            .allyPids = allyPids,
            .isAttacker = isAttacker,
            .unitMovedIntoZoc = unitMovedIntoZoc};

    for (const auto &commandFactory : commandFactories) {
        if (!onlyFollowUps || commandFactory->IncludeInFollowUps()) {
            commandFactory->AddAvailableCommands(oneUnitCommands, commandParams);

            if (!oneUnitCommands.empty() && oneUnitCommands.back()->IsRequiredToEndTurn()) break;
        }
    }
    if (battType->adjustsMorale &&
        unit->battalion().morale() < settings.Backing().minimum_morale_to_act()) {
        common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
            return cmd->CanDoWithLowMorale();
        });
    }
    if (unit->stun_rounds_remaining() > 0) {
        common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
            return cmd->CanDoWhileStunned();
        });
    }
    if (hasHero && unit->attached_hero().vigor() < settings.Backing().minimum_vigor_to_act()) {
        common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
            return cmd->CanDoWithLowVigor();
        });
    }
    if (unitMovedIntoZoc) {
        common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
            return cmd->CanDoAfterMovingIntoZoc();
        });
    }
    if (common::ContainsWhere(oneUnitCommands, [](const CommandSPtr &cmd) {
            return cmd->IsRequiredToEndTurn();
        })) {
        common::FilterInPlace(oneUnitCommands, [](const CommandSPtr &cmd) {
            return cmd->IsRequiredToEndTurn();
        });
    }

    commands.insert(commands.end(), oneUnitCommands.begin(), oneUnitCommands.end());
}

auto AvailableCommandsFactoryImpl::GetAvailableCommands(
        const GameStateW &gameState,
        PlayerId playerId,
        bool includeFollowUps) const -> CommandListSPtr {
    CommandList commands{};

    vector<AdjacentTile> adjacentTiles;

    playerSetupCommandFactory.AddAvailablePlayerSetupCommands(commands, playerId, gameState);
    if (!commands.empty()) return make_shared<CommandList>(commands);

    for (const Unit *unit : *gameState->units()) {
        if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;
        if (unit->player_id() != playerId) continue;
        AddAvailableCommandsForOneUnit(
                gameState,
                commands,
                unit,
                /* onlyFollowUps=*/false);
    }

    if (!common::ContainsWhere(commands, [](const CommandSPtr &command) {
            return command->IsRequiredToEndTurn();
        })) {
        commands.push_back(std::make_shared<EndTurnCommand>(playerId, gameState, settings));
    }

    // If we want follow-ups, make a copy of what this user believes to be the state of the game
    // and try each command that should include follow-ups to see if there is anything available
    if (!commands.empty() && includeFollowUps) {
        auto gsv = GameStateFilteredForPlayer(settings, gameState, playerId);
        auto guessedState = GameStateGuesser::GuessedState(playerId, settings, gsv);

        for (auto &cmd : commands) {
            if (cmd->GetCommandType() == net::eagle0::shardok::common::MOVE_COMMAND) {
                auto actor = cmd->GetCommandProto().actor().value();
                auto copy = GameStateW(guessedState);
                auto results = cmd->Execute(copy, settings.GetRandomGenerator());

                auto after = ApplyResults(copy, results, settings);

                CommandList followUpCommands{};
                AddAvailableCommandsForOneUnit(
                        after,
                        followUpCommands,
                        after->units()->Get(actor),
                        /* onlyFollowUps=*/true);

                std::unordered_set<CommandType> followUpTypes{};
                for (const auto &followUpCmd : followUpCommands) {
                    if (followUpCmd->CanBeFollowUp()) {
                        followUpTypes.insert(followUpCmd->GetCommandType());
                    }
                }

                cmd->AddFollowUpCommandTypes(followUpTypes);
            }
        }
    }

    if (commands.empty()) return nullptr;
    return make_shared<CommandList>(commands);
}

}  // namespace shardok
