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

using System.Drawing;

namespace System.Windows.Forms;

public static partial class ToolStripManager
{
    ///  <remarks>
    ///   <para>
    ///    This installs a message filter when a dropdown becomes active. The filter:
    ///   </para>
    ///   <list type="bullet">
    ///    <item><description>Eats WM_MOUSEMOVEs so that the window underneath doesnt get highlight processing/tooltips.</description></item>
    ///    <item><description>Dismisses the menu if clicked outside the dropdown.</description></item>
    ///    <item><description>Dismisses all dropdowns if the active window changes.</description></item>
    ///    <item><description>Redirects keyboard messages to the active dropdown.</description></item>
    ///   </list>
    ///   <para>
    ///    There should be one Message Filter per thread and it should be uninstalled once the last dropdown has gone away.
    ///    This is not part of <see cref="ToolStripManager"/> because it is DropDown specific and
    ///    we don't want to publicly expose this message filter.
    ///   </para>
    ///  </remarks>
    internal partial class ModalMenuFilter : IMessageModifyAndFilter
    {
        // The window that was active when we showed the dropdown
        private HandleRef<HWND> _activeHwnd;
        // The window that was last known to be active
        private HandleRef<HWND> _lastActiveWindow;
        private List<ToolStrip>? _inputFilterQueue;
        private bool _inMenuMode;
        private bool _caretHidden;
        private bool _showUnderlines;
        private bool _menuKeyToggle;
        private bool _suspendMenuMode;
        private HostedWindowsFormsMessageHook? _messageHook;
        private Timer? _ensureMessageProcessingTimer;
        private const int MessageProcessingInterval = 500;

        private ToolStrip? _toplevelToolStrip;

        private readonly WeakReference<IKeyboardToolTip?> _lastFocusedTool = new(null);

        [ThreadStatic]
        private static ModalMenuFilter? t_instance;

        internal static ModalMenuFilter Instance => t_instance ??= new ModalMenuFilter();

        private ModalMenuFilter()
        {
        }

        /// <summary>
        ///  The HWnd that was active when we popped the first dropdown.
        /// </summary>
        internal static HandleRef<HWND> ActiveHwnd => Instance.ActiveHwndInternal;

        // returns whether or not we should show focus cues for mnemonics.
        public bool ShowUnderlines
        {
            get => _showUnderlines;
            set
            {
                if (_showUnderlines != value)
                {
                    _showUnderlines = value;
                    NotifyMenuModeChange(invalidateText: true, activationChange: false);
                }
            }
        }

        private HandleRef<HWND> ActiveHwndInternal
        {
            get => _activeHwnd;
            set
            {
                if (_activeHwnd.Handle != value.Handle)
                {
                    Control? control = null;

                    // Unsubscribe from handle recreate.
                    if (_activeHwnd.Handle != IntPtr.Zero)
                    {
                        control = Control.FromHandle(_activeHwnd.Handle);
                        if (control is not null)
                        {
                            control.HandleCreated -= OnActiveHwndHandleCreated;
                        }
                    }

                    _activeHwnd = value;

                    // make sure we watch out for handle recreates.
                    control = Control.FromHandle(_activeHwnd.Handle);
                    if (control is not null)
                    {
                        control.HandleCreated += OnActiveHwndHandleCreated;
                    }
                }
            }
        }

        /// <summary>
        ///  Returns whether or not someone has called EnterMenuMode.
        /// </summary>
        internal static bool InMenuMode => Instance._inMenuMode;

        internal static bool MenuKeyToggle
        {
            get => Instance._menuKeyToggle;
            set
            {
                if (Instance._menuKeyToggle != value)
                {
                    Instance._menuKeyToggle = value;
                }
            }
        }

        /// <summary>
        ///  Used in scenarios where windows forms does not own the message pump,
        ///  but needs access to the message queue.
        /// </summary>
        private HostedWindowsFormsMessageHook MessageHook
            => _messageHook ??= new HostedWindowsFormsMessageHook();

        // ToolStrip analog to WM_ENTERMENULOOP
        private void EnterMenuModeCore()
        {
            Debug.Assert(!InMenuMode, "How did we get here if we're already in menu mode?");

            if (!InMenuMode)
            {
                HWND hwndActive = PInvoke.GetActiveWindow();
                if (!hwndActive.IsNull)
                {
                    ActiveHwndInternal = new(Control.FromHandle(hwndActive), hwndActive);
                }

                Application.ThreadContext.FromCurrent().AddMessageFilter(this);
                Application.ThreadContext.FromCurrent().TrackInput(true);

                if (!Application.ThreadContext.FromCurrent().GetMessageLoop(true))
                {
                    // Message filter isn't going to help as we don't own the message pump
                    // switch over to a MessageHook
                    MessageHook.HookMessages = true;
                }

                _inMenuMode = true;

                NotifyLastLastFocusedToolAboutFocusLoss();

                // Fire timer messages to force our filter to get evaluated.
                ProcessMessages(true);
            }
        }

