// 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;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
using Windows.Win32.UI.Input.KeyboardAndMouse;

namespace System.Windows.Forms;

/// <summary>
///  Provides methods for sending keystrokes to an application.
/// </summary>
public partial class SendKeys
{
    // It is unclear what significance the value 10 has, but it seems to make sense to make this a constant rather
    // than have 10 sprinkled throughout the code. It appears to be a sentinel value of some sort - indicating an
    // unknown grouping level.
    private const int UnknownGrouping = 10;

    private static readonly KeywordVk[] s_keywords = [
        new("ENTER",      Keys.Return),
        new("TAB",        Keys.Tab),
        new("ESC",        Keys.Escape),
        new("ESCAPE",     Keys.Escape),
        new("HOME",       Keys.Home),
        new("END",        Keys.End),
        new("LEFT",       Keys.Left),
        new("RIGHT",      Keys.Right),
        new("UP",         Keys.Up),
        new("DOWN",       Keys.Down),
        new("PGUP",       Keys.Prior),
        new("PGDN",       Keys.Next),
        new("NUMLOCK",    Keys.NumLock),
        new("SCROLLLOCK", Keys.Scroll),
        new("PRTSC",      Keys.PrintScreen),
        new("BREAK",      Keys.Cancel),
        new("BACKSPACE",  Keys.Back),
        new("BKSP",       Keys.Back),
        new("BS",         Keys.Back),
        new("CLEAR",      Keys.Clear),
        new("CAPSLOCK",   Keys.Capital),
        new("INS",        Keys.Insert),
        new("INSERT",     Keys.Insert),
        new("DEL",        Keys.Delete),
        new("DELETE",     Keys.Delete),
        new("HELP",       Keys.Help),
        new("F1",         Keys.F1),
        new("F2",         Keys.F2),
        new("F3",         Keys.F3),
        new("F4",         Keys.F4),
        new("F5",         Keys.F5),
        new("F6",         Keys.F6),
        new("F7",         Keys.F7),
        new("F8",         Keys.F8),
        new("F9",         Keys.F9),
        new("F10",        Keys.F10),
        new("F11",        Keys.F11),
        new("F12",        Keys.F12),
        new("F13",        Keys.F13),
        new("F14",        Keys.F14),
        new("F15",        Keys.F15),
        new("F16",        Keys.F16),
        new("MULTIPLY",   Keys.Multiply),
        new("ADD",        Keys.Add),
        new("SUBTRACT",   Keys.Subtract),
        new("DIVIDE",     Keys.Divide),
        new("+",          Keys.Add),
        new("%",          (Keys.D5 | Keys.Shift)),
        new("^",          (Keys.D6 | Keys.Shift)),
    ];

    // For use with VkKeyScanW()
    private const int ShiftKeyPressed = 0x0100;
    private const int CtrlKeyPressed = 0x0200;
    private const int AltKeyPressed = 0x0400;

    private static bool s_stopHook;
    private static HHOOK s_hhook;

    /// <summary>
    ///  Vector of events that we have yet to post to the journaling hook.
    /// </summary>
    private static readonly Queue<SKEvent> s_events = new();

    private static readonly Lock s_lock = new();
    private static bool s_startNewChar;
    private static readonly SKWindow s_messageWindow;

    private static SendMethodTypes? s_sendMethod;
    private static bool? s_hookSupported;

    // Used only for SendInput because SendInput alters the global state of the keyboard
    private static bool s_capslockChanged;
    private static bool s_numlockChanged;
    private static bool s_scrollLockChanged;
    private static bool s_kanaChanged;

    static SendKeys()
    {
        Application.ThreadExit += OnThreadExit;
        s_messageWindow = new SKWindow();
        s_messageWindow.CreateControl();
    }

    /// <summary>
    ///  Private constructor to prevent people from creating one of these. They should use public static methods.
    /// </summary>
    private SendKeys()
    {
    }

    /// <summary>
    ///  Adds an event to our list of events for the hook.
    /// </summary>
    private static void AddEvent(SKEvent skevent)
    {
        s_events.Enqueue(skevent);
    }

