// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

// <summary>
//     Keyboard/Mouse simulation.
// </summary>
// <history>
//     2008 created by Truong Do (ductdo).
//     2009-... modified by Truong Do (TruongDo).
//     2023- Included in PowerToys.
// </history>
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.InteropServices;
using System.ServiceProcess;
using System.Threading.Tasks;

using Microsoft.PowerToys.Settings.UI.Library;
using MouseWithoutBorders.Core;
using Windows.UI.Input.Preview.Injection;

using static MouseWithoutBorders.Class.NativeMethods;

[module: SuppressMessage("Microsoft.Portability", "CA1901:PInvokeDeclarationsShouldBePortable", Scope = "member", Target = "MouseWithoutBorders.InputSimulation.#keybd_event(System.Byte,System.Byte,System.UInt32,System.Int32)", MessageId = "3", Justification = "Dotnet port with style preservation")]
[module: SuppressMessage("Microsoft.Usage", "CA1806:DoNotIgnoreMethodResults", Scope = "member", Target = "MouseWithoutBorders.InputSimulation.#InputProcessKeyEx(System.Int32,System.Int32,System.Boolean&)", MessageId = "MouseWithoutBorders.NativeMethods.LockWorkStation", Justification = "Dotnet port with style preservation")]

namespace MouseWithoutBorders.Class
{
    internal class InputSimulation
    {
        public static InputInjector Injector;

        private InputSimulation()
        {
        }

        internal static InjectedInputMouseInfo MouseInputToInjectedInputMouseInfo(MOUSEINPUT mouseInput)
        {
            var injectedInput = new InjectedInputMouseInfo();

            injectedInput.DeltaX = mouseInput.dx;
            injectedInput.DeltaY = mouseInput.dy;
            injectedInput.MouseData = (uint)mouseInput.mouseData;
            injectedInput.MouseOptions = (InjectedInputMouseOptions)mouseInput.dwFlags;
            injectedInput.TimeOffsetInMilliseconds = (uint)mouseInput.time;

            return injectedInput;
        }

        private static uint SendInputEx(NativeMethods.INPUT input)
        {
            Common.PaintCount = 0;

            uint rv = 0;
            if (Common.Is64bitOS)
            {
                NativeMethods.INPUT64 input64 = default;
                input64.type = input.type;

                // Keyboard
                if (input.type == 1)
                {
                    input64.ki.wVk = input.ki.wVk;
                    input64.ki.wScan = input.ki.wScan;
                    input64.ki.dwFlags = input.ki.dwFlags;
                    input64.ki.time = input.ki.time;
                    input64.ki.dwExtraInfo = input.ki.dwExtraInfo;
                }

                // Mouse
                else
                {
                    input64.mi.dx = input.mi.dx;
                    input64.mi.dy = input.mi.dy;
                    input64.mi.dwFlags = input.mi.dwFlags;
                    input64.mi.mouseData = input.mi.mouseData;
                    input64.mi.time = input.mi.time;
                    input64.mi.dwExtraInfo = input.mi.dwExtraInfo;
                }

                if (input.type == 0 && (input.mi.dwFlags & (int)NativeMethods.MOUSEEVENTF.MOVE) != 0 && NativeMethods.InjectMouseInputAvailable)
                {
                    Injector.InjectMouseInput(new[] { MouseInputToInjectedInputMouseInfo(input64.mi) });
                }
                else
                {
                    NativeMethods.INPUT64[] inputs = { input64 };
                    rv = NativeMethods.SendInput64(1, inputs, Marshal.SizeOf(input64));
                }
            }
            else
            {
                if (input.type == 0 && (input.mi.dwFlags & (int)NativeMethods.MOUSEEVENTF.MOVE) != 0 && NativeMethods.InjectMouseInputAvailable)
                {
                    Injector.InjectMouseInput(new[] { MouseInputToInjectedInputMouseInfo(input.mi) });
                }
                else
                {
                    NativeMethods.INPUT[] inputs = { input };
                    rv = NativeMethods.SendInput(1, inputs, Marshal.SizeOf(input));
                }
            }

            return rv;
        }

