using Dalamud.Bindings.ImGui;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LightlessSync.LightlessConfiguration;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.Rendering;
using LightlessSync.UI;
using LightlessSync.UI.Services;
using LightlessSync.Utils;
using LightlessSync.UtilsEnum.Enum;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Pictomancy;
using System.Collections.Immutable;
using System.Globalization;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Task = System.Threading.Tasks.Task;

namespace LightlessSync.Services.LightFinder;

/// <summary>
/// The new lightfinder nameplate handler using ImGUI (pictomancy) for rendering the icon/labels.
/// </summary>
public unsafe class LightFinderPlateHandler : IHostedService, IMediatorSubscriber
{
    private readonly ILogger<LightFinderPlateHandler> _logger;
    private readonly IAddonLifecycle _addonLifecycle;
    private readonly IGameGui _gameGui;
    private readonly IObjectTable _objectTable;
    private readonly IClientState _clientState;
    private readonly LightlessConfigService _configService;
    private readonly PairUiService _pairUiService;
    private readonly LightlessMediator _mediator;

    public LightlessMediator Mediator => _mediator;

    private readonly IUiBuilder _uiBuilder;
    private bool _mEnabled;
    private bool _needsLabelRefresh;
    private bool _drawSubscribed;
    private AddonNamePlate* _mpNameplateAddon;
    private readonly Lock _labelLock = new();
    private readonly NameplateBuffers _buffers = new();
    private int _labelRenderCount;
    private LightfinderLabelRenderer _lastRenderer;

    private const string _defaultLabelText = "LightFinder";
    private const SeIconChar _defaultIcon = SeIconChar.Hyadelyn;
    private static readonly string _defaultIconGlyph = SeIconCharExtensions.ToIconString(_defaultIcon);
    private static readonly Vector2 _defaultPivot = new(0.5f, 1f);
    private uint _lastNamePlateDrawFrame;

    // / Overlay window flags
    private const ImGuiWindowFlags _overlayFlags =
        ImGuiWindowFlags.NoDecoration |
        ImGuiWindowFlags.NoBackground |
        ImGuiWindowFlags.NoMove |
        ImGuiWindowFlags.NoSavedSettings |
        ImGuiWindowFlags.NoNav |
        ImGuiWindowFlags.NoInputs;

    private readonly List<RectF> _uiRects = new(128);
    private ImmutableHashSet<string> _activeBroadcastingCids = [];

#if DEBUG
    // Debug controls

    // Debug counters (read-only from UI)
#endif

    private bool IsPictomancyRenderer => _configService.Current.LightfinderLabelRenderer == LightfinderLabelRenderer.Pictomancy;

    public LightFinderPlateHandler(
        ILogger<LightFinderPlateHandler> logger,
        IAddonLifecycle addonLifecycle,
        IGameGui gameGui,
        IClientState clientState,
        LightlessConfigService configService,
        LightlessMediator mediator,
        IObjectTable objectTable,
        PairUiService pairUiService,
        IDalamudPluginInterface pluginInterface,
        PictomancyService pictomancyService)
    {
        _logger = logger;
        _addonLifecycle = addonLifecycle;
        _gameGui = gameGui;
        _clientState = clientState;
        _configService = configService;
        _mediator = mediator;
        _objectTable = objectTable;
        _pairUiService = pairUiService;
        _uiBuilder = pluginInterface.UiBuilder ?? throw new ArgumentNullException(nameof(pluginInterface));
        _ = pictomancyService ?? throw new ArgumentNullException(nameof(pictomancyService));
        _lastRenderer = _configService.Current.LightfinderLabelRenderer;
    }

    private void RefreshRendererState()
    {
        var renderer = _configService.Current.LightfinderLabelRenderer;
        if (renderer == _lastRenderer)
            return;

        _lastRenderer = renderer;

        if (renderer == LightfinderLabelRenderer.Pictomancy)
        {
            FlagRefresh();
        }
        else
        {
            ClearNameplateCaches();
            _lastNamePlateDrawFrame = 0;
        }
    }

    internal void Init()
    {
        if (!_drawSubscribed)
        {
            _uiBuilder.Draw += OnUiBuilderDraw;
            _drawSubscribed = true;
        }

        EnableNameplate();
        _mediator.Subscribe<PriorityFrameworkUpdateMessage>(this, OnTick);
    }

    internal void Uninit()
    {
        DisableNameplate();
        if (_drawSubscribed)
        {
            _uiBuilder.Draw -= OnUiBuilderDraw;
            _drawSubscribed = false;
        }
        ClearLabelBuffer();
        _mediator.Unsubscribe<PriorityFrameworkUpdateMessage>(this);
        _mpNameplateAddon = null;
    }

