using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Memory;
using HSUI.Config;
using HSUI.Helpers;
using HSUI.Interface.GeneralElements;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
using System.Numerics;
using static FFXIVClientStructs.FFXIV.Client.UI.AddonNamePlate;
using static FFXIVClientStructs.FFXIV.Client.UI.RaptureAtkModule;
using static FFXIVClientStructs.FFXIV.Client.UI.UI3DModule;
using StructsFramework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework;
using StructsGameObject = FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject;

namespace HSUI.Interface.Nameplates
{
    internal class NameplatesManager : IDisposable
    {
        #region Singleton
        public static NameplatesManager Instance { get; private set; } = null!;
        private NameplatesGeneralConfig _config = null!;

        private NameplatesManager()
        {
            Plugin.ClientState.TerritoryChanged -= ClientStateOnTerritoryChangedEvent;
            ConfigurationManager.Instance.ResetEvent += OnConfigReset;

            OnConfigReset(ConfigurationManager.Instance);
        }

        public static void Initialize()
        {
            Instance = new NameplatesManager();
        }

        ~NameplatesManager()
        {
            Dispose(false);
        }

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

        protected void Dispose(bool disposing)
        {
            if (!disposing)
            {
                return;
            }

            Plugin.ClientState.TerritoryChanged -= ClientStateOnTerritoryChangedEvent;

            Instance = null!;
        }

        private void OnConfigReset(ConfigurationManager sender)
        {
            _config = sender.GetConfigObject<NameplatesGeneralConfig>();
        }
        #endregion Singleton

        private const int NameplateCount = 50;
        private const int NameplateDataArrayIndex = 4; // TODO: Rework to use NamePlateStringArray if it exists or use AtkStage.Instance()->GetStringArrayData(StringArrayType.NamePlate)
        private Vector2 _averageNameplateSize = new Vector2(250, 150);
        private List<NameplateData> _data = new List<NameplateData>();
        public IReadOnlyCollection<NameplateData> Data => _data.AsReadOnly();

        private NameplatesCache _cache = new NameplatesCache(50);
        private Dictionary<uint, Vector2> _smoothedPositions = new(50);
        private const float PositionSmoothFactor = 0.3f; // Lerp factor: lower = smoother, higher = more responsive
        private const float PlayerPositionSmoothFactor = 0.15f; // Stronger smoothing for player (camera-follow causes more jitter)

        private void ClientStateOnTerritoryChangedEvent(ushort territoryId)
        {
            _cache.Clear();
            _smoothedPositions.Clear();
        }