    /// <summary>
    ///  Helper function for ParseKeys for doing simple, self-describing characters.
    /// </summary>
    private static bool AddSimpleKey(
        char character,
        int repeat,
        HWND hwnd,
        ref (int HaveShift, int HaveCtrl, int HaveAlt) haveKeys,
        bool startNewChar,
        int cGrp)
    {
        int vk = PInvoke.VkKeyScan(character);

        if (vk != -1)
        {
            if (haveKeys.HaveShift == 0 && (vk & ShiftKeyPressed) != 0)
            {
                AddEvent(new SKEvent(PInvokeCore.WM_KEYDOWN, (uint)Keys.ShiftKey, startNewChar, hwnd));
                startNewChar = false;
                haveKeys.HaveShift = UnknownGrouping;
            }

            if (haveKeys.HaveCtrl == 0 && (vk & CtrlKeyPressed) != 0)
            {
                AddEvent(new SKEvent(PInvokeCore.WM_KEYDOWN, (uint)Keys.ControlKey, startNewChar, hwnd));
                startNewChar = false;
                haveKeys.HaveCtrl = UnknownGrouping;
            }

            if (haveKeys.HaveAlt == 0 && (vk & AltKeyPressed) != 0)
            {
                AddEvent(new SKEvent(PInvokeCore.WM_KEYDOWN, (uint)Keys.Menu, startNewChar, hwnd));
                startNewChar = false;
                haveKeys.HaveAlt = UnknownGrouping;
            }

            AddMsgsForVK(vk & 0xff, repeat, haveKeys.HaveAlt > 0 && haveKeys.HaveCtrl == 0, hwnd);
            CancelMods(ref haveKeys, UnknownGrouping, hwnd);
        }
        else
        {
            uint oemVal = PInvoke.OemKeyScan((ushort)(0xFF & character));
            for (int i = 0; i < repeat; i++)
            {
                AddEvent(new SKEvent(PInvokeCore.WM_CHAR, character, (oemVal & 0xFFFF), hwnd));
            }
        }

        if (cGrp != 0)
        {
            startNewChar = true;
        }

        return startNewChar;
    }

    /// <summary>
    ///  Given the vk, add the appropriate messages for it.
    /// </summary>
    private static void AddMsgsForVK(int vk, int repeat, bool altnoctrldown, HWND hwnd)
    {
        for (int i = 0; i < repeat; i++)
        {
            AddEvent(new SKEvent(altnoctrldown ? PInvokeCore.WM_SYSKEYDOWN : PInvokeCore.WM_KEYDOWN, (uint)vk, s_startNewChar, hwnd));
            AddEvent(new SKEvent(altnoctrldown ? PInvokeCore.WM_SYSKEYUP : PInvokeCore.WM_KEYUP, (uint)vk, s_startNewChar, hwnd));
        }
    }

    /// <summary>
    ///  Called whenever there is a closing parenthesis, or the end of a character. This generates events for the
    ///  end of a modifier.
    /// </summary>
    private static void CancelMods(ref (int HaveShift, int HaveCtrl, int HaveAlt) haveKeys, int level, HWND hwnd)
    {
        if (haveKeys.HaveShift == level)
        {
            AddEvent(new SKEvent(PInvokeCore.WM_KEYUP, (int)Keys.ShiftKey, false, hwnd));
            haveKeys.HaveShift = 0;
        }

        if (haveKeys.HaveCtrl == level)
        {
            AddEvent(new SKEvent(PInvokeCore.WM_KEYUP, (int)Keys.ControlKey, false, hwnd));
            haveKeys.HaveCtrl = 0;
        }

        if (haveKeys.HaveAlt == level)
        {
            AddEvent(new SKEvent(PInvokeCore.WM_SYSKEYUP, (int)Keys.Menu, false, hwnd));
            haveKeys.HaveAlt = 0;
        }
    }

    /// <summary>
    ///  Install the hook.
    /// </summary>
    private static unsafe void InstallHook()
    {
        if (s_hhook.IsNull)
        {
            s_stopHook = false;
            s_hhook = PInvoke.SetWindowsHookEx(
                WINDOWS_HOOK_ID.WH_JOURNALPLAYBACK,
                &SendKeysHookProc.Callback,
                PInvoke.GetModuleHandle((PCWSTR)null),
                0);

            if (s_hhook.IsNull)
            {
                throw new SecurityException(SR.SendKeysHookFailed);
            }
        }
    }

