//
// Created by Dan Crosby on 12/29/21.
//

#include "AIWaterCrossingCalculator.hpp"

#include "AIMinimumDistanceAndTarget.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/HexMapHelpers.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/util/HexMapUtils.hpp"

namespace shardok {

auto UnitIdsRequiringWaterCrossing(
        const GameState *gameState,
        const PlayerId pid,
        const CoordsSet &destinations,
        const APDCache &apdCache,
        const SettingsGetter &settings) -> vector<UnitId> {
    // Put out all the fires, except on bridges
    fb::HexMapW mapCopy = fb::CopyHexMap(gameState->hex_map());
    for (uint32_t index = 0; index < mapCopy->terrain()->size(); index++) {
        if (IsWater(mapCopy->terrain()->Get(index)->type())) continue;
        mapCopy->mutable_terrain()
                ->GetMutableObject(index)
                ->mutable_modifier()
                .mutable_fire()
                .mutate_present(false);
    }

    const auto mapId = ActionPointDistancesCache::GetMapId(mapCopy);

    vector<UnitId> unitIdsToReturn{};

    // Set this to -1 for unchecked, 0 for does not need crossing, 1 for does need crossing
    std::vector<int> startingPositionIndices(9, -1);
    for (const auto *unit : *gameState->units()) {
        if (unit->player_id() != pid) continue;

        const auto &battType = settings.GetBattalionType(unit->battalion().type());

        if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
            for (const Coords &destination : destinations) {
                if (!CanReach(unit->location(), destination, mapCopy, mapId, apdCache, battType)) {
                    unitIdsToReturn.push_back(unit->unit_id());
                    break;
                }
            }
        } else if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT) {
            if (startingPositionIndices[unit->starting_position_index()] == -1) {
                startingPositionIndices[unit->starting_position_index()] = 0;
                const auto *startingPositions = gameState->hex_map()
                                                        ->attacker_starting_positions()
                                                        ->Get(unit->starting_position_index())
                                                        ->positions();

                for (const Coords *pos : *startingPositions) {
                    for (const Coords &destination : destinations) {
                        if (!CanReach(*pos, destination, mapCopy, mapId, apdCache, battType)) {
                            startingPositionIndices[unit->starting_position_index()] = 1;
                            break;
                        }
                        if (startingPositionIndices[unit->starting_position_index()] == 1) break;
                    }
                }
            }

            if (startingPositionIndices[unit->starting_position_index()] == 1) {
                unitIdsToReturn.push_back(unit->unit_id());
            }
        }
    }

    return unitIdsToReturn;
}

auto UnitIdsToCreateWaterCrossing(
        const GameState *gameState,
        const PlayerId pid,
        const APDCache &apdCache,
        const SettingsGetter &settings) -> vector<UnitId> {
    vector<UnitId> unitIds{};

    for (const auto *unit : *gameState->units()) {
        if (unit->player_id() != pid) continue;
        if (unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
            unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT)
            continue;
        if (!unit->has_attached_hero()) continue;

        const auto profession = unit->attached_hero().profession_info().profession();
        const auto &battalionType = settings.GetBattalionType(unit->battalion().type());

        if (profession == net::eagle0::shardok::storage::fb::Profession_ENGINEER ||
            (profession == net::eagle0::shardok::storage::fb::Profession_MAGE &&
             battalionType->allowsCasting)) {
            unitIds.push_back(unit->unit_id());
        }
    }

    return unitIds;
}

auto CanReach(
        const Coords &origin,
        const Coords &destination,
        const HexMap *hexMap,
        const MapId &mapId,
        const APDCache &apdCache,
        const BattalionTypeSPtr &battalionType) -> bool {
    const DIST_T startingDistance =
            apdCache->GetRaw(hexMap, mapId, battalionType, false)->Distance(origin, destination);

    return startingDistance != ActionPointDistances::IMPOSSIBLE;
}

