using System.Collections.Concurrent;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.Interop;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using LightlessSync.Services.Mediator;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using DalamudObjectKind = Dalamud.Game.ClientState.Objects.Enums.ObjectKind;
using LightlessObjectKind = LightlessSync.API.Data.Enum.ObjectKind;

namespace LightlessSync.Services.ActorTracking;

public sealed class ActorObjectService : IHostedService, IDisposable
{
    public readonly record struct ActorDescriptor(
        string Name,
        string HashedContentId,
        nint Address,
        ushort ObjectIndex,
        bool IsLocalPlayer,
        bool IsInGpose,
        DalamudObjectKind ObjectKind,
        LightlessObjectKind? OwnedKind,
        uint OwnerEntityId);

    private readonly ILogger<ActorObjectService> _logger;
    private readonly IFramework _framework;
    private readonly IGameInteropProvider _interop;
    private readonly IObjectTable _objectTable;
    private readonly LightlessMediator _mediator;

    private readonly ConcurrentDictionary<nint, ActorDescriptor> _activePlayers = new();
    private readonly ConcurrentDictionary<string, ActorDescriptor> _actorsByHash = new(StringComparer.Ordinal);
    private readonly ConcurrentDictionary<string, ConcurrentDictionary<nint, ActorDescriptor>> _actorsByName = new(StringComparer.Ordinal);
    private readonly OwnedObjectTracker _ownedTracker = new();
    private ActorSnapshot _snapshot = ActorSnapshot.Empty;

    private Hook<Character.Delegates.OnInitialize>? _onInitializeHook;
    private Hook<Character.Delegates.Terminate>? _onTerminateHook;
    private Hook<Character.Delegates.Dtor>? _onDestructorHook;
    private Hook<Companion.Delegates.OnInitialize>? _onCompanionInitializeHook;
    private Hook<Companion.Delegates.Terminate>? _onCompanionTerminateHook;

    private bool _hooksActive;
    private static readonly TimeSpan SnapshotRefreshInterval = TimeSpan.FromSeconds(1);
    private DateTime _nextRefreshAllowed = DateTime.MinValue;

    public ActorObjectService(
        ILogger<ActorObjectService> logger,
        IFramework framework,
        IGameInteropProvider interop,
        IObjectTable objectTable,
        IClientState clientState,
        LightlessMediator mediator)
    {
        _logger = logger;
        _framework = framework;
        _interop = interop;
        _objectTable = objectTable;
        _mediator = mediator;
    }

    private ActorSnapshot Snapshot => Volatile.Read(ref _snapshot);

    public IReadOnlyList<nint> PlayerAddresses => Snapshot.PlayerAddresses;

    public IEnumerable<ActorDescriptor> PlayerDescriptors => _activePlayers.Values;
    public IReadOnlyList<ActorDescriptor> PlayerCharacterDescriptors => Snapshot.PlayerDescriptors;

    public bool TryGetActorByHash(string hash, out ActorDescriptor descriptor) => _actorsByHash.TryGetValue(hash, out descriptor);
    public bool TryGetValidatedActorByHash(string hash, out ActorDescriptor descriptor)
    {
        descriptor = default;
        if (!_actorsByHash.TryGetValue(hash, out var candidate))
            return false;

        if (!ValidateDescriptorThreadSafe(candidate))
            return false;

        descriptor = candidate;
        return true;
    }

    public bool TryGetPlayerByName(string name, out ActorDescriptor descriptor)
    {
        descriptor = default;

        if (!_actorsByName.TryGetValue(name, out var entries) || entries.IsEmpty)
            return false;

        ActorDescriptor? best = null;
        foreach (var candidate in entries.Values)
        {
            if (!ValidateDescriptorThreadSafe(candidate))
                continue;

            if (best is null || IsBetterNameMatch(candidate, best.Value))
            {
                best = candidate;
            }
        }

        if (best is { } selected)
        {
            descriptor = selected;
            return true;
        }

        return false;
    }
    public bool HooksActive => _hooksActive;
    public IReadOnlyList<nint> RenderedPlayerAddresses => Snapshot.OwnedObjects.RenderedPlayers;
    public IReadOnlyList<nint> RenderedCompanionAddresses => Snapshot.OwnedObjects.RenderedCompanions;
    public IReadOnlyList<nint> OwnedObjectAddresses => Snapshot.OwnedObjects.OwnedAddresses;
    public IReadOnlyDictionary<nint, LightlessObjectKind> OwnedObjects => Snapshot.OwnedObjects.Map;
    public nint LocalPlayerAddress => Snapshot.OwnedObjects.LocalPlayer;
    public nint LocalPetAddress => Snapshot.OwnedObjects.LocalPet;
    public nint LocalMinionOrMountAddress => Snapshot.OwnedObjects.LocalMinionOrMount;
    public nint LocalCompanionAddress => Snapshot.OwnedObjects.LocalCompanion;

