using HSUI.Config;
using HSUI.Interface.GeneralElements;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Dalamud.Bindings.ImGui;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Numerics;
using System.Runtime.CompilerServices;

namespace HSUI.Helpers
{
    public class ClipRectsHelper
    {
        #region Singleton
        private ClipRectsHelper()
        {
            ConfigurationManager.Instance.ResetEvent += OnConfigReset;
            OnConfigReset(ConfigurationManager.Instance);

            // other plugins can add clip rects for HSUI
            // rect start point = vector.X, vector.Y
            // rect end point = vector.Z, vector.W
            _thirdPartyClipRects = Plugin.PluginInterface.GetOrCreateData<Dictionary<string, Vector4>>(_sharedDataId, () => new());
        }

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

        public static ClipRectsHelper Instance { get; private set; } = null!;

        ~ClipRectsHelper()
        {
            Dispose(false);
        }

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

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

            ConfigurationManager.Instance.ResetEvent -= OnConfigReset;

            Plugin.PluginInterface.RelinquishData(_sharedDataId);

            Instance = null!;
        }
        #endregion

        private WindowClippingConfig _config = null!;

        private void OnConfigReset(ConfigurationManager sender)
        {
            _config = sender.GetConfigObject<WindowClippingConfig>();
        }

        public bool Enabled => _config.Enabled;
        public WindowClippingMode? Mode => _config.Enabled ? _config.Mode : null;

        private List<ClipRect> _clipRects = new List<ClipRect>();
        private List<ClipRect> _extraClipRects = new List<ClipRect>();

        private static Dictionary<string, Vector4> _thirdPartyClipRects = new();
        private static string _sharedDataId = "HSUI.ClipRects";

        private static List<string> _ignoredAddonNames = new List<string>()
        {
            "_FocusTargetInfo",
        };

        private readonly string[] _hotbarAddonNames = { "_ActionBar", "_ActionBar01", "_ActionBar02", "_ActionBar03", "_ActionBar04", "_ActionBar05", "_ActionBar06", "_ActionBar07", "_ActionBar08", "_ActionBar09" };

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

            _clipRects.Clear();
            _extraClipRects.Clear();

            // find clip rects for game windows
            AtkStage* stage = AtkStage.Instance();
            if (stage == null) { return; }

            RaptureAtkUnitManager* manager = stage->RaptureAtkUnitManager;
            if (manager == null) { return; }

            AtkUnitList* loadedUnitsList = &manager->AtkUnitManager.AllLoadedUnitsList;
            if (loadedUnitsList == null) { return; }

            for (int i = 0; i < loadedUnitsList->Count; i++)
            {
                try
                {
                    AtkUnitBase* addon = *(AtkUnitBase**)Unsafe.AsPointer(ref loadedUnitsList->Entries[i]);
                    if (addon == null || addon->RootNode == null || !addon->IsVisible || addon->WindowNode == null || addon->Scale == 0 || !addon->WindowNode->IsVisible())
                    {
                        continue;
                    }

                    string name = addon->NameString;
                    if (_ignoredAddonNames.Contains(name))
                    {
                        continue;
                    }

                    float margin = 5 * addon->Scale;
                    float bottomMargin = 13 * addon->Scale;

                    Vector2 pos = new Vector2(addon->RootNode->X + margin, addon->RootNode->Y + margin);
                    Vector2 size = new Vector2(
                        addon->RootNode->Width * addon->Scale - margin,
                        addon->RootNode->Height * addon->Scale - bottomMargin
                    );

                    // just in case this causes weird issues / crashes (doubt it though...)
                    ClipRect clipRect = new ClipRect(pos, pos + size);
                    if (clipRect.Max.X < clipRect.Min.X || clipRect.Max.Y < clipRect.Min.Y)
                    {
                        continue;
                    }

                    _clipRects.Add(clipRect);
                }
                catch { }
            }