    private static unsafe void TestHook()
    {
        s_hookSupported = false;
        try
        {
            HHOOK hookHandle = PInvoke.SetWindowsHookEx(
                WINDOWS_HOOK_ID.WH_JOURNALPLAYBACK,
                &EmptyHookCallback,
                PInvoke.GetModuleHandle((PCWSTR)null),
                0);

            s_hookSupported = !hookHandle.IsNull;

            if (!hookHandle.IsNull)
            {
                PInvoke.UnhookWindowsHookEx(hookHandle);
            }
        }
        catch
        {
            // Ignore any exceptions to keep existing SendKeys behavior
        }
    }

#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant
    [UnmanagedCallersOnly(CallConvs = [typeof(CallConvStdcall)])]
#pragma warning restore CS3016
    private static unsafe LRESULT EmptyHookCallback(int nCode, WPARAM wparam, LPARAM lparam) => (LRESULT)0;

    private static void LoadSendMethodFromConfig()
    {
        if (!s_sendMethod.HasValue)
        {
            s_sendMethod = SendMethodTypes.Default;

            try
            {
                // Read SendKeys value from config file, not case sensitive.
                string? value = Configuration.ConfigurationManager.AppSettings.Get("SendKeys");

                if (string.IsNullOrEmpty(value))
                {
                    return;
                }

                if (value.Equals("JournalHook", StringComparison.OrdinalIgnoreCase))
                {
                    s_sendMethod = SendMethodTypes.JournalHook;
                }
                else if (value.Equals("SendInput", StringComparison.OrdinalIgnoreCase))
                {
                    s_sendMethod = SendMethodTypes.SendInput;
                }
            }
            catch { } // ignore any exceptions to keep existing SendKeys behavior
        }
    }

    /// <summary>
    ///  Tells us to shut down the server, perhaps if we're shutting down and the hook is still running
    /// </summary>
    private static void JournalCancel()
    {
        if (!s_hhook.IsNull)
        {
            s_stopHook = false;
            s_events.Clear();

            s_hhook = default;
        }
    }

    private static unsafe void GetKeyboardState(Span<byte> keystate)
    {
        if (keystate.Length < 256)
            throw new InvalidOperationException();

        fixed (byte* b = keystate)
        {
            PInvoke.GetKeyboardState(b);
        }
    }

    private static unsafe void SetKeyboardState(ReadOnlySpan<byte> keystate)
    {
        if (keystate.Length < 256)
            throw new InvalidOperationException();

        fixed (byte* b = keystate)
        {
            PInvoke.SetKeyboardState(b);
        }
    }

    /// <summary>
    ///  Before we do a sendkeys, we want to clear the state of a couple of keys [capslock, numlock, scrolllock]
    ///  so they don't interfere.
    /// </summary>
    private static void ClearKeyboardState()
    {
        Span<byte> keystate = stackalloc byte[256];
        GetKeyboardState(keystate);
        keystate[(int)Keys.Capital] = 0;
        keystate[(int)Keys.NumLock] = 0;
        keystate[(int)Keys.Scroll] = 0;
        SetKeyboardState(keystate);
    }

    /// <summary>
    ///  Given the string, match the keyword to a VK. Return -1 if we don't match anything.
    /// </summary>
    private static int MatchKeyword(string keyword)
    {
        for (int i = 0; i < s_keywords.Length; i++)
        {
            if (string.Equals(s_keywords[i].Keyword, keyword, StringComparison.OrdinalIgnoreCase))
            {
                return s_keywords[i].VK;
            }
        }

        return -1;
    }

    /// <summary>
    ///  This event is raised from Application when each window thread terminates. It gives us a chance to
    ///  uninstall our journal hook if we had one installed.
    /// </summary>
    private static void OnThreadExit(object? sender, EventArgs e)
    {
        try
        {
            UninstallJournalingHook();
        }
        catch
        {
        }
    }