    /// <summary>
    /// Enable nameplate handling.
    /// </summary>
    internal void EnableNameplate()
    {
        if (!_mEnabled)
        {
            try
            {
                _addonLifecycle.RegisterListener(AddonEvent.PostDraw, "NamePlate", NameplateDrawDetour);
                _mEnabled = true;
            }
            catch (Exception e)
            {
                _logger.LogError(e, "Unknown error while trying to enable nameplate.");
                DisableNameplate();
            }
        }
    }

    /// <summary>
    /// Disable nameplate handling.
    /// </summary>
    internal void DisableNameplate()
    {
        if (_mEnabled)
        {
            try
            {
                _addonLifecycle.UnregisterListener(NameplateDrawDetour);
            }
            catch (Exception e)
            {
                _logger.LogError(e, "Unknown error while unregistering nameplate listener.");
            }

            _mEnabled = false;
            ClearNameplateCaches();
        }
    }

    /// <summary>
    /// Draw detour for nameplate addon.
    /// </summary>
    private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
    {
        RefreshRendererState();
        if (!IsPictomancyRenderer)
        {
            ClearLabelBuffer();
            _lastNamePlateDrawFrame = 0;
            return;
        }

        // Hide our overlay when the user hides the entire game UI (ScrollLock).
        if (_gameGui.GameUiHidden)
        {
            ClearLabelBuffer();
            Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
            _lastNamePlateDrawFrame = 0;
            return;
        }

        // gpose: do not draw.
        if (_clientState.IsGPosing)
        {
            ClearLabelBuffer();
            Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
            _lastNamePlateDrawFrame = 0;
            return;
        }

        if (args.Addon.Address == nint.Zero)
        {
            if (_logger.IsEnabled(LogLevel.Warning))
                _logger.LogWarning("Nameplate draw detour received a null addon address, skipping update.");
            return;
        }

        var fw = Framework.Instance();
        if (fw != null)
            _lastNamePlateDrawFrame = fw->FrameCounter;

#if DEBUG
        DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif

        var pNameplateAddon = (AddonNamePlate*)args.Addon.Address;

        if (_mpNameplateAddon != pNameplateAddon)
        {
            ClearNameplateCaches();
            _mpNameplateAddon = pNameplateAddon;
        }

        UpdateNameplateNodes();
    }

