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

#include "MoveCommandFactory.hpp"

#include <queue>

#include "src/main/cpp/net/eagle0/shardok/library/commands/MoveCommand.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/ZoneOfControlCalculator.hpp"

namespace shardok {

inline auto str(const Coords &coords) -> std::string {
    return std::string("(") + std::to_string(coords.row()) + ", " +
           std::to_string(coords.column()) + ")";
}

struct AccumulatedMoveInfo {
    Coords startLocation;
    Coords endLocation;
    ActionPoints pointCost = 0;
    bool willUnhide = false;
    vector<Coords> targets = {};

    AccumulatedMoveInfo(
            const Coords &startLocation,
            const Coords &endLocation,
            const ActionPoints pointCost,
            const bool willUnhide,
            const vector<Coords> &targets)
        : startLocation(startLocation),
          endLocation(endLocation),
          pointCost(pointCost),
          willUnhide(willUnhide),
          targets(targets) {}

    auto operator<(const AccumulatedMoveInfo &rhs) const -> bool {
        return (pointCost < rhs.pointCost);
    }

    auto operator>(const AccumulatedMoveInfo &rhs) const -> bool {
        return (pointCost > rhs.pointCost);
    }
};

auto AdjacentMoveDestinations(
        const AccumulatedMoveInfo &baseInfo,
        const Unit *movingUnit,
        ActionPoints remainingActionPoints,
        bool inEnemyZoc,
        const Units *units,
        const HexMap *hexMap,
        const SettingsGetter &settings,
        const vector<PlayerId> &allyPids) -> vector<AccumulatedMoveInfo>;

auto ConstructMoveDestinations(
        const Units *units,
        const vector<PlayerId> &allyPids,
        const Unit *movingUnit,
        ActionPoints startingActionPoints,
        const HexMap *hexMap,
        const SettingsGetter &settings) -> vector<AccumulatedMoveInfo>;

void MoveCommandFactory::AddAvailableMoveCommands(
        CommandList &existingCommands,
        const vector<PlayerId> &allyPids,
        const Unit *movingUnit,
        const ActionPoints remainingActionPoints,
        const HexMap *hexMap,
        const Units *units) const {
    if (movingUnit->stun_rounds_remaining() > 0) return;
    if (movingUnit->has_attached_hero() &&
        movingUnit->attached_hero().vigor() < settings.Backing().minimum_vigor_to_act()) {
        return;
    }

    const auto playerId = movingUnit->player_id();

    const auto destinations = ConstructMoveDestinations(
            units,
            allyPids,
            movingUnit,
            remainingActionPoints,
            hexMap,
            settings);

    existingCommands.reserve(existingCommands.size() + destinations.size());

    auto zocCoords = TilesInEnemyZoc(settings, playerId, units, hexMap, allyPids);

    // Create commands
    for (const auto &destInfo : destinations) {
        auto cmd = std::make_shared<MoveCommand>(
                destInfo.pointCost,
                movingUnit,
                settings,
                destInfo.targets,
                units,
                allyPids,
                hexMap,
                destInfo.willUnhide,
                zocCoords);

        existingCommands.push_back(cmd);
    }
}
void MoveCommandFactory::AddAvailableCommands(
        CommandList &commands,
        const CommandFactory::CommandParams &params) const {
    if (!params.unitMovedIntoZoc) {
        AddAvailableMoveCommands(
                commands,
                params.allyPids,
                params.unit,
                params.remainingActionPoints,
                params.hexMap,
                params.units);
    }
}

auto UnoccupiedAdjacentCoords(
        const PlayerId playerId,
        const HexMap *hexMap,
        const Coords &from,
        const Units *units,
        const vector<PlayerId> &allyPids) -> CoordsSet {
    CoordsSet toReturn(hexMap);
    for (const auto &adjacentTile : HexMapUtils::GetAdjacentCoords(hexMap, from)) {
        const auto *occupant = KnownOccupant(playerId, units, allyPids, adjacentTile);
        if (!occupant) { toReturn.Add(adjacentTile); }
    }
    return toReturn;
}

auto AdjacentMoveDestinations(
        const AccumulatedMoveInfo &baseInfo,
        const Unit *movingUnit,
        const ActionPoints remainingActionPoints,
        const bool inEnemyZoc,
        const Units *units,
        const HexMap *hexMap,
        const SettingsGetter &settings,
        const vector<PlayerId> &allyPids) -> vector<AccumulatedMoveInfo> {
    vector<AccumulatedMoveInfo> validDestinations{};

    const auto nextCandidates = UnoccupiedAdjacentCoords(
            movingUnit->player_id(),
            hexMap,
            baseInfo.endLocation,
            units,
            allyPids);

    validDestinations.reserve(nextCandidates.size());
    for (const auto &candidate : nextCandidates) {
        // Maybe check for already hit?

        const auto *candidateTerrain = GetTerrain(hexMap, candidate);
        // Can't move onto a burning tile
        if (candidateTerrain->modifier().fire().present()) { continue; }

        ActionCost costToMove = settings.GetBattalionType(movingUnit->battalion().type())
                                        ->GetCostToEnterTerrain(candidateTerrain);
        if (inEnemyZoc) { costToMove.points += settings.Backing().extra_cost_for_zoc_movement(); }
        if (costToMove.IsPossible(remainingActionPoints)) {
            validDestinations.emplace_back(
                    baseInfo.startLocation,
                    candidate,
                    baseInfo.pointCost + costToMove.points,
                    movingUnit->hidden() &&
                            (baseInfo.willUnhide || !AllowsHiding(candidateTerrain)),
                    baseInfo.targets);
            validDestinations.back().targets.push_back(candidate);
        }
    }
    return validDestinations;
}

auto ConstructMoveDestinations(
        const Units *units,
        const vector<PlayerId> &allyPids,
        const Unit *movingUnit,
        const ActionPoints startingActionPoints,
        const HexMap *hexMap,
        const SettingsGetter &settings) -> vector<AccumulatedMoveInfo> {
    const Coords &startingLocation = movingUnit->location();

    if (startingActionPoints == 0) { return {}; }

    std::vector<AccumulatedMoveInfo> allAmis{};
    CoordsSet checkedDestinations(hexMap);
    checkedDestinations.Add(startingLocation);
    const AccumulatedMoveInfo baseInfo(startingLocation, startingLocation, 0, false, {});

    const auto zocCoords =
            TilesInEnemyZoc(settings, movingUnit->player_id(), units, hexMap, allyPids);

    const bool inEnemyZoc = zocCoords.Contains(startingLocation);

    auto uncheckedDestinations = AdjacentMoveDestinations(
            baseInfo,
            movingUnit,
            startingActionPoints,
            inEnemyZoc,
            units,
            hexMap,
            settings,
            allyPids);

    while (!uncheckedDestinations.empty()) {
        // Sort descending by point cost so we can pop the end for the cheapest one
        std::sort(
                std::begin(uncheckedDestinations),
                std::end(uncheckedDestinations),
                std::greater<>());
        auto last = uncheckedDestinations.back();
        uncheckedDestinations.pop_back();

        const auto endCoords = last.endLocation;
        // Check to see if we've tried this destination; since we're doing cheapest first, we know
        // another one would be more expensive
        if (checkedDestinations.Contains(endCoords)) { continue; }

        checkedDestinations.Add(endCoords);
        allAmis.push_back(last);
        const auto remainingActionPoints = startingActionPoints - last.pointCost;
        if (remainingActionPoints == 0) {
            // All the remaining ones are more expensive, but we'll just continue through the loop
            // for now
            continue;
        }

        const bool newInEnemyZoc = zocCoords.Contains(endCoords);
        auto newUnchecked = AdjacentMoveDestinations(
                last,
                movingUnit,
                remainingActionPoints,
                newInEnemyZoc,
                units,
                hexMap,
                settings,
                allyPids);
        uncheckedDestinations.insert(
                std::end(uncheckedDestinations),
                std::begin(newUnchecked),
                std::end(newUnchecked));
    }

    return allAmis;
}

}  // namespace shardok