        internal static void SendKey(KEYBDDATA kd)
        {
            string log = string.Empty;
            NativeMethods.KEYEVENTF dwFlags = NativeMethods.KEYEVENTF.KEYDOWN;
            uint scanCode = 0;

            // http://msdn.microsoft.com/en-us/library/ms644967(VS.85).aspx
            if ((kd.dwFlags & (int)WM.LLKHF.UP) == (int)WM.LLKHF.UP)
            {
                dwFlags = NativeMethods.KEYEVENTF.KEYUP;
            }

            if ((kd.dwFlags & (int)WM.LLKHF.EXTENDED) == (int)WM.LLKHF.EXTENDED)
            {
                dwFlags |= NativeMethods.KEYEVENTF.EXTENDEDKEY;
            }

            scanCode = NativeMethods.MapVirtualKey((uint)kd.wVk, 0);

            InputProcessKeyEx(kd.wVk, kd.dwFlags, out bool eatKey);

            if (!eatKey)
            {
                InputHook.RealData = false;
#if !USING_keybd_event
                // #if USING_SendInput
                NativeMethods.INPUT structInput;
                structInput = default;
                structInput.type = 1;
                structInput.ki.wScan = (short)scanCode;
                structInput.ki.time = 0;
                structInput.ki.wVk = (short)kd.wVk;
                structInput.ki.dwFlags = (int)dwFlags;
                structInput.ki.dwExtraInfo = NativeMethods.GetMessageExtraInfo();

                Common.DoSomethingInTheInputSimulationThread(() =>
                {
                    // key press simulation
                    SendInputEx(structInput);
                });

#else
                keybd_event((byte)kd.wVk, (byte)scanCode, (UInt32)dwFlags, 0);
#endif
                InputHook.RealData = true;
            }

            log += "*"; // ((Keys)kd.wVk).ToString(CultureInfo.InvariantCulture);
            Logger.LogDebug(log);
        }

        // Md.X, Md.Y is from 0 to 65535
        internal static uint SendMouse(MOUSEDATA md)
        {
            uint rv = 0;
            NativeMethods.INPUT mouse_input = default;

            long w65535 = (MachineStuff.DesktopBounds.Right - MachineStuff.DesktopBounds.Left) * 65535 / Common.ScreenWidth;
            long h65535 = (MachineStuff.DesktopBounds.Bottom - MachineStuff.DesktopBounds.Top) * 65535 / Common.ScreenHeight;
            long l65535 = MachineStuff.DesktopBounds.Left * 65535 / Common.ScreenWidth;
            long t65535 = MachineStuff.DesktopBounds.Top * 65535 / Common.ScreenHeight;
            mouse_input.type = 0;
            long dx = (md.X * w65535 / 65535) + l65535;
            long dy = (md.Y * h65535 / 65535) + t65535;
            mouse_input.mi.dx = (int)dx;
            mouse_input.mi.dy = (int)dy;
            mouse_input.mi.mouseData = md.WheelDelta;

            if (md.dwFlags != WM.WM_MOUSEMOVE)
            {
                Logger.LogDebug($"InputSimulation.SendMouse: x = {md.X}, y = {md.Y}, WheelDelta = {md.WheelDelta}, dwFlags = {md.dwFlags}.");
            }

            switch (md.dwFlags)
            {
                case WM.WM_MOUSEMOVE:
                    mouse_input.mi.dwFlags |= (int)(NativeMethods.MOUSEEVENTF.MOVE | NativeMethods.MOUSEEVENTF.ABSOLUTE);
                    break;
                case WM.WM_LBUTTONDOWN:
                    mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.LEFTDOWN;
                    break;
                case WM.WM_LBUTTONUP:
                    mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.LEFTUP;
                    break;
                case WM.WM_RBUTTONDOWN:
                    mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.RIGHTDOWN;
                    break;
                case WM.WM_RBUTTONUP:
                    mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.RIGHTUP;
                    break;
                case WM.WM_MBUTTONDOWN:
                    mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.MIDDLEDOWN;
                    break;
                case WM.WM_MBUTTONUP:
                    mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.MIDDLEUP;
                    break;
                case WM.WM_MOUSEWHEEL:
                    mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.WHEEL;
                    break;
                case WM.WM_MOUSEHWHEEL:
                    mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.HWHEEL;
                    break;
                case WM.WM_XBUTTONUP:
                    mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.XUP;
                    break;
                case WM.WM_XBUTTONDOWN:
                    mouse_input.mi.dwFlags |= (int)NativeMethods.MOUSEEVENTF.XDOWN;
                    break;

                default:
                    break;
            }

            Common.DoSomethingInTheInputSimulationThread(() =>
            {
                InputHook.RealData = false;
                rv = SendInputEx(mouse_input);
            });

            if (Common.MainFormVisible && !DragDrop.IsDropping)
            {
                Helper.MainFormDot();
            }

            return rv;
        }

