// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Runtime.InteropServices;

namespace System.Windows.Forms.PropertyGridInternal;

internal partial class PropertyGridView
{
    internal partial class MouseHook
    {
        private readonly PropertyGridView _gridView;
        private readonly Control _control;
        private readonly IMouseHookClient _client;

        private uint _thisProcessId;
        private HOOKPROC? _callBack;
        private HHOOK _mouseHookHandle;
        private bool _hookDisable;

        private bool _processing;

        private readonly Lock _lock = new();

        public MouseHook(Control control, IMouseHookClient client, PropertyGridView gridView)
        {
            _control = control;
            _gridView = gridView;
            _client = client;
#if DEBUG
            _callingStack = Environment.StackTrace;
#endif
        }

#if DEBUG
        private readonly string _callingStack;
        ~MouseHook()
        {
            if (!_mouseHookHandle.IsNull)
            {
                throw new InvalidOperationException($"Finalizing an active mouse hook. This will crash the process. Calling stack: {_callingStack}");
            }
        }
#endif

        public bool DisableMouseHook
        {
            set
            {
                _hookDisable = value;
                if (value)
                {
                    UnhookMouse();
                }
            }
        }

        public virtual bool HookMouseDown
        {
            get => !_mouseHookHandle.IsNull;
            set
            {
                if (value && !_hookDisable)
                {
                    HookMouse();
                }
                else
                {
                    UnhookMouse();
                }
            }
        }

        public void Dispose() => UnhookMouse();

        /// <summary>
        ///  Sets up the needed windows hooks to catch messages.
        /// </summary>
        private unsafe void HookMouse()
        {
            lock (_lock)
            {
                if (!_mouseHookHandle.IsNull)
                {
                    return;
                }

                if (_thisProcessId == 0)
                {
                    PInvokeCore.GetWindowThreadProcessId(_control, out _thisProcessId);
                }

                _callBack = MouseHookProc;
                IntPtr hook = Marshal.GetFunctionPointerForDelegate(_callBack);
                _mouseHookHandle = PInvoke.SetWindowsHookEx(
                    WINDOWS_HOOK_ID.WH_MOUSE,
                    (delegate* unmanaged[Stdcall]<int, WPARAM, LPARAM, LRESULT>)hook,
                    HINSTANCE.Null,
                    PInvokeCore.GetCurrentThreadId());

                Debug.Assert(!_mouseHookHandle.IsNull, "Failed to install mouse hook");
            }
        }

        /// <summary>
        ///  HookProc used for catch mouse messages.
        /// </summary>
        private unsafe LRESULT MouseHookProc(int nCode, WPARAM wparam, LPARAM lparam)
        {
            if (nCode == PInvoke.HC_ACTION)
            {
                var mhs = (MOUSEHOOKSTRUCT*)(nint)lparam;
                if (mhs is not null)
                {
                    switch ((uint)wparam)
                    {
                        case PInvokeCore.WM_LBUTTONDOWN:
                        case PInvokeCore.WM_MBUTTONDOWN:
                        case PInvokeCore.WM_RBUTTONDOWN:
                        case PInvokeCore.WM_NCLBUTTONDOWN:
                        case PInvokeCore.WM_NCMBUTTONDOWN:
                        case PInvokeCore.WM_NCRBUTTONDOWN:
                        case PInvokeCore.WM_MOUSEACTIVATE:
                            if (ProcessMouseDown(mhs->hwnd))
                            {
                                return (LRESULT)1;
                            }

                            break;
                    }
                }
            }

            return PInvoke.CallNextHookEx(_mouseHookHandle, nCode, wparam, lparam);
        }

        /// <summary>
        ///  Removes the windowshook that was installed.
        /// </summary>
        private void UnhookMouse()
        {
            lock (_lock)
            {
                if (!_mouseHookHandle.IsNull)
                {
                    PInvoke.UnhookWindowsHookEx(_mouseHookHandle);
                    _mouseHookHandle = default;
                }
            }
        }

        private bool ProcessMouseDown(HWND hwnd)
        {
            // If we put up the "invalid" message box, it appears this method is getting called reentrantly
            // when it shouldn't be. This prevents us from recursing.
            if (_processing)
            {
                return false;
            }

            IntPtr handle = _control.HandleInternal;

            // If it is us or one of our children just process as normal.
            if (hwnd != handle
                && FromHandle(hwnd) is Control targetControl
                && !_control.Contains(targetControl))
            {
                Debug.Assert(_thisProcessId != 0, "Didn't get our process id!");

                // Make sure the window is in our process.
                PInvokeCore.GetWindowThreadProcessId(hwnd, out uint pid);

                // If this isn't our process, unhook the mouse.
                if (pid != _thisProcessId)
                {
                    HookMouseDown = false;
                    return false;
                }

                // If this a sibling control (e.g. the drop down or buttons), just forward the message and skip the commit
                bool needCommit = targetControl is null || !IsSiblingControl(_control, targetControl);

                try
                {
                    _processing = true;

                    if (needCommit && _client.OnClickHooked())
                    {
                        return true; // there was an error, so eat the mouse
                    }
                }
                finally
                {
                    _processing = false;
                }

                // Cancel our hook at this point.
                HookMouseDown = false;
            }

            return false;
        }
    }
}
