//
// Created by Dan Crosby on 4/11/21.
//

#include "AIVictoryConditionScoreCalculator.hpp"

#include "AIAttackLocations.hpp"
#include "AIDistanceDebuf.hpp"
#include "src/main/cpp/net/eagle0/common/ContainerUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/ai/AIAttackGroups.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/victory_condition.hpp"

namespace shardok {

constexpr ScoreValue MAX_DEFENDER_HELD_VALUE = -1200;
constexpr ScoreValue BADLY_HELD_VALUE = -50;
constexpr ScoreValue UNHELD_VALUE = 100;
constexpr ScoreValue ON_FIRE_VALUE = 200;
constexpr ScoreValue SURVIVING_ENEMY_VALUE = -200;

auto CanClaimCriticalTile(const Unit* unit) {
    return unit->battalion().type() != net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD;
}

static inline auto IsLateGame(const GameState* gs) { return gs->current_round() > 18; }

// For every critical tile, reduce score by
// 1) nothing, if the attacker already holds the tile with a claimable unit
// 2) ON_FIRE_VALUE modified by the distance debuf to an adjacent position for units capable of
//    extinguishing, if the castle is on fire
// 3) UNHELD_VALUE modified by the distance debuf to a claimable unit, if the castle is open or
//    held by an unclaimable
// 4) MAX_DEFENDER_HELD_VALUE modified by the distance debuf to an attack position, if the castle is
//    held by the defender.

auto AttackerDebufForOnFireCriticalTile(
        const Coords& criticalTileLocation,
        const HexMap* map,
        const MapId& mapId,
        const vector<const Unit*>& extinguishingUnits,
        const APDCache& apdCache,
        const ALCache& alCache,
        const SettingsGetter& settings,
        const int braveWaterActionPointCost,
        const bool lateGame) -> double {
    double minDebuf = 99999.9;

    for (const Coords& adj : HexMapUtils::GetAdjacentCoords(map, criticalTileLocation)) {
        const auto adjTerrain = GetTerrain(map, adj);
        // Don't look to go to an adjacent tile that is itself on fire
        if (adjTerrain->modifier().fire().present()) continue;

        const double newDebuf = DefenderDistanceBuf(
                criticalTileLocation,
                map,
                mapId,
                extinguishingUnits,
                apdCache,
                alCache,
                settings,
                braveWaterActionPointCost,
                lateGame,
                /* includeUndead = */ false);
        if (newDebuf < minDebuf) minDebuf = newDebuf;
    }
    return ON_FIRE_VALUE * minDebuf;
}

// case 3
auto AttackerDebufForUnoccupiedCriticalTile(
        const Coords& criticalTileLocation,
        const HexMap* map,
        const MapId& mapId,
        const vector<const Unit*>& claimableUnits,
        const APDCache& apdCache,
        const ALCache& alCache,
        const SettingsGetter& settings,
        const int braveWaterActionPointCost,
        const bool lateGame) -> double {
    return UNHELD_VALUE * DefenderDistanceBuf(
                                  criticalTileLocation,
                                  map,
                                  mapId,
                                  claimableUnits,
                                  apdCache,
                                  alCache,
                                  settings,
                                  braveWaterActionPointCost,
                                  lateGame,
                                  /* includeUndead = */ false);
}

auto AttackerDebufForDefenderOccupiedCriticalTile(
        const Unit* defenderUnit,
        const HexMap* map,
        const MapId& mapId,
        const vector<const Unit*>& attackerUnits,
        const APDCache& apdCache,
        const ALCache& alCache,
        const SettingsGetter& settings,
        const int braveWaterActionPointCost,
        const bool lateGame) {
    const double baseUnitValue =
            defenderUnit->battalion().size() +
            (defenderUnit->has_attached_hero() ? defenderUnit->attached_hero().vigor() : 0.0);

    constexpr auto multiplier = 1.0;

    return multiplier * baseUnitValue *
           DefenderDistanceBuf(
                   defenderUnit->location(),
                   map,
                   mapId,
                   attackerUnits,
                   apdCache,
                   alCache,
                   settings,
                   braveWaterActionPointCost,
                   lateGame,
                   /* includeUndead = */ false);
}

auto DefenderHoldsCriticalTilesVictoryScore(
        const net::eagle0::shardok::storage::fb::GameState* gameState,
        const CoordsSet& criticalTileLocations,
        const PlayerInfo* player,
        const APDCache& apdCache,
        const ALCache& alCache,
        const SettingsGetter& settings) -> ScoreValue {
    ScoreValue total = 0.0;

    const auto rc = gameState->hex_map()->row_count();
    const auto cc = gameState->hex_map()->column_count();
    const auto occupants = Occupants(*gameState->units(), rc, cc);

    for (const Coords& criticalTileLocation : criticalTileLocations) {
        const auto index = criticalTileLocation.row() * cc + criticalTileLocation.column();
        const auto& occupant = occupants[index];

        if (occupant && occupant->battalion().type() !=
                                net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD) {
            if (occupant->player_id() == player->player_id()) {
                total -= MAX_DEFENDER_HELD_VALUE;
            } else {
                total += BADLY_HELD_VALUE;
            }
        }
    }

    return total;
}

auto AttackerHoldsCriticalTilesVictoryScore(
        const net::eagle0::shardok::storage::fb::GameState* gameState,
        const CoordsSet& criticalTileLocations,
        const PlayerInfo* player,
        const APDCache& apdCache,
        const ALCache& alCache,
        const SettingsGetter& settings) -> ScoreValue {
    vector<const Unit*> playerUnits{};
    vector<const Unit*> claimablePlayerUnits{};
    for (const Unit* unit : *gameState->units()) {
        if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
            unit->player_id() == player->player_id()) {
            playerUnits.push_back(unit);

            if (CanClaimCriticalTile(unit)) { claimablePlayerUnits.push_back(unit); }
        }
    }

