// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
// Partial Copyright (C) Michael Möller <mmoeller@openhardwaremonitor.org> and Contributors.
// All Rights Reserved.

using System;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
#if !NETFRAMEWORK
using Mono.Unix.Native;
#else
using System.Reflection;
#endif
using Windows.Win32;
using Windows.Win32.System.Memory;

namespace LibreHardwareMonitor.Hardware;

internal static class OpCode
{
    public static CpuidDelegate CpuId;
    public static RdtscDelegate Rdtsc;

    private static IntPtr _codeBuffer;
    private static ulong _size;

    // void __stdcall cpuidex(unsigned int index, unsigned int ecxValue,
    //   unsigned int* eax, unsigned int* ebx, unsigned int* ecx,
    //   unsigned int* edx)
    // {
    //   int info[4];
    //   __cpuidex(info, index, ecxValue);
    //   *eax = info[0];
    //   *ebx = info[1];
    //   *ecx = info[2];
    //   *edx = info[3];
    // }

    private static readonly byte[] CpuId32 =
    [
        0x55, // push ebp
        0x8B,
        0xEC, // mov ebp, esp
        0x83,
        0xEC,
        0x10, // sub esp, 10h
        0x8B,
        0x45,
        0x08, // mov eax, dword ptr [ebp+8]
        0x8B,
        0x4D,
        0x0C, // mov ecx, dword ptr [ebp+0Ch]
        0x53, // push ebx
        0x0F,
        0xA2, // cpuid
        0x56, // push esi
        0x8D,
        0x75,
        0xF0, // lea esi, [info]
        0x89,
        0x06, // mov dword ptr [esi],eax
        0x8B,
        0x45,
        0x10, // mov eax, dword ptr [eax]
        0x89,
        0x5E,
        0x04, // mov dword ptr [esi+4], ebx
        0x89,
        0x4E,
        0x08, // mov dword ptr [esi+8], ecx
        0x89,
        0x56,
        0x0C, // mov dword ptr [esi+0Ch], edx
        0x8B,
        0x4D,
        0xF0, // mov ecx, dword ptr [info]
        0x89,
        0x08, // mov dword ptr [eax], ecx
        0x8B,
        0x45,
        0x14, // mov eax, dword ptr [ebx]
        0x8B,
        0x4D,
        0xF4, // mov ecx, dword ptr [ebp-0Ch]
        0x89,
        0x08, // mov dword ptr [eax], ecx
        0x8B,
        0x45,
        0x18, // mov eax, dword ptr [ecx]
        0x8B,
        0x4D,
        0xF8, // mov ecx, dword ptr [ebp-8]
        0x89,
        0x08, // mov dword ptr [eax], ecx
        0x8B,
        0x45,
        0x1C, // mov eax, dword ptr [edx]
        0x8B,
        0x4D,
        0xFC, // mov ecx, dword ptr [ebp-4]
        0x5E, // pop esi
        0x89,
        0x08, // mov dword ptr [eax], ecx
        0x5B, // pop ebx
        0xC9, // leave
        0xC2,
        0x18,
        0x00 // ret 18h
    ];

    private static readonly byte[] CpuId64Linux =
    [
        0x49,
        0x89,
        0xD2, // mov r10, rdx
        0x49,
        0x89,
        0xCB, // mov r11, rcx
        0x53, // push rbx
        0x89,
        0xF8, // mov eax, edi
        0x89,
        0xF1, // mov ecx, esi
        0x0F,
        0xA2, // cpuid
        0x41,
        0x89,
        0x02, // mov dword ptr [r10], eax
        0x41,
        0x89,
        0x1B, // mov dword ptr [r11], ebx
        0x41,
        0x89,
        0x08, // mov dword ptr [r8], ecx
        0x41,
        0x89,
        0x11, // mov dword ptr [r9], edx
        0x5B, // pop rbx
        0xC3 // ret
    ];

    private static readonly byte[] CpuId64Windows =
    [
        0x48,
        0x89,
        0x5C,
        0x24,
        0x08, // mov qword ptr [rsp+8], rbx
        0x8B,
        0xC1, // mov eax, ecx
        0x8B,
        0xCA, // mov ecx, edx
        0x0F,
        0xA2, // cpuid
        0x41,
        0x89,
        0x00, // mov dword ptr [r8], eax
        0x48,
        0x8B,
        0x44,
        0x24,
        0x28, // mov rax, qword ptr [rsp+28h]
        0x41,
        0x89,
        0x19, // mov dword ptr [r9], ebx
        0x48,
        0x8B,
        0x5C,
        0x24,
        0x08, // mov rbx, qword ptr [rsp+8]
        0x89,
        0x08, // mov dword ptr [rax], ecx
        0x48,
        0x8B,
        0x44,
        0x24,
        0x30, // mov rax, qword ptr [rsp+30h]
        0x89,
        0x10, // mov dword ptr [rax], edx
        0xC3 // ret
    ];