        internal void NotifyLastLastFocusedToolAboutFocusLoss()
        {
            IKeyboardToolTip? lastFocusedTool = KeyboardToolTipStateMachine.Instance.LastFocusedTool;
            if (lastFocusedTool is not null)
            {
                _lastFocusedTool.SetTarget(lastFocusedTool);
                KeyboardToolTipStateMachine.Instance.NotifyAboutLostFocus(lastFocusedTool);
            }
        }

        internal static void ExitMenuMode() => Instance.ExitMenuModeCore();

        private void ExitMenuModeCore()
        {
            // Ensure we've cleaned up the timer.
            ProcessMessages(process: false);

            if (!InMenuMode)
            {
                return;
            }

            try
            {
                if (_messageHook is not null)
                {
                    // Message filter isn't going to help as we don't own the message pump
                    // Switch over to a MessageHook
                    _messageHook.HookMessages = false;
                }

                Application.ThreadContext.FromCurrent().RemoveMessageFilter(this);
                Application.ThreadContext.FromCurrent().TrackInput(false);

                if (!ActiveHwnd.Handle.IsNull)
                {
                    // Unsubscribe from handle creates
                    Control? control = Control.FromHandle(ActiveHwnd.Handle);
                    if (control is not null)
                    {
                        control.HandleCreated -= OnActiveHwndHandleCreated;
                    }

                    ActiveHwndInternal = default;
                }

                _inputFilterQueue?.Clear();
                if (_caretHidden)
                {
                    _caretHidden = false;
                    PInvoke.ShowCaret(HWND.Null);
                }

                if (_lastFocusedTool.TryGetTarget(out IKeyboardToolTip? tool) && tool is not null)
                {
                    KeyboardToolTipStateMachine.Instance.NotifyAboutGotFocus(tool);
                }
            }
            finally
            {
                _inMenuMode = false;

                // Skip the setter here so we only iterate through the toolstrips once.
                bool textStyleChanged = _showUnderlines;
                _showUnderlines = false;
                NotifyMenuModeChange(invalidateText: textStyleChanged, activationChange: true);
            }
        }

        internal static ToolStrip? GetActiveToolStrip() => Instance.GetActiveToolStripInternal();

        internal ToolStrip? GetActiveToolStripInternal()
        {
            if (_inputFilterQueue is not null && _inputFilterQueue.Count > 0)
            {
                return _inputFilterQueue[^1];
            }

            return null;
        }

        /// <summary>
        ///  Returns the ToolStrip that is at the root.
        /// </summary>
        private ToolStrip? GetCurrentTopLevelToolStrip()
        {
            if (_toplevelToolStrip is null)
            {
                ToolStrip? activeToolStrip = GetActiveToolStripInternal();
                if (activeToolStrip is not null)
                {
                    _toplevelToolStrip = activeToolStrip.GetToplevelOwnerToolStrip();
                }
            }

            return _toplevelToolStrip;
        }

        private void OnActiveHwndHandleCreated(object? sender, EventArgs e)
        {
            ActiveHwndInternal = new(sender as Control);
        }

        internal static void ProcessMenuKeyDown(ref Message m)
        {
            Keys keyData = (Keys)(nint)m.WParamInternal;

            if (Control.FromHandle(m.HWnd) is ToolStrip toolStrip && !toolStrip.IsDropDown)
            {
                return;
            }

            // Handle the case where the ALT key has been pressed down while a dropdown was open. We need to clear
            // off the MenuKeyToggle so the next ALT will activate the menu.
            if (IsMenuKey(keyData))
            {
                if (!InMenuMode && MenuKeyToggle)
                {
                    MenuKeyToggle = false;
                }
                else if (!MenuKeyToggle)
                {
                    Instance.ShowUnderlines = true;
                }
            }
        }

        internal static void CloseActiveDropDown(ToolStripDropDown activeToolStripDropDown, ToolStripDropDownCloseReason reason)
        {
            activeToolStripDropDown.SetCloseReason(reason);
            activeToolStripDropDown.Visible = false;

            // There's no more dropdowns left in the chain
            if (GetActiveToolStrip() is null)
            {
                ExitMenuMode();

                // Make sure we roll selection off  the toplevel toolstrip.
                activeToolStripDropDown.OwnerItem?.Unselect();
            }
        }