    public bool TryGetOwnedKind(nint address, out LightlessObjectKind kind)
        => OwnedObjects.TryGetValue(address, out kind);

    public bool TryGetOwnedActor(LightlessObjectKind kind, out ActorDescriptor descriptor)
    {
        descriptor = default;
        if (!TryGetOwnedObject(kind, out var address))
            return false;
        return TryGetDescriptor(address, out descriptor);
    }

    public bool TryGetOwnedObjectByIndex(ushort objectIndex, out LightlessObjectKind ownedKind)
    {
        ownedKind = default;
        var ownedSnapshot = OwnedObjects;
        foreach (var (address, kind) in ownedSnapshot)
        {
            if (!TryGetDescriptor(address, out var descriptor))
                continue;

            if (descriptor.ObjectIndex == objectIndex)
            {
                ownedKind = kind;
                return true;
            }
        }

        return false;
    }

    public bool TryGetOwnedObject(LightlessObjectKind kind, out nint address)
    {
        var ownedSnapshot = Snapshot.OwnedObjects;
        address = kind switch
        {
            LightlessObjectKind.Player => ownedSnapshot.LocalPlayer,
            LightlessObjectKind.Pet => ownedSnapshot.LocalPet,
            LightlessObjectKind.MinionOrMount => ownedSnapshot.LocalMinionOrMount,
            LightlessObjectKind.Companion => ownedSnapshot.LocalCompanion,
            _ => nint.Zero
        };

        return address != nint.Zero;
    }

    public bool TryGetOwnedActor(uint ownerEntityId, LightlessObjectKind? kindFilter, out ActorDescriptor descriptor)
    {
        descriptor = default;
        foreach (var candidate in _activePlayers.Values)
        {
            if (candidate.OwnerEntityId != ownerEntityId)
                continue;

            if (kindFilter.HasValue && candidate.OwnedKind != kindFilter)
                continue;

            descriptor = candidate;
            return true;
        }

        return false;
    }

    public bool TryGetPlayerAddressByHash(string hash, out nint address)
    {
        if (TryGetValidatedActorByHash(hash, out var descriptor) && descriptor.Address != nint.Zero)
        {
            address = descriptor.Address;
            return true;
        }

        address = nint.Zero;
        return false;
    }

    public async Task WaitForFullyLoadedAsync(nint address, CancellationToken cancellationToken = default)
    {
        if (address == nint.Zero)
            throw new ArgumentException("Address cannot be zero.", nameof(address));

        while (true)
        {
            cancellationToken.ThrowIfCancellationRequested();

            var isLoaded = await _framework.RunOnFrameworkThread(() => IsObjectFullyLoaded(address)).ConfigureAwait(false);
            if (isLoaded)
                return;

            await Task.Delay(100, cancellationToken).ConfigureAwait(false);
        }
    }

    private bool ValidateDescriptorThreadSafe(ActorDescriptor descriptor)
    {
        if (_framework.IsInFrameworkUpdateThread)
            return ValidateDescriptorInternal(descriptor);

        return _framework.RunOnFrameworkThread(() => ValidateDescriptorInternal(descriptor)).GetAwaiter().GetResult();
    }

    private bool ValidateDescriptorInternal(ActorDescriptor descriptor)
    {
        if (descriptor.Address == nint.Zero)
            return false;

        if (descriptor.ObjectKind == DalamudObjectKind.Player &&
            !string.IsNullOrEmpty(descriptor.HashedContentId))
        {
            if (!TryGetLivePlayerHash(descriptor, out var liveHash))
            {
                UntrackGameObject(descriptor.Address);
                return false;
            }

            if (!string.Equals(liveHash, descriptor.HashedContentId, StringComparison.Ordinal))
            {
                UntrackGameObject(descriptor.Address);
                return false;
            }
        }

        return true;
    }

