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

using System.ComponentModel.Design;
using System.Drawing;
using System.Runtime.InteropServices;

namespace System.Windows.Forms.Design.Behavior;

public sealed partial class BehaviorService
{
    private partial class AdornerWindow
    {
        /// <summary>
        ///  <para>
        ///   This class knows how to hook all the messages to a given process/thread.
        ///  </para>
        ///  <para>
        ///   On any mouse clicks, it asks the designer what to do with the message, that is to eat it or propagate
        ///   it to the control it was meant for. This allows us to synchronously process mouse messages when
        ///   the AdornerWindow itself may be pumping messages.
        ///  </para>
        /// </summary>
        private class MouseHook
        {
            private AdornerWindow? _currentAdornerWindow;
            private uint _thisProcessID;
            private HOOKPROC? _callBack;
            private HHOOK _mouseHookHandle;
            private bool _processingMessage;

            private bool _isHooked;
            private int _lastLButtonDownTimeStamp;
            private readonly Lock _lock = new();

            public MouseHook()
            {
#if DEBUG
                _callingStack = Environment.StackTrace;
#endif
                HookMouse();
            }

#if DEBUG
            private readonly string _callingStack;
#pragma warning disable CA1821 // Remove empty Finalizers
            ~MouseHook()
#pragma warning restore CA1821
            {
                Debug.Assert(
                    _mouseHookHandle == 0,
                    $"Finalizing an active mouse hook. This will crash the process. Calling stack: {_callingStack}");
            }
#endif

            public void Dispose()
            {
                UnhookMouse();
            }

            private unsafe void HookMouse()
            {
                Debug.Assert(s_adornerWindowList.Count > 0, "No AdornerWindow available to create the mouse hook");
                lock (_lock)
                {
                    if (_mouseHookHandle != 0 || s_adornerWindowList.Count == 0)
                    {
                        return;
                    }

                    if (_thisProcessID == 0)
                    {
                        AdornerWindow adornerWindow = s_adornerWindowList[0];
                        PInvokeCore.GetWindowThreadProcessId(adornerWindow, 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());

                    _isHooked = _mouseHookHandle != 0;

                    Debug.Assert(_isHooked, "Failed to install mouse hook.");
                }
            }

            private unsafe LRESULT MouseHookProc(int nCode, WPARAM wparam, LPARAM lparam)
            {
                if (_isHooked && nCode == PInvoke.HC_ACTION && lparam != 0)
                {
                    MOUSEHOOKSTRUCT* mhs = (MOUSEHOOKSTRUCT*)(nint)lparam;

                    try
                    {
                        if (ProcessMouseMessage(mhs->hwnd, (MessageId)(nuint)wparam, mhs->pt.X, mhs->pt.Y))
                        {
                            return (LRESULT)1;
                        }
                    }
                    catch (Exception ex)
                    {
                        _currentAdornerWindow!.Capture = false;

                        if (ex != CheckoutException.Canceled)
                        {
                            _currentAdornerWindow._behaviorService.ShowError(ex);
                        }

                        if (ex.IsCriticalException())
                        {
                            throw;
                        }
                    }
                    finally
                    {
                        _currentAdornerWindow = null;
                    }
                }

                Debug.Assert(_isHooked, "How did we get here when we are disposed?");
                return PInvoke.CallNextHookEx(_mouseHookHandle, nCode, wparam, lparam);
            }

            private void UnhookMouse()
            {
                lock (_lock)
                {
                    if (_mouseHookHandle != 0)
                    {
                        if (!PInvoke.UnhookWindowsHookEx(_mouseHookHandle))
                        {
                            Debug.Fail("Failed to remove mouse hook.");
                        }

                        _mouseHookHandle = default;
                        _isHooked = false;
                    }
                }
            }

            private bool ProcessMouseMessage(HWND hwnd, MessageId msg, int x, int y)
            {
                if (_processingMessage)
                {
                    return false;
                }

                foreach (AdornerWindow adornerWindow in s_adornerWindowList)
                {
                    if (!adornerWindow.DesignerFrameValid)
                    {
                        continue;
                    }

                    _currentAdornerWindow = adornerWindow;

                    // If it's us or one of our children, just process as normal
                    if (adornerWindow.ProcessingDrag
                        || (hwnd != adornerWindow.DesignerFrame.Handle && PInvoke.IsChild(adornerWindow.DesignerFrame, hwnd)))
                    {
                        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, bail
                        if (pid != _thisProcessID)
                        {
                            return false;
                        }

                        try
                        {
                            _processingMessage = true;
                            Point pt = adornerWindow.PointToClient(new Point(x, y));
                            Message m = Message.Create(hwnd, msg, 0u, PARAM.FromLowHigh(pt.Y, pt.X));

                            // No one knows why we get an extra click here from VS. As a workaround, we check the TimeStamp and discard it.
                            if (m.Msg == (int)PInvokeCore.WM_LBUTTONDOWN)
                            {
                                _lastLButtonDownTimeStamp = PInvoke.GetMessageTime();
                            }
                            else if (m.Msg == (int)PInvokeCore.WM_LBUTTONDBLCLK)
                            {
                                int lButtonDoubleClickTimeStamp = PInvoke.GetMessageTime();
                                if (lButtonDoubleClickTimeStamp == _lastLButtonDownTimeStamp)
                                {
                                    return true;
                                }
                            }

                            if (!adornerWindow.WndProcProxy(ref m, pt.X, pt.Y))
                            {
                                // We did the work, stop the message propagation
                                return true;
                            }
                        }
                        finally
                        {
                            _processingMessage = false;
                        }

                        // No need to enumerate the other adorner windows since only one can be focused at a time.
                        break;
                    }
                }

                return false;
            }
        }
    }
}
