using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator;
using LightlessSync.UI;
using LightlessSync.UI.Services;
using LightlessSync.Utils;
using LightlessSync.UtilsEnum.Enum;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Collections.Immutable;
using Task = System.Threading.Tasks.Task;

namespace LightlessSync.Services.LightFinder;

/// <summary>
/// Native nameplate handler that injects LightFinder labels via the signature hook path.
/// </summary>
public unsafe class LightFinderNativePlateHandler : DisposableMediatorSubscriberBase, IHostedService
{
    private const uint NameplateNodeIdBase = 0x7D99D500;
    private const string DefaultLabelText = "LightFinder";

    private readonly ILogger<LightFinderNativePlateHandler> _logger;
    private readonly IClientState _clientState;
    private readonly IObjectTable _objectTable;
    private readonly LightlessConfigService _configService;
    private readonly PairUiService _pairUiService;
    private readonly NameplateUpdateHookService _nameplateUpdateHookService;

    private readonly int[] _cachedNameplateTextWidths = new int[AddonNamePlate.NumNamePlateObjects];
    private readonly int[] _cachedNameplateTextHeights = new int[AddonNamePlate.NumNamePlateObjects];
    private readonly int[] _cachedNameplateContainerHeights = new int[AddonNamePlate.NumNamePlateObjects];
    private readonly int[] _cachedNameplateTextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
    private readonly string?[] _lastLabelByIndex = new string?[AddonNamePlate.NumNamePlateObjects];

    private ImmutableHashSet<string> _activeBroadcastingCids = [];
    private LightfinderLabelRenderer _lastRenderer;
    private uint _lastSignatureUpdateFrame;
    private bool _isUpdating;
    private string _lastLabelContent = DefaultLabelText;

    public LightFinderNativePlateHandler(
        ILogger<LightFinderNativePlateHandler> logger,
        IClientState clientState,
        LightlessConfigService configService,
        LightlessMediator mediator,
        IObjectTable objectTable,
        PairUiService pairUiService,
        NameplateUpdateHookService nameplateUpdateHookService) : base(logger, mediator)
    {
        _logger = logger;
        _clientState = clientState;
        _configService = configService;
        _objectTable = objectTable;
        _pairUiService = pairUiService;
        _nameplateUpdateHookService = nameplateUpdateHookService;
        _lastRenderer = _configService.Current.LightfinderLabelRenderer;

        Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
    }

    private bool IsSignatureMode => _configService.Current.LightfinderLabelRenderer == LightfinderLabelRenderer.SignatureHook;