        internal static void MoveMouseEx(int x, int y)
        {
            NativeMethods.INPUT mouse_input = default;

            long w65535 = (MachineStuff.DesktopBounds.Right - MachineStuff.DesktopBounds.Left) * 65535 / Common.ScreenWidth;
            long h65535 = (MachineStuff.DesktopBounds.Bottom - MachineStuff.DesktopBounds.Top) * 65535 / Common.ScreenHeight;
            long l65535 = MachineStuff.DesktopBounds.Left * 65535 / Common.ScreenWidth;
            long t65535 = MachineStuff.DesktopBounds.Top * 65535 / Common.ScreenHeight;
            mouse_input.type = 0;
            long dx = (x * w65535 / 65535) + l65535;
            long dy = (y * h65535 / 65535) + t65535;
            mouse_input.mi.dx = (int)dx;
            mouse_input.mi.dy = (int)dy;

            Logger.LogDebug($"InputSimulation.MoveMouseEx: x = {x}, y = {y}.");

            mouse_input.mi.dwFlags |= (int)(NativeMethods.MOUSEEVENTF.MOVE | NativeMethods.MOUSEEVENTF.ABSOLUTE);

            Common.DoSomethingInTheInputSimulationThread(() =>
            {
                InputHook.RealData = false;
                SendInputEx(mouse_input);
            });
        }

        // x, y is in pixel
        internal static void MoveMouse(int x, int y)
        {
            // Common.Log("Mouse move: " + x.ToString(CultureInfo.CurrentCulture) + "," + y.ToString(CultureInfo.CurrentCulture));
            NativeMethods.INPUT mouse_input = default;
            mouse_input.type = 0;
            mouse_input.mi.dx = x * 65535 / Common.ScreenWidth;
            mouse_input.mi.dy = y * 65535 / Common.ScreenHeight;
            mouse_input.mi.mouseData = 0;
            mouse_input.mi.dwFlags = (int)(NativeMethods.MOUSEEVENTF.MOVE | NativeMethods.MOUSEEVENTF.ABSOLUTE);

            Logger.LogDebug($"InputSimulation.MoveMouse: x = {x}, y = {y}.");

            Common.DoSomethingInTheInputSimulationThread(() =>
            {
                InputHook.RealData = false;
                SendInputEx(mouse_input);

                // NativeMethods.SetCursorPos(x, y);
            });
        }

        // dx, dy is in pixel, relative
        internal static void MoveMouseRelative(int dx, int dy)
        {
            NativeMethods.INPUT mouse_input = default;
            mouse_input.type = 0;
            mouse_input.mi.dx = dx; // *65535 / Common.ScreenWidth;
            mouse_input.mi.dy = dy; // *65535 / Common.ScreenHeight;
            mouse_input.mi.mouseData = 0;
            mouse_input.mi.dwFlags = (int)NativeMethods.MOUSEEVENTF.MOVE;

            Logger.LogDebug($"InputSimulation.MoveMouseRelative: x = {dx}, y = {dy}.");

            Common.DoSomethingInTheInputSimulationThread(() =>
            {
                InputHook.RealData = false;
                SendInputEx(mouse_input);

                // NativeMethods.SetCursorPos(x, y);
            });
        }

        internal static void MouseUp()
        {
            Common.DoSomethingInTheInputSimulationThread(() =>
            {
                NativeMethods.INPUT input = default;
                input.type = 0;
                input.mi.dx = 0;
                input.mi.dy = 0;
                input.mi.mouseData = 0;
                input.mi.dwFlags = (int)NativeMethods.MOUSEEVENTF.LEFTUP;

                InputHook.SkipMouseUpCount++;
                _ = SendInputEx(input);
                Logger.LogDebug("MouseUp() called");
            });
        }

