#include "Readers/EntityCategoryProjector.h"

#include <algorithm>
#include <string_view>
#include <unordered_set>

// Returns true if the player entity itself looks like an admin-invisible entity.
// Currently: entity has no model at all (empty model name = never a real survivor).
static bool IsAdminEntityModel(const std::string& modelName)
{
    return modelName.empty();
}

// -------------------------------------------------------------------------
// EnumerateEntities
// Merges near + far + slow lists, deduplicates by address, and returns a
// stable-sorted flat vector.
// -------------------------------------------------------------------------

std::vector<EntityCategoryProjector::EntityProjection>
EntityCategoryProjector::EnumerateEntities(
    const std::vector<DayZNearEntityEntry>& near,
    const std::vector<DayZFarEntityEntry>&  far,
    const std::vector<DayZSlowEntityEntry>& slow)
{
    std::unordered_set<uint64_t> seen;
    std::vector<EntityProjection> result;

    // Reserve a reasonable upper bound to avoid repeated reallocations.
    result.reserve(near.size() + far.size() + slow.size());

    // Lambda: insert an EntityProjection from a near or far entry (has networkId).
    auto addWithId = [&](uint64_t address, uint32_t networkId,
                          const std::optional<Vector3>& pos,
                          const std::optional<float>& heading,
                          const std::string& entityName,
                          const std::string& typeName,
                          const std::string& configName,
                          const std::string& modelName)
    {
        if (address == 0) return;
        if (!seen.insert(address).second) return; // duplicate

        EntityProjection ep;
        ep.address        = address;
        ep.networkId      = networkId;
        ep.position       = pos;
        ep.headingDegrees = heading;
        ep.entityName     = entityName;
        ep.typeName       = typeName;
        ep.configName     = configName;
        ep.modelName      = modelName;
        result.push_back(std::move(ep));
    };

    for (const auto& e : near) {
        addWithId(e.address, e.networkId, e.position, e.headingDegrees,
                  e.entityName, e.typeName, e.configName, e.modelName);
    }

    for (const auto& e : far) {
        addWithId(e.address, e.networkId, e.position, e.headingDegrees,
                  e.entityName, e.typeName, e.configName, e.modelName);
    }

    // Slow entries have no networkId.
    for (const auto& e : slow) {
        if (e.address == 0) continue;
        if (!seen.insert(e.address).second) continue;

        EntityProjection ep;
        ep.address        = e.address;
        ep.networkId      = 0;
        ep.position       = e.position;
        ep.headingDegrees = e.headingDegrees;
        ep.entityName     = e.entityName;
        ep.typeName       = e.typeName;
        ep.configName     = e.configName;
        ep.modelName      = e.modelName;
        result.push_back(std::move(ep));
    }

    // Stable sort by (entityName, address) for deterministic output.
    std::sort(result.begin(), result.end(),
              [](const EntityProjection& a, const EntityProjection& b) {
                  if (a.entityName != b.entityName) return a.entityName < b.entityName;
                  return a.address < b.address;
              });

    return result;
}

// -------------------------------------------------------------------------
// IsKnownCategoryType
// -------------------------------------------------------------------------

bool EntityCategoryProjector::IsKnownCategoryType(const std::string& typeName) {
    return typeName == kTypePlayer  ||
           typeName == kTypeAnimal  ||
           typeName == kTypeZombie  ||
           typeName == kTypeBoat    ||
           typeName == kTypeCar     ||
           typeName == kTypeHeli    ||
           typeName == kTypePlane;
}

// -------------------------------------------------------------------------
// GetVehicleKind
// -------------------------------------------------------------------------

std::optional<VehicleKind>
EntityCategoryProjector::GetVehicleKind(const std::string& typeName) {
    if (typeName == kTypeCar)  return VehicleKind::Car;
    if (typeName == kTypeBoat) return VehicleKind::Boat;
    if (typeName == kTypeHeli) return VehicleKind::Helicopter;
    if (typeName == kTypePlane)return VehicleKind::Plane;
    return std::nullopt;
}

// -------------------------------------------------------------------------
// BuildPlayers
// -------------------------------------------------------------------------

std::vector<DayZPlayerEntry> EntityCategoryProjector::BuildPlayers(
    const std::vector<DayZNearEntityEntry>& near,
    const std::vector<DayZFarEntityEntry>&  far,
    const std::vector<DayZSlowEntityEntry>& slow,
    const std::unordered_map<uint32_t, std::string>& scoreboardNames,
    std::function<bool(uint64_t)>                        deadResolver,
    std::function<std::string(uint64_t)>                 heldItemResolver,
    std::function<float(uint64_t)>                       healthResolver,
    std::function<bool(uint64_t)>                        adminResolver,
    std::function<std::vector<std::string>(uint64_t)>    wornClothesResolver)
{
    auto all = EnumerateEntities(near, far, slow);

    std::vector<DayZPlayerEntry> result;
    result.reserve(all.size());

    for (const auto& ep : all) {
        if (ep.typeName != kTypePlayer) continue;

        DayZPlayerEntry entry;
        entry.address        = ep.address;
        entry.networkId      = ep.networkId;
        entry.position       = ep.position;
        entry.headingDegrees = ep.headingDegrees;
        entry.typeName       = ep.typeName;
        entry.configName     = ep.configName;
        entry.modelName      = ep.modelName;
        // Entity has no model → almost certainly admin-invisible; otherwise check clothes.
        entry.isAdmin = IsAdminEntityModel(ep.modelName)
                     || (adminResolver && adminResolver(ep.address));

        // Nickname from scoreboard (keyed by networkId).
        if (ep.networkId != 0) {
            auto it = scoreboardNames.find(ep.networkId);
            if (it != scoreboardNames.end()) {
                entry.nickname = it->second;
            }
        }

        // Dead state, held item, health, and worn clothes — resolved via caller-provided callbacks.
        if (deadResolver)         entry.isDead      = deadResolver(ep.address);
        if (heldItemResolver)     entry.itemInHands  = heldItemResolver(ep.address);
        if (healthResolver)       entry.health       = healthResolver(ep.address);
        if (wornClothesResolver)  entry.wornItems    = wornClothesResolver(ep.address);

        result.push_back(std::move(entry));
    }

    return result;
}