    /// <summary>
    /// Starts listening for nameplate updates from the hook service.
    /// </summary>
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _nameplateUpdateHookService.NameplateUpdated += OnNameplateUpdated;
        return Task.CompletedTask;
    }

    /// <summary>
    /// Stops listening for nameplate updates and tears down any constructed nodes.
    /// </summary>
    public Task StopAsync(CancellationToken cancellationToken)
    {
        _nameplateUpdateHookService.NameplateUpdated -= OnNameplateUpdated;
        UnsubscribeAll();
        TryDestroyNameplateNodes();
        return Task.CompletedTask;
    }

    /// <summary>
    /// Triggered by the sig hook to refresh native nameplate labels.
    /// </summary>
    private void HandleNameplateUpdate(RaptureAtkModule* raptureAtkModule)
    {
        if (_isUpdating)
            return;

        _isUpdating = true;
        try
        {
            RefreshRendererState();
            if (!IsSignatureMode)
                return;

            if (raptureAtkModule == null)
                return;

            var namePlateAddon = GetNamePlateAddon(raptureAtkModule);
            if (namePlateAddon == null)
                return;

            if (_clientState.IsGPosing)
            {
                HideAllNameplateNodes(namePlateAddon);
                return;
            }

            var fw = Framework.Instance();
            if (fw == null)
                return;

            var frame = fw->FrameCounter;
            if (_lastSignatureUpdateFrame == frame)
                return;

            _lastSignatureUpdateFrame = frame;
            UpdateNameplateNodes(namePlateAddon);
        }
        finally
        {
            _isUpdating = false;
        }
    }

    /// <summary>
    /// Hook callback from the nameplate update signature.
    /// </summary>
    private void OnNameplateUpdated(RaptureAtkModule* raptureAtkModule, RaptureAtkModule.NamePlateInfo* namePlateInfo,
        NumberArrayData* numArray, StringArrayData* stringArray, BattleChara* battleChara, int numArrayIndex, int stringArrayIndex)
    {
        HandleNameplateUpdate(raptureAtkModule);
    }

    /// <summary>
    /// Updates the active broadcasting CID set and requests a nameplate redraw.
    /// </summary>
    public void UpdateBroadcastingCids(IEnumerable<string> cids)
    {
        var newSet = cids.ToImmutableHashSet(StringComparer.Ordinal);
        if (ReferenceEquals(_activeBroadcastingCids, newSet) || _activeBroadcastingCids.SetEquals(newSet))
            return;

        _activeBroadcastingCids = newSet;
        if (_logger.IsEnabled(LogLevel.Trace))
            _logger.LogTrace("Active broadcast IDs (native): {Cids}", string.Join(',', _activeBroadcastingCids));
        RequestNameplateRedraw();
    }

    /// <summary>
    /// Sync renderer state with config and clear/remove native nodes if needed.
    /// </summary>
    private void RefreshRendererState()
    {
        var renderer = _configService.Current.LightfinderLabelRenderer;
        if (renderer == _lastRenderer)
            return;

        _lastRenderer = renderer;

        if (renderer == LightfinderLabelRenderer.SignatureHook)
        {
            ClearNameplateCaches();
            RequestNameplateRedraw();
        }
        else
        {
            TryDestroyNameplateNodes();
            ClearNameplateCaches();
        }
    }

    /// <summary>
    /// Requests a full nameplate update through the native addon.
    /// </summary>
    private void RequestNameplateRedraw()
    {
        if (!IsSignatureMode)
            return;

        var raptureAtkModule = GetRaptureAtkModule();
        if (raptureAtkModule == null)
            return;

        var namePlateAddon = GetNamePlateAddon(raptureAtkModule);
        if (namePlateAddon == null)
            return;

        namePlateAddon->DoFullUpdate = 1;
    }

    private HashSet<ulong> VisibleUserIds
        => [.. _pairUiService.GetSnapshot().PairsByUid.Values
            .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
            .Select(u => (ulong)u.PlayerCharacterId)];

    /// <summary>
    /// Creates/updates LightFinder label nodes for active broadcasts.
    /// </summary>
    private void UpdateNameplateNodes(AddonNamePlate* namePlateAddon)
    {
        if (namePlateAddon == null)
        {
            if (_logger.IsEnabled(LogLevel.Debug))
                _logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh.");
            return;
        }

        if (!IsNameplateAddonVisible(namePlateAddon))
            return;

        if (!IsSignatureMode)
        {
            HideAllNameplateNodes(namePlateAddon);
            return;
        }

        if (_activeBroadcastingCids.Count == 0)
        {
            HideAllNameplateNodes(namePlateAddon);
            return;
        }

        var framework = Framework.Instance();
        if (framework == null)
        {
            if (_logger.IsEnabled(LogLevel.Debug))
                _logger.LogDebug("Framework instance unavailable during nameplate update, skipping.");
            return;
        }

        var uiModule = framework->GetUIModule();
        if (uiModule == null)
        {
            if (_logger.IsEnabled(LogLevel.Debug))
                _logger.LogDebug("UI module unavailable during nameplate update, skipping.");
            return;
        }

        var ui3DModule = uiModule->GetUI3DModule();
        if (ui3DModule == null)
        {
            if (_logger.IsEnabled(LogLevel.Debug))
                _logger.LogDebug("UI3D module unavailable during nameplate update, skipping.");
            return;
        }

        var vec = ui3DModule->NamePlateObjectInfoPointers;
        if (vec.IsEmpty)
            return;

        var config = _configService.Current;
        var visibleUserIdsSnapshot = VisibleUserIds;
        var labelColor = UIColors.Get("Lightfinder");
        var edgeColor = UIColors.Get("LightfinderEdge");
        var scaleMultiplier = Math.Clamp(config.LightfinderLabelScale, 0.5f, 2.0f);
        var baseScale = config.LightfinderLabelUseIcon ? 1.0f : 0.5f;
        var effectiveScale = baseScale * scaleMultiplier;
        var labelContent = config.LightfinderLabelUseIcon
            ? LightFinderPlateHandler.NormalizeIconGlyph(config.LightfinderLabelIconGlyph)
            : DefaultLabelText;

        if (!config.LightfinderLabelUseIcon && (string.IsNullOrWhiteSpace(labelContent) || string.Equals(labelContent, "-", StringComparison.Ordinal)))
            labelContent = DefaultLabelText;

        if (!string.Equals(_lastLabelContent, labelContent, StringComparison.Ordinal))
        {
            _lastLabelContent = labelContent;
            Array.Fill(_lastLabelByIndex, null);
        }

        var desiredFontType = config.LightfinderLabelUseIcon ? FontType.Axis : FontType.MiedingerMed;
        var baseFontSize = config.LightfinderLabelUseIcon ? 36f : 24f;
        var desiredFontSize = (byte)Math.Clamp((int)Math.Round(baseFontSize * scaleMultiplier), 1, 255);
        var desiredFlags = config.LightfinderLabelUseIcon
            ? TextFlags.Edge | TextFlags.Glare | TextFlags.AutoAdjustNodeSize
            : TextFlags.Edge | TextFlags.Glare;
        var desiredLineSpacing = (byte)Math.Clamp((int)Math.Round(24 * scaleMultiplier), 0, byte.MaxValue);
        var defaultNodeWidth = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
        var defaultNodeHeight = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);

        var safeCount = Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length);
        var visibleIndices = new bool[AddonNamePlate.NumNamePlateObjects];

        for (int i = 0; i < safeCount; ++i)
        {
            var objectInfoPtr = vec[i];
            if (objectInfoPtr == null)
                continue;

            var objectInfo = objectInfoPtr.Value;
            if (objectInfo == null || objectInfo->GameObject == null)
                continue;

            var nameplateIndex = objectInfo->NamePlateIndex;
            if (nameplateIndex < 0 || nameplateIndex >= AddonNamePlate.NumNamePlateObjects)
                continue;

            var gameObject = objectInfo->GameObject;
            if ((ObjectKind)gameObject->ObjectKind != ObjectKind.Player)
                continue;

            var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
            if (cid == null || !_activeBroadcastingCids.Contains(cid))
                continue;

            var local = _objectTable.LocalPlayer;
            if (!config.LightfinderLabelShowOwn && local != null &&
                objectInfo->GameObject->GetGameObjectId() == local.GameObjectId)
                continue;

            var hidePaired = !config.LightfinderLabelShowPaired;
            var goId = (ulong)gameObject->GetGameObjectId();
            if (hidePaired && visibleUserIdsSnapshot.Contains(goId))
                continue;

            var nameplateObject = namePlateAddon->NamePlateObjectArray[nameplateIndex];
            var root = nameplateObject.RootComponentNode;
            var nameContainer = nameplateObject.NameContainer;
            var nameText = nameplateObject.NameText;
            var marker = nameplateObject.MarkerIcon;

            if (root == null || root->Component == null || nameContainer == null || nameText == null)
            {
                if (_logger.IsEnabled(LogLevel.Debug))
                    _logger.LogDebug("Nameplate {Index} missing required nodes during update, skipping.", nameplateIndex);
                continue;
            }

            var nodeId = GetNameplateNodeId(nameplateIndex);
            var pNode = EnsureNameplateTextNode(nameContainer, root, nodeId, out var nodeCreated);
            if (pNode == null)
                continue;

            bool isVisible =
                ((marker != null) && marker->AtkResNode.IsVisible()) ||
                (nameContainer->IsVisible() && nameText->AtkResNode.IsVisible()) ||
                config.LightfinderLabelShowHidden;

            if (!isVisible)
                continue;

            if (!pNode->AtkResNode.IsVisible())
                pNode->AtkResNode.ToggleVisibility(enable: true);
            visibleIndices[nameplateIndex] = true;

            if (nodeCreated)
                pNode->AtkResNode.SetUseDepthBasedPriority(enable: true);

            var scaleMatches = NearlyEqual(pNode->AtkResNode.ScaleX, effectiveScale) &&
                NearlyEqual(pNode->AtkResNode.ScaleY, effectiveScale);
            if (!scaleMatches)
                pNode->AtkResNode.SetScale(effectiveScale, effectiveScale);

            var fontTypeChanged = pNode->FontType != desiredFontType;
            if (fontTypeChanged)
                pNode->FontType = desiredFontType;

            var fontSizeChanged = pNode->FontSize != desiredFontSize;
            if (fontSizeChanged)
                pNode->FontSize = desiredFontSize;

            var needsTextUpdate = nodeCreated ||
                !string.Equals(_lastLabelByIndex[nameplateIndex], labelContent, StringComparison.Ordinal);
            if (needsTextUpdate)
            {
                pNode->SetText(labelContent);
                _lastLabelByIndex[nameplateIndex] = labelContent;
            }

            var flagsChanged = pNode->TextFlags != desiredFlags;
            var nodeWidth = (int)pNode->AtkResNode.GetWidth();
            if (nodeWidth <= 0)
                nodeWidth = defaultNodeWidth;
            var nodeHeight = defaultNodeHeight;
            AlignmentType alignment;

            var textScaleY = nameText->AtkResNode.ScaleY;
            if (textScaleY <= 0f)
                textScaleY = 1f;

            var blockHeight = Math.Abs((int)nameplateObject.TextH);
            if (blockHeight > 0)
            {
                _cachedNameplateTextHeights[nameplateIndex] = blockHeight;
            }
            else
            {
                blockHeight = _cachedNameplateTextHeights[nameplateIndex];
            }

            if (blockHeight <= 0)
            {
                blockHeight = GetScaledTextHeight(nameText);
                if (blockHeight <= 0)
                    blockHeight = nodeHeight;

                _cachedNameplateTextHeights[nameplateIndex] = blockHeight;
            }

            var containerHeight = (int)nameContainer->Height;
            if (containerHeight > 0)
            {
                _cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
            }
            else
            {
                containerHeight = _cachedNameplateContainerHeights[nameplateIndex];
            }

            if (containerHeight <= 0)
            {
                containerHeight = blockHeight + (int)Math.Round(8 * textScaleY);
                if (containerHeight <= blockHeight)
                    containerHeight = blockHeight + 1;

                _cachedNameplateContainerHeights[nameplateIndex] = containerHeight;
            }

            var blockTop = containerHeight - blockHeight;
            if (blockTop < 0)
                blockTop = 0;
            var verticalPadding = (int)Math.Round(4 * effectiveScale);

            var positionY = blockTop - verticalPadding - nodeHeight;

            var textWidth = Math.Abs((int)nameplateObject.TextW);
            if (textWidth <= 0)
            {
                textWidth = GetScaledTextWidth(nameText);
                if (textWidth <= 0)
                    textWidth = nodeWidth;
            }

            if (textWidth > 0)
            {
                _cachedNameplateTextWidths[nameplateIndex] = textWidth;
            }

            var textOffset = (int)Math.Round(nameText->AtkResNode.X);
            var hasValidOffset = false;

            if (Math.Abs((int)nameplateObject.TextW) > 0 || textOffset != 0)
            {
                _cachedNameplateTextOffsets[nameplateIndex] = textOffset;
                hasValidOffset = true;
            }
            else if (_cachedNameplateTextOffsets[nameplateIndex] != int.MinValue)
            {
                hasValidOffset = true;
            }

            int positionX;

            if (!config.LightfinderLabelUseIcon)
            {
                var needsWidthRefresh = nodeCreated || needsTextUpdate || !scaleMatches || fontTypeChanged || fontSizeChanged || flagsChanged;
                if (flagsChanged)
                    pNode->TextFlags = desiredFlags;

                if (needsWidthRefresh)
                {
                    if (pNode->AtkResNode.Width != 0)
                        pNode->AtkResNode.Width = 0;
                    nodeWidth = (int)pNode->AtkResNode.GetWidth();
                    if (nodeWidth <= 0)
                        nodeWidth = defaultNodeWidth;
                }

                if (pNode->AtkResNode.Width != (ushort)nodeWidth)
                    pNode->AtkResNode.Width = (ushort)nodeWidth;
            }
            else
            {
                var needsWidthRefresh = nodeCreated || needsTextUpdate || !scaleMatches || fontTypeChanged || fontSizeChanged || flagsChanged;
                if (flagsChanged)
                    pNode->TextFlags = desiredFlags;

                if (needsWidthRefresh && pNode->AtkResNode.Width != 0)
                    pNode->AtkResNode.Width = 0;
                nodeWidth = pNode->AtkResNode.GetWidth();
            }

            if (config.LightfinderAutoAlign && nameContainer != null && hasValidOffset)
            {
                var nameplateWidth = (int)nameContainer->Width;

                int leftPos = nameplateWidth / 8;
                int rightPos = nameplateWidth - nodeWidth - (nameplateWidth / 8);
                int centrePos = (nameplateWidth - nodeWidth) / 2;
                int staticMargin = 24;
                int calcMargin = (int)(nameplateWidth * 0.08f);

                switch (config.LabelAlignment)
                {
                    case LabelAlignment.Left:
                        positionX = config.LightfinderLabelUseIcon ? leftPos + staticMargin : leftPos;
                        alignment = AlignmentType.BottomLeft;
                        break;
                    case LabelAlignment.Right:
                        positionX = config.LightfinderLabelUseIcon ? rightPos - staticMargin : nameplateWidth - nodeWidth + calcMargin;
                        alignment = AlignmentType.BottomRight;
                        break;
                    default:
                        positionX = config.LightfinderLabelUseIcon ? centrePos : centrePos + calcMargin;
                        alignment = AlignmentType.Bottom;
                        break;
                }
            }
            else
            {
                positionX = 58 + config.LightfinderLabelOffsetX;
                alignment = AlignmentType.Bottom;
            }

            positionY += config.LightfinderLabelOffsetY;

            alignment = (AlignmentType)Math.Clamp((int)alignment, 0, 8);
            if (pNode->AtkResNode.Color.A != 255)
                pNode->AtkResNode.Color.A = 255;

            var textR = (byte)(labelColor.X * 255);
            var textG = (byte)(labelColor.Y * 255);
            var textB = (byte)(labelColor.Z * 255);
            var textA = (byte)(labelColor.W * 255);

            if (pNode->TextColor.R != textR || pNode->TextColor.G != textG ||
                pNode->TextColor.B != textB || pNode->TextColor.A != textA)
            {
                pNode->TextColor.R = textR;
                pNode->TextColor.G = textG;
                pNode->TextColor.B = textB;
                pNode->TextColor.A = textA;
            }

            var edgeR = (byte)(edgeColor.X * 255);
            var edgeG = (byte)(edgeColor.Y * 255);
            var edgeB = (byte)(edgeColor.Z * 255);
            var edgeA = (byte)(edgeColor.W * 255);

            if (pNode->EdgeColor.R != edgeR || pNode->EdgeColor.G != edgeG ||
                pNode->EdgeColor.B != edgeB || pNode->EdgeColor.A != edgeA)
            {
                pNode->EdgeColor.R = edgeR;
                pNode->EdgeColor.G = edgeG;
                pNode->EdgeColor.B = edgeB;
                pNode->EdgeColor.A = edgeA;
            }

            var desiredAlignment = config.LightfinderLabelUseIcon ? alignment : AlignmentType.Bottom;
            if (pNode->AlignmentType != desiredAlignment)
                pNode->AlignmentType = desiredAlignment;

            var desiredX = (short)Math.Clamp(positionX, short.MinValue, short.MaxValue);
            var desiredY = (short)Math.Clamp(positionY, short.MinValue, short.MaxValue);
            if (!NearlyEqual(pNode->AtkResNode.X, desiredX) || !NearlyEqual(pNode->AtkResNode.Y, desiredY))
                pNode->AtkResNode.SetPositionShort(desiredX, desiredY);

            if (pNode->LineSpacing != desiredLineSpacing)
                pNode->LineSpacing = desiredLineSpacing;
            if (pNode->CharSpacing != 1)
                pNode->CharSpacing = 1;
        }

        HideUnmarkedNodes(namePlateAddon, visibleIndices);
    }

    /// <summary>
    /// Resolve the current RaptureAtkModule for native UI access.
    /// </summary>
    private static RaptureAtkModule* GetRaptureAtkModule()
    {
        var framework = Framework.Instance();
        if (framework == null)
            return null;

        var uiModule = framework->GetUIModule();
        if (uiModule == null)
            return null;

        return uiModule->GetRaptureAtkModule();
    }

    /// <summary>
    /// Resolve the NamePlate addon from the given RaptureAtkModule.
    /// </summary>
    private static AddonNamePlate* GetNamePlateAddon(RaptureAtkModule* raptureAtkModule)
    {
        if (raptureAtkModule == null)
            return null;

        var addon = raptureAtkModule->RaptureAtkUnitManager.GetAddonByName("NamePlate");
        return addon != null ? (AddonNamePlate*)addon : null;
    }

    private static uint GetNameplateNodeId(int index)
        => NameplateNodeIdBase + (uint)index;

    /// <summary>
    /// Checks if the NamePlate addon is visible and safe to touch.
    /// </summary>
    private static bool IsNameplateAddonVisible(AddonNamePlate* namePlateAddon)
    {
        if (namePlateAddon == null)
            return false;

        var root = namePlateAddon->AtkUnitBase.RootNode;
        return root != null && root->IsVisible();
    }

    /// <summary>
    /// Finds a LightFinder text node by ID in the name container.
    /// </summary>
    private static AtkTextNode* FindNameplateTextNode(AtkResNode* nameContainer, uint nodeId)
    {
        if (nameContainer == null)
            return null;

        var child = nameContainer->ChildNode;
        while (child != null)
        {
            if (child->NodeId == nodeId &&
                child->Type == NodeType.Text &&
                child->ParentNode == nameContainer)
                return (AtkTextNode*)child;

            child = child->PrevSiblingNode;
        }

        return null;
    }

    /// <summary>
    /// Ensures a LightFinder text node exists for the given nameplate index.
    /// </summary>
    private static AtkTextNode* EnsureNameplateTextNode(AtkResNode* nameContainer, AtkComponentNode* root, uint nodeId, out bool created)
    {
        created = false;
        if (nameContainer == null || root == null || root->Component == null)
            return null;

        var existing = FindNameplateTextNode(nameContainer, nodeId);
        if (existing != null)
            return existing;

        if (nameContainer->ChildNode == null)
            return null;

        var newNode = AtkNodeHelpers.CreateOrphanTextNode(nodeId, TextFlags.Edge | TextFlags.Glare);
        if (newNode == null)
            return null;

        var lastChild = nameContainer->ChildNode;
        while (lastChild->PrevSiblingNode != null)
            lastChild = lastChild->PrevSiblingNode;

        newNode->AtkResNode.NextSiblingNode = lastChild;
        newNode->AtkResNode.ParentNode = nameContainer;
        lastChild->PrevSiblingNode = (AtkResNode*)newNode;
        root->Component->UldManager.UpdateDrawNodeList();
        newNode->AtkResNode.SetUseDepthBasedPriority(true);

        created = true;
        return newNode;
    }

    /// <summary>
    /// Hides all native LightFinder nodes on the nameplate addon.
    /// </summary>
    private static void HideAllNameplateNodes(AddonNamePlate* namePlateAddon)
    {
        if (namePlateAddon == null)
            return;

        if (!IsNameplateAddonVisible(namePlateAddon))
            return;

        for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
        {
            HideNameplateTextNode(namePlateAddon->NamePlateObjectArray[i], GetNameplateNodeId(i));
        }
    }

    /// <summary>
    /// Hides all LightFinder nodes not marked as visible this frame.
    /// </summary>
    private static void HideUnmarkedNodes(AddonNamePlate* namePlateAddon, bool[] visibleIndices)
    {
        if (namePlateAddon == null)
            return;

        if (!IsNameplateAddonVisible(namePlateAddon))
            return;

        var visibleLength = visibleIndices.Length;
        for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
        {
            if (i < visibleLength && visibleIndices[i])
                continue;

            HideNameplateTextNode(namePlateAddon->NamePlateObjectArray[i], GetNameplateNodeId(i));
        }
    }

    /// <summary>
    /// Hides the LightFinder text node for a single nameplate object.
    /// </summary>
    private static void HideNameplateTextNode(AddonNamePlate.NamePlateObject nameplateObject, uint nodeId)
    {
        var nameContainer = nameplateObject.NameContainer;
        if (nameContainer == null)
            return;

        var node = FindNameplateTextNode(nameContainer, nodeId);
        if (!IsValidNameplateTextNode(node, nameContainer))
            return;

        node->AtkResNode.ToggleVisibility(false);
    }

    /// <summary>
    /// Attempts to destroy all constructed LightFinder nodes safely.
    /// </summary>
    private void TryDestroyNameplateNodes()
    {
        var raptureAtkModule = GetRaptureAtkModule();
        if (raptureAtkModule == null)
        {
            if (_logger.IsEnabled(LogLevel.Debug))
                _logger.LogDebug("Unable to destroy nameplate nodes because the RaptureAtkModule is not available.");
            return;
        }

        var namePlateAddon = GetNamePlateAddon(raptureAtkModule);
        if (namePlateAddon == null)
        {
            if (_logger.IsEnabled(LogLevel.Debug))
                _logger.LogDebug("Unable to destroy nameplate nodes because the NamePlate addon is not available.");
            return;
        }

        DestroyNameplateNodes(namePlateAddon);
    }

    /// <summary>
    /// Removes all constructed LightFinder nodes from the given nameplate addon.
    /// </summary>
    private void DestroyNameplateNodes(AddonNamePlate* namePlateAddon)
    {
        if (namePlateAddon == null)
            return;

        for (int i = 0; i < AddonNamePlate.NumNamePlateObjects; ++i)
        {
            var nameplateObject = namePlateAddon->NamePlateObjectArray[i];
            var root = nameplateObject.RootComponentNode;
            var nameContainer = nameplateObject.NameContainer;
            if (root == null || root->Component == null || nameContainer == null)
                continue;

            var nodeId = GetNameplateNodeId(i);
            var textNode = FindNameplateTextNode(nameContainer, nodeId);
            if (!IsValidNameplateTextNode(textNode, nameContainer))
                continue;

            try
            {
                var resNode = &textNode->AtkResNode;

                if (resNode->PrevSiblingNode != null)
                    resNode->PrevSiblingNode->NextSiblingNode = resNode->NextSiblingNode;
                if (resNode->NextSiblingNode != null)
                    resNode->NextSiblingNode->PrevSiblingNode = resNode->PrevSiblingNode;

                root->Component->UldManager.UpdateDrawNodeList();
                resNode->Destroy(true);
            }
            catch (Exception e)
            {
                _logger.LogError(e, "Unknown error while removing text node 0x{Node:X} for nameplate {Index} on component node 0x{Component:X}", (IntPtr)textNode, i, (IntPtr)root);
            }
        }

        ClearNameplateCaches();
    }

    /// <summary>
    /// Validates that a node is a LightFinder text node owned by the container.
    /// </summary>
    private static bool IsValidNameplateTextNode(AtkTextNode* node, AtkResNode* nameContainer)
    {
        if (node == null || nameContainer == null)
            return false;

        var resNode = &node->AtkResNode;
        return resNode->Type == NodeType.Text && resNode->ParentNode == nameContainer;
    }

    /// <summary>
    /// Float comparison helper for UI values.
    /// </summary>
    private static bool NearlyEqual(float a, float b, float epsilon = 0.001f)
        => Math.Abs(a - b) <= epsilon;

    private static int GetScaledTextHeight(AtkTextNode* node)
    {
        if (node == null)
            return 0;

        var resNode = &node->AtkResNode;
        var rawHeight = (int)resNode->GetHeight();
        if (rawHeight <= 0 && node->LineSpacing > 0)
            rawHeight = node->LineSpacing;
        if (rawHeight <= 0)
            rawHeight = AtkNodeHelpers.DefaultTextNodeHeight;

        var scale = resNode->ScaleY;
        if (scale <= 0f)
            scale = 1f;

        var computed = (int)Math.Round(rawHeight * scale);
        return Math.Max(1, computed);
    }

    private static int GetScaledTextWidth(AtkTextNode* node)
    {
        if (node == null)
            return 0;

        var resNode = &node->AtkResNode;
        var rawWidth = (int)resNode->GetWidth();
        if (rawWidth <= 0)
            rawWidth = AtkNodeHelpers.DefaultTextNodeWidth;

        var scale = resNode->ScaleX;
        if (scale <= 0f)
            scale = 1f;

        var computed = (int)Math.Round(rawWidth * scale);
        return Math.Max(1, computed);
    }

    /// <summary>
    /// Clears cached text sizing and label state for nameplates.
    /// </summary>
    public void ClearNameplateCaches()
    {
        Array.Clear(_cachedNameplateTextWidths, 0, _cachedNameplateTextWidths.Length);
        Array.Clear(_cachedNameplateTextHeights, 0, _cachedNameplateTextHeights.Length);
        Array.Clear(_cachedNameplateContainerHeights, 0, _cachedNameplateContainerHeights.Length);
        Array.Fill(_cachedNameplateTextOffsets, int.MinValue);
        Array.Fill(_lastLabelByIndex, null);
    }

}