    if (claimablePlayerUnits.size() < criticalTileLocations.size()) {
        return criticalTileLocations.size() * MAX_DEFENDER_HELD_VALUE;
    }

    const int braveWaterActionPointCost = settings.Backing().brave_water_action_point_cost();
    const MapId mapId = apdCache->GetMapId(gameState->hex_map());

    ScoreValue total = 0.0;

    const auto rc = gameState->hex_map()->row_count();
    const auto cc = gameState->hex_map()->column_count();
    const auto occupants = Occupants(*gameState->units(), rc, cc);

    for (const Coords& criticalTileLocation : criticalTileLocations) {
        const auto index = criticalTileLocation.row() * cc + criticalTileLocation.column();
        const auto terrain = GetTerrain(gameState->hex_map(), criticalTileLocation);
        const auto& occupant = occupants[index];

        if (occupant) {
            if (occupant->player_id() == player->player_id()) {
                if (CanClaimCriticalTile(occupant)) {
                    // case 1 above
                    continue;
                } else {
                    total -= AttackerDebufForUnoccupiedCriticalTile(
                            criticalTileLocation,
                            gameState->hex_map(),
                            mapId,
                            claimablePlayerUnits,
                            apdCache,
                            alCache,
                            settings,
                            braveWaterActionPointCost,
                            IsLateGame(gameState));
                    total += BADLY_HELD_VALUE;
                }
            } else {
                total -= AttackerDebufForDefenderOccupiedCriticalTile(
                        occupant,
                        gameState->hex_map(),
                        mapId,
                        playerUnits,
                        apdCache,
                        alCache,
                        settings,
                        braveWaterActionPointCost,
                        IsLateGame(gameState));
            }
        } else if (terrain->modifier().fire().present()) {
            total -= AttackerDebufForOnFireCriticalTile(
                    criticalTileLocation,
                    gameState->hex_map(),
                    mapId,
                    claimablePlayerUnits,
                    apdCache,
                    alCache,
                    settings,
                    braveWaterActionPointCost,
                    IsLateGame(gameState));
        } else {
            total -= AttackerDebufForUnoccupiedCriticalTile(
                    criticalTileLocation,
                    gameState->hex_map(),
                    mapId,
                    claimablePlayerUnits,
                    apdCache,
                    alCache,
                    settings,
                    braveWaterActionPointCost,
                    IsLateGame(gameState));
        }
    }

    return total;
}

auto LastPlayerStandingVictoryScore(
        const GameState* gameState,
        const PlayerInfo* player,
        const APDCache& apdCache,
        const ALCache& alCache,
        const SettingsGetter& settings) -> ScoreValue {
    if (!common::Contains(
                *player->victory_conditions(),
                net::eagle0::shardok::storage::fb::
                        VictoryCondition_VICTORY_CONDITION_LAST_PLAYER_STANDING)) {
        return 0;
    }

    vector<const Unit*> playerUnits{};
    vector<const Unit*> otherUnits{};

    for (const Unit* unit : *gameState->units()) {
        if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) continue;
        if (unit->player_id() == player->player_id()) {
            playerUnits.push_back(unit);
        } else {
            otherUnits.push_back(unit);
        }
    }

    double debuf = 0.0;
    const MapId mapId = apdCache->GetMapId(gameState->hex_map());
    for (const Unit* otherUnit : otherUnits) {
        if (otherUnit->battalion().type() ==
            net::eagle0::shardok::storage::fb::BattalionTypeId_UNDEAD)
            continue;
        debuf += SURVIVING_ENEMY_VALUE * DefenderDistanceBuf(
                                                 otherUnit->location(),
                                                 gameState->hex_map(),
                                                 mapId,
                                                 playerUnits,
                                                 apdCache,
                                                 alCache,
                                                 settings,
                                                 5,
                                                 IsLateGame(gameState),
                                                 /* includeUndead = */ true);
    }

    return debuf;
}

}  // namespace shardok