    /// <summary>
    ///  Parse the string the user has given us, and generate the appropriate events for the journaling hook.
    /// </summary>
    private static void ParseKeys(string keys, HWND hwnd)
    {
        int i = 0;

        // These four variables are used for grouping
        (int HaveShift, int HaveCtrl, int HaveAlt) haveKeys = default;
        int cGrp = 0;

        // startNewChar indicates that the next msg will be the first of a char or char group. This is needed for
        // IntraApp Nesting of SendKeys.

        s_startNewChar = true;

        // Start walking through the characters one at a time.

        int keysLen = keys.Length;
        while (i < keysLen)
        {
            int repeat = 1;
            char ch = keys[i];
            int vk;

            switch (ch)
            {
                case '}':
                    // If these appear at this point they are out of context, so return an error. KeyStart
                    // processes ochKeys up to the appropriate KeyEnd.
                    throw new ArgumentException(string.Format(SR.InvalidSendKeysString, keys));

                case '{':
                    int j = i + 1;

                    // There's a unique class of strings of the form "{} n}" where n is an integer - in this case
                    // we want to send n copies of the '}' character. Here we test for the possibility of this
                    // class of problems, and skip the first '}' in the string if necessary.
                    if (j + 1 < keysLen && keys[j] == '}')
                    {
                        // Scan for the final '}' character
                        int final = j + 1;
                        while (final < keysLen && keys[final] != '}')
                        {
                            final++;
                        }

                        if (final < keysLen)
                        {
                            // Found the special case, so skip the first '}' in the string. The remainder of the
                            // code will attempt to find the repeat count.
                            j++;
                        }
                    }

                    // We're in a {<KEYWORD>...} situation. Look for the keyword.
                    while (j < keysLen && keys[j] != '}'
                           && !char.IsWhiteSpace(keys[j]))
                    {
                        j++;
                    }

                    if (j >= keysLen)
                    {
                        throw new ArgumentException(SR.SendKeysKeywordDelimError);
                    }

                    // Have our KEYWORD. Verify it's one we know about.
                    string keyName = keys[(i + 1)..j];

                    // See if we have a space, which would mean a repeat count.
                    if (char.IsWhiteSpace(keys[j]))
                    {
                        int digit;
                        while (j < keysLen && char.IsWhiteSpace(keys[j]))
                        {
                            j++;
                        }

                        if (j >= keysLen)
                        {
                            throw new ArgumentException(SR.SendKeysKeywordDelimError);
                        }

                        if (char.IsDigit(keys[j]))
                        {
                            digit = j;
                            while (j < keysLen && char.IsDigit(keys[j]))
                            {
                                j++;
                            }

                            repeat = int.Parse(keys.AsSpan(digit, j - digit), CultureInfo.InvariantCulture);
                        }
                    }

                    if (j >= keysLen)
                    {
                        throw new ArgumentException(SR.SendKeysKeywordDelimError);
                    }

                    if (keys[j] != '}')
                    {
                        throw new ArgumentException(SR.InvalidSendKeysRepeat);
                    }

                    vk = MatchKeyword(keyName);
                    if (vk != -1)
                    {
                        // Unlike AddSimpleKey, the bit mask uses Keys, rather than scan keys
                        if (haveKeys.HaveShift == 0 && (vk & (int)Keys.Shift) != 0)
                        {
                            AddEvent(new SKEvent(PInvokeCore.WM_KEYDOWN, (uint)Keys.ShiftKey, s_startNewChar, hwnd));
                            s_startNewChar = false;
                            haveKeys.HaveShift = UnknownGrouping;
                        }

                        if (haveKeys.HaveCtrl == 0 && (vk & (int)Keys.Control) != 0)
                        {
                            AddEvent(new SKEvent(PInvokeCore.WM_KEYDOWN, (uint)Keys.ControlKey, s_startNewChar, hwnd));
                            s_startNewChar = false;
                            haveKeys.HaveCtrl = UnknownGrouping;
                        }

                        if (haveKeys.HaveAlt == 0 && (vk & (int)Keys.Alt) != 0)
                        {
                            AddEvent(new SKEvent(PInvokeCore.WM_KEYDOWN, (uint)Keys.Menu, s_startNewChar, hwnd));
                            s_startNewChar = false;
                            haveKeys.HaveAlt = UnknownGrouping;
                        }

                        AddMsgsForVK(vk, repeat, haveKeys.HaveAlt > 0 && haveKeys.HaveCtrl == 0, hwnd);
                        CancelMods(ref haveKeys, UnknownGrouping, hwnd);
                    }
                    else if (keyName.Length == 1)
                    {
                        s_startNewChar = AddSimpleKey(keyName[0], repeat, hwnd, ref haveKeys, s_startNewChar, cGrp);
                    }
                    else
                    {
                        throw new ArgumentException(string.Format(SR.InvalidSendKeysKeyword, keys[(i + 1)..j]));
                    }

                    // don't forget to position ourselves at the end of the {...} group
                    i = j;
                    break;

                case '+':
                    if (haveKeys.HaveShift != 0)
                    {
                        throw new ArgumentException(string.Format(SR.InvalidSendKeysString, keys));
                    }

                    AddEvent(new SKEvent(PInvokeCore.WM_KEYDOWN, (uint)Keys.ShiftKey, s_startNewChar, hwnd));
                    s_startNewChar = false;
                    haveKeys.HaveShift = UnknownGrouping;
                    break;

                case '^':
                    if (haveKeys.HaveCtrl != 0)
                    {
                        throw new ArgumentException(string.Format(SR.InvalidSendKeysString, keys));
                    }

                    AddEvent(new SKEvent(PInvokeCore.WM_KEYDOWN, (uint)Keys.ControlKey, s_startNewChar, hwnd));
                    s_startNewChar = false;
                    haveKeys.HaveCtrl = UnknownGrouping;
                    break;

                case '%':
                    if (haveKeys.HaveAlt != 0)
                    {
                        throw new ArgumentException(string.Format(SR.InvalidSendKeysString, keys));
                    }

                    AddEvent(new SKEvent(
                        haveKeys.HaveCtrl != 0 ? PInvokeCore.WM_KEYDOWN : PInvokeCore.WM_SYSKEYDOWN,
                        (int)Keys.Menu,
                        s_startNewChar,
                        hwnd));

                    s_startNewChar = false;
                    haveKeys.HaveAlt = UnknownGrouping;
                    break;

                case '(':
                    // Convert all immediate mode states to group mode. Allows multiple keys with the same shift,
                    // etc. state. Nests three deep.
                    cGrp++;
                    if (cGrp > 3)
                    {
                        throw new ArgumentException(SR.SendKeysNestingError);
                    }

                    if (haveKeys.HaveShift == UnknownGrouping)
                    {
                        haveKeys.HaveShift = cGrp;
                    }

                    if (haveKeys.HaveCtrl == UnknownGrouping)
                    {
                        haveKeys.HaveCtrl = cGrp;
                    }

                    if (haveKeys.HaveAlt == UnknownGrouping)
                    {
                        haveKeys.HaveAlt = cGrp;
                    }

                    break;

                case ')':
                    if (cGrp < 1)
                    {
                        throw new ArgumentException(string.Format(SR.InvalidSendKeysString, keys));
                    }

                    CancelMods(ref haveKeys, cGrp, hwnd);
                    cGrp--;
                    if (cGrp == 0)
                    {
                        s_startNewChar = true;
                    }

                    break;

                case '~':
                    vk = (int)Keys.Return;
                    AddMsgsForVK(vk, repeat, haveKeys.HaveAlt > 0 && haveKeys.HaveCtrl == 0, hwnd);
                    break;

                default:
                    s_startNewChar = AddSimpleKey(keys[i], repeat, hwnd, ref haveKeys, s_startNewChar, cGrp);
                    break;
            }

            // Next element in the string.
            i++;
        }

        if (cGrp != 0)
        {
            throw new ArgumentException(SR.SendKeysGroupDelimError);
        }

        CancelMods(ref haveKeys, UnknownGrouping, hwnd);
    }