    /// <summary>
    /// Updates the nameplate nodes with LightFinder objects.
    /// </summary>
    private void UpdateNameplateNodes()
    {
        // If the user has hidden the UI, don't compute any labels.
        if (_gameGui.GameUiHidden)
        {
            ClearLabelBuffer();
            return;
        }

        var currentHandle = _gameGui.GetAddonByName("NamePlate");
        if (currentHandle.Address == nint.Zero)
        {
            if (_logger.IsEnabled(LogLevel.Debug))
                _logger.LogDebug("NamePlate addon unavailable during update, skipping label refresh.");
            ClearLabelBuffer();
            return;
        }

        var currentAddon = (AddonNamePlate*)currentHandle.Address;
        if (_mpNameplateAddon == null || currentAddon == null || currentAddon != _mpNameplateAddon)
        {
            if (_mpNameplateAddon != null && _logger.IsEnabled(LogLevel.Debug))
                _logger.LogDebug("Cached NamePlate addon pointer differs from current: waiting for new hook (cached {Cached}, current {Current}).", (IntPtr)_mpNameplateAddon, (IntPtr)currentAddon);
            return;
        }

        if (!IsNamePlateAddonVisible())
        {
            ClearLabelBuffer();
            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)
        {
            ClearLabelBuffer();
            return;
        }

        var visibleUserIdsSnapshot = VisibleUserIds;
        var safeCount = Math.Min(ui3DModule->NamePlateObjectInfoCount, vec.Length);
        var currentConfig = _configService.Current;
        var labelColor = UIColors.Get("Lightfinder");
        var edgeColor = UIColors.Get("LightfinderEdge");
        var scratchCount = 0;

        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;

            // CID gating - only show for active broadcasters
            var cid = DalamudUtilService.GetHashedCIDFromPlayerPointer((nint)gameObject);
            if (cid == null || !_activeBroadcastingCids.Contains(cid))
                continue;

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

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

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

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

            root->Component->UldManager.UpdateDrawNodeList();

            bool isNameplateVisible =
                nameContainer->IsVisible() &&
                nameText->AtkResNode.IsVisible();

            if (!currentConfig.LightfinderLabelShowHidden && !isNameplateVisible)
                continue;

            // Prepare label content and scaling factors
            var scaleMultiplier = Math.Clamp(currentConfig.LightfinderLabelScale, 0.5f, 2.0f);
            var baseScale = currentConfig.LightfinderLabelUseIcon ? 1.0f : 0.5f;
            var effectiveScale = baseScale * scaleMultiplier;
            var baseFontSize = currentConfig.LightfinderLabelUseIcon ? 36f : 24f;
            var targetFontSize = (int)Math.Round(baseFontSize * scaleMultiplier);
            var labelContent = currentConfig.LightfinderLabelUseIcon
                ? NormalizeIconGlyph(currentConfig.LightfinderLabelIconGlyph)
                : _defaultLabelText;

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

            var nodeWidth = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
            var nodeHeight = (int)Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
            AlignmentType alignment;

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

            var blockHeight = ResolveCache(
                _buffers.TextHeights,
                nameplateIndex,
                Math.Abs((int)nameplateObject.TextH),
                () => GetScaledTextHeight(nameText),
                nodeHeight);

            var containerHeight = ResolveCache(
                _buffers.ContainerHeights,
                nameplateIndex,
                (int)nameContainer->Height,
                () =>
                {
                    var computed = blockHeight + (int)Math.Round(8 * textScaleY);
                    return computed <= blockHeight ? blockHeight + 1 : computed;
                },
                blockHeight + 1);

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

            var positionY = blockTop - verticalPadding;

            var rawTextWidth = (int)nameplateObject.TextW;
            var textWidth = ResolveCache(
                _buffers.TextWidths,
                nameplateIndex,
                Math.Abs(rawTextWidth),
                () => GetScaledTextWidth(nameText),
                nodeWidth);

            // Text offset caching
            var textOffset = (int)Math.Round(nameText->AtkResNode.X);
            var hasValidOffset = TryCacheTextOffset(nameplateIndex, rawTextWidth, textOffset);

            var res = nameContainer;

            // X scale
            var worldScaleX = GetWorldScaleX(res);
            if (worldScaleX <= 0f) worldScaleX = 1f;

            // Y scale
            var worldScaleY = GetWorldScaleY(res);
            if (worldScaleY <= 0f) worldScaleY = 1f;

            positionY += currentConfig.LightfinderLabelOffsetY;
            var positionYScreen = positionY * worldScaleY;

            float finalX;
            if (currentConfig.LightfinderAutoAlign)
            {
                // auto X positioning
                var measuredWidth = Math.Max(1, textWidth > 0 ? textWidth : nodeWidth);
                var measuredWidthF = (float)measuredWidth;

                // consider icon width
                var containerWidthLocal = res->Width > 0 ? res->Width : measuredWidthF;
                var containerWidthScreen = containerWidthLocal * worldScaleX;

                // container bounds for positions
                var containerLeft = res->ScreenX;
                var containerRight = containerLeft + containerWidthScreen;
                var containerCenter = containerLeft + (containerWidthScreen * 0.5f);

                var iconMargin = currentConfig.LightfinderLabelUseIcon
                    ? MathF.Min(containerWidthScreen * 0.1f, 14f * worldScaleX)
                    : 0f;

                var offsetXScreen = currentConfig.LightfinderLabelOffsetX * worldScaleX;

                // alignment based on config setting
                switch (currentConfig.LabelAlignment)
                {
                    case LabelAlignment.Left:
                        finalX = containerLeft + iconMargin + offsetXScreen;
                        alignment = AlignmentType.BottomLeft;
                        break;
                    case LabelAlignment.Right:
                        finalX = containerRight - iconMargin + offsetXScreen;
                        alignment = AlignmentType.BottomRight;
                        break;
                    default:
                        finalX = containerCenter + offsetXScreen;
                        alignment = AlignmentType.Bottom;
                        break;
                }
            }
            else
            {
                // manual X positioning with optional cached offset
                var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
                var hasCachedOffset = cachedTextOffset != int.MinValue;
                var baseOffsetXLocal = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset)
                    ? cachedTextOffset
                    : 0;

                finalX =
                    res->ScreenX
                    + (baseOffsetXLocal * worldScaleX)
                    + (58f * worldScaleX)
                    + (currentConfig.LightfinderLabelOffsetX * worldScaleX);

                alignment = AlignmentType.Bottom;
            }

            alignment = (AlignmentType)Math.Clamp((int)alignment, 0, 8);

            // final position before smoothing
            var finalPosition = new Vector2(finalX, res->ScreenY + positionYScreen);
            var dpiScale = ImGui.GetIO().DisplayFramebufferScale.X;
            var fw = Framework.Instance();
            float dt = fw->RealFrameDeltaTime;

            //smoothing.. snap.. smooth.. snap
            finalPosition = SnapToPixels(finalPosition, dpiScale);
            finalPosition = SmoothPosition(nameplateIndex, finalPosition, dt);
            finalPosition = SnapToPixels(finalPosition, dpiScale);

            // prepare label info for rendering
            var pivot = (currentConfig.LightfinderAutoAlign || currentConfig.LightfinderLabelUseIcon)
                ? AlignmentToPivot(alignment)
                : _defaultPivot;
            var textColorPacked = PackColor(labelColor);
            var edgeColorPacked = PackColor(edgeColor);

