//
// 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,
        const vector<const Unit *> &occupants) -> vector<AccumulatedMoveInfo>;

auto ConstructMoveDestinations(
        const Units *units,
        const vector<PlayerId> &allyPids,
        const Unit *movingUnit,
        ActionPoints startingActionPoints,
        const HexMap *hexMap,
        const SettingsGetter &settings,
        const CoordsSet &zocCoords) -> 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();

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

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

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

    // Create commands
    for (const auto &destInfo : destinations) {
        auto cmd = std::make_shared<MoveCommand>(
                destInfo.pointCost,
                movingUnit->player_id(),
                movingUnit->unit_id(),
                settings,
                destInfo.targets,
                allyPids,
                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 vector<PlayerId> &allyPids,
        const vector<const Unit *> &occupants) -> CoordsSet {
    CoordsSet toReturn(hexMap);
    const int columnCount = hexMap->column_count();
    for (const auto &adjacentTile : HexMapUtils::GetAdjacentCoords(hexMap, from)) {
        // Use spatial index for O(1) lookup instead of O(N) linear search
        const int index = adjacentTile.row() * columnCount + adjacentTile.column();
        const auto *occupant = occupants[index];

        // Check if tile is occupied by a known unit
        bool isOccupied = false;
        if (occupant) {
            if (occupant->player_id() == playerId ||
                std::ranges::contains(allyPids, occupant->player_id()) || !occupant->hidden()) {
                isOccupied = true;
            }
        }

        if (!isOccupied) { 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,
        const vector<const Unit *> &occupants) -> vector<AccumulatedMoveInfo> {
    vector<AccumulatedMoveInfo> validDestinations{};

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

    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,
        const CoordsSet &zocCoords) -> vector<AccumulatedMoveInfo> {
    const Coords &startingLocation = movingUnit->location();

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

    // Build spatial index once for O(1) occupancy lookups
    const auto occupants = Occupants(*units, hexMap->row_count(), hexMap->column_count());

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

    const bool inEnemyZoc = zocCoords.Contains(startingLocation);

    // Use priority queue for efficient min-heap operations
    std::priority_queue<AccumulatedMoveInfo, vector<AccumulatedMoveInfo>, std::greater<>>
            uncheckedDestinations;

    auto initialDestinations = AdjacentMoveDestinations(
            baseInfo,
            movingUnit,
            startingActionPoints,
            inEnemyZoc,
            units,
            hexMap,
            settings,
            allyPids,
            occupants);
    for (auto &dest : initialDestinations) { uncheckedDestinations.push(std::move(dest)); }

    while (!uncheckedDestinations.empty()) {
        auto current = uncheckedDestinations.top();
        uncheckedDestinations.pop();

        const auto endCoords = current.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(current);
        const auto remainingActionPoints = startingActionPoints - current.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(
                current,
                movingUnit,
                remainingActionPoints,
                newInEnemyZoc,
                units,
                hexMap,
                settings,
                allyPids,
                occupants);
        for (auto &dest : newUnchecked) { uncheckedDestinations.push(std::move(dest)); }
    }

    return allAmis;
}

}  // namespace shardok