    // unsigned __int64 __stdcall rdtsc() {
    //   return __rdtsc();
    // }

    private static readonly byte[] Rdtsc32 =
    [
        0x0F,
        0x31, // rdtsc
        0xC3 // ret
    ];

    private static readonly byte[] Rdtsc64 =
    [
        0x0F,
        0x31, // rdtsc
        0x48,
        0xC1,
        0xE2,
        0x20, // shl rdx, 20h
        0x48,
        0x0B,
        0xC2, // or rax, rdx
        0xC3 // ret
    ];

    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    public delegate bool CpuidDelegate(uint index, uint ecxValue, out uint eax, out uint ebx, out uint ecx, out uint edx);

    [UnmanagedFunctionPointer(CallingConvention.StdCall)]
    public delegate ulong RdtscDelegate();

#if NET
    [RequiresUnreferencedCode("Dynamic code generation for OpCode delegates")]
#endif
    public static unsafe void Open()
    {
        byte[] rdTscCode;
        byte[] cpuidCode;
        if (IntPtr.Size == 4)
        {
            rdTscCode = Rdtsc32;
            cpuidCode = CpuId32;
        }
        else
        {
            rdTscCode = Rdtsc64;
            cpuidCode = Software.OperatingSystem.IsUnix ? CpuId64Linux : CpuId64Windows;
        }

        _size = (ulong)(rdTscCode.Length + cpuidCode.Length);

        if (Software.OperatingSystem.IsUnix)
        {
#if NETFRAMEWORK
            Assembly assembly = Assembly.Load("Mono.Posix, Version=2.0.0.0, Culture=neutral, " + "PublicKeyToken=0738eb9f132ed756");

            Type sysCall = assembly.GetType("Mono.Unix.Native.Syscall");
            MethodInfo mmap = sysCall.GetMethod("mmap");

            Type mmapProts = assembly.GetType("Mono.Unix.Native.MmapProts");
            object mmapProtsParam = Enum.ToObject(mmapProts,
                                                  (int)mmapProts.GetField("PROT_READ").GetValue(null) |
                                                  (int)mmapProts.GetField("PROT_WRITE").GetValue(null) |
                                                  (int)mmapProts.GetField("PROT_EXEC").GetValue(null));

            Type mmapFlags = assembly.GetType("Mono.Unix.Native.MmapFlags");
            object mmapFlagsParam = Enum.ToObject(mmapFlags,
                                                  (int)mmapFlags.GetField("MAP_ANONYMOUS").GetValue(null) |
                                                  (int)mmapFlags.GetField("MAP_PRIVATE").GetValue(null));

            if (mmap != null)
                _codeBuffer = (IntPtr)mmap.Invoke(null, [IntPtr.Zero, _size, mmapProtsParam, mmapFlagsParam, -1, 0]);
#else
            _codeBuffer = Syscall.mmap(IntPtr.Zero, _size,
                MmapProts.PROT_READ | MmapProts.PROT_WRITE | MmapProts.PROT_EXEC,
                MmapFlags.MAP_ANONYMOUS | MmapFlags.MAP_PRIVATE, -1, 0);
#endif
        }
        else
        {
            _codeBuffer = (IntPtr)PInvoke.VirtualAlloc(null,
                                                       (UIntPtr)_size,
                                                       VIRTUAL_ALLOCATION_TYPE.MEM_COMMIT | VIRTUAL_ALLOCATION_TYPE.MEM_RESERVE,
                                                       PAGE_PROTECTION_FLAGS.PAGE_EXECUTE_READWRITE);
        }

        Marshal.Copy(rdTscCode, 0, _codeBuffer, rdTscCode.Length);
        Rdtsc = Marshal.GetDelegateForFunctionPointer<RdtscDelegate>(_codeBuffer);
        IntPtr cpuidAddress = (IntPtr)((long)_codeBuffer + rdTscCode.Length);
        Marshal.Copy(cpuidCode, 0, cpuidAddress, cpuidCode.Length);
        CpuId = Marshal.GetDelegateForFunctionPointer<CpuidDelegate>(cpuidAddress);
    }

    public static unsafe void Close()
    {
        Rdtsc = null;
        CpuId = null;

        if (Software.OperatingSystem.IsUnix)
        {
#if NETFRAMEWORK
            Assembly assembly = Assembly.Load("Mono.Posix, Version=2.0.0.0, Culture=neutral, " + "PublicKeyToken=0738eb9f132ed756");

            Type sysCall = assembly.GetType("Mono.Unix.Native.Syscall");
            MethodInfo method = sysCall.GetMethod("munmap");
            method?.Invoke(null, [_codeBuffer, _size]);
#else
            Syscall.munmap(_codeBuffer, _size);
#endif
        }
        else
        {
            PInvoke.VirtualFree((void*)_codeBuffer, UIntPtr.Zero, VIRTUAL_FREE_TYPE.MEM_RELEASE);
        }
    }
}