            _buffers.LabelScratch[scratchCount++] = new NameplateLabelInfo(
                finalPosition,
                labelContent,
                textColorPacked,
                edgeColorPacked,
                targetFontSize,
                pivot,
                currentConfig.LightfinderLabelUseIcon);
        }

        lock (_labelLock)
        {
            if (scratchCount == 0)
            {
                _labelRenderCount = 0;
            }
            else
            {
                Array.Copy(_buffers.LabelScratch, _buffers.LabelRender, scratchCount);
                _labelRenderCount = scratchCount;
            }
        }
    }

    /// <summary>
    /// On each tick, process any needed updates for the UI Builder.
    /// </summary>
    private void OnUiBuilderDraw()
    {
        RefreshRendererState();
        if (!IsPictomancyRenderer)
            return;

        if (!_mEnabled)
            return;

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

        // If UI is hidden, do not render.
        if (_gameGui.GameUiHidden)
        {
            ClearLabelBuffer();
            Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
            _lastNamePlateDrawFrame = 0;

#if DEBUG
            DebugLabelCountLastFrame = 0;
            DebugUiRectCountLastFrame = 0;
            DebugOccludedCountLastFrame = 0;
            DebugLastNameplateFrame = 0;
#endif
            return;
        }

        // Frame skip check - skip if more than 1 frame has passed since last nameplate draw.
        var frame = fw->FrameCounter;

        if (_lastNamePlateDrawFrame == 0 || (frame - _lastNamePlateDrawFrame) > 1)
        {
            ClearLabelBuffer();
            Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);

#if DEBUG
            DebugLabelCountLastFrame = 0;
            DebugUiRectCountLastFrame = 0;
            DebugOccludedCountLastFrame = 0;
            DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif
            return;
        }

        // Gpose Check - do not render.
        if (_clientState.IsGPosing)
        {
            ClearLabelBuffer();
            Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
            _lastNamePlateDrawFrame = 0;

#if DEBUG
            DebugLabelCountLastFrame = 0;
            DebugUiRectCountLastFrame = 0;
            DebugOccludedCountLastFrame = 0;
            DebugLastNameplateFrame = 0;
#endif
            return;
        }

        // If nameplate addon is not visible, skip rendering entirely.
        if (!IsNamePlateAddonVisible())
        {
#if DEBUG
            DebugLabelCountLastFrame = 0;
            DebugUiRectCountLastFrame = 0;
            DebugOccludedCountLastFrame = 0;
            DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif
            return;
        }

        int copyCount;
        lock (_labelLock)
        {
            copyCount = _labelRenderCount;
            if (copyCount == 0)
            {
#if DEBUG
                DebugLabelCountLastFrame = 0;
                DebugUiRectCountLastFrame = 0;
                DebugOccludedCountLastFrame = 0;
                DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif
                return;
            }

            Array.Copy(_buffers.LabelRender, _buffers.LabelCopy, copyCount);
        }

        var uiModule = fw->GetUIModule();
        if (uiModule != null)
        {
            var rapture = uiModule->GetRaptureAtkModule();
            if (rapture != null)
                RefreshUiRects(&rapture->RaptureAtkUnitManager);
            else
                _uiRects.Clear();
        }
        else
        {
            _uiRects.Clear();
        }

        // Needed for imgui overlay viewport for the multi window view.
        var vp = ImGui.GetMainViewport();
        var vpPos = vp.Pos;

        ImGuiHelpers.ForceNextWindowMainViewport();
        ImGui.SetNextWindowPos(vp.Pos);
        ImGui.SetNextWindowSize(vp.Size);

        ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0);
        ImGui.PushStyleVar(ImGuiStyleVar.WindowBorderSize, 0);

        ImGui.Begin("##LightFinderOverlay", _overlayFlags);

        ImGui.PopStyleVar(2);

        // Debug flags
        bool dbgEnabled = false;
        bool dbgDisableOcc = false;
        bool dbgDrawUiRects = false;
        bool dbgDrawLabelRects = false;
#if DEBUG
        dbgEnabled = DebugEnabled;
        dbgDisableOcc = DebugDisableOcclusion;
        dbgDrawUiRects = DebugDrawUiRects;
        dbgDrawLabelRects = DebugDrawLabelRects;