        public unsafe void Update()
        {
            if (!_config.Enabled) { return; }

            UIModule* uiModule = StructsFramework.Instance()->GetUIModule();
            if (uiModule == null) { return; }

            UI3DModule* ui3DModule = uiModule->GetUI3DModule();
            if (ui3DModule == null) { return; }

            AddonNamePlate* addon = (AddonNamePlate*)Plugin.GameGui.GetAddonByName("NamePlate", 1).Address;
            if (addon == null) { return; }

            RaptureAtkModule* atkModule = uiModule->GetRaptureAtkModule();
            if (atkModule == null || atkModule->AtkModule.AtkArrayDataHolder.StringArrayCount <= NameplateDataArrayIndex) { return; }

            StringArrayData* stringArray = atkModule->AtkModule.AtkArrayDataHolder.StringArrays[NameplateDataArrayIndex];
            Span<NamePlateInfo> infoArray = atkModule->NamePlateInfoEntries;
            Camera camera = Control.Instance()->CameraManager.Camera->CameraBase.SceneCamera;

            IGameObject? target = Plugin.TargetManager.Target;
            bool foundTarget = false;
            NameplateData? targetData = null;

            _data = new List<NameplateData>();
            int activeCount = ui3DModule->NamePlateObjectInfoCount;
            var nextSmoothed = new Dictionary<uint, Vector2>(Math.Min(activeCount + 4, 54));

            for (int i = 0; i < activeCount; i++)
            {
                try
                {
                    ObjectInfo* objectInfo = ui3DModule->NamePlateObjectInfoPointers[i];
                    if (objectInfo == null || objectInfo->NamePlateIndex >= NameplateCount) { continue; }

                    // actor
                    StructsGameObject* obj = objectInfo->GameObject;
                    if (obj == null) { continue; }

                    bool isTarget = false;
                    IGameObject? gameObject = Plugin.ObjectTable.CreateObjectReference(new IntPtr(obj));
                    if (target != null && new IntPtr(obj) == target.Address)
                    {
                        isTarget = true;
                        foundTarget = true;
                    }

                    // ui nameplate (may be stale when addon is hidden)
                    NamePlateObject nameplateObject = addon->NamePlateObjectArray[objectInfo->NamePlateIndex];

                    Vector3 worldPos = new Vector3(obj->Position.X, obj->Position.Y + obj->Height * 2.2f, obj->Position.Z);

                    // Screen position: use addon when available (game's logic, stable). WorldToScreen when addon hidden/stale.
                    var hudConfig = ConfigurationManager.Instance?.GetConfigObject<HUDOptionsConfig>();
                    bool hidingAddon = _config.Enabled && (hudConfig?.HideDefaultHudWhenReplaced ?? true);
                    bool isPlayer = Plugin.ObjectTable.LocalPlayer != null && new IntPtr(obj) == Plugin.ObjectTable.LocalPlayer.Address;

                    Vector2 screenPos;
                    bool addonNodeValid = nameplateObject.RootComponentNode != null;
                    // Use addon position when visible, or when hiding (game may still update nodes). Fall back to WorldToScreen if stale.
                    bool useAddonPos = addonNodeValid && (addon->IsVisible || hidingAddon);
                    if (useAddonPos)
                    {
                        float nx = nameplateObject.RootComponentNode->AtkResNode.X;
                        float ny = nameplateObject.RootComponentNode->AtkResNode.Y;
                        float nw = nameplateObject.RootComponentNode->AtkResNode.Width;
                        float nh = nameplateObject.RootComponentNode->AtkResNode.Height;
                        screenPos = new Vector2(nx + nw / 2f, ny + nh);
                        // Sanity: when addon hidden, if pos looks stale (off-screen), fall back to WorldToScreen
                        if (hidingAddon && (screenPos.X < -500 || screenPos.X > 3000 || screenPos.Y < -500 || screenPos.Y > 3000))
                        {
                            Plugin.GameGui.WorldToScreen(worldPos, out screenPos);
                            useAddonPos = false;
                        }
                    }
                    else
                    {
                        Plugin.GameGui.WorldToScreen(worldPos, out screenPos);
                    }
                    screenPos = ClampScreenPosition(screenPos);
                    // Temporal smoothing for WorldToScreen-sourced positions (addon pos is usually stable)
                    uint objId = obj->GetGameObjectId().ObjectId;
                    if (!useAddonPos && _smoothedPositions.TryGetValue(objId, out Vector2 prev))
                    {
                        float factor = isPlayer ? PlayerPositionSmoothFactor : PositionSmoothFactor;
                        screenPos = Vector2.Lerp(prev, screenPos, factor);
                    }
                    nextSmoothed[objId] = screenPos;

                    // distance
                    float distance = Vector3.Distance(camera.Object.Position, worldPos);

                    // name
                    NamePlateInfo info = infoArray[objectInfo->NamePlateIndex];
                    string name = info.Name.ToString();

                    // title
                    string title = info.Title.ToString();
                    bool isTitlePrefix = info.IsPrefixTitle;

                    // Get the title from Honorific, if it exists
                    TitleData? customTitleData = HonorificHelper.Instance?.GetTitle(gameObject);
                    if (customTitleData != null)
                    {
                        title = customTitleData.Title;
                        isTitlePrefix = customTitleData.IsPrefix;
                    }

                    // Quest/state icon: use GameObject.NamePlateIconId (game logic, works when addon hidden).
                    // Fallback to addon's NameIcon texture if GameObject has none (addon must be visible).
                    int iconId = (int)obj->NamePlateIconId;
                    if (iconId == 0)
                    {
                        try
                        {
                            AtkUldAsset* textureInfo = nameplateObject.NameIcon->PartsList->Parts[nameplateObject.NameIcon->PartId].UldAsset;
                            if (textureInfo != null && textureInfo->AtkTexture.Resource != null)
                                iconId = (int)textureInfo->AtkTexture.Resource->IconId;
                        }
                        catch { /* addon node may be null/stale when hidden */ }
                    }

                    // order
                    int arrayIndex = 200 + (activeCount - nameplateObject.Priority - 1);
                    string order = "";
                    try
                    {
                        if (stringArray->AtkArrayData.Size > arrayIndex && stringArray->StringArray[arrayIndex] != null)
                        {
                            order = MemoryHelper.ReadSeStringNullTerminated(new IntPtr(stringArray->StringArray[arrayIndex])).ToString();
                        }
                    }
                    catch { }

                    NameplateData data = new NameplateData(
                        gameObject,
                        name,
                        title,
                        isTitlePrefix,
                        iconId,
                        order,
                        (ObjectKind)obj->ObjectKind,
                        obj->SubKind,
                        screenPos,
                        worldPos,
                        distance
                    );

                    if (isTarget)
                    {
                        targetData = data;
                    }
                    else
                    {
                        _data.Add(data);
                    }

                    _cache.Add(obj->GetGameObjectId().ObjectId, data);
                }
                catch { }
            }

            _smoothedPositions = nextSmoothed;
            _data.Reverse();

            // add target nameplate last
            if (foundTarget && targetData.HasValue)
            {
                _data.Add(targetData.Value);
            }
            // create nameplate for target?
            else if (_config.AlwaysShowTargetNameplate && target != null && !foundTarget)
            {
                StructsGameObject* obj = (StructsGameObject*)target.Address;
                NameplateData? cachedData = _cache[(uint)target.GameObjectId];

                Vector3 worldPos = new Vector3(target.Position.X, target.Position.Y + obj->Height * 2.2f, target.Position.Z);
                float distance = Vector3.Distance(camera.Object.Position, worldPos);

                Plugin.GameGui.WorldToScreen(worldPos, out Vector2 screenPos);
                screenPos = ClampScreenPosition(screenPos);

                targetData = new NameplateData(
                    target,
                    target.Name.ToString(),
                    cachedData?.Title ?? "",
                    cachedData?.IsTitlePrefix ?? true,
                    cachedData?.NamePlateIconId ?? 0,
                    cachedData?.Order ?? "",
                    target.ObjectKind,
                    target.SubKind,
                    screenPos,
                    worldPos,
                    distance,
                    true
                );

                _data.Add(targetData.Value);
            }
        }

