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

#ifndef HexMapUtils_hpp
#define HexMapUtils_hpp

#include <utility>
#include <vector>

#include "src/main/cpp/net/eagle0/common/ContainerUtils.hpp"
#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 *>;
using std::vector;

struct AdjacentTile {
    HexMapDirection directionTo;
    Coords coords;

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

    auto operator==(const AdjacentTile &rhs) const -> bool {
        return coords == rhs.coords && directionTo == rhs.directionTo;
    }
} __attribute__((aligned(16)));

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>;

    const static 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)
            -> 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;

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

auto GetTerrain(const HexMap *map, const Coords &coords) -> const Terrain *;
auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain *;

auto CoordsAreValid(const HexMap *map, const Coords &coords) -> bool;
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;

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;
}

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;
}

static inline auto Occupants(
        const vector<const Unit *> &units,
        const int rowCount,
        const int columnCount) -> vector<const Unit *> {
    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;
}

static inline auto Occupants(
        const flatbuffers::Vector<const Unit *> &units,
        const int rowCount,
        const int columnCount) -> vector<const Unit *> {
    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;
}

static inline auto Occupant(const 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>
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::optional<typename UnitContainer::value_type>();
}

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

    return std::optional<U>();
}

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

    return std::optional<U>();
}

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

    return std::optional<U>();
}

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

    return nullptr;
}

template<class U, class UnitContainer>
auto FriendlyOccupant(
        PlayerId pid,
        const UnitContainer &units,
        const 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 || common::Contains(allyPids, occupant.player_id())) {
            return occupant;
        }
    }

    return {};
}

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

template<class UnitContainer>
auto KnownEnemyOccupant(
        PlayerId playerId,
        const UnitContainer &units,
        const 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 &&
            !common::Contains(allyPids, occupant.player_id())) {
            return occupant;
        }
    }

    return std::optional<typename UnitContainer::value_type>();
}

[[nodiscard]] static inline auto KnownOccupant(
        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->player_id() == playerId ||
            common::Contains(allyPids, occupant->player_id()) || !occupant->hidden()) {
            return occupant;
        }
    }

    return nullptr;
}

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

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

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

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

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

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);

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

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

}  // namespace shardok

#endif /* HexMapUtils_hpp */