#endif

        int occludedThisFrame = 0;

        try
        {
            using var drawList = PictoService.Draw();
            if (drawList == null)
                return;

            // Debug drawing uses the window drawlist (so it always draws in the correct viewport).
            var dbgDl = ImGui.GetWindowDrawList();
            var useViewportOffset = ImGui.GetIO().ConfigFlags.HasFlag(ImGuiConfigFlags.ViewportsEnable);

            for (int i = 0; i < copyCount; ++i)
            {
                ref var info = ref _buffers.LabelCopy[i];

                // final draw position with viewport offset (only when viewports are enabled)
                var drawPos = info.ScreenPosition;
                if (useViewportOffset)
                    drawPos += vpPos;

                var font = default(ImFontPtr);
                if (info.UseIcon)
                {
                    var ioFonts = ImGui.GetIO().Fonts;
                    font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
                }
                else
                {
                    font = ImGui.GetFont();
                }

                if (!font.IsNull)
                    ImGui.PushFont(font);

                // calculate size for occlusion checking
                var baseSize = ImGui.CalcTextSize(info.Text);
                var baseFontSize = ImGui.GetFontSize();

                if (!font.IsNull)
                    ImGui.PopFont();

                // scale size based on font size
                var scale = baseFontSize > 0 ? (info.FontSize / baseFontSize) : 1f;
                var size = baseSize * scale;

                var topLeft = info.ScreenPosition - new Vector2(size.X * info.Pivot.X, size.Y * info.Pivot.Y);
                var labelRect = new RectF(topLeft.X, topLeft.Y, topLeft.X + size.X, topLeft.Y + size.Y);

                bool wouldOcclude = IsOccludedByAnyUi(labelRect);
                if (wouldOcclude)
                    occludedThisFrame++;

                // Debug: draw label rects
                if (dbgEnabled && dbgDrawLabelRects)
                {
                    var tl = new Vector2(labelRect.L, labelRect.T);
                    var br = new Vector2(labelRect.R, labelRect.B);

                    if (useViewportOffset) { tl += vpPos; br += vpPos; }

                    // green = visible, red = would be occluded (even if forced)
                    var col = wouldOcclude
                        ? ImGui.GetColorU32(new Vector4(1f, 0f, 0f, 0.6f))
                        : ImGui.GetColorU32(new Vector4(0f, 1f, 0f, 0.6f));

                    dbgDl.AddRect(tl, br, col);
                }

                // occlusion check (allow debug to disable)
                if (!dbgDisableOcc && wouldOcclude)
                    continue;

                drawList.AddScreenText(drawPos, info.Text, info.TextColor, info.FontSize, info.Pivot, info.EdgeColor, font);
            }

            // Debug: draw UI rects if any
            if (dbgEnabled && dbgDrawUiRects && _uiRects.Count > 0)
            {
                var useOff = useViewportOffset ? vpPos : Vector2.Zero;
                var col = ImGui.GetColorU32(new Vector4(1f, 1f, 1f, 0.35f));

                for (int i = 0; i < _uiRects.Count; i++)
                {
                    var r = _uiRects[i];
                    dbgDl.AddRect(new Vector2(r.L, r.T) + useOff, new Vector2(r.R, r.B) + useOff, col);
                }
            }
        }
        finally
        {
            ImGui.End();
        }

#if DEBUG
        DebugLabelCountLastFrame = copyCount;
        DebugUiRectCountLastFrame = _uiRects.Count;
        DebugOccludedCountLastFrame = occludedThisFrame;
        DebugLastNameplateFrame = _lastNamePlateDrawFrame;
