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.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.Logging;
using Microsoft.Extensions.Hosting;
using Pictomancy;
using System.Collections.Immutable;
using System.Globalization;
using System.Numerics;
using Task = System.Threading.Tasks.Task;

namespace LightlessSync.Services.LightFinder;

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 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 object _labelLock = new();
    private readonly NameplateBuffers _buffers = new();
    private int _labelRenderCount;

    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 ImmutableHashSet<string> _activeBroadcastingCids = [];

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

    }

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

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

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

            _mEnabled = false;
            ClearNameplateCaches();
        }
    }

    private void NameplateDrawDetour(AddonEvent type, AddonArgs args)
    {
        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 pNameplateAddon = (AddonNamePlate*)args.Addon.Address;

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

        UpdateNameplateNodes();
    }

    private void UpdateNameplateNodes()
    {
        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;
        }

        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 = System.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
            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;
            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;
            }

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

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

            if (!isVisible)
                continue;

            var scaleMultiplier = System.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)System.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)System.Math.Round(AtkNodeHelpers.DefaultTextNodeWidth * effectiveScale);
            var nodeHeight = (int)System.Math.Round(AtkNodeHelpers.DefaultTextNodeHeight * effectiveScale);
            AlignmentType alignment;

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

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

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

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

            var positionY = blockTop - verticalPadding;

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

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

            if (nameContainer == null)
            {
                if (_logger.IsEnabled(LogLevel.Debug))
                    _logger.LogDebug("Nameplate {Index} container became unavailable during update, skipping.", nameplateIndex);
                continue;
            }

            float finalX;
            if (currentConfig.LightfinderAutoAlign)
            {
                var measuredWidth = System.Math.Max(1, textWidth > 0 ? textWidth : nodeWidth);
                var measuredWidthF = (float)measuredWidth;
                var alignmentType = currentConfig.LabelAlignment;

                var containerScale = nameContainer->ScaleX;
                if (containerScale <= 0f)
                    containerScale = 1f;
                var containerWidthRaw = (float)nameContainer->Width;
                if (containerWidthRaw <= 0f)
                    containerWidthRaw = measuredWidthF;
                var containerWidth = containerWidthRaw * containerScale;
                if (containerWidth <= 0f)
                    containerWidth = measuredWidthF;

                var containerLeft = nameContainer->ScreenX;
                var containerRight = containerLeft + containerWidth;
                var containerCenter = containerLeft + (containerWidth * 0.5f);

                var iconMargin = currentConfig.LightfinderLabelUseIcon
                    ? System.Math.Min(containerWidth * 0.1f, 14f * containerScale)
                    : 0f;

                switch (alignmentType)
                {
                    case LabelAlignment.Left:
                        finalX = containerLeft + iconMargin;
                        alignment = AlignmentType.BottomLeft;
                        break;
                    case LabelAlignment.Right:
                        finalX = containerRight - iconMargin;
                        alignment = AlignmentType.BottomRight;
                        break;
                    default:
                        finalX = containerCenter;
                        alignment = AlignmentType.Bottom;
                        break;
                }

                finalX += currentConfig.LightfinderLabelOffsetX;
            }
            else
            {
                var cachedTextOffset = _buffers.TextOffsets[nameplateIndex];
                var hasCachedOffset = cachedTextOffset != int.MinValue;
                var baseOffsetX = (!currentConfig.LightfinderLabelUseIcon && hasValidOffset && hasCachedOffset) ? cachedTextOffset : 0;
                finalX = nameContainer->ScreenX + baseOffsetX + 58 + currentConfig.LightfinderLabelOffsetX;
                alignment = AlignmentType.Bottom;
            }

            positionY += currentConfig.LightfinderLabelOffsetY;
            alignment = (AlignmentType)System.Math.Clamp((int)alignment, 0, 8);

            var finalPosition = new Vector2(finalX, nameContainer->ScreenY + positionY);
            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;
            }
        }
    }

    private void OnUiBuilderDraw()
    {
        if (!_mEnabled)
            return;

        int copyCount;
        lock (_labelLock)
        {
            copyCount = _labelRenderCount;
            if (copyCount == 0)
                return;

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

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

        for (int i = 0; i < copyCount; ++i)
        {
            ref var info = ref _buffers.LabelCopy[i];
            var font = default(ImFontPtr);
            if (info.UseIcon)
            {
                var ioFonts = ImGui.GetIO().Fonts;
                font = ioFonts.Fonts.Size > 1 ? new ImFontPtr(ioFonts.Fonts[1]) : ImGui.GetFont();
            }

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

    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)System.Math.Clamp(color.X * 255f, 0f, 255f);
        var g = (byte)System.Math.Clamp(color.Y * 255f, 0f, 255f);
        var b = (byte)System.Math.Clamp(color.Z * 255f, 0f, 255f);
        var a = (byte)System.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)System.Math.Round(rawHeight * scale);
        return System.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)System.Math.Round(rawWidth * scale);
        return System.Math.Max(1, computed);
    }

    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 (System.Math.Abs(measuredTextWidth) > 0 || textOffset != 0)
        {
            _buffers.TextOffsets[nameplateIndex] = textOffset;
            return true;
        }

        return false;
    }

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

    internal static string ToIconEditorString(string? rawInput)
    {
        var normalized = NormalizeIconGlyph(rawInput);
        var runeEnumerator = normalized.EnumerateRunes();
        return runeEnumerator.MoveNext()
            ? runeEnumerator.Current.Value.ToString("X4", CultureInfo.InvariantCulture)
            : DefaultIconGlyph;
    }
    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; }
    }

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

    public void FlagRefresh()
    {
        _needsLabelRefresh = true;
    }

    public void OnTick(PriorityFrameworkUpdateMessage _)
    {
        if (_needsLabelRefresh)
        {
            UpdateNameplateNodes();
            _needsLabelRefresh = false;
        }
    }

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

    public void ClearNameplateCaches()
    {
        _buffers.Clear();
        ClearLabelBuffer();
    }

    private sealed class NameplateBuffers
    {
        public NameplateBuffers()
        {
            TextOffsets = new int[AddonNamePlate.NumNamePlateObjects];
            System.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 void Clear()
        {
            System.Array.Clear(TextWidths, 0, TextWidths.Length);
            System.Array.Clear(TextHeights, 0, TextHeights.Length);
            System.Array.Clear(ContainerHeights, 0, ContainerHeights.Length);
            System.Array.Fill(TextOffsets, int.MinValue);
        }
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        Init();
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        Uninit();
        return Task.CompletedTask;
    }

}