    /// <summary>
    ///  Uses User32 SendInput to send keystrokes.
    /// </summary>
    private static unsafe void SendInput(ReadOnlySpan<byte> oldKeyboardState, SKEvent[]? previousEvents)
    {
        // Should be a no-op most of the time
        AddCancelModifiersForPreviousEvents(previousEvents);

        // SKEvents are sent as 1 or 2 inputs:
        //
        //   currentInput[0] represents the SKEvent
        //   currentInput[1] is a KeyUp to prevent all identical WM_CHARs to be sent as one message
        Span<INPUT> currentInput = stackalloc INPUT[2];

        // All events are Keyboard events.
        currentInput[0].type = INPUT_TYPE.INPUT_KEYBOARD;
        currentInput[1].type = INPUT_TYPE.INPUT_KEYBOARD;

        // Set KeyUp values for currentInput[1].
        currentInput[1].Anonymous.ki.wVk = 0;
        currentInput[1].Anonymous.ki.dwFlags = KEYBD_EVENT_FLAGS.KEYEVENTF_UNICODE | KEYBD_EVENT_FLAGS.KEYEVENTF_KEYUP;

        // Initialize unused members.
        currentInput[0].Anonymous.ki.dwExtraInfo = UIntPtr.Zero;
        currentInput[0].Anonymous.ki.time = 0;
        currentInput[1].Anonymous.ki.dwExtraInfo = UIntPtr.Zero;
        currentInput[1].Anonymous.ki.time = 0;

        // Send each of our SKEvents using SendInput.
        int INPUTSize = sizeof(INPUT);

        // Need these outside the lock below.
        uint eventsSent = 0;
        int eventsTotal;

        // A lock here will allow multiple threads to SendInput at the same time. This mimics the JournalHook
        // method of using the message loop to mitigate threading issues. There is still a theoretical thread
        // issue with adding to the events Queue (both JournalHook and SendInput), but we do not want to alter
        // the timings of the existing shipped behavior. Tested with 2 threads on a multiproc machine.
        lock (s_lock)
        {
            // Block keyboard and mouse input events from reaching applications.
            bool blockInputSuccess = PInvoke.BlockInput(true);

            try
            {
                eventsTotal = s_events.Count;
                ClearGlobalKeys();

                for (int i = 0; i < eventsTotal; i++)
                {
                    SKEvent skEvent = s_events.Dequeue();

                    currentInput[0].Anonymous.ki.dwFlags = 0;

                    if (skEvent.WM == PInvokeCore.WM_CHAR)
                    {
                        // For WM_CHAR, send a KEYEVENTF_UNICODE instead of a Keyboard event to support extended
                        // ASCII characters with no keyboard equivalent. Send currentInput[1] in this case.
                        currentInput[0].Anonymous.ki.wVk = 0;
                        currentInput[0].Anonymous.ki.wScan = (ushort)skEvent.ParamL;
                        currentInput[0].Anonymous.ki.dwFlags = KEYBD_EVENT_FLAGS.KEYEVENTF_UNICODE;
                        currentInput[1].Anonymous.ki.wScan = (ushort)skEvent.ParamL;

                        // Call SendInput, increment the eventsSent but subtract 1 for the extra one sent.
                        eventsSent += PInvoke.SendInput(currentInput, INPUTSize) - 1;
                    }
                    else
                    {
                        // Just need to send currentInput[0] for skEvent.
                        currentInput[0].Anonymous.ki.wScan = 0;

                        // Add KeyUp flag if we have a KeyUp.
                        if (skEvent.WM == PInvokeCore.WM_KEYUP || skEvent.WM == PInvokeCore.WM_SYSKEYUP)
                        {
                            currentInput[0].Anonymous.ki.dwFlags |= KEYBD_EVENT_FLAGS.KEYEVENTF_KEYUP;
                        }

                        // Sets KEYEVENTF_EXTENDEDKEY flag if necessary.
                        if (IsExtendedKey(skEvent))
                        {
                            currentInput[0].Anonymous.ki.dwFlags |= KEYBD_EVENT_FLAGS.KEYEVENTF_EXTENDEDKEY;
                        }

                        currentInput[0].Anonymous.ki.wVk = (VIRTUAL_KEY)skEvent.ParamL;

                        // Send only currentInput[0].
                        eventsSent += PInvoke.SendInput(currentInput[..1], INPUTSize);

                        CheckGlobalKeys(skEvent);
                    }

                    // We need this slight delay here for Alt-Tab to work on Vista when the Aero theme is running.
                    // Although this does not look good, a delay here actually more closely resembles the old
                    // JournalHook that processes each event individually in the hook callback.
                    Thread.Sleep(1);
                }

                // Reset the keyboard back to what it was before inputs were sent, SendInput modifies the global
                // lights on the keyboard (caps, scroll..) so we need to call it again to undo those changes.
                ResetKeyboardUsingSendInput(INPUTSize);
            }
            finally
            {
                SetKeyboardState(oldKeyboardState);

                // Unblock input if it was previously blocked.
                if (blockInputSuccess)
                {
                    PInvoke.BlockInput(false);
                }
            }
        }

        // Check to see if we sent the number of events we're supposed to
        if (eventsSent != eventsTotal)
        {
            // Should try to move this up to where we fail out as the last error is likely to get overwritten.
            throw new Win32Exception();
        }
    }

