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

#ifndef EAGLE0_SHARDOK_LIBRARY_UTIL_HEX_MAP_UTILS_HPP
#define EAGLE0_SHARDOK_LIBRARY_UTIL_HEX_MAP_UTILS_HPP

#include <algorithm>
#include <ranges>
#include <utility>
#include <vector>

#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/map/HexMapDirection.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/hex_map.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
#include "src/main/protobuf/net/eagle0/shardok/api/unit_view.pb.h"

namespace shardok {
using Coords = net::eagle0::shardok::storage::fb::Coords;
using HexMap = net::eagle0::shardok::storage::fb::HexMap;
using HexMapT = net::eagle0::shardok::storage::fb::HexMapT;
using Terrain = net::eagle0::shardok::storage::fb::Terrain;
using UnitView = net::eagle0::shardok::api::UnitView;
using Unit = net::eagle0::shardok::storage::fb::Unit;
using Units = flatbuffers::Vector<const Unit *>;

struct alignas(16) AdjacentTile {
    HexMapDirection directionTo;
    Coords coords;

    AdjacentTile(const Coords co, const HexMapDirection dir) : directionTo(dir), coords(co) {}

    [[nodiscard]] auto operator==(const AdjacentTile &rhs) const -> bool {
        return coords == rhs.coords && directionTo == rhs.directionTo;
    }
};

class HexMapUtils {
public:
    static constexpr int STANDARD_MAP_ROWS = 12;
    static constexpr int STANDARD_MAP_COLUMNS = 14;

private:
    [[nodiscard]] static auto
    ComputeAdjacentCoords(int rowCount, int columnCount, const Coords &origin) -> CoordsSet;
    [[nodiscard]] static auto ComputeAllStandardAdjacentCoords() -> std::vector<CoordsSet>;

    static const std::vector<CoordsSet> standardCoords;

public:
    [[nodiscard]] static auto GetAdjacentCoords(const HexMap *map, const Coords &origin)
            -> const CoordsSet &;

    [[nodiscard]] static auto GetAdjacentTiles(const HexMap *map, const Coords &coords)
            -> std::vector<AdjacentTile>;

    [[nodiscard]] static auto LineIsBlockedByMountains(
            const HexMap *map,
            const Coords &coords1,
            const Coords &coords2) -> bool;

    [[nodiscard]] static auto TilesWithinDistanceUnblockedByMountains(
            const HexMap *map,
            const Coords &coords,
            int distance) -> CoordsSet;
};

[[nodiscard]] auto AllCastleCoords(const HexMap *hexMap) -> CoordsSet;

[[nodiscard]] auto HasForestAccess(
        const Coords &coords,
        const Units *units,
        const HexMap *hexMap,
        const std::vector<PlayerId> &allyPids,
        PlayerId player) -> bool;

[[nodiscard]] auto GetTerrain(const HexMap *map, const Coords &coords) -> const Terrain *;
[[nodiscard]] auto GetMutableTerrain(HexMap *map, const Coords &coords) -> Terrain *;
// Overload for const HexMap - uses const_cast internally. Safe when the underlying buffer is
// mutable.
[[nodiscard]] auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain *;

[[nodiscard]] auto CoordsAreValid(const HexMap *map, const Coords &coords) -> bool;
[[nodiscard]] auto CoordsAreValid(int rowCount, int columnCount, const Coords &coords) -> bool;

[[nodiscard]] auto GetTilesAcrossWater(const HexMap *map, const Coords &coords) -> CoordsSet;

[[nodiscard]] auto TilesWithExactDistance(const HexMap *map, const Coords &coords, int distance)
        -> CoordsSet;

[[nodiscard]] auto TilesWithinDistance(const HexMap *map, const Coords &coords, int distance)
        -> CoordsSet;

[[nodiscard]] auto TwoAwayTilesUnblockedByMountains(const HexMap *map, const Coords &coords)
        -> CoordsSet;

[[nodiscard]] static inline auto Occupant(const Units *units, const Coords &coords)
        -> const Unit * {
    for (const auto &unit : *units) {
        if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
            unit->location() == coords) {
            return unit;
        }
    }

    return nullptr;
}

[[nodiscard]] static inline auto Occupant(const Units &units, const Coords &coords)
        -> const Unit * {
    for (const auto &unit : units) {
        if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
            unit->location() == coords) {
            return unit;
        }
    }

    return nullptr;
}

[[nodiscard]] static inline auto Occupants(
        const std::vector<const Unit *> &units,
        const int rowCount,
        const int columnCount) -> std::vector<const Unit *> {
    std::vector<const Unit *> positions(rowCount * columnCount);

    for (const auto &unit : units) {
        if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
            const int index = unit->location().row() * columnCount + unit->location().column();
            positions[index] = unit;
        }
    }

    return positions;
}

[[nodiscard]] static inline auto Occupants(
        const flatbuffers::Vector<const Unit *> &units,
        const int rowCount,
        const int columnCount) -> std::vector<const Unit *> {
    std::vector<const Unit *> positions(rowCount * columnCount);

    for (const auto &unit : units) {
        if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT) {
            const int index = unit->location().row() * columnCount + unit->location().column();
            positions[index] = unit;
        }
    }

    return positions;
}

[[nodiscard]] static inline auto Occupant(
        const std::vector<const Unit *> &units,
        const Coords &coords) -> const Unit * {
    for (const auto &unit : units) {
        if (unit->status() == net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT &&
            unit->location() == coords) {
            return unit;
        }
    }

    return nullptr;
}

