//
//  HexMapUtils.cpp
//  eagle0_engine
//
//  Created by Dan Crosby on 12/31/18.
//  Copyright © 2018 none. All rights reserved.
//

#include "HexMapUtils.hpp"

#include "HexCubeUtils.hpp"
#include "src/main/cpp/net/eagle0/common/FilesystemUtils.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"

namespace shardok {

using std::any_of;
using std::begin;
using std::end;
using Terrain = net::eagle0::shardok::storage::fb::Terrain;

const vector<Coords> ADJACENT_TILE_MODS_EVEN_ROW =
        {Coords(-1, 0), Coords(0, 1), Coords(1, 0), Coords(1, -1), Coords(0, -1), Coords(-1, -1)};

const vector<Coords> ADJACENT_TILE_MODS_ODD_ROW =
        {Coords(-1, 1), Coords(0, 1), Coords(1, 1), Coords(1, 0), Coords(0, -1), Coords(-1, 0)};

auto AllCastleCoords(const HexMap *hexMap) -> CoordsSet {
    CoordsSet castleCoords(hexMap);
    for (int16_t r = 0; r < hexMap->row_count(); r++) {
        for (int16_t c = 0; c < hexMap->column_count(); c++) {
            Coords coords(r, c);
            if (GetTerrain(hexMap, coords)->modifier().castle().present()) {
                castleCoords.Add(coords);
            }
        }
    }

    return castleCoords;
}

auto CoordsAreValid(const int rowCount, const int columnCount, const Coords &coords) -> bool {
    if (coords.row() < 0) return false;
    if (coords.column() < 0) return false;
    if (coords.row() >= rowCount) return false;
    if (coords.column() >= columnCount) return false;

    return true;
}

auto CoordsAreValid(const HexMap *map, const Coords &coords) -> bool {
    if (coords.row() < 0) return false;
    if (coords.column() < 0) return false;
    if (coords.row() >= map->row_count()) return false;
    if (coords.column() >= map->column_count()) return false;

    return true;
}

auto GetTerrain(const HexMap *map, const Coords &coords) -> const Terrain * {
    return map->terrain()->Get(coords.row() * map->column_count() + coords.column());
}

auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain * {
    return map->terrain()->GetMutableObject(coords.row() * map->column_count() + coords.column());
}

auto HasForestAccess(
        const Coords &coords,
        const Units *units,
        const HexMap *hexMap,
        const vector<PlayerId> &allyPids,
        const PlayerId player) -> bool {
    if (GetTerrain(hexMap, coords)->type() ==
        net::eagle0::shardok::storage::fb::Terrain_::Type_FOREST)
        return true;

    const CoordsSet adjacentCoords = HexMapUtils::GetAdjacentCoords(hexMap, coords);

    return any_of(begin(adjacentCoords), end(adjacentCoords), [&](const Coords &adjCoords) {
        if (KnownEnemyOccupant(player, units, allyPids, adjCoords)) return false;
        if (GetTerrain(hexMap, adjCoords)->type() ==
            net::eagle0::shardok::storage::fb::Terrain_::Type_FOREST)
            return true;
        return false;
    });
}

const std::vector<CoordsSet> HexMapUtils::standardCoords =
        HexMapUtils::ComputeAllStandardAdjacentCoords();

auto HexMapUtils::ComputeAllStandardAdjacentCoords() -> std::vector<CoordsSet> {
    std::vector<CoordsSet> arr;
    arr.reserve(size_t(STANDARD_MAP_ROWS * STANDARD_MAP_COLUMNS));

    for (int index = 0; index < STANDARD_MAP_ROWS * STANDARD_MAP_COLUMNS; index++) {
        arr.push_back(ComputeAdjacentCoords(
                STANDARD_MAP_ROWS,
                STANDARD_MAP_COLUMNS,
                Coords(MapIndex(index / STANDARD_MAP_COLUMNS),
                       MapIndex(index % STANDARD_MAP_COLUMNS))));
    }

    return arr;
}

auto HexMapUtils::ComputeAdjacentCoords(
        const int rowCount,
        const int columnCount,
        const Coords &origin) -> CoordsSet {
    CoordsSet cs(columnCount, rowCount);

    const int originIndex = origin.row() * columnCount + origin.column();

    // Row above
    if (origin.row() > 0) {
        cs.AddIndex(originIndex - columnCount);

        if (origin.row() % 2 == 0) {
            if (origin.column() > 0) { cs.AddIndex(originIndex - columnCount - 1); }
        } else if (origin.column() < columnCount - 1) {
            cs.AddIndex(originIndex - columnCount + 1);
        }
    }
    // This row
    if (origin.column() > 0) { cs.AddIndex(originIndex - 1); }
    if (origin.column() < columnCount - 1) { cs.AddIndex(originIndex + 1); }
    // Row below
    if (origin.row() < rowCount - 1) {
        cs.AddIndex(originIndex + columnCount);

        if (origin.row() % 2 == 0) {
            if (origin.column() > 0) { cs.AddIndex(originIndex + columnCount - 1); }
        } else if (origin.column() < columnCount - 1) {
            cs.AddIndex(originIndex + columnCount + 1);
        }
    }

    return cs;
}

auto HexMapUtils::GetAdjacentCoords(const HexMap *map, const Coords &origin) -> const CoordsSet & {
    if (map->row_count() == STANDARD_MAP_ROWS && map->column_count() == STANDARD_MAP_COLUMNS) {
        return standardCoords[origin.row() * STANDARD_MAP_COLUMNS + origin.column()];
    }

    // UNFORTUNATE HACK
    // We really want to return a reference from here to avoid allocations, but that only works for
    // the standard map size, and tests usually use something else. So let's use some statics here
    // for tests, knowing that we generally make a new one between runs.

    static std::vector<CoordsSet *> *cachedCoords = nullptr;
    if (cachedCoords == nullptr) {
        cachedCoords = new vector<CoordsSet *>(map->column_count() * map->row_count());
    }
    const auto index = origin.row() * map->column_count() + origin.column();
    if ((*cachedCoords)[index] == nullptr) {
        (*cachedCoords)[index] =
                new CoordsSet(ComputeAdjacentCoords(map->row_count(), map->column_count(), origin));
    }
    return *(*cachedCoords)[index];
}

auto HexMapUtils::GetAdjacentTiles(const HexMap *map, const Coords &origin)
        -> vector<AdjacentTile> {
    vector<AdjacentTile> adjacentTiles{};

    const auto &modifierSet =
            (origin.row() % 2 ? ADJACENT_TILE_MODS_ODD_ROW : ADJACENT_TILE_MODS_EVEN_ROW);
    auto dir = HexMapDirection(0);
    for (const Coords &modifier : modifierSet) {
        Coords adjacentCoords =
                Coords(origin.row() + modifier.row(), origin.column() + modifier.column());

        if (CoordsAreValid(map, adjacentCoords)) {
            adjacentTiles.emplace_back(adjacentCoords, dir);
        }
        ++dir;
    }

    return adjacentTiles;
}

[[nodiscard]] auto GetTilesAcrossWater(const HexMap *map, const Coords &coords) -> CoordsSet {
    const vector<AdjacentTile> adjacentTiles = HexMapUtils::GetAdjacentTiles(map, coords);
    CoordsSet braveWaterTargets(map);

    for (const AdjacentTile &adjacentTile : adjacentTiles) {
        const auto *terrain = GetTerrain(map, adjacentTile.coords);
        if (IsWater((net::eagle0::shardok::common::Terrain_Type)terrain->type())) {
            if (terrain->modifier().bridge().present()) continue;
            if (terrain->modifier().ice().present()) continue;
            for (int i = -1; i <= 1; i++) {
                const auto nextDirection =
                        (HexMapDirection)((adjacentTile.directionTo + i + 6) % 6);
                const auto &maybeNext =
                        MaybeTileInDirection(map, adjacentTile.coords, nextDirection);

                if (maybeNext.has_value()) { braveWaterTargets.Add(maybeNext.value()); }
            }
        }
    }

    return braveWaterTargets;
}

void MaybeAdd(const Cube &cube, CoordsSet &set, const HexMap *map) {
    const Coords c = CubeToOffset(cube);
    if (CoordsAreValid(map, c)) { set.Add(c); }
}

[[nodiscard]] auto TwoAwayTilesUnblockedByMountains(const HexMap *map, const Coords &origin)
        -> CoordsSet {
    CoordsSet unblockedTwoAways(map);
    const Cube cubeOrigin = OffsetToCube(origin);

    // x constant
    const Cube x1 = Cube(cubeOrigin.x, cubeOrigin.y + 1, cubeOrigin.z - 1);
    const bool x1Clear = !CoordsAreValid(map, CubeToOffset(x1)) ||
                         GetTerrain(map, CubeToOffset(x1))->type() !=
                                 net::eagle0::shardok::storage::fb::Terrain_::Type_MOUNTAIN;

    const Cube x2 = Cube(cubeOrigin.x, cubeOrigin.y - 1, cubeOrigin.z + 1);
    const bool x2Clear = !CoordsAreValid(map, CubeToOffset(x2)) ||
                         GetTerrain(map, CubeToOffset(x2))->type() !=
                                 net::eagle0::shardok::storage::fb::Terrain_::Type_MOUNTAIN;

    // y constant
    const Cube y1 = Cube(cubeOrigin.x + 1, cubeOrigin.y, cubeOrigin.z - 1);
    const bool y1Clear = !CoordsAreValid(map, CubeToOffset(y1)) ||
                         GetTerrain(map, CubeToOffset(y1))->type() !=
                                 net::eagle0::shardok::storage::fb::Terrain_::Type_MOUNTAIN;

    const Cube y2 = Cube(cubeOrigin.x - 1, cubeOrigin.y, cubeOrigin.z + 1);
    const bool y2Clear = !CoordsAreValid(map, CubeToOffset(y2)) ||
                         GetTerrain(map, CubeToOffset(y2))->type() !=
                                 net::eagle0::shardok::storage::fb::Terrain_::Type_MOUNTAIN;

    // z constant
    const Cube z1 = Cube(cubeOrigin.x + 1, cubeOrigin.y - 1, cubeOrigin.z);
    const bool z1Clear = !CoordsAreValid(map, CubeToOffset(z1)) ||
                         GetTerrain(map, CubeToOffset(z1))->type() !=
                                 net::eagle0::shardok::storage::fb::Terrain_::Type_MOUNTAIN;

    const Cube z2 = Cube(cubeOrigin.x - 1, cubeOrigin.y + 1, cubeOrigin.z);
    const bool z2Clear = !CoordsAreValid(map, CubeToOffset(z2)) ||
                         GetTerrain(map, CubeToOffset(z2))->type() !=
                                 net::eagle0::shardok::storage::fb::Terrain_::Type_MOUNTAIN;

    if (x1Clear) {
        MaybeAdd(Cube(cubeOrigin.x, cubeOrigin.y + 2, cubeOrigin.z - 2), unblockedTwoAways, map);
    }
    if (x1Clear && y1Clear) {
        MaybeAdd(
                Cube(cubeOrigin.x + 1, cubeOrigin.y + 1, cubeOrigin.z - 2),
                unblockedTwoAways,
                map);
    }
    if (y1Clear) {
        MaybeAdd(Cube(cubeOrigin.x + 2, cubeOrigin.y, cubeOrigin.z - 2), unblockedTwoAways, map);
    }
    if (y1Clear && z1Clear) {
        MaybeAdd(
                Cube(cubeOrigin.x + 2, cubeOrigin.y - 1, cubeOrigin.z - 1),
                unblockedTwoAways,
                map);
    }
    if (z1Clear) {
        MaybeAdd(Cube(cubeOrigin.x + 2, cubeOrigin.y - 2, cubeOrigin.z), unblockedTwoAways, map);
    }
    if (z1Clear && x2Clear) {
        MaybeAdd(
                Cube(cubeOrigin.x + 1, cubeOrigin.y - 2, cubeOrigin.z + 1),
                unblockedTwoAways,
                map);
    }
    if (x2Clear) {
        MaybeAdd(Cube(cubeOrigin.x, cubeOrigin.y - 2, cubeOrigin.z + 2), unblockedTwoAways, map);
    }
    if (x2Clear && y2Clear) {
        MaybeAdd(
                Cube(cubeOrigin.x - 1, cubeOrigin.y - 1, cubeOrigin.z + 2),
                unblockedTwoAways,
                map);
    }
    if (y2Clear) {
        MaybeAdd(Cube(cubeOrigin.x - 2, cubeOrigin.y, cubeOrigin.z + 2), unblockedTwoAways, map);
    }
    if (y2Clear && z2Clear) {
        MaybeAdd(
                Cube(cubeOrigin.x - 2, cubeOrigin.y + 1, cubeOrigin.z + 1),
                unblockedTwoAways,
                map);
    }
    if (z2Clear) {
        MaybeAdd(Cube(cubeOrigin.x - 2, cubeOrigin.y + 2, cubeOrigin.z), unblockedTwoAways, map);
    }
    if (z2Clear && x1Clear) {
        MaybeAdd(
                Cube(cubeOrigin.x - 1, cubeOrigin.y + 2, cubeOrigin.z - 1),
                unblockedTwoAways,
                map);
    }

    return unblockedTwoAways;
}

[[nodiscard]] auto HexMapUtils::LineIsBlockedByMountains(
        const HexMap *map,
        const Coords &coords1,
        const Coords &coords2) -> bool {
    const auto c1 = OffsetToCube(coords1);
    const auto c2 = OffsetToCube(coords2);

    auto line = CubeLineInclusive(c1, c2);
    return std::any_of(begin(line), end(line), [&map](const Cube &c) {
        const auto betweenCoords = CubeToOffset(c);
        if (CoordsAreValid(map, betweenCoords) &&
            GetTerrain(map, betweenCoords)->type() ==
                    net::eagle0::shardok::storage::fb::Terrain_::Type_MOUNTAIN) {
            return true;
        }

        return false;
    });
}

[[nodiscard]] auto HexMapUtils::TilesWithinDistanceUnblockedByMountains(
        const HexMap *map,
        const Coords &coords,
        const int distance) -> CoordsSet {
    auto tiles = TilesWithinDistance(map, coords, distance);

    tiles.Filter([&map, &coords](const int x, const int y) {
        return !LineIsBlockedByMountains(map, coords, Coords(x, y));
    });

    return tiles;
}

[[nodiscard]] auto TilesWithExactDistance(
        const HexMap *hexMap,
        const Coords &origin,
        const int distance) -> CoordsSet {
    CoordsSet tiles(hexMap);
    const auto cubes = CubesWithExactDistance(OffsetToCube(origin), distance);

    for (const Cube &cube : cubes) {
        const auto &coords = CubeToOffset(cube);
        if (CoordsAreValid(hexMap, coords)) tiles.Add(coords);
    }

    return tiles;
}

void PopulateTilesWithinDistance(
        const HexMap *map,
        const Coords &origin,
        const int distance,
        CoordsSet &toPopulate) {
    const auto cubes = CubesWithinDistance(OffsetToCube(origin), distance);

    for (const auto &cube : cubes) {
        Coords c = CubeToOffset(cube);
        if (CoordsAreValid(map, c)) { toPopulate.Add(c); }
    }
}

[[nodiscard]] auto TilesWithinDistance(const HexMap *map, const Coords &origin, const int distance)
        -> CoordsSet {
    CoordsSet tiles(map);
    PopulateTilesWithinDistance(map, origin, distance, tiles);
    return tiles;
}

auto DirectionsTo(const int columnCount, const Coords &origin, const Coords &destination)
        -> HexMapDirectionsTo {
    const int tripledRowDiff = 3 * (destination.row() - origin.row());
    const int doubledColumnDiff =
            (2 * destination.column() + (destination.row() % 2 == 0 ? 0 : 1)) -
            (2 * origin.column() + (origin.row() % 2 == 0 ? 0 : 1));
    HexMapDirectionsTo directionsTo;
    if (tripledRowDiff <= 0) {
        // generally north or neutral
        if (doubledColumnDiff < tripledRowDiff) {
            directionsTo = HexMapDirectionsTo{WEST, WEST, false};
        } else if (doubledColumnDiff == tripledRowDiff) {
            directionsTo = HexMapDirectionsTo{WEST, NORTHWEST, true};
        } else if (doubledColumnDiff < 0) {
            directionsTo = HexMapDirectionsTo{NORTHWEST, NORTHWEST, false};
        } else if (doubledColumnDiff == 0) {
            if (origin.row() % 2 == 0 && origin.column() == 0) {
                directionsTo = HexMapDirectionsTo{NORTHEAST, NORTHEAST, false};
            } else if ((origin.row() % 2 == 1) && origin.column() == columnCount - 1) {
                directionsTo = HexMapDirectionsTo{NORTHWEST, NORTHWEST, false};
            } else {
                directionsTo = HexMapDirectionsTo{NORTHWEST, NORTHEAST, true};
            }
        } else if (doubledColumnDiff < -tripledRowDiff) {
            directionsTo = HexMapDirectionsTo{NORTHEAST, NORTHEAST, false};
        } else if (doubledColumnDiff == -tripledRowDiff) {
            directionsTo = HexMapDirectionsTo{NORTHEAST, EAST, true};
        } else {
            directionsTo = HexMapDirectionsTo{EAST, EAST, false};
        }
    } else {
        if (doubledColumnDiff < -tripledRowDiff) {
            directionsTo = HexMapDirectionsTo{WEST, WEST, false};
        } else if (doubledColumnDiff == -tripledRowDiff) {
            directionsTo = HexMapDirectionsTo{WEST, SOUTHWEST, true};
        } else if (doubledColumnDiff < 0) {
            directionsTo = HexMapDirectionsTo{SOUTHWEST, SOUTHWEST, false};
        } else if (doubledColumnDiff == 0) {
            if (origin.row() % 2 == 0 && origin.column() == 0) {
                directionsTo = HexMapDirectionsTo{SOUTHEAST, SOUTHEAST, false};
            } else if ((origin.row() % 2 == 1) && origin.column() == columnCount - 1) {
                directionsTo = HexMapDirectionsTo{SOUTHWEST, SOUTHWEST, false};
            } else {
                directionsTo = HexMapDirectionsTo{SOUTHWEST, SOUTHEAST, true};
            }
        } else if (doubledColumnDiff < tripledRowDiff) {
            directionsTo = HexMapDirectionsTo{SOUTHEAST, SOUTHEAST, false};
        } else if (doubledColumnDiff == tripledRowDiff) {
            directionsTo = HexMapDirectionsTo{SOUTHEAST, EAST, true};
        } else {
            directionsTo = HexMapDirectionsTo{EAST, EAST, false};
        }
    }

    return directionsTo;
}

auto MaybeTileInDirectionImpl(const Coords &inCoords, const HexMapDirection orientation) -> Coords {
    const MapIndex row = inCoords.row();
    const MapIndex column = inCoords.column();

    Coords outCoordinates = Coords(-1, -1);

    switch (orientation) {
        case NORTHEAST:
            if (row == 0) return outCoordinates;

            if (row % 2 == 0) {  // even row; northeast has the same column index
                outCoordinates = Coords(row - 1, column);
            } else {  // odd row; column is + 1
                outCoordinates = Coords(row - 1, column + 1);
            }
            break;

        case EAST: outCoordinates = Coords(row, column + 1); break;

        case SOUTHEAST:
            if (row % 2 == 0) {  // Even row; southeast has the same column index
                outCoordinates = Coords(row + 1, column);
            } else {  // Odd row; southeast column is +1
                outCoordinates = Coords(row + 1, column + 1);
            }
            break;

        case SOUTHWEST:
            if (row % 2 == 0) {  // Even row; southwest has column - 1
                if (column == 0) return outCoordinates;
                outCoordinates = Coords(row + 1, column - 1);
            } else {  // Odd row; southwest has the same column index
                outCoordinates = Coords(row + 1, column);
            }
            break;

        case WEST:
            if (column == 0) return outCoordinates;
            outCoordinates = Coords(row, column - 1);
            break;

        case NORTHWEST:
            if (row == 0) return outCoordinates;

            if (row % 2 == 0) {  // Even row; northwest has column - 1
                if (column == 0) return outCoordinates;
                outCoordinates = Coords(row - 1, column - 1);
            } else {  // Odd row; northwest has same column index
                outCoordinates = Coords(row - 1, column);
            }
            break;

        case net::eagle0::shardok::common::HexMapDirection_INT_MIN_SENTINEL_DO_NOT_USE_:
        case net::eagle0::shardok::common::HexMapDirection_INT_MAX_SENTINEL_DO_NOT_USE_:
            throw ShardokInternalErrorException("Unknown direction " + std::to_string(orientation));
            break;
    }

    return outCoordinates;
}

auto MaybeTileInDirection(
        const HexMap *map,
        const Coords &inCoords,
        const HexMapDirection orientation) -> std::optional<Coords> {
    if (!CoordsAreValid(map, inCoords)) return {};

    auto outCoordinates = MaybeTileInDirectionImpl(inCoords, orientation);

    if (CoordsAreValid(map, outCoordinates)) return outCoordinates;
    return {};
}

auto MaybeOccupantInDirection(
        const HexMap *map,
        const Units *units,
        const Coords &coords,
        const HexMapDirection direction) -> const Unit * {
    const auto &maybeTile = MaybeTileInDirection(map, coords, direction);
    if (maybeTile.has_value()) {
        return Occupant(units, maybeTile.value());
    } else {
        return nullptr;
    }
}

auto GetAllCoords(const HexMap *hexMap) -> CoordsSet {
    CoordsSet cs(hexMap);
    for (int row = 0; row < hexMap->row_count(); row++) {
        for (int column = 0; column < hexMap->column_count(); column++) { cs.Add(row, column); }
    }

    return cs;
}

void MutatingSetTileModifier(
        HexMap *hexMap,
        int row,
        int column,
        const TileModifierProto &TileModifierProto);

void MutatingSetTileModifier(
        HexMap *hexMap,
        const Coords &coords,
        const TileModifierProto &TileModifierProto) {
    return MutatingSetTileModifier(hexMap, coords.row(), coords.column(), TileModifierProto);
}

void MutatingSetTileModifier(
        HexMap *hexMap,
        const CoordsProto &coords,
        const TileModifierProto &TileModifierProto) {
    return MutatingSetTileModifier(hexMap, coords.row(), coords.column(), TileModifierProto);
}

void MutatingSetTileModifier(
        HexMapT *hexMap,
        const CoordsProto &coords,
        const TileModifierProto &tileModifierProto) {
    const int row = coords.row();
    const int column = coords.column();

    auto &terr = hexMap->terrain[row * hexMap->column_count + column];

    if (tileModifierProto.has_bridge()) {
        terr.mutable_modifier().mutable_bridge().mutate_present(true);
        terr.mutable_modifier().mutable_bridge().mutate_integrity(
                tileModifierProto.bridge().integrity().value());
    } else {
        terr.mutable_modifier().mutable_bridge().mutate_present(false);
    }

    if (tileModifierProto.has_castle()) {
        terr.mutable_modifier().mutable_castle().mutate_present(true);
        terr.mutable_modifier().mutable_castle().mutate_integrity(
                tileModifierProto.castle().integrity().value());
    } else {
        terr.mutable_modifier().mutable_castle().mutate_present(false);
    }

    terr.mutable_modifier().mutable_fire().mutate_present(tileModifierProto.has_fire());

    if (tileModifierProto.has_ice()) {
        terr.mutable_modifier().mutable_ice().mutate_present(true);
        terr.mutable_modifier().mutable_ice().mutate_integrity(
                tileModifierProto.ice().integrity().value());
    } else {
        terr.mutable_modifier().mutable_ice().mutate_present(false);
    }

    if (tileModifierProto.has_snow()) {
        terr.mutable_modifier().mutable_snow().mutate_present(true);
        terr.mutable_modifier().mutable_snow().mutate_integrity(
                tileModifierProto.snow().integrity().value());
    } else {
        terr.mutable_modifier().mutable_snow().mutate_present(false);
    }
}

void MutatingSetTileModifier(
        HexMap *hexMap,
        const int row,
        const int column,
        const TileModifierProto &TileModifierProto) {
    auto *terr = hexMap->mutable_terrain()->GetMutableObject(row * hexMap->column_count() + column);

    if (TileModifierProto.has_bridge()) {
        terr->mutable_modifier().mutable_bridge().mutate_present(true);
        terr->mutable_modifier().mutable_bridge().mutate_integrity(
                TileModifierProto.bridge().integrity().value());
    } else {
        terr->mutable_modifier().mutable_bridge().mutate_present(false);
    }

    if (TileModifierProto.has_castle()) {
        terr->mutable_modifier().mutable_castle().mutate_present(true);
        terr->mutable_modifier().mutable_castle().mutate_integrity(
                TileModifierProto.castle().integrity().value());
    } else {
        terr->mutable_modifier().mutable_castle().mutate_present(false);
    }

    terr->mutable_modifier().mutable_fire().mutate_present(TileModifierProto.has_fire());

    if (TileModifierProto.has_ice()) {
        terr->mutable_modifier().mutable_ice().mutate_present(true);
        terr->mutable_modifier().mutable_ice().mutate_integrity(
                TileModifierProto.ice().integrity().value());
    } else {
        terr->mutable_modifier().mutable_ice().mutate_present(false);
    }

    if (TileModifierProto.has_snow()) {
        terr->mutable_modifier().mutable_snow().mutate_present(true);
        terr->mutable_modifier().mutable_snow().mutate_integrity(
                TileModifierProto.snow().integrity().value());
    } else {
        terr->mutable_modifier().mutable_snow().mutate_present(false);
    }
}

auto GetCriticalTileLocations(const HexMap *hexMap) -> CoordsSet {
    CoordsSet criticalTileLocations(hexMap);

    for (const Coords &coords : GetAllCoords(hexMap)) {
        const auto terrain = GetTerrain(hexMap, coords);
        if (terrain->type() == net::eagle0::shardok::storage::fb::Terrain_::Type_CITY ||
            terrain->modifier().castle().present()) {
            criticalTileLocations.Add(coords);
        }
    }

    return criticalTileLocations;
}

auto IsBelievedEmpty(const PlayerId playerId, const Coords &coords, const Units *units) -> bool {
    const Unit *occupantOptional = Occupant(units, coords);
    if (occupantOptional) {
        return (occupantOptional->hidden() && occupantOptional->player_id() != playerId);
    } else {
        return true;
    }
}

auto DistanceBetween(const Coords &c1, const Coords &c2) -> int {
    return CubeDistance(OffsetToCube(c1), OffsetToCube(c2));
}

auto KnownAdjacentEnemies(
        const PlayerId playerId,
        const Units *units,
        const HexMap *map,
        const vector<PlayerId> &allyPids,
        const Coords &coords) -> vector<const Unit *> {
    vector<const Unit *> knownAdjacentEnemies{};

    for (const Coords &adjCoords : HexMapUtils::GetAdjacentCoords(map, coords)) {
        const auto *possibleEnemy = KnownEnemyOccupant(playerId, units, allyPids, adjCoords);
        if (possibleEnemy) { knownAdjacentEnemies.push_back(possibleEnemy); }
    }

    return knownAdjacentEnemies;
}

auto KnownEnemyOccupant(
        const PlayerId playerId,
        const Units *units,
        const vector<PlayerId> &allyPids,
        const Coords &coords) -> const Unit * {
    const auto *occupant = Occupant(units, coords);
    if (occupant) {
        if (!occupant->hidden() && occupant->player_id() != playerId &&
            !common::Contains(allyPids, occupant->player_id())) {
            return occupant;
        }
    }

    return nullptr;
}

}  // namespace shardok