// -------------------------------------------------------------------------
// BuildAnimals
// -------------------------------------------------------------------------

std::vector<DayZAnimalEntry> EntityCategoryProjector::BuildAnimals(
    const std::vector<DayZNearEntityEntry>& near,
    const std::vector<DayZFarEntityEntry>&  far,
    const std::vector<DayZSlowEntityEntry>& slow)
{
    auto all = EnumerateEntities(near, far, slow);

    std::vector<DayZAnimalEntry> result;
    result.reserve(all.size());

    for (const auto& ep : all) {
        if (ep.typeName != kTypeAnimal) continue;

        DayZAnimalEntry entry;
        entry.address        = ep.address;
        entry.position       = ep.position;
        entry.headingDegrees = ep.headingDegrees;
        entry.entityName     = ep.entityName;
        entry.typeName       = ep.typeName;
        entry.configName     = ep.configName;
        entry.modelName      = ep.modelName;
        result.push_back(std::move(entry));
    }

    return result;
}

// -------------------------------------------------------------------------
// BuildZombies
// -------------------------------------------------------------------------

std::vector<DayZZombieEntry> EntityCategoryProjector::BuildZombies(
    const std::vector<DayZNearEntityEntry>& near,
    const std::vector<DayZFarEntityEntry>&  far,
    const std::vector<DayZSlowEntityEntry>& slow)
{
    auto all = EnumerateEntities(near, far, slow);

    std::vector<DayZZombieEntry> result;
    result.reserve(all.size());

    for (const auto& ep : all) {
        if (ep.typeName != kTypeZombie) continue;

        DayZZombieEntry entry;
        entry.address        = ep.address;
        entry.position       = ep.position;
        entry.headingDegrees = ep.headingDegrees;
        entry.entityName     = ep.entityName;
        entry.typeName       = ep.typeName;
        entry.configName     = ep.configName;
        entry.modelName      = ep.modelName;
        result.push_back(std::move(entry));
    }

    return result;
}

// -------------------------------------------------------------------------
// BuildCarsAndBoats
// -------------------------------------------------------------------------

std::vector<DayZCarAndBoatEntry> EntityCategoryProjector::BuildCarsAndBoats(
    const std::vector<DayZNearEntityEntry>& near,
    const std::vector<DayZFarEntityEntry>&  far,
    const std::vector<DayZSlowEntityEntry>& slow)
{
    auto all = EnumerateEntities(near, far, slow);

    std::vector<DayZCarAndBoatEntry> result;
    result.reserve(all.size());

    for (const auto& ep : all) {
        auto kind = GetVehicleKind(ep.typeName);
        if (!kind.has_value()) continue;

        DayZCarAndBoatEntry entry;
        entry.address        = ep.address;
        entry.position       = ep.position;
        entry.headingDegrees = ep.headingDegrees;
        entry.entityName     = ep.entityName;
        entry.typeName       = ep.typeName;
        entry.configName     = ep.configName;
        entry.modelName      = ep.modelName;
        entry.kind           = *kind;
        result.push_back(std::move(entry));
    }

    return result;
}

// -------------------------------------------------------------------------
// BuildOtherEntities
// -------------------------------------------------------------------------

std::vector<DayZOtherEntityEntry> EntityCategoryProjector::BuildOtherEntities(
    const std::vector<DayZNearEntityEntry>& near,
    const std::vector<DayZFarEntityEntry>&  far,
    const std::vector<DayZSlowEntityEntry>& slow)
{
    auto all = EnumerateEntities(near, far, slow);

    std::vector<DayZOtherEntityEntry> result;
    result.reserve(all.size());

    for (const auto& ep : all) {
        // Skip entities that belong to an explicitly-handled category.
        if (IsKnownCategoryType(ep.typeName)) continue;

        DayZOtherEntityEntry entry;
        entry.address        = ep.address;
        entry.position       = ep.position;
        entry.headingDegrees = ep.headingDegrees;
        entry.entityName     = ep.entityName;
        entry.typeName       = ep.typeName;
        entry.configName     = ep.configName;
        entry.modelName      = ep.modelName;
        result.push_back(std::move(entry));
    }

    return result;
}