        private Vector2 ClampScreenPosition(Vector2 pos)
        {
            if (!_config.ClampToScreen) { return pos; }

            Vector2 screenSize = ImGui.GetMainViewport().Size;
            Vector2 nameplateSize = _averageNameplateSize / 2f;
            float margin = 20;

            if (pos.X + nameplateSize.X > screenSize.X)
                pos.X = screenSize.X - nameplateSize.X - margin;
            else if (pos.X - nameplateSize.X < 0)
                pos.X = nameplateSize.X + margin;

            if (pos.Y + nameplateSize.Y > screenSize.Y)
                pos.Y = screenSize.Y - nameplateSize.Y - margin;
            else if (pos.Y - nameplateSize.Y < 0)
                pos.Y = nameplateSize.Y + margin;

            return pos;
        }
    }

    #region utils
    public class NameplatesCache
    {

        private int _limit;
        private Dictionary<uint, NameplateData> _dict;
        private Queue<uint> _queue;

        public NameplatesCache(int limit)
        {
            _limit = limit;
            _dict = new Dictionary<uint, NameplateData>(limit);
            _queue = new Queue<uint>(limit);
        }

        public void Add(uint key, NameplateData data)
        {
            if (key == 0 || key == 0xE0000000) { return; }

            if (_dict.Count == _limit)
            {
                uint oldestKey = _queue.Dequeue();
                _dict.Remove(oldestKey);
            }

            if (_dict.ContainsKey(key))
            {
                _dict[key] = data;
            }
            else
            {
                _dict.Add(key, data);
                _queue.Enqueue(key);
            }
        }

        public void Clear()
        {
            _dict.Clear();
            _queue.Clear();
        }

        public NameplateData? this[uint key]
        {
            get
            {
                if (_dict.TryGetValue(key, out NameplateData data))
                {
                    return data;
                }

                return null;
            }
        }
    }

    public struct NameplateData
    {
        public IGameObject? GameObject;
        public string Name;
        public string Title;
        public bool IsTitlePrefix;
        public int NamePlateIconId;
        public string Order;
        public ObjectKind Kind;
        public byte SubKind;
        public Vector2 ScreenPosition;
        public Vector3 WorldPosition;
        public float Distance;
        public bool IgnoreOcclusion;

        public NameplateData(IGameObject? gameObject, string name, string title, bool isTitlePrefix, int namePlateIconId, string order, ObjectKind kind, byte subKind, Vector2 screenPosition, Vector3 worldPosition, float distance, bool ignoreOcclusion = false)
        {
            GameObject = gameObject;
            Name = name;
            Title = title;
            IsTitlePrefix = isTitlePrefix;
            NamePlateIconId = namePlateIconId;
            Order = order;
            Kind = kind;
            SubKind = subKind;
            ScreenPosition = screenPosition;
            WorldPosition = worldPosition;
            Distance = distance;
            IgnoreOcclusion = ignoreOcclusion;
        }
    }
    #endregion
}
