/*
Copyright(c) 2021 attickdoor (https://github.com/attickdoor/MOActionPlugin)
Modifications Copyright(c) 2021 HSUI
09/21/2021 - Used original's code hooks and action validations while using
HSUI's own logic to select a target.

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Game.ClientState.Objects.SubKinds;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using HSUI.Config;
using HSUI.Interface.GeneralElements;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using FFXIVClientStructs.FFXIV.Component.GUI;
using System.Runtime.CompilerServices;
using Dalamud.Bindings.ImGui;
using Lumina.Excel;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using static FFXIVClientStructs.FFXIV.Client.Game.ActionManager;
using Action = Lumina.Excel.Sheets.Action;
using BattleNpcSubKind = Dalamud.Game.ClientState.Objects.Enums.BattleNpcSubKind;

namespace HSUI.Helpers
{
    public unsafe class InputsHelper : IDisposable
    {
        private delegate bool UseActionDelegate(ActionManager* manager, ActionType actionType, uint actionId, ulong targetId, uint extraParam, UseActionMode mode, uint comboRouteId, bool* outOptAreaTargeted);

        private delegate byte ExecuteSlotByIdDelegate(RaptureHotbarModule* module, uint hotbarId, uint slotId);

        #region Singleton
        private InputsHelper()
        {
            _sheet = Plugin.DataManager.GetExcelSheet<Action>();

            //try
            //{
            //    /*
            //     Part of setUIMouseOverActorId disassembly signature
            //    .text:00007FF64830FD70                   sub_7FF64830FD70 proc near
            //    .text:00007FF64830FD70 48 89 91 90 02 00+mov     [rcx+290h], rdx
            //    .text:00007FF64830FD70 00
            //    */

            //    _uiMouseOverActorHook = Plugin.GameInteropProvider.HookFromSignature<OnSetUIMouseoverActor>(
            //        "E8 ?? ?? ?? ?? 48 8B 7C 24 ?? 4C 8B 74 24 ?? 83 FD 02",
            //        HandleUIMouseOverActorId
            //    );
            //}
            //catch
            //{
            //    Plugin.Logger.Error("InputsHelper OnSetUIMouseoverActor Hook failed!!!");
            //}

            try
            {
                _requestActionHook = Plugin.GameInteropProvider.HookFromSignature<UseActionDelegate>(
                    ActionManager.Addresses.UseAction.String,
                    HandleRequestAction
                );
                _requestActionHook?.Enable();
            }
            catch
            {
                Plugin.Logger.Error("InputsHelper UseActionDelegate Hook failed!!!");
            }

            try
            {
                nint addr = (nint)RaptureHotbarModule.Addresses.ExecuteSlotById.Value;
                if (addr != IntPtr.Zero)
                {
                    _executeSlotByIdHook = Plugin.GameInteropProvider.HookFromAddress<ExecuteSlotByIdDelegate>(addr, HandleExecuteSlotById);
                    _executeSlotByIdHook.Enable();
                    Plugin.Logger.Info("[HSUI] ExecuteSlotById hook installed (drag-drop overwrite protection)");
                }
                else
                {
                    _executeSlotByIdHook = Plugin.GameInteropProvider.HookFromSignature<ExecuteSlotByIdDelegate>(
                        "4C 8B C9 41 83 F8 10 73 45",
                        HandleExecuteSlotById
                    );
                    _executeSlotByIdHook?.Enable();
                    Plugin.Logger.Info("[HSUI] ExecuteSlotById hook installed via signature");
                }
            }
            catch (Exception ex)
            {
                Plugin.Logger.Error($"InputsHelper ExecuteSlotById Hook failed: {ex.Message}");
            }

            // mouseover setting
            ConfigurationManager.Instance.ResetEvent += OnConfigReset;
            Plugin.Framework.Update += OnFrameworkUpdate;

            OnConfigReset(ConfigurationManager.Instance);
        }

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

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

        public static int InitializationDelay = 5;

        ~InputsHelper()
        {
            Dispose(false);
        }

        public void Dispose()
        {
            Plugin.Logger.Info("\tDisposing InputsHelper...");
            Dispose(true);
            GC.SuppressFinalize(this);
        }

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

            // Restore WndProc first so left-click works again immediately after unload.
            // After a game/Dalamud patch, other singletons may be disposed before us;
            // if we unsub or touch config first and that throws, we would never restore and LButton stays broken.
            RestoreWndProc();

            try
            {
                ConfigurationManager.Instance.ResetEvent -= OnConfigReset;
            }
            catch { /* Instance may already be disposed */ }

            try
            {
                Plugin.Framework.Update -= OnFrameworkUpdate;
            }
            catch { /* Framework may already be disposed */ }

            Plugin.Logger.Info("\t\tDisposing _requestActionHook: " + (_requestActionHook?.Address.ToString("X") ?? "null"));
            _requestActionHook?.Disable();
            _requestActionHook?.Dispose();
            _executeSlotByIdHook?.Disable();
            _executeSlotByIdHook?.Dispose();

            Instance = null!;
        }
        #endregion

        private HUDOptionsConfig _config = null!;

        //private Hook<OnSetUIMouseoverActor>? _uiMouseOverActorHook;

        private Hook<UseActionDelegate>? _requestActionHook;
        private Hook<ExecuteSlotByIdDelegate>? _executeSlotByIdHook;

        private ExcelSheet<Action>? _sheet;

        public bool HandlingMouseInputs { get; private set; } = false;
        private IGameObject? _target = null;
        private bool _ignoringMouseover = false;

        public bool IsProxyEnabled => _config.InputsProxyEnabled;

        public void ToggleProxy(bool enabled)
        {
            _config.InputsProxyEnabled = enabled;
            ConfigurationManager.Instance.SaveConfigurations();
        }

        public void SetTarget(IGameObject? target, bool ignoreMouseover = false)
        {
            if (!IsProxyEnabled &&
                ClipRectsHelper.Instance?.IsPointClipped(ImGui.GetMousePos()) == false)
            {
                ImGui.SetNextFrameWantCaptureMouse(true);
            }

            _target = target;
            HandlingMouseInputs = true;
            _ignoringMouseover = ignoreMouseover;

            if (!_ignoringMouseover)
            {
                long address = _target != null && _target.GameObjectId != 0 ? (long)_target.Address : 0;
                SetGameMouseoverTarget(address);
            }
        }

        public void ClearTarget()
        {
            _target = null;
            HandlingMouseInputs = false;

            SetGameMouseoverTarget(0);
        }

        public void StartHandlingInputs()
        {
            HandlingMouseInputs = true;
        }

        public void StopHandlingInputs()
        {
            HandlingMouseInputs = false;
            _ignoringMouseover = false;
        }

        private unsafe void SetGameMouseoverTarget(long address)
        {
            if (!_config.MouseoverEnabled || _config.MouseoverAutomaticMode || _ignoringMouseover)
            {
                return;
            }

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

            PronounModule* pronounModule = uiModule->GetPronounModule();
            if (pronounModule == null) { return; }

            pronounModule->UiMouseOverTarget = (GameObject*)address;
        }

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

        //private void HandleUIMouseOverActorId(long arg1, long arg2)
        //{
        //Plugin.Logger.Log("MO: {0} - {1}", arg1.ToString("X"), arg2.ToString("X"));
        //_uiMouseOverActorHook?.Original(arg1, arg2);
        //}

        private bool HandleRequestAction(
            ActionManager* manager,
            ActionType actionType,
            uint actionId,
            ulong targetId,
            uint extraParam,
            UseActionMode mode,
            uint comboRouteId,
            bool* outOptAreaTargeted
        )
        {
            if (_requestActionHook == null) { return false; }

            // Block UseAction when we just placed this action via drag-drop (game may execute from drop via path that bypasses WndProc)
            var (suppressActionId, suppressUntil) = _suppressUseActionForDrop;
            if (suppressActionId != 0 && actionId == suppressActionId && ImGui.GetTime() < suppressUntil)
            {
                if (IsActionBarDragDropDebugEnabled())
                    Plugin.Logger.Information($"[HSUI DragDrop DBG] UseAction SUPPRESSED actionId={actionId} (drop cooldown)");
                _suppressUseActionForDrop = (0, 0);
                return false;
            }
            if (ImGui.GetTime() >= suppressUntil)
                _suppressUseActionForDrop = (0, 0);

            if (_config.MouseoverEnabled &&
                _config.MouseoverAutomaticMode &&
                _target != null &&
                IsActionValid(actionId, _target) &&
                !_ignoringMouseover)
            {
                return _requestActionHook.Original(manager, actionType, actionId, _target.GameObjectId, extraParam, mode, comboRouteId, outOptAreaTargeted);
            }

            return _requestActionHook.Original(manager, actionType, actionId, targetId, extraParam, mode, comboRouteId, outOptAreaTargeted);
        }

        private byte HandleExecuteSlotById(RaptureHotbarModule* module, uint hotbarId, uint slotId)
        {
            var (suppressBar, suppressSlot, suppressUntil) = _suppressExecuteSlotByIdForDrop;
            if (suppressBar < 10 && hotbarId == suppressBar && slotId == suppressSlot && ImGui.GetTime() < suppressUntil)
            {
                if (IsActionBarDragDropDebugEnabled())
                    Plugin.Logger.Information($"[HSUI DragDrop DBG] ExecuteSlotById SUPPRESSED bar={hotbarId} slot={slotId} (drop cooldown)");
                _suppressExecuteSlotByIdForDrop = (99, 99, 0);
                return 0;
            }
            if (ImGui.GetTime() >= suppressUntil)
                _suppressExecuteSlotByIdForDrop = (99, 99, 0);

            // Record for keypress flash (hotbarId 0-9 = bars 1-10, slotId 0-11)
            LastExecutedBar = (int)hotbarId + 1;
            LastExecutedSlot = (int)slotId;
            LastExecutedTime = ImGui.GetTime();

            return _executeSlotByIdHook != null ? _executeSlotByIdHook.Original(module, hotbarId, slotId) : (byte)0;
        }

        /// <summary>Bar 1-10, Slot 0-11. Set when ExecuteSlotById is invoked (keybind or click).</summary>
        public static int LastExecutedBar { get; private set; } = -1;
        public static int LastExecutedSlot { get; private set; } = -1;
        public static double LastExecutedTime { get; private set; }

        /// <summary>True if this slot was executed within the given duration (seconds).</summary>
        public static bool WasSlotJustExecuted(int hotbarIndex, int slotIndex, double durationSec = 0.2)
        {
            if (LastExecutedBar != hotbarIndex || LastExecutedSlot != slotIndex) return false;
            return (ImGui.GetTime() - LastExecutedTime) <= durationSec;
        }

        /// <summary>0=just pressed, 1=fully faded. For smooth flash decay.</summary>
        public static float GetKeypressFlashAlpha(int hotbarIndex, int slotIndex, double durationSec = 0.2)
        {
            if (!WasSlotJustExecuted(hotbarIndex, slotIndex, durationSec)) return 0f;
            double elapsed = ImGui.GetTime() - LastExecutedTime;
            return (float)(1.0 - (elapsed / durationSec));
        }

        private bool IsActionValid(ulong actionID, IGameObject? target)
        {
            if (target == null || actionID == 0 || _sheet == null)
            {
                return false;
            }

            bool found = _sheet.TryGetRow((uint)actionID, out Action action);
            if (!found)
            {
                return false;
            }

            // handle actions that automatically switch to other actions
            // ie GNB Continuation or SMN Egi Assaults
            // these actions dont have an attack type or animation so in these cases
            // we assume its a hostile spell
            // if this doesn't work on all cases we can switch to a hardcoded list
            // of special cases later
            if (action.AttackType.RowId == 0 && action.AnimationStart.RowId == 0 &&
                (!action.CanTargetAlly && !action.CanTargetHostile && !action.CanTargetParty && action.CanTargetSelf))
            {
                // special case for AST cards and SMN rekindle
                if (actionID is 37019 or 37020 or 37021 or 25822)
                {
                    return target is IPlayerCharacter or IBattleNpc { BattleNpcKind: BattleNpcSubKind.Chocobo };
                }

                return target is IBattleNpc npcTarget && npcTarget.BattleNpcKind == BattleNpcSubKind.Enemy;
            }

            // friendly player (TODO: pvp? lol)
            if (target is IPlayerCharacter)
            {
                return action.CanTargetAlly || action.CanTargetParty || action.CanTargetSelf;
            }

            // friendly npc
            if (target is IBattleNpc npc)
            {
                if (npc.BattleNpcKind != BattleNpcSubKind.Enemy)
                {
                    return action.CanTargetAlly || action.CanTargetParty || action.CanTargetSelf;
                }
            }

            return action.CanTargetHostile;
        }

        #region mouseover inputs proxy
        private bool? _leftButtonClicked = null;
        public bool LeftButtonClicked => _leftButtonClicked.HasValue ?
            _leftButtonClicked.Value :
            (IsProxyEnabled ? false : ImGui.IsMouseClicked(ImGuiMouseButton.Left));

        private bool? _rightButtonClicked = null;
        public bool RightButtonClicked => _rightButtonClicked.HasValue ?
            _rightButtonClicked.Value :
            (IsProxyEnabled ? false : ImGui.IsMouseClicked(ImGuiMouseButton.Right));

        private bool _leftButtonWasDown = false;
        private bool _rightButtonWasDown = false;


        public void ClearClicks()
        {
            if (IsProxyEnabled)
            {
                WndProcDetour(_wndHandle, WM_LBUTTONUP, 0, 0);
                WndProcDetour(_wndHandle, WM_RBUTTONUP, 0, 0);
            }
        }

        // wnd proc detour
        // if we're "eating" inputs, we only process left and right clicks
        // any other message is passed along to the ImGui scene
        private IntPtr WndProcDetour(IntPtr hWnd, uint msg, ulong wParam, long lParam)
        {
            // When the game has an active hotbar-relevant drag (Action, Macro, Item, etc.) AND the cursor is over
            // an HSUI hotbar, eat LBUTTONUP so the game doesn't interpret it as a click on the (hidden) default
            // hotbar and execute the ability. Do NOT eat when cursor is over game UI (Character Config, etc.) —
            // the game uses the same icon/drag system for config submenus, so we must only intercept when we're
            // actually dropping on our hotbars.
            if (msg == WM_LBUTTONUP && IsHotbarRelevantGameDrag() && ActionBarsHitTestHelper.IsMouseOverAnyHSUIHotbar())
            {
                if (IsActionBarDragDropDebugEnabled())
                    Plugin.Logger.Information("[HSUI DragDrop DBG] WndProc: EATING LBUTTONUP (hotbar drag over HSUI bar)");
                TryCancelGameDragDrop();
                ImGui.GetIO().AddMouseButtonEvent((int)ImGuiMouseButton.Left, false);
                return (IntPtr)0;
            }

            // eat left and right clicks?
            if (HandlingMouseInputs && IsProxyEnabled)
            {
                switch (msg)
                {
                    // mouse clicks
                    case WM_LBUTTONDOWN:
                    case WM_RBUTTONDOWN:
                    case WM_LBUTTONUP:
                    case WM_RBUTTONUP:

                        // if there's not a game window covering the cursor location
                        // we eat the message and handle the inputs manually
                        if (ClipRectsHelper.Instance?.IsPointClipped(ImGui.GetMousePos()) == false)
                        {
                            _leftButtonClicked = _leftButtonWasDown && msg == WM_LBUTTONUP;
                            _rightButtonClicked = _rightButtonWasDown && msg == WM_RBUTTONUP;


                            _leftButtonWasDown = msg == WM_LBUTTONDOWN;
                            _rightButtonWasDown = msg == WM_RBUTTONDOWN;

                            // never eat BUTTONUP messages to prevent clicks from getting stuck!!!
                            if (msg != WM_LBUTTONUP && msg != WM_RBUTTONUP)
                            {
                                // INPUT EATEN!!!
                                return (IntPtr)0;
                            }
                        }
                        // otherwise we let imgui handle the inputs
                        else
                        {
                            _leftButtonClicked = null;
                            _rightButtonClicked = null;
                        }
                        break;
                }
            }

            // call imgui's wnd proc
            return (IntPtr)CallWindowProc(_imguiWndProcPtr, hWnd, msg, wParam, lParam);
        }

        public void OnFrameworkUpdate(IFramework framework)
        {
            try
            {
                // When leaving PvP, force load PvE hotbars so HSUI bars don't keep showing PvP actions.
                ActionBarsManager.TryRestorePvEHotbarsAfterLeavePvP();

                // Controller bar custom keybinds (when controller hotbars enabled)
                ControllerBarKeybindExecutor.Process(framework);

                // Keep WndProc hooked when: proxy mode (for mouseover) OR we need to block game drag
                // release (so dropping on HSUI action bar doesn't execute the ability).
                bool needHook = IsProxyEnabled || ShouldBlockGameDragRelease();
                if (needHook && _wndProcPtr == IntPtr.Zero)
                {
                    HookWndProc();
                    // Only log when we actually installed (HookWndProc can return early during init delay)
                    if (_wndProcPtr != IntPtr.Zero && IsActionBarDragDropDebugEnabled())
                        Plugin.Logger.Information("[HSUI DragDrop DBG] WndProc hook INSTALLED (needHook=true for drag-block)");
                }
                else if (!needHook && _wndProcPtr != IntPtr.Zero)
                    RestoreWndProc();
            }
            catch (Exception ex)
            {
                Plugin.Logger.Warning($"[HSUI InputsHelper] OnFrameworkUpdate: {ex.Message}");
            }
        }

        private static bool ShouldBlockGameDragRelease()
        {
            try
            {
                var hotbarsConfig = ConfigurationManager.Instance?.GetConfigObject<HSUI.Interface.GeneralElements.HotbarsConfig>();
                return hotbarsConfig != null && hotbarsConfig.Enabled;
            }
            catch { return false; }
        }

        private static bool IsActionBarDragDropDebugEnabled()
        {
            try
            {
                var configs = ConfigurationManager.Instance?.GetObjects<HSUI.Interface.GeneralElements.HotbarBarConfig>();
                return configs != null && configs.Exists(c => c.DebugDragDrop);
            }
            catch { return false; }
        }

        /// <summary>When we place an action via drag-drop, suppress the next UseAction for that action to prevent
        /// the game from executing it (game may interpret drop as click via a path that bypasses WndProc).</summary>
        private static (uint ActionId, double SuppressUntil) _suppressUseActionForDrop = (0, 0);

        public static void SuppressUseActionAfterDrop(uint actionId, int durationMs = 300)
        {
            _suppressUseActionForDrop = (actionId, ImGui.GetTime() + durationMs / 1000.0);
        }

        /// <summary>Suppress ExecuteSlotById when we just placed via drag-drop (game hotbar click goes through this).</summary>
        private static (uint HotbarId, uint SlotId, double SuppressUntil) _suppressExecuteSlotByIdForDrop = (99, 99, 0);

        public static void SuppressExecuteSlotByIdAfterDrop(uint hotbarId, uint slotId, int durationMs = 300)
        {
            _suppressExecuteSlotByIdForDrop = (hotbarId, slotId, ImGui.GetTime() + durationMs / 1000.0);
        }

        public void OnFrameEnd()
        {
            _leftButtonClicked = null;
            _rightButtonClicked = null;
        }

        private void HookWndProc()
        {
            if (Plugin.LoadTime <= 0 ||
                ImGui.GetTime() - Plugin.LoadTime < InitializationDelay)
            {
                return;
            }

            ulong processId = (ulong)Process.GetCurrentProcess().Id;

            IntPtr hWnd = IntPtr.Zero;
            do
            {
                hWnd = FindWindowExW(IntPtr.Zero, hWnd, "FFXIVGAME", null);
                if (hWnd == IntPtr.Zero) { return; }

                ulong wndProcessId = 0;
                GetWindowThreadProcessId(hWnd, ref wndProcessId);

                if (wndProcessId == processId)
                {
                    break;
                }

            } while (hWnd != IntPtr.Zero);

            if (hWnd == IntPtr.Zero) { return; }

            _wndHandle = hWnd;
            _wndProcDelegate = WndProcDetour;
            _wndProcHandle = GCHandle.Alloc(_wndProcDelegate, GCHandleType.Normal); // Prevent GC of delegate while hooked
            _wndProcPtr = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate);
            _imguiWndProcPtr = SetWindowLongPtr(hWnd, GWL_WNDPROC, _wndProcPtr);

            Plugin.Logger.Info("Initializing HSUI Inputs v" + Plugin.Version);
            Plugin.Logger.Info("\tHooking WndProc for window: " + hWnd.ToString("X"));
            Plugin.Logger.Info("\tOld WndProc: " + _imguiWndProcPtr.ToString("X"));
        }

        /// <summary>Restore the game window's WndProc so left-click works again. Idempotent.</summary>
        internal void RestoreWndProc()
        {
            if (_wndHandle == IntPtr.Zero || _imguiWndProcPtr == IntPtr.Zero)
                return;

            try
            {
                Plugin.Logger.Info("\t\tRestoring WndProc");
                SetWindowLongPtr(_wndHandle, GWL_WNDPROC, _imguiWndProcPtr);
                Plugin.Logger.Info("\t\t\tWndProc restored.");
            }
            catch (Exception ex)
            {
                Plugin.Logger.Warning($"\t\tRestoreWndProc failed: {ex.Message}");
            }
            finally
            {
                if (_wndProcHandle.IsAllocated)
                    _wndProcHandle.Free();
                _wndHandle = IntPtr.Zero;
                _imguiWndProcPtr = IntPtr.Zero;
                _wndProcPtr = IntPtr.Zero;
            }
        }

        /// <summary>Call at the very start of plugin disable so left-click is restored even if disposal fails later.</summary>
        public static void RestoreWndProcIfNeeded()
        {
            try
            {
                Instance?.RestoreWndProc();
            }
            catch (Exception ex)
            {
                Plugin.Logger?.Warning($"[HSUI] RestoreWndProcIfNeeded: {ex.Message}");
            }
        }

        private IntPtr _wndHandle = IntPtr.Zero;
        private WndProcDelegate _wndProcDelegate = null!;
        private GCHandle _wndProcHandle; // Keeps delegate alive while WndProc is hooked; freed in RestoreWndProc
        private IntPtr _wndProcPtr = IntPtr.Zero;
        private IntPtr _imguiWndProcPtr = IntPtr.Zero;

        public delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, ulong wParam, long lParam);

        [DllImport("user32.dll", EntryPoint = "SetWindowLongPtrW", SetLastError = true)]
        public static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong);

        [DllImport("user32.dll", EntryPoint = "CallWindowProcW")]
        public static extern long CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, uint Msg, ulong wParam, long lParam);

        [DllImport("user32.dll", EntryPoint = "FindWindowExW", SetLastError = true)]
        public static extern IntPtr FindWindowExW(IntPtr hWndParent, IntPtr hWndChildAfter, [MarshalAs(UnmanagedType.LPWStr)] string? lpszClass, [MarshalAs(UnmanagedType.LPWStr)] string? lpszWindow);

        [DllImport("user32.dll", EntryPoint = "GetWindowThreadProcessId", SetLastError = true)]
        public static extern ulong GetWindowThreadProcessId(IntPtr hWnd, ref ulong id);

        private const uint WM_LBUTTONDOWN = 513;
        private const uint WM_LBUTTONUP = 514;
        private const uint WM_RBUTTONDOWN = 516;
        private const uint WM_RBUTTONUP = 517;

        private const int GWL_WNDPROC = -4;

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static unsafe bool IsGameDragDropActive()
        {
            try
            {
                var stage = AtkStage.Instance();
                if (stage == null) return false;
                var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
                return dm != null && dm->IsDragging;
            }
            catch { return false; }
        }

        /// <summary>True when the game has an active drag that can be placed on a hotbar (Action, Macro, Item, etc.).
        /// Used to avoid eating LBUTTONUP for other UI drags (e.g. Character Config menus).</summary>
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private static unsafe bool IsHotbarRelevantGameDrag()
        {
            try
            {
                if (!IsGameDragDropActive()) return false;
                var stage = AtkStage.Instance();
                if (stage == null) return false;
                var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
                var dd = dm->DragDrop1;
                if (dd == null) return false;
                var slotType = UIGlobals.GetHotbarSlotTypeFromDragDropType(dd->DragDropType);
                return slotType != RaptureHotbarModule.HotbarSlotType.Empty;
            }
            catch { return false; }
        }


        private static unsafe void TryCancelGameDragDrop()
        {
            try
            {
                var stage = AtkStage.Instance();
                if (stage == null) return;
                var dm = (AtkDragDropManager*)Unsafe.AsPointer(ref stage->DragDropManager);
                if (dm != null)
                    dm->CancelDragDrop(true, true);
            }
            catch { /* best-effort */ }
        }
        #endregion
    }
}
