//
// Created by Dan Crosby on 6/19/23.
//

#include "AIAttackGroups.hpp"

#include <unordered_map>

#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"

namespace shardok {

constexpr double kOverpowerRatio = 2.0;
constexpr double kBraveWaterCostMultiplier = 1.2;

using std::pair;
using std::shared_ptr;

DIST_T NormalizedCostWhenBraving(const DIST_T cost) {
    if (cost >= static_cast<double>(ActionPointDistances::IMPOSSIBLE) / kBraveWaterCostMultiplier)
        return ActionPointDistances::IMPOSSIBLE;

    return static_cast<DIST_T>(kBraveWaterCostMultiplier * cost);
}

struct TargetAndDistance {
    Coords target;
    CoordsSet attackLocations;
    double targetPower;
    DIST_T distance;

    TargetAndDistance(const Coords& t, const CoordsSet& al, const double tp, const DIST_T d)
        : target(t),
          attackLocations(al),
          targetPower(tp),
          distance(d) {}
};

auto MinDistance(
        const Coords& start,
        const CoordsSet& destinations,
        const ActionPointDistances* apd) -> DIST_T {
    DIST_T minDistance = ActionPointDistances::IMPOSSIBLE;

    for (const Coords& dest : destinations) {
        if (const DIST_T distance = apd->Distance(start, dest); distance < minDistance) {
            minDistance = distance;
        }
    }

    return minDistance;
}

auto MinDistanceIncludingBraving(
        const Coords& start,
        const CoordsSet& destinations,
        const ActionPointDistances* notBravingApd,
        const ActionPointDistances* bravingApd) {
    // First try to get there without braving
    if (const DIST_T notBravingDistance = MinDistance(start, destinations, notBravingApd);
        notBravingDistance < ActionPointDistances::IMPOSSIBLE) {
        return notBravingDistance;
    }

    // Then normalize the cost with braving
    if (bravingApd) {
        return NormalizedCostWhenBraving(MinDistance(start, destinations, bravingApd));
    }
    return ActionPointDistances::IMPOSSIBLE;
}

auto EffectiveDistance(
        const Unit* unit,
        const HexMap* map,
        const MapId& mapId,
        const APDCache& apdCache,
        const AttackLocations& attackLocations,
        const SettingsGetter& settings,
        const int braveWaterCost) -> DIST_T {
    return EffectiveDistance(
            unit,
            map,
            mapId,
            apdCache,
            attackLocations.LocationsWithEnemyInRange(unit),
            settings,
            braveWaterCost);
}

auto EffectiveDistance(
        const Unit* unit,
        const HexMap* map,
        const MapId& mapId,
        const APDCache& apdCache,
        const CoordsSet& locations,
        const SettingsGetter& settings,
        const int braveWaterCost) -> DIST_T {
    const auto& battType = settings.GetBattalionType(unit->battalion().type());
    const auto* notBravingApd = apdCache->GetRaw(map, mapId, battType, false);
    const ActionPointDistances* bravingApd = nullptr;
    if (battType->allowsBraveWater) {
        bravingApd = apdCache->GetRaw(map, mapId, battType, true, braveWaterCost);
    }

    return MinDistanceIncludingBraving(unit->location(), locations, notBravingApd, bravingApd);
}

auto EffectiveDistance(
        const Unit* unit,
        const ActionPointDistances* notBravingApd,
        const ActionPointDistances* bravingApd,
        const CoordsSet& locations) -> DIST_T {
    return MinDistanceIncludingBraving(unit->location(), locations, notBravingApd, bravingApd);
}

auto Power(const Unit* unit) -> double { return unit->battalion().size(); }

auto CoordsIndex(const Coords& coords, const int columnCount) {
    return coords.row() * columnCount + coords.column();
}

// Chooses a list of targets in priority order for each unit.
auto GenerateTargetPriorities(
        const vector<const Unit*>& occupants,
        const HexMap* map,
        const CoordsSet& targets,
        const PlayerId attackerId,
        const vector<const Unit*>& remainingUnits,
        const APDCache& apdCache,
        const ALCache& alCache,
        const MapId& mapId,
        const SettingsGetter& settings,
        const bool isLateGame) -> vector<TargetPriorityList> {
    auto cc = map->column_count();

    const auto braveWaterCost = settings.Backing().brave_water_action_point_cost();

    vector<TargetPriorityList> allTargetsUnitsAndDistances{};
    allTargetsUnitsAndDistances.reserve(remainingUnits.size());

    vector<double> powerAttackingEachTarget(occupants.size());

    vector<const Unit*> sortedAttackers = remainingUnits;
    // Handle stronger units first
    std::ranges::sort(sortedAttackers, [](const Unit* left, const Unit* right) {
        return Power(left) > Power(right);
    });

    // APDCache now has built-in thread-local caching - no need for local apdByBattType map
    // For each unit, sort the targets by distance from the unit to an attack location for the
    // target
    for (const Unit* unit : sortedAttackers) {
        auto& tpl = allTargetsUnitsAndDistances.emplace_back(
                unit->unit_id(),
                vector<TargetAndAttackLocations>{});

        vector<TargetAndDistance> targetsWithDistance;

        // Get APDs directly from cache (now with built-in thread-local optimization)
        const auto& battType = settings.GetBattalionType(unit->battalion().type());
        const auto* notBravingApd = apdCache->GetRaw(map, mapId, battType, false);
        const ActionPointDistances* bravingApd = nullptr;
        if (battType->allowsBraveWater) {
            bravingApd = apdCache->GetRaw(map, mapId, battType, true, braveWaterCost);
        }

        for (const Coords& targetLocation : targets) {
            const auto coordsIndex = CoordsIndex(targetLocation, cc);
            const auto& occupant = occupants[coordsIndex];

            const auto& attackLocations = alCache->CachedLocations(targetLocation, isLateGame);

            if (occupant && occupant->player_id() != attackerId) {
                double occupantPower = Power(occupant);

                if (unit->location().row() >= 0) {
                    const auto& attackLocsForUnit = attackLocations.LocationsWithEnemyInRange(unit);
                    auto distance =
                            EffectiveDistance(unit, notBravingApd, bravingApd, attackLocsForUnit);

                    targetsWithDistance.emplace_back(
                            targetLocation,
                            attackLocsForUnit,
                            occupantPower,
                            distance);
                } else {
                    targetsWithDistance.emplace_back(
                            targetLocation,
                            attackLocations.LocationsWithEnemyInRange(unit),
                            occupantPower,
                            ActionPointDistances::IMPOSSIBLE);
                }
            }
        }

        // Sort by distance
        std::ranges::sort(
                targetsWithDistance,
                [&powerAttackingEachTarget,
                 cc](const TargetAndDistance& left, const TargetAndDistance& right) {
                    const auto leftIndex = CoordsIndex(left.target, cc);
                    const auto rightIndex = CoordsIndex(right.target, cc);

                    const double powerAttackingLeft = powerAttackingEachTarget[leftIndex];
                    const double powerAttackingRight = powerAttackingEachTarget[rightIndex];

                    const bool leftAlreadyOverwhelmed =
                            powerAttackingLeft >= kOverpowerRatio * left.targetPower;
                    const bool rightAlreadyOverwhelmed =
                            powerAttackingRight >= kOverpowerRatio * right.targetPower;

                    if (leftAlreadyOverwhelmed == rightAlreadyOverwhelmed)
                        return left.distance < right.distance;

                    return leftAlreadyOverwhelmed < rightAlreadyOverwhelmed;
                });

        if (!targetsWithDistance.empty()) {
            powerAttackingEachTarget[CoordsIndex(targetsWithDistance.front().target, cc)] +=
                    Power(unit);
        }

        tpl.priorityOrder = common::Map(targetsWithDistance, [](const TargetAndDistance& tad) {
            return TargetAndAttackLocations{
                    .target = tad.target,
                    .attackLocations = tad.attackLocations};
        });
    }

    return allTargetsUnitsAndDistances;
}

}  // namespace shardok