#endif
    }

    private static Vector2 AlignmentToPivot(AlignmentType alignment) => alignment switch
    {
        AlignmentType.BottomLeft => new Vector2(0f, 1f),
        AlignmentType.BottomRight => new Vector2(1f, 1f),
        AlignmentType.TopLeft => new Vector2(0f, 0f),
        AlignmentType.TopRight => new Vector2(1f, 0f),
        AlignmentType.Top => new Vector2(0.5f, 0f),
        AlignmentType.Left => new Vector2(0f, 0.5f),
        AlignmentType.Right => new Vector2(1f, 0.5f),
        _ => _defaultPivot
    };

    private static uint PackColor(Vector4 color)
    {
        var r = (byte)Math.Clamp(color.X * 255f, 0f, 255f);
        var g = (byte)Math.Clamp(color.Y * 255f, 0f, 255f);
        var b = (byte)Math.Clamp(color.Z * 255f, 0f, 255f);
        var a = (byte)Math.Clamp(color.W * 255f, 0f, 255f);
        return (uint)((a << 24) | (b << 16) | (g << 8) | r);
    }

    private void ClearLabelBuffer()
    {
        lock (_labelLock)
        {
            _labelRenderCount = 0;
        }
    }

    private static unsafe 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 unsafe 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>
    /// Resolves a cached value for the given index.
    /// </summary>
    private static int ResolveCache(
        int[] cache,
        int index,
        int rawValue,
        Func<int> fallback,
        int fallbackWhenZero)
    {
        if (rawValue > 0)
        {
            cache[index] = rawValue;
            return rawValue;
        }

        var cachedValue = cache[index];
        if (cachedValue > 0)
            return cachedValue;

        var computed = fallback();
        if (computed <= 0)
            computed = fallbackWhenZero;

        cache[index] = computed;
        return computed;
    }

    private bool TryCacheTextOffset(int nameplateIndex, int measuredTextWidth, int textOffset)
    {
        if (Math.Abs(measuredTextWidth) > 0 || textOffset != 0)
        {
            _buffers.TextOffsets[nameplateIndex] = textOffset;
            return true;
        }

        return false;
    }

    /// <summary>
    /// Snapping a position to pixel grid based on DPI scale.
    /// </summary>
    private static Vector2 SnapToPixels(Vector2 p, float dpiScale)
    {
        // snap to pixel grid
        var x = MathF.Round(p.X * dpiScale) / dpiScale;
        var y = MathF.Round(p.Y * dpiScale) / dpiScale;
        return new Vector2(x, y);
    }

    /// <summary>
    /// Smooths the position using exponential smoothing.
    /// </summary>
    private Vector2 SmoothPosition(int idx, Vector2 target, float dt, float responsiveness = 24f)
    {
        // exponential smoothing
        if (!_buffers.HasSmoothed[idx])
        {
            _buffers.HasSmoothed[idx] = true;
            _buffers.SmoothedPos[idx] = target;
            return target;
        }

        // get current smoothed position
        var cur = _buffers.SmoothedPos[idx];

        // compute smoothing factor
        var a = 1f - MathF.Exp(-responsiveness * dt);

        // snap if close enough
        if (Vector2.DistanceSquared(cur, target) < 0.25f)
            return cur;

        // lerp towards target
        cur = Vector2.Lerp(cur, target, a);
        _buffers.SmoothedPos[idx] = cur;
        return cur;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool IsFinite(float f) => !(float.IsNaN(f) || float.IsInfinity(f));

    private static bool TryGetAddonRect(AtkUnitBase* addon, Vector2 screen, out RectF rect)
    {
        rect = default;
        if (addon == null)
            return false;

        // Addon must be visible
        if (!addon->IsVisible)
            return false;

        // Root must be visible
        var root = addon->RootNode;
        if (root == null || !root->IsVisible())
            return false;

        // Must have multiple nodes to be useful
        var nodeCount = addon->UldManager.NodeListCount;
        var nodeList = addon->UldManager.NodeList;
        if (nodeCount <= 1 || nodeList == null)
            return false;

        float rsx = GetWorldScaleX(root);
        float rsy = GetWorldScaleY(root);
        if (!IsFinite(rsx) || rsx <= 0f) rsx = 1f;
        if (!IsFinite(rsy) || rsy <= 0f) rsy = 1f;

        // clamp insane root scales (rare but prevents explosions)
        rsx = MathF.Min(rsx, 6f);
        rsy = MathF.Min(rsy, 6f);

        float rw = root->Width * rsx;
        float rh = root->Height * rsy;
        if (!IsFinite(rw) || !IsFinite(rh) || rw <= 2f || rh <= 2f)
            return false;

        float rl = root->ScreenX;
        float rt = root->ScreenY;
        if (!IsFinite(rl) || !IsFinite(rt))
            return false;

        float rr = rl + rw;
        float rb = rt + rh;

        // If root is basically fullscreen, it’s not a useful occluder for our purpose.
        if (rw >= screen.X * 0.98f && rh >= screen.Y * 0.98f)
            return false;

        // Clip root to screen so it stays sane
        float rootL = MathF.Max(0f, rl);
        float rootT = MathF.Max(0f, rt);
        float rootR = MathF.Min(screen.X, rr);
        float rootB = MathF.Min(screen.Y, rb);
        if (rootR <= rootL || rootB <= rootT)
            return false;

        // Root dimensions
        var rootW = rootR - rootL;
        var rootH = rootB - rootT;

        // Find union of all probably-drawable nodes intersecting root
        bool any = false;
        float l = float.MaxValue, t = float.MaxValue, r = float.MinValue, b = float.MinValue;

        // Allow a small bleed outside root; some addons draw small bits outside their root container.
        const float rootPad = 24f;
        float padL = rootL - rootPad;
        float padT = rootT - rootPad;
        float padR = rootR + rootPad;
        float padB = rootB + rootPad;

        for (int i = 1; i < nodeCount; i++)
        {
            var n = nodeList[i];
            if (!IsProbablyDrawableNode(n))
                continue;

            float w = n->Width;
            float h = n->Height;
            if (!IsFinite(w) || !IsFinite(h) || w <= 1f || h <= 1f)
                continue;

            float sx = GetWorldScaleX(n);
            float sy = GetWorldScaleY(n);

            if (!IsFinite(sx) || sx <= 0f) sx = 1f;
            if (!IsFinite(sy) || sy <= 0f) sy = 1f;

            sx = MathF.Min(sx, 6f);
            sy = MathF.Min(sy, 6f);

            w *= sx;
            h *= sy;

            if (!IsFinite(w) || !IsFinite(h) || w < 2f || h < 2f)
                continue;

            float nl = n->ScreenX;
            float nt = n->ScreenY;
            if (!IsFinite(nl) || !IsFinite(nt))
                continue;

            float nr = nl + w;
            float nb = nt + h;

            // Must intersect root (with padding). This is the big mitigation.
            if (nr <= padL || nb <= padT || nl >= padR || nt >= padB)
                continue;

            // Reject nodes that are wildly larger than the root (common on targeting).
            if (w > rootW * 2.0f || h > rootH * 2.0f)
                continue;

            // Clip node to root and then to screen (prevents offscreen junk stretching union)
            float cl = MathF.Max(rootL, nl);
            float ct = MathF.Max(rootT, nt);
            float cr = MathF.Min(rootR, nr);
            float cb = MathF.Min(rootB, nb);

            cl = MathF.Max(0f, cl);
            ct = MathF.Max(0f, ct);
            cr = MathF.Min(screen.X, cr);
            cb = MathF.Min(screen.Y, cb);

            if (cr <= cl || cb <= ct)
                continue;

            any = true;
            if (cl < l) l = cl;
            if (ct < t) t = ct;
            if (cr > r) r = cr;
            if (cb > b) b = cb;
        }

        // If nothing usable, fallback to root rect (still a sane occluder)
        if (!any)
        {
            rect = new RectF(rootL, rootT, rootR, rootB);
            return true;
        }

        // Validate final union rect
        var uw = r - l;
        var uh = b - t;
        if (uw < 4f || uh < 4f)
        {
            rect = new RectF(rootL, rootT, rootR, rootB);
            return true;
        }

        // If union is excessively larger than root, fallback to root rect
        if (uw > rootW * 1.35f || uh > rootH * 1.35f)
        {
            rect = new RectF(rootL, rootT, rootR, rootB);
            return true;
        }

        rect = new RectF(l, t, r, b);
        return true;
    }

    private static bool IsProbablyDrawableNode(AtkResNode* n)
    {
        if (n == null || !n->IsVisible())
            return false;

        // Check alpha
        if (n->Color.A == 16)
            return false;

        // Check node type
        return n->Type switch
        {
            NodeType.Text => true,
            NodeType.Image => true,
            NodeType.NineGrid => true,
            NodeType.Counter => true,
            NodeType.Component => true,
            _ => false,
        };
    }

    /// <summary>
    /// Refreshes the cached UI rects for occlusion checking.
    /// </summary>
    private void RefreshUiRects(RaptureAtkUnitManager* unitMgr)
    {
        _uiRects.Clear();
        if (unitMgr == null)
            return;

        var screen = ImGui.GetIO().DisplaySize;

        ref var list = ref unitMgr->AllLoadedUnitsList;
        var count = (int)list.Count;

        for (int i = 0; i < count; i++)
        {
            var addon = list.Entries[i].Value;
            if (addon == null)
                continue;

            if (_mpNameplateAddon != null && addon == (AtkUnitBase*)_mpNameplateAddon)
                continue;

            if (TryGetAddonRect(addon, screen, out var r))
                _uiRects.Add(r);
        }

#if DEBUG
        DebugUiRectCountLastFrame = _uiRects.Count;
#endif
    }

    /// <summary>
    /// Is the given label rect occluded by any UI rects?
    /// </summary>
    private bool IsOccludedByAnyUi(RectF labelRect)
    {
        for (int i = 0; i < _uiRects.Count; i++)
        {
            if (_uiRects[i].Intersects(labelRect))
                return true;
        }
        return false;
    }

    /// <summary>
    /// Gets the world scale X of the given node.
    /// </summary>
    private static float GetWorldScaleX(AtkResNode* n)
    {
        var t = n->Transform;
        return MathF.Sqrt(t.M11 * t.M11 + t.M12 * t.M12);
    }

    /// <summary>
    /// Gets the world scale Y of the given node.
    /// </summary>
    private static float GetWorldScaleY(AtkResNode* n)
    {
        var t = n->Transform;
        return MathF.Sqrt(t.M21 * t.M21 + t.M22 * t.M22);
    }

    /// <summary>
    /// Normalize an icon glyph input into a valid string.
    /// </summary>
    internal static string NormalizeIconGlyph(string? rawInput)
    {
        if (string.IsNullOrWhiteSpace(rawInput))
            return _defaultIconGlyph;

        var trimmed = rawInput.Trim();

        if (Enum.TryParse<SeIconChar>(trimmed, true, out var iconEnum))
            return SeIconCharExtensions.ToIconString(iconEnum);

        var hexCandidate = trimmed.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
            ? trimmed[2..]
            : trimmed;

        if (ushort.TryParse(hexCandidate, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var hexValue))
            return char.ConvertFromUtf32(hexValue);

        var enumerator = trimmed.EnumerateRunes();
        if (enumerator.MoveNext())
            return enumerator.Current.ToString();

        return _defaultIconGlyph;
    }

    /// <summary>
    /// Is the nameplate addon visible?
    /// </summary>
    private bool IsNamePlateAddonVisible()
    {
        if (_mpNameplateAddon == null)
            return false;

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

    private readonly struct NameplateLabelInfo
    {
        public NameplateLabelInfo(
            Vector2 screenPosition,
            string text,
            uint textColor,
            uint edgeColor,
            float fontSize,
            Vector2 pivot,
            bool useIcon)
        {
            ScreenPosition = screenPosition;
            Text = text;
            TextColor = textColor;
            EdgeColor = edgeColor;
            FontSize = fontSize;
            Pivot = pivot;
            UseIcon = useIcon;
        }

        public Vector2 ScreenPosition { get; }
        public string Text { get; }
        public uint TextColor { get; }
        public uint EdgeColor { get; }
        public float FontSize { get; }
        public Vector2 Pivot { get; }
        public bool UseIcon { get; }
    }

    /// <summary>
    /// Visible paired user IDs snapshot.
    /// </summary>
    private HashSet<ulong> VisibleUserIds
        => [.. _pairUiService.GetSnapshot().PairsByUid.Values
            .Where(u => u.IsVisible && u.PlayerCharacterId != uint.MaxValue)
            .Select(u => (ulong)u.PlayerCharacterId)];

    public int DebugLabelCountLastFrame { get; set; }
    public int DebugUiRectCountLastFrame { get; set; }
    public int DebugOccludedCountLastFrame { get; set; }
    public uint DebugLastNameplateFrame { get; set; }
    public bool DebugDrawUiRects { get; set; }
    public bool DebugDrawLabelRects { get; set; } = true;
    public bool DebugDisableOcclusion { get; set; }
    public bool DebugEnabled { get; set; }

    public void FlagRefresh()
    {
        _needsLabelRefresh = true;
    }

    public void OnTick(PriorityFrameworkUpdateMessage _)
    {
        if (!IsPictomancyRenderer)
        {
            _needsLabelRefresh = false;
            return;
        }

        if (_needsLabelRefresh)
        {
            UpdateNameplateNodes();
            _needsLabelRefresh = false;
        }
    }

    /// <summary>
    /// Update the active broadcasting CIDs.
    /// </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: {Cids}", string.Join(',', _activeBroadcastingCids));
        FlagRefresh();
    }

    /// <summary>
    /// Clears all nameplate related caches.
    /// </summary>
    public void ClearNameplateCaches()
    {
        _buffers.Clear();
        ClearLabelBuffer();

        Array.Clear(_buffers.HasSmoothed, 0, _buffers.HasSmoothed.Length);
        Array.Clear(_buffers.SmoothedPos, 0, _buffers.SmoothedPos.Length);
    }

    private sealed class NameplateBuffers
    {
        public NameplateBuffers()
        {
            TextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
            Array.Fill(TextOffsets, int.MinValue);
        }

        public int[] TextWidths { get; } = new int[AddonNamePlate.NumNamePlateObjects];
        public int[] TextHeights { get; } = new int[AddonNamePlate.NumNamePlateObjects];
        public int[] ContainerHeights { get; } = new int[AddonNamePlate.NumNamePlateObjects];
        public int[] TextOffsets { get; }
        public NameplateLabelInfo[] LabelScratch { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
        public NameplateLabelInfo[] LabelRender { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];
        public NameplateLabelInfo[] LabelCopy { get; } = new NameplateLabelInfo[AddonNamePlate.NumNamePlateObjects];

        public Vector2[] SmoothedPos = new Vector2[AddonNamePlate.NumNamePlateObjects];
        public bool[] HasSmoothed = new bool[AddonNamePlate.NumNamePlateObjects];

        public void Clear()
        {
            Array.Clear(TextWidths, 0, TextWidths.Length);
            Array.Clear(TextHeights, 0, TextHeights.Length);
            Array.Clear(ContainerHeights, 0, ContainerHeights.Length);
            Array.Fill(TextOffsets, int.MinValue);
        }
    }

    /// <summary>
    /// Starts the LightFinder Plate Handler.
    /// </summary>
    public Task StartAsync(CancellationToken cancellationToken)
    {
        Init();
        return Task.CompletedTask;
    }

    /// <summary>
    /// Stops the LightFinder Plate Handler.
    /// </summary>
    public Task StopAsync(CancellationToken cancellationToken)
    {
        Uninit();
        return Task.CompletedTask;
    }

    /// <summary>
    /// Rectangle with float coordinates for intersection testing.
    /// </summary>
    [StructLayout(LayoutKind.Auto)]
    private readonly struct RectF
    {
        public readonly float L, T, R, B;
        public RectF(float l, float t, float r, float b) { L = l; T = t; R = r; B = b; }

        public bool Intersects(in RectF o) =>
            !(R <= o.L || o.R <= L || B <= o.T || o.B <= T);
    }
}