    /// <summary>
    ///  For SendInput, these previous events that stick around if an Exception was thrown could modify the state
    ///  of the keyboard modifiers (alt, ctrl, shift). We must send a KeyUp for those, JournalHook doesn't
    ///  permanently set the state so it's ok.
    /// </summary>
    private static void AddCancelModifiersForPreviousEvents(SKEvent[]? previousEvents)
    {
        if (previousEvents is null)
        {
            return;
        }

        bool shift = false;
        bool ctrl = false;
        bool alt = false;

        foreach (SKEvent skEvent in previousEvents)
        {
            bool isOn;
            if ((skEvent.WM == PInvokeCore.WM_KEYUP) || (skEvent.WM == PInvokeCore.WM_SYSKEYUP))
            {
                isOn = false;
            }
            else if ((skEvent.WM == PInvokeCore.WM_KEYDOWN) || (skEvent.WM == PInvokeCore.WM_SYSKEYDOWN))
            {
                isOn = true;
            }
            else
            {
                continue;
            }

            if (skEvent.ParamL == (int)Keys.ShiftKey)
            {
                shift = isOn;
            }
            else if (skEvent.ParamL == (int)Keys.ControlKey)
            {
                ctrl = isOn;
            }
            else if (skEvent.ParamL == (int)Keys.Menu)
            {
                alt = isOn;
            }
        }

        if (shift)
        {
            AddEvent(new SKEvent(PInvokeCore.WM_KEYUP, (int)Keys.ShiftKey, false, default));
        }
        else if (ctrl)
        {
            AddEvent(new SKEvent(PInvokeCore.WM_KEYUP, (int)Keys.ControlKey, false, default));
        }
        else if (alt)
        {
            AddEvent(new SKEvent(PInvokeCore.WM_SYSKEYUP, (int)Keys.Menu, false, default));
        }
    }