    private bool TryGetLivePlayerHash(ActorDescriptor descriptor, out string liveHash)
    {
        liveHash = string.Empty;

        if (_objectTable.CreateObjectReference(descriptor.Address) is not IPlayerCharacter playerCharacter)
            return false;

        return DalamudUtilService.TryGetHashedCID(playerCharacter, out liveHash);
    }

    public void RefreshTrackedActors(bool force = false)
    {
        var now = DateTime.UtcNow;
        if (!force && _hooksActive)
        {
            if (now < _nextRefreshAllowed)
                return;

            _nextRefreshAllowed = now + SnapshotRefreshInterval;
        }

        if (_framework.IsInFrameworkUpdateThread)
        {
            RefreshTrackedActorsInternal();
        }
        else
        {
            _ = _framework.RunOnFrameworkThread(RefreshTrackedActorsInternal);
        }
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        try
        {
            InitializeHooks();
            var warmupTask = WarmupExistingActors();
            return warmupTask;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to initialize ActorObjectService hooks, falling back to empty cache.");
            DisposeHooks();
            return Task.CompletedTask;
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        DisposeHooks();
        _activePlayers.Clear();
        _actorsByHash.Clear();
        _actorsByName.Clear();
        _ownedTracker.Reset();
        Volatile.Write(ref _snapshot, ActorSnapshot.Empty);
        return Task.CompletedTask;
    }

    private unsafe void InitializeHooks()
    {
        if (_hooksActive)
            return;

        _onInitializeHook = _interop.HookFromAddress<Character.Delegates.OnInitialize>(
            (nint)Character.StaticVirtualTablePointer->OnInitialize,
            OnCharacterInitialized);

        _onTerminateHook = _interop.HookFromAddress<Character.Delegates.Terminate>(
            (nint)Character.StaticVirtualTablePointer->Terminate,
            OnCharacterTerminated);

        _onDestructorHook = _interop.HookFromAddress<Character.Delegates.Dtor>(
            (nint)Character.StaticVirtualTablePointer->Dtor,
            OnCharacterDisposed);

        _onCompanionInitializeHook = _interop.HookFromAddress<Companion.Delegates.OnInitialize>(
            (nint)Companion.StaticVirtualTablePointer->OnInitialize,
            OnCompanionInitialized);

        _onCompanionTerminateHook = _interop.HookFromAddress<Companion.Delegates.Terminate>(
            (nint)Companion.StaticVirtualTablePointer->Terminate,
            OnCompanionTerminated);

        _onInitializeHook.Enable();
        _onTerminateHook.Enable();
        _onDestructorHook.Enable();
        _onCompanionInitializeHook.Enable();
        _onCompanionTerminateHook.Enable();

        _hooksActive = true;
        _logger.LogDebug("ActorObjectService hooks enabled.");
    }

    private Task WarmupExistingActors()
    {
        return _framework.RunOnFrameworkThread(() =>
        {
            RefreshTrackedActorsInternal();
            _nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
        });
    }

    private unsafe void OnCharacterInitialized(Character* chara)
    {
        try
        {
            _onInitializeHook!.Original(chara);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error invoking original character initialize.");
        }

        QueueFrameworkUpdate(() => TrackGameObject((GameObject*)chara));
    }

    private unsafe void OnCharacterTerminated(Character* chara)
    {
        var address = (nint)chara;
        QueueFrameworkUpdate(() => UntrackGameObject(address));
        try
        {
            _onTerminateHook!.Original(chara);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error invoking original character terminate.");
        }
    }

    private unsafe GameObject* OnCharacterDisposed(Character* chara, byte freeMemory)
    {
        var address = (nint)chara;
        QueueFrameworkUpdate(() => UntrackGameObject(address));
        try
        {
            return _onDestructorHook!.Original(chara, freeMemory);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error invoking original character destructor.");
            return null;
        }
    }

    private unsafe void TrackGameObject(GameObject* gameObject)
    {
        if (gameObject == null)
            return;

        var objectKind = (DalamudObjectKind)gameObject->ObjectKind;

        if (!IsSupportedObjectKind(objectKind))
            return;

        if (BuildDescriptor(gameObject, objectKind) is not { } descriptor)
            return;

        if (descriptor.ObjectKind != DalamudObjectKind.Player && descriptor.OwnedKind is null)
            return;

        if (_activePlayers.TryGetValue(descriptor.Address, out var existing))
        {
            RemoveDescriptor(existing);
        }

        AddDescriptor(descriptor);

        if (_logger.IsEnabled(LogLevel.Debug))
        {
            _logger.LogDebug("Actor tracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind} local={Local} gpose={Gpose}",
                descriptor.Name,
                descriptor.Address,
                descriptor.ObjectIndex,
                descriptor.OwnedKind?.ToString() ?? "<none>",
                descriptor.IsLocalPlayer,
                descriptor.IsInGpose);
        }

        _mediator.Publish(new ActorTrackedMessage(descriptor));
    }

    private unsafe ActorDescriptor? BuildDescriptor(GameObject* gameObject, DalamudObjectKind objectKind)
    {
        if (gameObject == null)
            return null;

        var address = (nint)gameObject;
        string name = string.Empty;
        ushort objectIndex = gameObject->ObjectIndex;
        bool isInGpose = objectIndex >= 200;
        bool isLocal = _objectTable.LocalPlayer?.Address == address;
        string hashedCid = string.Empty;

        IPlayerCharacter? resolvedPlayer = null;
        if (_objectTable.CreateObjectReference(address) is IPlayerCharacter playerCharacter)
        {
            resolvedPlayer = playerCharacter;
            name = playerCharacter.Name.TextValue ?? string.Empty;
            objectIndex = playerCharacter.ObjectIndex;
            isInGpose = objectIndex >= 200;
            isLocal = playerCharacter.Address == _objectTable.LocalPlayer?.Address;
        }
        else
        {
            name = gameObject->NameString ?? string.Empty;
        }

        if (objectKind == DalamudObjectKind.Player)
        {
            if (resolvedPlayer == null || !DalamudUtilService.TryGetHashedCID(resolvedPlayer, out hashedCid))
            {
                hashedCid = DalamudUtilService.GetHashedCIDFromPlayerPointer(address);
            }
        }

        var (ownedKind, ownerEntityId) = DetermineOwnedKind(gameObject, objectKind, isLocal);

        return new ActorDescriptor(name, hashedCid, address, objectIndex, isLocal, isInGpose, objectKind, ownedKind, ownerEntityId);
    }

    private unsafe (LightlessObjectKind? OwnedKind, uint OwnerEntityId) DetermineOwnedKind(GameObject* gameObject, DalamudObjectKind objectKind, bool isLocalPlayer)
    {
        if (gameObject == null)
            return (null, 0);

        if (objectKind == DalamudObjectKind.Player)
        {
            var entityId = ((Character*)gameObject)->EntityId;
            return (isLocalPlayer ? LightlessObjectKind.Player : null, entityId);
        }

        if (isLocalPlayer)
        {
            var entityId = ((Character*)gameObject)->EntityId;
            return (LightlessObjectKind.Player, entityId);
        }

        if (_objectTable.LocalPlayer is not { } localPlayer)
            return (null, 0);

        var ownerId = gameObject->OwnerId;
        if (ownerId == 0)
        {
            var character = (Character*)gameObject;
            if (character != null)
            {
                ownerId = character->CompanionOwnerId;
                if (ownerId == 0)
                {
                    var parent = character->GetParentCharacter();
                    if (parent != null)
                    {
                        ownerId = parent->EntityId;
                    }
                }
            }
        }

        if (ownerId == 0 || ownerId != localPlayer.EntityId)
            return (null, ownerId);

        var ownedKind = objectKind switch
        {
            DalamudObjectKind.MountType => LightlessObjectKind.MinionOrMount,
            DalamudObjectKind.Companion => LightlessObjectKind.MinionOrMount,
            DalamudObjectKind.BattleNpc => gameObject->BattleNpcSubKind switch
            {
                BattleNpcSubKind.Buddy => LightlessObjectKind.Companion,
                BattleNpcSubKind.Pet => LightlessObjectKind.Pet,
                _ => (LightlessObjectKind?)null,
            },
            _ => (LightlessObjectKind?)null,
        };

        return (ownedKind, ownerId);
    }

    private void UntrackGameObject(nint address)
    {
        if (address == nint.Zero)
            return;

        if (_activePlayers.TryRemove(address, out var descriptor))
        {
            RemoveDescriptor(descriptor);
            if (_logger.IsEnabled(LogLevel.Debug))
            {
                _logger.LogDebug("Actor untracked: {Name} addr={Address:X} idx={Index} owned={OwnedKind}",
                    descriptor.Name,
                    descriptor.Address,
                    descriptor.ObjectIndex,
                    descriptor.OwnedKind?.ToString() ?? "<none>");
            }

            _mediator.Publish(new ActorUntrackedMessage(descriptor));
        }
    }

    private unsafe void RefreshTrackedActorsInternal()
    {
        var addresses = EnumerateActiveCharacterAddresses();
        HashSet<nint> seen = new(addresses.Count);

        foreach (var address in addresses)
        {
            if (address == nint.Zero)
                continue;

            if (!seen.Add(address))
                continue;

            if (_activePlayers.ContainsKey(address))
                continue;

            TrackGameObject((GameObject*)address);
        }

        var stale = _activePlayers.Keys.Where(addr => !seen.Contains(addr)).ToList();
        foreach (var staleAddress in stale)
        {
            UntrackGameObject(staleAddress);
        }

        if (_hooksActive)
        {
            _nextRefreshAllowed = DateTime.UtcNow + SnapshotRefreshInterval;
        }
    }

    private void IndexDescriptor(ActorDescriptor descriptor)
    {
        if (!string.IsNullOrEmpty(descriptor.HashedContentId))
        {
            _actorsByHash[descriptor.HashedContentId] = descriptor;
        }

        if (descriptor.ObjectKind == DalamudObjectKind.Player && !string.IsNullOrEmpty(descriptor.Name))
        {
            var bucket = _actorsByName.GetOrAdd(descriptor.Name, _ => new ConcurrentDictionary<nint, ActorDescriptor>());
            bucket[descriptor.Address] = descriptor;
        }
    }

    private static bool IsBetterNameMatch(ActorDescriptor candidate, ActorDescriptor current)
    {
        if (!candidate.IsInGpose && current.IsInGpose)
            return true;
        if (candidate.IsInGpose && !current.IsInGpose)
            return false;

        return candidate.ObjectIndex < current.ObjectIndex;
    }

    private bool TryGetDescriptor(nint address, out ActorDescriptor descriptor)
        => _activePlayers.TryGetValue(address, out descriptor);

    private unsafe void OnCompanionInitialized(Companion* companion)
    {
        try
        {
            _onCompanionInitializeHook!.Original(companion);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error invoking original companion initialize.");
        }

        QueueFrameworkUpdate(() => TrackGameObject((GameObject*)companion));
    }

    private unsafe void OnCompanionTerminated(Companion* companion)
    {
        var address = (nint)companion;
        QueueFrameworkUpdate(() => UntrackGameObject(address));
        try
        {
            _onCompanionTerminateHook!.Original(companion);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error invoking original companion terminate.");
        }
    }

    private void RemoveDescriptorFromIndexes(ActorDescriptor descriptor)
    {
        if (!string.IsNullOrEmpty(descriptor.HashedContentId))
        {
            _actorsByHash.TryRemove(descriptor.HashedContentId, out _);
        }

        if (descriptor.ObjectKind == DalamudObjectKind.Player
            && !string.IsNullOrEmpty(descriptor.Name)
            && _actorsByName.TryGetValue(descriptor.Name, out var bucket))
        {
            bucket.TryRemove(descriptor.Address, out _);
            if (bucket.IsEmpty)
            {
                _actorsByName.TryRemove(descriptor.Name, out _);
            }
        }
    }

    private void AddDescriptor(ActorDescriptor descriptor)
    {
        _activePlayers[descriptor.Address] = descriptor;
        IndexDescriptor(descriptor);
        _ownedTracker.OnDescriptorAdded(descriptor);
        PublishSnapshot();
    }

    private void RemoveDescriptor(ActorDescriptor descriptor)
    {
        RemoveDescriptorFromIndexes(descriptor);
        _ownedTracker.OnDescriptorRemoved(descriptor);
        PublishSnapshot();
    }

    private void PublishSnapshot()
    {
        var playerDescriptors = _activePlayers.Values
            .Where(descriptor => descriptor.ObjectKind == DalamudObjectKind.Player)
            .ToArray();
        var playerAddresses = new nint[playerDescriptors.Length];
        for (var i = 0; i < playerDescriptors.Length; i++)
            playerAddresses[i] = playerDescriptors[i].Address;

        var ownedSnapshot = _ownedTracker.CreateSnapshot();
        var nextGeneration = Snapshot.Generation + 1;
        var snapshot = new ActorSnapshot(playerDescriptors, playerAddresses, ownedSnapshot, nextGeneration);
        Volatile.Write(ref _snapshot, snapshot);
    }

    private void QueueFrameworkUpdate(Action action)
    {
        if (action == null)
            return;

        if (_framework.IsInFrameworkUpdateThread)
        {
            action();
            return;
        }

        _ = _framework.RunOnFrameworkThread(action);
    }

    private void DisposeHooks()
    {
        var hadHooks = _hooksActive
            || _onInitializeHook is not null
            || _onTerminateHook is not null
            || _onDestructorHook is not null
            || _onCompanionInitializeHook is not null
            || _onCompanionTerminateHook is not null;

        _onInitializeHook?.Disable();
        _onTerminateHook?.Disable();
        _onDestructorHook?.Disable();
        _onCompanionInitializeHook?.Disable();
        _onCompanionTerminateHook?.Disable();

        _onInitializeHook?.Dispose();
        _onTerminateHook?.Dispose();
        _onDestructorHook?.Dispose();
        _onCompanionInitializeHook?.Dispose();
        _onCompanionTerminateHook?.Dispose();

        _onInitializeHook = null;
        _onTerminateHook = null;
        _onDestructorHook = null;
        _onCompanionInitializeHook = null;
        _onCompanionTerminateHook = null;

        _hooksActive = false;

        if (hadHooks)
        {
            _logger.LogDebug("ActorObjectService hooks disabled.");
        }
    }

    public void Dispose()
    {
        DisposeHooks();
        GC.SuppressFinalize(this);
    }

    private static bool IsSupportedObjectKind(DalamudObjectKind objectKind) =>
        objectKind is DalamudObjectKind.Player
            or DalamudObjectKind.BattleNpc
            or DalamudObjectKind.Companion
            or DalamudObjectKind.MountType;

    private static unsafe List<nint> EnumerateActiveCharacterAddresses()
    {
        var results = new List<nint>(64);
        var manager = GameObjectManager.Instance();
        if (manager == null)
            return results;

        const int objectLimit = 200;

        unsafe
        {
            for (var i = 0; i < objectLimit; i++)
            {
                Pointer<GameObject> objPtr = manager->Objects.IndexSorted[i];
                var obj = objPtr.Value;
                if (obj == null)
                    continue;

                var objectKind = (DalamudObjectKind)obj->ObjectKind;
                if (!IsSupportedObjectKind(objectKind))
                    continue;

                results.Add((nint)obj);
            }
        }

        return results;
    }

    private static unsafe bool IsObjectFullyLoaded(nint address)
    {
        if (address == nint.Zero)
            return false;

        var gameObject = (GameObject*)address;
        if (gameObject == null)
            return false;

        var drawObject = gameObject->DrawObject;
        if (drawObject == null)
            return false;

        if ((gameObject->RenderFlags & VisibilityFlags.Nameplate) != VisibilityFlags.None)
            return false;

        var characterBase = (CharacterBase*)drawObject;
        if (characterBase == null)
            return false;

        if (characterBase->HasModelInSlotLoaded != 0)
            return false;

        if (characterBase->HasModelFilesInSlotLoaded != 0)
            return false;

        return true;
    }

    private sealed class OwnedObjectTracker
    {
        private readonly HashSet<nint> _renderedPlayers = new();
        private readonly HashSet<nint> _renderedCompanions = new();
        private readonly Dictionary<nint, LightlessObjectKind> _ownedObjects = new();
        private nint _localPlayerAddress = nint.Zero;
        private nint _localPetAddress = nint.Zero;
        private nint _localMinionMountAddress = nint.Zero;
        private nint _localCompanionAddress = nint.Zero;

        public void OnDescriptorAdded(ActorDescriptor descriptor)
        {
            if (descriptor.ObjectKind == DalamudObjectKind.Player)
            {
                _renderedPlayers.Add(descriptor.Address);
                if (descriptor.IsLocalPlayer)
                    _localPlayerAddress = descriptor.Address;
            }
            else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
            {
                _renderedCompanions.Add(descriptor.Address);
            }

            if (descriptor.OwnedKind is { } ownedKind)
            {
                _ownedObjects[descriptor.Address] = ownedKind;
                switch (ownedKind)
                {
                    case LightlessObjectKind.Player:
                        _localPlayerAddress = descriptor.Address;
                        break;
                    case LightlessObjectKind.Pet:
                        _localPetAddress = descriptor.Address;
                        break;
                    case LightlessObjectKind.MinionOrMount:
                        _localMinionMountAddress = descriptor.Address;
                        break;
                    case LightlessObjectKind.Companion:
                        _localCompanionAddress = descriptor.Address;
                        break;
                }
            }
        }

        public void OnDescriptorRemoved(ActorDescriptor descriptor)
        {
            if (descriptor.ObjectKind == DalamudObjectKind.Player)
            {
                _renderedPlayers.Remove(descriptor.Address);
                if (descriptor.IsLocalPlayer && _localPlayerAddress == descriptor.Address)
                    _localPlayerAddress = nint.Zero;
            }
            else if (descriptor.ObjectKind == DalamudObjectKind.Companion)
            {
                _renderedCompanions.Remove(descriptor.Address);
                if (_localCompanionAddress == descriptor.Address)
                    _localCompanionAddress = nint.Zero;
            }

            if (descriptor.OwnedKind is { } ownedKind)
            {
                _ownedObjects.Remove(descriptor.Address);
                switch (ownedKind)
                {
                    case LightlessObjectKind.Player when _localPlayerAddress == descriptor.Address:
                        _localPlayerAddress = nint.Zero;
                        break;
                    case LightlessObjectKind.Pet when _localPetAddress == descriptor.Address:
                        _localPetAddress = nint.Zero;
                        break;
                    case LightlessObjectKind.MinionOrMount when _localMinionMountAddress == descriptor.Address:
                        _localMinionMountAddress = nint.Zero;
                        break;
                    case LightlessObjectKind.Companion when _localCompanionAddress == descriptor.Address:
                        _localCompanionAddress = nint.Zero;
                        break;
                }
            }
        }

        public OwnedObjectSnapshot CreateSnapshot()
            => new(
                _renderedPlayers.ToArray(),
                _renderedCompanions.ToArray(),
                _ownedObjects.Keys.ToArray(),
                new Dictionary<nint, LightlessObjectKind>(_ownedObjects),
                _localPlayerAddress,
                _localPetAddress,
                _localMinionMountAddress,
                _localCompanionAddress);

        public void Reset()
        {
            _renderedPlayers.Clear();
            _renderedCompanions.Clear();
            _ownedObjects.Clear();
            _localPlayerAddress = nint.Zero;
            _localPetAddress = nint.Zero;
            _localMinionMountAddress = nint.Zero;
            _localCompanionAddress = nint.Zero;
        }
    }

    private sealed record OwnedObjectSnapshot(
        IReadOnlyList<nint> RenderedPlayers,
        IReadOnlyList<nint> RenderedCompanions,
        IReadOnlyList<nint> OwnedAddresses,
        IReadOnlyDictionary<nint, LightlessObjectKind> Map,
        nint LocalPlayer,
        nint LocalPet,
        nint LocalMinionOrMount,
        nint LocalCompanion)
    {
        public static OwnedObjectSnapshot Empty { get; } = new(
            Array.Empty<nint>(),
            Array.Empty<nint>(),
            Array.Empty<nint>(),
            new Dictionary<nint, LightlessObjectKind>(),
            nint.Zero,
            nint.Zero,
            nint.Zero,
            nint.Zero);
    }

    private sealed record ActorSnapshot(
        IReadOnlyList<ActorDescriptor> PlayerDescriptors,
        IReadOnlyList<nint> PlayerAddresses,
        OwnedObjectSnapshot OwnedObjects,
        int Generation)
    {
        public static ActorSnapshot Empty { get; } = new(
            Array.Empty<ActorDescriptor>(),
            Array.Empty<nint>(),
            OwnedObjectSnapshot.Empty,
            0);
    }
}