template<class UnitContainer>
[[nodiscard]] auto Occupant(const UnitContainer &units, const Coords &coords)
        -> std::optional<typename UnitContainer::value_type> {
    for (const auto &unit : units) {
        if (unit.location() == coords) { return unit; }
    }

    return std::nullopt;
}

template<class U>
[[nodiscard]] auto Occupant(const std::map<UnitId, U> &units, const Coords &coords)
        -> std::optional<U> {
    for (const auto &[unitId, unit] : units) {
        if (unit.location() == coords) { return unit; }
    }

    return std::nullopt;
}

template<class U>
[[nodiscard]] auto Occupant(const ::google::protobuf::Map<UnitId, U> &units, const Coords &coords)
        -> std::optional<U> {
    for (const auto &[unitId, unit] : units) {
        if (unit.location() == coords) { return unit; }
    }

    return std::nullopt;
}

template<class U, class UnitContainer>
[[nodiscard]] auto Occupant(const UnitContainer &units, const Coords &coords) -> std::optional<U> {
    for (const auto &unit : units) {
        if (unit.location() == coords) { return unit; }
    }

    return std::nullopt;
}

[[nodiscard]] static inline auto FriendlyOccupant(
        const PlayerId pid,
        const Units *units,
        const std::vector<PlayerId> &allyPids,
        const Coords &coords) -> const Unit * {
    const auto *occupant = Occupant(units, coords);
    if (occupant) {
        if (occupant->player_id() == pid ||
            std::ranges::contains(allyPids, occupant->player_id())) {
            return occupant;
        }
    }

    return nullptr;
}

template<class U, class UnitContainer>
[[nodiscard]] auto FriendlyOccupant(
        PlayerId pid,
        const UnitContainer &units,
        const std::vector<PlayerId> &allyPids,
        const Coords &coords) -> std::optional<U> {
    auto occupantOptional = Occupant(units, coords);
    if (occupantOptional.has_value()) {
        const auto &occupant = *occupantOptional;
        if (occupant.player_id() == pid || std::ranges::contains(allyPids, occupant.player_id())) {
            return occupant;
        }
    }

    return {};
}

[[nodiscard]] auto KnownEnemyOccupant(
        PlayerId playerId,
        const Units *units,
        const std::vector<PlayerId> &allyPids,
        const Coords &coords) -> const Unit *;

template<class UnitContainer>
[[nodiscard]] auto KnownEnemyOccupant(
        PlayerId playerId,
        const UnitContainer &units,
        const std::vector<PlayerId> &allyPids,
        const Coords &coords) -> std::optional<typename UnitContainer::value_type> {
    auto occupantOptional = Occupant(units, coords);
    if (occupantOptional.has_value()) {
        const auto &occupant = *occupantOptional;
        if (!occupant.hidden() && occupant.player_id() != playerId &&
            !std::ranges::contains(allyPids, occupant.player_id())) {
            return occupant;
        }
    }

    return std::nullopt;
}

[[nodiscard]] static inline auto KnownOccupant(
        const PlayerId playerId,
        const Units *units,
        const std::vector<PlayerId> &allyPids,
        const Coords &coords) -> const Unit * {
    const auto *occupant = Occupant(units, coords);
    if (occupant) {
        if (occupant->player_id() == playerId ||
            std::ranges::contains(allyPids, occupant->player_id()) || !occupant->hidden()) {
            return occupant;
        }
    }

    return nullptr;
}

[[nodiscard]] auto KnownAdjacentEnemies(
        PlayerId playerId,
        const Units *units,
        const HexMap *map,
        const std::vector<PlayerId> &allyPids,
        const Coords &coords) -> std::vector<const Unit *>;

[[nodiscard]] auto DirectionsTo(int columnCount, const Coords &origin, const Coords &destination)
        -> HexMapDirectionsTo;

[[nodiscard]] static inline auto DirectionsTo(
        const HexMap *map,
        const Coords &origin,
        const Coords &destination) -> HexMapDirectionsTo {
    return DirectionsTo(map->column_count(), origin, destination);
}

[[nodiscard]] auto MaybeTileInDirection(
        const HexMap *map,
        const Coords &inCoords,
        HexMapDirection orientation) -> std::optional<Coords>;

[[nodiscard]] auto MaybeOccupantInDirection(
        const HexMap *map,
        const Units *units,
        const Coords &coords,
        HexMapDirection direction) -> const Unit *;

[[nodiscard]] auto GetAllCoords(const HexMap *hexMap) -> CoordsSet;

void MutatingSetTileModifier(
        HexMapT *hexMap,
        const CoordsProto &coords,
        const TileModifierProto &TileModifierProto);

void MutatingSetTileModifier(
        HexMap *hexMap,
        const Coords &coords,
        const TileModifierProto &TileModifierProto);

void MutatingSetTileModifier(
        HexMap *hexMap,
        const CoordsProto &coords,
        const TileModifierProto &TileModifierProto);

[[nodiscard]] auto GetCriticalTileLocations(const HexMap *hexMap) -> CoordsSet;

[[nodiscard]] auto IsBelievedEmpty(PlayerId playerId, const Coords &coords, const Units *units)
        -> bool;

}  // namespace shardok

#endif  // EAGLE0_SHARDOK_LIBRARY_UTIL_HEX_MAP_UTILS_HPP