        internal static void MouseClickDotForm(int x, int y)
        {
            _ = Task.Factory.StartNew(
                () =>
            {
                NativeMethods.INPUT input = default;
                input.type = 0;
                input.mi.dx = 0;
                input.mi.dy = 0;
                input.mi.mouseData = 0;

                InputHook.SkipMouseUpDown = true;

                try
                {
                    MoveMouse(x, y);

                    InputHook.RealData = false;
                    input.mi.dwFlags = (int)NativeMethods.MOUSEEVENTF.LEFTDOWN;
                    _ = SendInputEx(input);

                    InputHook.RealData = false;
                    input.mi.dwFlags = (int)NativeMethods.MOUSEEVENTF.LEFTUP;
                    _ = SendInputEx(input);

                    Logger.LogDebug("MouseClick() called");
                    Thread.Sleep(200);
                }
                finally
                {
                    InputHook.SkipMouseUpDown = false;
                }
            },
                System.Threading.CancellationToken.None,
                TaskCreationOptions.None,
                TaskScheduler.Default);
        }

        private static bool winDown;
        private static bool ctrlDown;
        private static bool altDown;
        private static bool shiftDown;
        internal static readonly string[] Args = new string[] { "CAD" };

        private static void ResetModifiersState(HotkeySettings matchingHotkey)
        {
            ctrlDown = ctrlDown && matchingHotkey.Ctrl;
            altDown = altDown && matchingHotkey.Alt;
            shiftDown = shiftDown && matchingHotkey.Shift;
            winDown = winDown && matchingHotkey.Win;
        }

        private static void InputProcessKeyEx(int vkCode, int flags, out bool eatKey)
        {
            eatKey = false;

            if ((flags & (int)WM.LLKHF.UP) == (int)WM.LLKHF.UP)
            {
                switch ((VK)vkCode)
                {
                    case VK.LWIN:
                    case VK.RWIN:
                        winDown = false;
                        break;

                    case VK.LCONTROL:
                    case VK.RCONTROL:
                        ctrlDown = false;
                        break;

                    case VK.LMENU:
                    case VK.RMENU:
                        altDown = false;
                        break;

                    case VK.LSHIFT:
                    case VK.RSHIFT:
                        shiftDown = false;
                        break;

                    default:
                        break;
                }
            }
            else
            {
                if (Common.HotkeyMatched(vkCode, winDown, ctrlDown, altDown, shiftDown, Setting.Values.HotKeyLockMachine))
                {
                    if (!Common.RunOnLogonDesktop
                        && !Common.RunOnScrSaverDesktop)
                    {
                        ResetModifiersState(Setting.Values.HotKeyLockMachine);
                        eatKey = true;
                        InitAndCleanup.ReleaseAllKeys();
                        _ = NativeMethods.LockWorkStation();
                    }
                }

                switch ((VK)vkCode)
                {
                    case VK.LWIN:
                    case VK.RWIN:
                        winDown = true;
                        break;

                    case VK.LCONTROL:
                    case VK.RCONTROL:
                        ctrlDown = true;
                        break;

                    case VK.LMENU:
                    case VK.RMENU:
                        altDown = true;
                        break;

                    case VK.LSHIFT:
                    case VK.RSHIFT:
                        shiftDown = true;
                        break;

                    case VK.DELETE:
                        if (ctrlDown && altDown)
                        {
                            ctrlDown = altDown = false;
                            eatKey = true;
                            InitAndCleanup.ReleaseAllKeys();
                        }

                        break;

                    case (VK)'L':
                        if (winDown)
                        {
                            winDown = false;
                            eatKey = true;
                            InitAndCleanup.ReleaseAllKeys();
                            uint rv = NativeMethods.LockWorkStation();
                            Logger.LogDebug("LockWorkStation returned " + rv.ToString(CultureInfo.CurrentCulture));
                        }

                        break;

                    case VK.END:
                        if (ctrlDown && altDown)
                        {
                            ctrlDown = altDown = false;
                            new ServiceController("MouseWithoutBordersSvc").Start(Args);
                        }

                        break;

                    default:
                        break;
                }
            }
        }

        internal static void ResetSystemKeyFlags()
        {
            ctrlDown = winDown = altDown = false;
        }
    }
}