        /// <summary>
        ///  Fire a timer event to ensure we have a message in the queue every 500ms
        /// </summary>
        private void ProcessMessages(bool process)
        {
            if (process)
            {
                _ensureMessageProcessingTimer ??= new Timer();
                _ensureMessageProcessingTimer.Interval = MessageProcessingInterval;
                _ensureMessageProcessingTimer.Enabled = true;
            }
            else if (_ensureMessageProcessingTimer is not null)
            {
                _ensureMessageProcessingTimer.Enabled = false;
                _ensureMessageProcessingTimer.Dispose();
                _ensureMessageProcessingTimer = null;
            }
        }

        private void ProcessMouseButtonPressed(HWND hwndMouseMessageIsFrom, Point location)
        {
            int countDropDowns = _inputFilterQueue?.Count ?? 0;
            for (int i = 0; i < countDropDowns; i++)
            {
                ToolStrip? activeToolStrip = GetActiveToolStripInternal();

                if (activeToolStrip is not null)
                {
                    Point translatedLocation = location;
                    PInvoke.MapWindowPoints(hwndMouseMessageIsFrom, activeToolStrip, ref translatedLocation);
                    if (!activeToolStrip.ClientRectangle.Contains(translatedLocation))
                    {
                        if (activeToolStrip is ToolStripDropDown activeToolStripDropDown)
                        {
                            if (!(activeToolStripDropDown.OwnerToolStrip is not null
                                && activeToolStripDropDown.OwnerToolStrip.HWND == hwndMouseMessageIsFrom
                                && activeToolStripDropDown.OwnerDropDownItem is not null
                                && activeToolStripDropDown.OwnerDropDownItem.DropDownButtonArea.Contains(location)))
                            {
                                // The owner item should handle closing the dropdown
                                // this allows code such as if (DropDown.Visible) { Hide, Show } etc.
                                CloseActiveDropDown(activeToolStripDropDown, ToolStripDropDownCloseReason.AppClicked);
                            }
                        }
                        else
                        {
                            // Make sure we clear the selection.
                            activeToolStrip.NotifySelectionChange(item: null);

                            // We're a toplevel toolstrip and we've clicked somewhere else. Exit menu mode.
                            ExitMenuModeCore();
                        }
                    }
                    else
                    {
                        // We've found a dropdown that intersects with the mouse message
                        break;
                    }
                }
                else
                {
                    break;
                }
            }
        }

        private bool ProcessActivationChange()
        {
            int countDropDowns = _inputFilterQueue?.Count ?? 0;
            for (int i = 0; i < countDropDowns; i++)
            {
                if (GetActiveToolStripInternal() is ToolStripDropDown activeDropDown && activeDropDown.AutoClose)
                {
                    activeDropDown.Visible = false;
                }
            }

            ExitMenuModeCore();
            return true;
        }

        internal static void SetActiveToolStrip(ToolStrip toolStrip, bool menuKeyPressed)
        {
            if (!InMenuMode && menuKeyPressed)
            {
                Instance.ShowUnderlines = true;
            }

            Instance.SetActiveToolStripCore(toolStrip);
        }

        internal static void SetActiveToolStrip(ToolStrip toolStrip)
            => Instance.SetActiveToolStripCore(toolStrip);