    private static bool IsExtendedKey(SKEvent skEvent)
    {
        return (VIRTUAL_KEY)skEvent.ParamL is VIRTUAL_KEY.VK_UP
            or VIRTUAL_KEY.VK_DOWN
            or VIRTUAL_KEY.VK_LEFT
            or VIRTUAL_KEY.VK_RIGHT
            or VIRTUAL_KEY.VK_PRIOR
            or VIRTUAL_KEY.VK_NEXT
            or VIRTUAL_KEY.VK_HOME
            or VIRTUAL_KEY.VK_END
            or VIRTUAL_KEY.VK_INSERT
            or VIRTUAL_KEY.VK_DELETE;
    }

    private static void ClearGlobalKeys()
    {
        s_capslockChanged = false;
        s_numlockChanged = false;
        s_scrollLockChanged = false;
        s_kanaChanged = false;
    }

    private static void CheckGlobalKeys(SKEvent skEvent)
    {
        if (skEvent.WM == PInvokeCore.WM_KEYDOWN)
        {
            switch (skEvent.ParamL)
            {
                case (int)Keys.CapsLock:
                    s_capslockChanged = !s_capslockChanged;
                    break;
                case (int)Keys.NumLock:
                    s_numlockChanged = !s_numlockChanged;
                    break;
                case (int)Keys.Scroll:
                    s_scrollLockChanged = !s_scrollLockChanged;
                    break;
                case (int)Keys.KanaMode:
                    s_kanaChanged = !s_kanaChanged;
                    break;
            }
        }
    }

    private static void ResetKeyboardUsingSendInput(int INPUTSize)
    {
        // If the new state is the same, we don't need to do anything.
        if (!(s_capslockChanged || s_numlockChanged || s_scrollLockChanged || s_kanaChanged))
        {
            return;
        }

        // INPUT struct for resetting the keyboard.
        Span<INPUT> keyboardInput = stackalloc INPUT[2];

        keyboardInput[0].type = INPUT_TYPE.INPUT_KEYBOARD;
        keyboardInput[0].Anonymous.ki.dwFlags = 0;

        keyboardInput[1].type = INPUT_TYPE.INPUT_KEYBOARD;
        keyboardInput[1].Anonymous.ki.dwFlags = KEYBD_EVENT_FLAGS.KEYEVENTF_KEYUP;

        // SendInputs to turn on or off these keys. Inputs are pairs because KeyUp is sent for each one.
        if (s_capslockChanged)
        {
            keyboardInput[0].Anonymous.ki.wVk = VIRTUAL_KEY.VK_CAPITAL;
            keyboardInput[1].Anonymous.ki.wVk = VIRTUAL_KEY.VK_CAPITAL;
            PInvoke.SendInput(keyboardInput, INPUTSize);
        }

        if (s_numlockChanged)
        {
            keyboardInput[0].Anonymous.ki.wVk = VIRTUAL_KEY.VK_NUMLOCK;
            keyboardInput[1].Anonymous.ki.wVk = VIRTUAL_KEY.VK_NUMLOCK;
            PInvoke.SendInput(keyboardInput, INPUTSize);
        }

        if (s_scrollLockChanged)
        {
            keyboardInput[0].Anonymous.ki.wVk = VIRTUAL_KEY.VK_SCROLL;
            keyboardInput[1].Anonymous.ki.wVk = VIRTUAL_KEY.VK_SCROLL;
            PInvoke.SendInput(keyboardInput, INPUTSize);
        }

        if (s_kanaChanged)
        {
            keyboardInput[0].Anonymous.ki.wVk = VIRTUAL_KEY.VK_KANA;
            keyboardInput[1].Anonymous.ki.wVk = VIRTUAL_KEY.VK_KANA;
            PInvoke.SendInput(keyboardInput, INPUTSize);
        }
    }