            if (_config.ThirdPartyClipRectsEnabled)
            {
                // find clip rects from other plugins
                Dictionary<string, Vector4> dict = _thirdPartyClipRects;
                foreach (Vector4 vector in dict.Values)
                {
                    ClipRect clipRect = new ClipRect(new(vector.X, vector.Y), new(vector.Z, vector.W));
                    _clipRects.Add(clipRect);
                }
            }
        }

        private List<ClipRect> ActiveClipRects()
        {
            return [.. _clipRects, .. _extraClipRects];
        }

        public void AddNameplatesClipRects()
        {
            if (!_config.NameplatesClipRectsEnabled) { return; }

            // target cast bar
            ClipRect? targetCastbarClipRect = GetTargetCastbarClipRect();
            if (targetCastbarClipRect.HasValue)
            {
                _extraClipRects.Add(targetCastbarClipRect.Value);
            }

            // hotbars
            _extraClipRects.AddRange(GetHotbarsClipRects());

            // chat bubbles
            _extraClipRects.AddRange(GetNPCChatBubbleClipRect());
            _extraClipRects.AddRange(GetPlayerChatBubbleClipRect());
        }

        public void RemoveNameplatesClipRects()
        {
            _extraClipRects.Clear();
        }

        private unsafe ClipRect? GetTargetCastbarClipRect()
        {
            if (!_config.TargetCastbarClipRectEnabled) { return null; }

            AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName("_TargetInfoCastBar", 1).Address;
            if (addon == null || !addon->IsVisible) { return null; }

            AtkResNode* baseNode = addon->GetNodeById(2);
            AtkImageNode* imageNode = addon->GetImageNodeById(7);

            if (baseNode == null || !baseNode->IsVisible()) { return null; }
            if (imageNode == null || !imageNode->IsVisible()) { return null; }

            Vector2 pos = new Vector2(
                addon->X + (baseNode->X * addon->Scale),
                addon->Y + (baseNode->Y * addon->Scale)
            );
            Vector2 size = new Vector2(
                imageNode->Width * addon->Scale,
                imageNode->Height * addon->Scale
            );

            return new ClipRect(pos, pos + size);
        }

        private unsafe List<ClipRect> GetHotbarsClipRects()
        {
            List<ClipRect> rects = new List<ClipRect>();
            if (!_config.HotbarsClipRectsEnabled) { return rects; }

            foreach (string addonName in _hotbarAddonNames)
            {
                AtkUnitBase* addon = (AtkUnitBase*)Plugin.GameGui.GetAddonByName(addonName, 1).Address;
                if (addon == null || !addon->IsVisible) { continue; }

                AtkComponentNode* firstNode = addon->GetComponentNodeById(8);
                AtkComponentNode* lastNode = addon->GetComponentNodeById(19);

                if (firstNode == null || lastNode == null) { continue; }


                float margin = 10f * addon->Scale;

                Vector2 min = new Vector2(
                    addon->X + (firstNode->AtkResNode.X * addon->Scale) + margin,
                    addon->Y + (firstNode->AtkResNode.Y * addon->Scale) + margin
                );
                Vector2 max = new Vector2(
                    addon->X + (lastNode->AtkResNode.X * addon->Scale) + (lastNode->AtkResNode.Width * addon->Scale) - margin,
                    addon->Y + (lastNode->AtkResNode.Y * addon->Scale) + (lastNode->AtkResNode.Height * addon->Scale) - margin
                );

                rects.Add(new ClipRect(min, max));
            }

            return rects;
        }

        private unsafe List<ClipRect> GetNPCChatBubbleClipRect()
        {
            List<ClipRect> rects = new List<ClipRect>();
            if (!_config.ChatBubblesNPCClipRectsEnabled) { return rects; }

            var addon = (AddonMiniTalk*) Plugin.GameGui.GetAddonByName("_MiniTalk").Address;
            if (addon is null)
            {
                return rects;
            }

            foreach (var talkBubble in addon->TalkBubbles) {
                if (!talkBubble.ComponentNode->IsVisible())
                {
                    continue;
                }

                AtkNineGridNode* bubbleNineGridNode = talkBubble.BubbleNineGridNode;

                Vector2 position = new Vector2(
                    bubbleNineGridNode->ScreenX,
                    bubbleNineGridNode->ScreenY
                );
                Vector2 scale = GetNodeScale((AtkResNode*) bubbleNineGridNode, new Vector2(bubbleNineGridNode->ScaleX, bubbleNineGridNode->ScaleY));
                Vector2 size = new Vector2(
                    bubbleNineGridNode->Width,
                    bubbleNineGridNode->Height
                ) * scale;

                rects.Add(new ClipRect(position, position + size));
            }

            return rects;
        }

        public unsafe List<ClipRect> GetPlayerChatBubbleClipRect()
        {
            List<ClipRect> rects = new List<ClipRect>();
            if (!_config.ChatBubblesPlayersClipRectsEnabled) { return rects; }

            AtkUnitBase* addon = (AtkUnitBase*) Plugin.GameGui.GetAddonByName("MiniTalkPlayer").Address;
            if (addon is null)
            {
                return rects;
            }

            foreach (var node in addon->UldManager.Nodes) {
                if (node.Value is null)
                {
                    continue;
                }

                if (node.Value->GetNodeType() is not NodeType.Component || !node.Value->IsVisible())
                {
                    continue;
                }

                AtkComponentNode* componentNode = (AtkComponentNode*)node.Value;
                AtkComponentBase* component = componentNode->GetComponent();
                if (component is null)
                {
                    continue;
                }

                AtkResNode* bubbleNode = component->UldManager.SearchNodeById(4);
                if (bubbleNode is null)
                {
                    continue;
                }

                Vector2 position = new Vector2(
                    componentNode->ScreenX,
                    componentNode->ScreenY
                );
                Vector2 scale = GetNodeScale(bubbleNode, new Vector2(bubbleNode->ScaleX, bubbleNode->ScaleY));
                Vector2 size = new Vector2(
                    bubbleNode->Width,
                    bubbleNode->Height
                ) * scale;

                rects.Add(new ClipRect(position, position + size));
            }

            return rects;
        }

        public ClipRect? GetClipRectForArea(Vector2 pos, Vector2 size)
        {
            if (!_config.Enabled) { return null; }

            List<ClipRect> rects = ActiveClipRects();

            foreach (ClipRect clipRect in rects)
            {
                ClipRect area = new ClipRect(pos, pos + size);
                if (clipRect.IntersectsWith(area))
                {
                    return clipRect;
                }
            }

            return null;
        }

        public static ClipRect[] GetInvertedClipRects(ClipRect clipRect)
        {
            float maxX = ImGui.GetMainViewport().Size.X;
            float maxY = ImGui.GetMainViewport().Size.Y;

            Vector2 aboveMin = new Vector2(0, 0);
            Vector2 aboveMax = new Vector2(maxX, clipRect.Min.Y);
            Vector2 leftMin = new Vector2(0, clipRect.Min.Y);
            Vector2 leftMax = new Vector2(clipRect.Min.X, maxY);

            Vector2 rightMin = new Vector2(clipRect.Max.X, clipRect.Min.Y);
            Vector2 rightMax = new Vector2(maxX, clipRect.Max.Y);
            Vector2 belowMin = new Vector2(clipRect.Min.X, clipRect.Max.Y);
            Vector2 belowMax = new Vector2(maxX, maxY);

            ClipRect[] invertedClipRects = new ClipRect[4];
            invertedClipRects[0] = new ClipRect(aboveMin, aboveMax);
            invertedClipRects[1] = new ClipRect(leftMin, leftMax);
            invertedClipRects[2] = new ClipRect(rightMin, rightMax);
            invertedClipRects[3] = new ClipRect(belowMin, belowMax);

            return invertedClipRects;
        }

        public bool IsPointClipped(Vector2 point)
        {
            if (!_config.Enabled) { return false; }

            List<ClipRect> rects = ActiveClipRects();

            foreach (ClipRect clipRect in rects)
            {
                if (clipRect.Contains(point))
                {
                    return true;
                }
            }

            return false;
        }

        public static unsafe Vector2 GetNodeScale(AtkResNode* node, Vector2 currentScale) {
            if (node is null)
            {
                return currentScale;
            }

            if (node->ParentNode is not null) {
                currentScale.X *= node->ParentNode->GetScaleX();
                currentScale.Y *= node->ParentNode->GetScaleY();

                return GetNodeScale(node->ParentNode, currentScale);
            }

            return currentScale;
        }
    }

    public struct ClipRect
    {
        public readonly Vector2 Min;
        public readonly Vector2 Max;

        private readonly Rectangle Rectangle;

        public ClipRect(Vector2 min, Vector2 max)
        {
            Vector2 screenSize = ImGui.GetMainViewport().Size;

            Min = Clamp(min, Vector2.Zero, screenSize);
            Max = Clamp(max, Vector2.Zero, screenSize);

            Vector2 size = Max - Min;

            Rectangle = new Rectangle((int)Min.X, (int)Min.Y, (int)size.X, (int)size.Y);
        }

        public bool Contains(Vector2 point)
        {
            return Rectangle.Contains((int)point.X, (int)point.Y);
        }

        public bool IntersectsWith(ClipRect other)
        {
            return Rectangle.IntersectsWith(other.Rectangle);
        }

        public ClipRect? Intersect(ClipRect other)
        {
            float minX = Math.Max(Min.X, other.Min.X);
            float minY = Math.Max(Min.Y, other.Min.Y);
            float maxX = Math.Min(Max.X, other.Max.X);
            float maxY = Math.Min(Max.Y, other.Max.Y);
            if (minX >= maxX || minY >= maxY) return null;
            return new ClipRect(new Vector2(minX, minY), new Vector2(maxX, maxY));
        }

        private static Vector2 Clamp(Vector2 vector, Vector2 min, Vector2 max)
        {
            return new Vector2(Math.Max(min.X, Math.Min(max.X, vector.X)), Math.Max(min.Y, Math.Min(max.Y, vector.Y)));
        }
    }
}