        private void SetActiveToolStripCore(ToolStrip toolStrip)
        {
            if (toolStrip is null)
            {
                return;
            }

            if (toolStrip.IsDropDown)
            {
                // For something that never closes, don't use menu mode.
                ToolStripDropDown dropDown = (ToolStripDropDown)toolStrip;

                if (!dropDown.AutoClose)
                {
                    // Store off the current active hwnd
                    HWND hwndActive = PInvoke.GetActiveWindow();
                    if (!hwndActive.IsNull)
                    {
                        ActiveHwndInternal = new(Control.FromHandle(hwndActive), hwndActive);
                    }

                    // Don't actually enter menu mode.
                    return;
                }
            }

            toolStrip.KeyboardActive = true;

            if (_inputFilterQueue is null)
            {
                // Use list because we want to be able to remove at any point.
                _inputFilterQueue = [];
            }
            else
            {
                ToolStrip? currentActiveToolStrip = GetActiveToolStripInternal();

                // ToolStrip dropdowns push/pull their activation based on visibility.
                // we have to account for the toolstrips that aren't dropdowns
                if (currentActiveToolStrip is not null)
                {
                    if (!currentActiveToolStrip.IsDropDown)
                    {
                        _inputFilterQueue.Remove(currentActiveToolStrip);
                    }
                    else if (toolStrip.IsDropDown
                        && (ToolStripDropDown.GetFirstDropDown(toolStrip)
                            != ToolStripDropDown.GetFirstDropDown(currentActiveToolStrip)))
                    {
                        _inputFilterQueue.Remove(currentActiveToolStrip);

                        ToolStripDropDown currentActiveToolStripDropDown = (ToolStripDropDown)currentActiveToolStrip;
                        currentActiveToolStripDropDown.DismissAll();
                    }
                }
            }

            // Reset the toplevel toolstrip
            _toplevelToolStrip = null;

            if (!_inputFilterQueue.Contains(toolStrip))
            {
                _inputFilterQueue.Add(toolStrip);
            }

            if (!InMenuMode && _inputFilterQueue.Count > 0)
            {
                EnterMenuModeCore();
            }

            // Hide the caret if we're showing a toolstrip dropdown
            if (!_caretHidden && toolStrip.IsDropDown && InMenuMode)
            {
                _caretHidden = true;
                PInvoke.HideCaret(HWND.Null);
            }
        }

        internal static void SuspendMenuMode()
        {
            Instance._suspendMenuMode = true;
        }

        internal static void ResumeMenuMode()
        {
            Instance._suspendMenuMode = false;
        }

        internal static void RemoveActiveToolStrip(ToolStrip toolStrip)
        {
            Instance.RemoveActiveToolStripCore(toolStrip);
        }

        private void RemoveActiveToolStripCore(ToolStrip toolStrip)
        {
            // Precautionary - remove the active toplevel toolstrip.
            _toplevelToolStrip = null;
            _inputFilterQueue?.Remove(toolStrip);
        }

        private static bool IsChildOrSameWindow<T>(in T hwndParent, in T hwndChild) where T : IHandle<HWND>
            => hwndParent.Handle == hwndChild.Handle || PInvoke.IsChild(hwndParent, hwndChild);

        private static bool IsKeyOrMouseMessage(Message m)
        {
            if (m.IsMouseMessage())
            {
                return true;
            }
            else if (m.Msg is >= ((int)PInvoke.WM_NCLBUTTONDOWN) and <= ((int)PInvoke.WM_NCMBUTTONDBLCLK))
            {
                return true;
            }
            else if (m.IsKeyMessage())
            {
                return true;
            }

            return false;
        }