// Returns the *non*-water tiles from which you could bridge/freeze water to allow a unit to cross
// from origin to destination
auto CrossingStartLocations(const HexMap *hexMap, const CoordsSet &waterCrossingTiles)
        -> CoordsSet {
    CoordsSet adjacentToWaterCrossingTiles(hexMap);

    for (const Coords &waterTile : waterCrossingTiles) {
        AssertValid(waterTile, hexMap);
        auto adjacentCoords = HexMapUtils::GetAdjacentCoords(hexMap, waterTile);
        for (const auto &adj : adjacentCoords) {
            AssertValid(adj, hexMap);
            if (adjacentToWaterCrossingTiles.Contains(adj)) continue;

            const auto &terr = GetTerrain(hexMap, adj);
            if (IsWater(terr->type())) continue;
            if (terr->type() == net::eagle0::shardok::storage::fb::Terrain_::Type_MOUNTAIN)
                continue;

            adjacentToWaterCrossingTiles.Add(adj);
        }
    }

    return adjacentToWaterCrossingTiles;
}

auto WaterCrossingTiles(
        const Coords &origin,
        const Coords &destination,
        const HexMap *hexMap,
        const APDCache &apdCache,
        const BattalionTypeSPtr &battalionType) -> CoordsSet {
    AssertValid(origin, hexMap);
    AssertValid(destination, hexMap);
    auto returnCoords = CoordsSet(hexMap);

    fb::HexMapW mapCopy = fb::CopyHexMap(hexMap);
    const int mapSize = hexMap->row_count() * hexMap->column_count();

    // For each tile on the map, check if and only if:
    //  1) the tile is water
    //  2) the tile does not already have a bridge (that is not on fire)
    //  3) the tile is not already frozen
    for (int index = 0; index < mapSize; index++) {
        const auto startTerrain = *hexMap->terrain()->Get(index);

        if (!IsWater(startTerrain.type())) continue;

        const auto modifier = startTerrain.modifier();
        if (modifier.bridge().present() && !modifier.fire().present()) continue;
        if (modifier.ice().present() && !modifier.fire().present()) continue;

        // Now try adding a bridge to the tile to see if it helps
        mapCopy->mutable_terrain()
                ->GetMutableObject(index)
                ->mutable_modifier()
                .mutable_bridge()
                .mutate_present(true);
        mapCopy->mutable_terrain()
                ->GetMutableObject(index)
                ->mutable_modifier()
                .mutable_fire()
                .mutate_present(false);

        auto hash = ActionPointDistancesCache::GetMapId(mapCopy);

        if (const auto *distances = apdCache->GetRaw(mapCopy, hash, battalionType, false);
            distances->Distance(origin, destination) != ActionPointDistances::IMPOSSIBLE) {
            returnCoords.Add(index / hexMap->column_count(), index % hexMap->column_count());
        }

        // Undo the new bridge for the next iteration of the loop
        mapCopy->mutable_terrain()
                ->GetMutableObject(index)
                ->mutable_modifier()
                .mutable_bridge()
                .mutate_present(false);
    }

    return returnCoords;
}

// Returns the set of tiles that the attacker should try to approach in order to bridge/freeze
auto IntendedCrossingStarts(
        const GameState *gameState,
        const vector<UnitId> &unitIdsCreatingCrossing,
        const CoordsSet &tilesToStartCrossingFrom,
        const MapId &mapId,
        const APDCache &apdCache,
        const SettingsGetter &settings) -> CoordsSet {
    CoordsSet intendedCrossingStarts(gameState->hex_map());
    for (const UnitId uid : unitIdsCreatingCrossing) {
        const Unit *unit = gameState->units()->Get(uid);
        const Coords &location = unit->location();
        const auto &battalionType = settings.GetBattalionType(unit->battalion().type());
        const auto *apd = apdCache->GetRaw(gameState->hex_map(), mapId, battalionType, false);

        if (location.row() >= 0) {
            Coords intended =
                    MinimumDistanceAndTarget(apd, location, tilesToStartCrossingFrom).coords;
            if (intended.row() >= 0) { intendedCrossingStarts.Add(intended); }
        }
    }

    return intendedCrossingStarts;
}

}  // namespace shardok