    /// <summary>
    ///  Sends keystrokes to the active application.
    /// </summary>
    public static void Send(string keys) => Send(keys, null, false);

    private static void Send(string keys, Control? control, bool wait)
    {
        if (string.IsNullOrEmpty(keys))
        {
            return;
        }

        // If we're not going to wait, make sure there is a pump.
        if (!wait && !Application.MessageLoop)
        {
            throw new InvalidOperationException(SR.SendKeysNoMessageLoop);
        }

        // For SendInput only, see AddCancelModifiersForPreviousEvents for details.
        SKEvent[]? previousEvents = null;
        if (s_events.Count != 0)
        {
            previousEvents = [.. s_events];
        }

        // Generate the list of events that we're going to fire off with the hook.
        ParseKeys(keys, (control is not null) ? (HWND)control.Handle : default);

        // If there weren't any events posted as a result, we're done!
        if (s_events.Count == 0)
        {
            return;
        }

        LoadSendMethodFromConfig();

        Span<byte> oldState = stackalloc byte[256];
        GetKeyboardState(oldState);

        if (s_sendMethod!.Value != SendMethodTypes.SendInput)
        {
            if (!s_hookSupported.HasValue && s_sendMethod.Value == SendMethodTypes.Default)
            {
                // We don't know if the JournalHook will work, test it out so we know whether or not to call
                // ClearKeyboardState. ClearKeyboardState does nothing for JournalHooks but inversely affects
                // SendInput.
                TestHook();
            }

            if (s_sendMethod.Value == SendMethodTypes.JournalHook || s_hookSupported!.Value)
            {
                ClearKeyboardState();
                InstallHook();
                SetKeyboardState(oldState);
            }
        }

        if (s_sendMethod.Value == SendMethodTypes.SendInput ||
            (s_sendMethod.Value == SendMethodTypes.Default && !s_hookSupported!.Value))
        {
            // Either SendInput is configured or JournalHooks failed by default, call SendInput
            SendInput(oldState, previousEvents);
        }

        if (wait)
        {
            Flush();
        }
    }

    /// <summary>
    ///  Sends the given keys to the active application, and then waits for  the messages to be processed.
    /// </summary>
    public static void SendWait(string keys) => SendWait(keys, null);

    /// <summary>
    ///  Sends the given keys to the active application, and then waits for the messages to be processed.
    /// </summary>
    /// <remarks>
    ///  <para>
    ///   WARNING: this method will never work if control is not null, because while Windows journaling *looks* like it
    ///   can be directed to a specific HWND, it can't.
    ///  </para>
    /// </remarks>
    private static void SendWait(string keys, Control? control)
    {
        Send(keys, control, true);
    }

    /// <summary>
    ///  Processes all the Windows messages currently in the message queue.
    /// </summary>
    public static void Flush()
    {
        Application.DoEvents();
        while (s_events.Count > 0)
        {
            Application.DoEvents();
        }
    }

    /// <summary>
    ///  Cleans up and uninstalls the hook.
    /// </summary>
    private static void UninstallJournalingHook()
    {
        if (!s_hhook.IsNull)
        {
            s_stopHook = false;
            s_events.Clear();

            PInvoke.UnhookWindowsHookEx(s_hhook);
            s_hhook = default;
        }
    }
}