        public bool PreFilterMessage(ref Message m)
        {
            if (_suspendMenuMode)
            {
                return false;
            }

            ToolStrip? activeToolStrip = GetActiveToolStrip();
            if (activeToolStrip is null)
            {
                return false;
            }

            if (activeToolStrip.IsDisposed)
            {
                RemoveActiveToolStripCore(activeToolStrip);
                return false;
            }

            HandleRef<HWND> activeToolStripHandle = new(activeToolStrip);
            var activeWindowHandle = Control.GetHandleRef(PInvoke.GetActiveWindow());

            if (activeWindowHandle != _lastActiveWindow)
            {
                // If another window has gotten activation - we should dismiss.
                if (activeWindowHandle.Handle.IsNull)
                {
                    // We don't know what it was cause it's on another thread or doesnt exist.
                    ProcessActivationChange();
                }
                else if (Control.FromChildHandle(activeWindowHandle.Handle) is not ToolStripDropDown
                    && !IsChildOrSameWindow(activeWindowHandle, activeToolStripHandle)
                    && !IsChildOrSameWindow(activeWindowHandle, ActiveHwnd))
                {
                    // Not a dropdown, and not a child of the active toolstrip or the active window.
                    ProcessActivationChange();
                }
            }

            // Store this off so we don't have to do activation processing next time
            _lastActiveWindow = activeWindowHandle;

            // Performance: skip over things like WM_PAINT.
            if (!IsKeyOrMouseMessage(m))
            {
                return false;
            }

            DPI_AWARENESS_CONTEXT context = GetDpiAwarenessContextForWindow(m.HWND);

            using (ScaleHelper.EnterDpiAwarenessScope(context))
            {
                switch (m.MsgInternal)
                {
                    case PInvoke.WM_MOUSEMOVE:
                    case PInvoke.WM_NCMOUSEMOVE:
                        // Mouse move messages should be eaten if they aren't for a dropdown.
                        // this prevents things like ToolTips and mouse over highlights from
                        // being processed.
                        Control? control = Control.FromChildHandle(m.HWnd);
                        if (control is null || control.TopLevelControlInternal is not ToolStripDropDown)
                        {
                            // Double check it's not a child control of the active toolstrip.
                            if (!IsChildOrSameWindow<IHandle<HWND>>(activeToolStripHandle, m))
                            {
                                // It is NOT a child of the current active toolstrip.

                                ToolStrip? toplevelToolStrip = GetCurrentTopLevelToolStrip();
                                if (toplevelToolStrip is not null && IsChildOrSameWindow<IHandle<HWND>>(toplevelToolStrip, m))
                                {
                                    // Don't eat mouse message.
                                    // The mouse message is from an HWND that is part of the toplevel toolstrip - let the mouse move through so
                                    // when you have something like the file menu open and mouse over the edit menu
                                    // the file menu will dismiss.

                                    return false;
                                }
                                else if (!IsChildOrSameWindow<IHandle<HWND>>(ActiveHwnd, m))
                                {
                                    // Don't eat mouse message.
                                    // the mouse message is from another toplevel HWND.
                                    return false;
                                }

                                // Eat mouse message
                                // the HWND is
                                //      not part of the active toolstrip
                                //      not the toplevel toolstrip (e.g. MenuStrip).
                                //      not parented to the toplevel toolstrip (e.g a combo box on a menu strip).
                                return true;
                            }
                        }

                        break;
                    case PInvoke.WM_LBUTTONDOWN:
                    case PInvoke.WM_RBUTTONDOWN:
                    case PInvoke.WM_MBUTTONDOWN:
                        // When a mouse button is pressed, we should determine if it is within the client coordinates
                        // of the active dropdown. If not, we should dismiss it.
                        ProcessMouseButtonPressed(m.HWND, PARAM.ToPoint(m.LParamInternal));
                        break;
                    case PInvoke.WM_NCLBUTTONDOWN:
                    case PInvoke.WM_NCRBUTTONDOWN:
                    case PInvoke.WM_NCMBUTTONDOWN:
                        // When a mouse button is pressed, we should determine if it is within the client coordinates
                        // of the active dropdown. If not, we should dismiss it.
                        ProcessMouseButtonPressed(default, PARAM.ToPoint(m.LParamInternal));
                        break;

                    case PInvoke.WM_KEYDOWN:
                    case PInvoke.WM_KEYUP:
                    case PInvoke.WM_CHAR:
                    case PInvoke.WM_DEADCHAR:
                    case PInvoke.WM_SYSKEYDOWN:
                    case PInvoke.WM_SYSKEYUP:
                    case PInvoke.WM_SYSCHAR:
                    case PInvoke.WM_SYSDEADCHAR:

                        if (!activeToolStrip.ContainsFocus)
                        {
                            // Route all keyboard messages to the active dropdown.
                            m.HWnd = activeToolStrip.Handle;
                        }

                        break;
                }
            }

            return false;
        }

        internal static DPI_AWARENESS_CONTEXT GetDpiAwarenessContextForWindow(HWND hwnd)
        {
            DPI_AWARENESS_CONTEXT dpiAwarenessContext = DPI_AWARENESS_CONTEXT.UNSPECIFIED_DPI_AWARENESS_CONTEXT;

            if (OsVersion.IsWindows10_1607OrGreater())
            {
                // Works only >= Windows 10/1607
                DPI_AWARENESS_CONTEXT awarenessContext = PInvoke.GetWindowDpiAwarenessContext(hwnd);
                DPI_AWARENESS awareness = PInvoke.GetAwarenessFromDpiAwarenessContext(awarenessContext);
                dpiAwarenessContext = ConvertToDpiAwarenessContext(awareness);
            }

            return dpiAwarenessContext;
        }

        private static DPI_AWARENESS_CONTEXT ConvertToDpiAwarenessContext(DPI_AWARENESS dpiAwareness) => dpiAwareness switch
        {
            DPI_AWARENESS.DPI_AWARENESS_UNAWARE => DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_UNAWARE,
            DPI_AWARENESS.DPI_AWARENESS_SYSTEM_AWARE => DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_SYSTEM_AWARE,
            DPI_AWARENESS.DPI_AWARENESS_PER_MONITOR_AWARE => DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
            _ => DPI_AWARENESS_CONTEXT.DPI_AWARENESS_CONTEXT_SYSTEM_AWARE,
        };
    }
}
