using Ryujinx.Common.GraphicsDriver.NVAPI;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Ryujinx.Common.GraphicsDriver
{
    static class NVThreadedOptimization
    {
        private const string ProfileName = "Ryujinx Nvidia Profile";

        private const uint NvAPI_Initialize_ID = 0x0150E828;
        private const uint NvAPI_DRS_CreateSession_ID = 0x0694D52E;
        private const uint NvAPI_DRS_LoadSettings_ID = 0x375DBD6B;
        private const uint NvAPI_DRS_FindProfileByName_ID = 0x7E4A9A0B;
        private const uint NvAPI_DRS_CreateProfile_ID = 0x0CC176068;
        private const uint NvAPI_DRS_CreateApplication_ID = 0x4347A9DE;
        private const uint NvAPI_DRS_SetSetting_ID = 0x577DD202;
        private const uint NvAPI_DRS_SaveSettings_ID = 0xFCBC7E14;
        private const uint NvAPI_DRS_DestroySession_ID = 0x0DAD9CFF8;

        [DllImport("nvapi64")]
        private static extern IntPtr nvapi_QueryInterface(uint id);

        private delegate int NvAPI_InitializeDelegate();
        private static NvAPI_InitializeDelegate NvAPI_Initialize;

        private delegate int NvAPI_DRS_CreateSessionDelegate(out IntPtr handle);
        private static NvAPI_DRS_CreateSessionDelegate NvAPI_DRS_CreateSession;

        private delegate int NvAPI_DRS_LoadSettingsDelegate(IntPtr handle);
        private static NvAPI_DRS_LoadSettingsDelegate NvAPI_DRS_LoadSettings;

        private delegate int NvAPI_DRS_FindProfileByNameDelegate(IntPtr handle, NvapiUnicodeString profileName, out IntPtr profileHandle);
        private static NvAPI_DRS_FindProfileByNameDelegate NvAPI_DRS_FindProfileByName;

        private delegate int NvAPI_DRS_CreateProfileDelegate(IntPtr handle, ref NvdrsProfile profileInfo, out IntPtr profileHandle);
        private static NvAPI_DRS_CreateProfileDelegate NvAPI_DRS_CreateProfile;

        private delegate int NvAPI_DRS_CreateApplicationDelegate(IntPtr handle, IntPtr profileHandle, ref NvdrsApplicationV4 app);
        private static NvAPI_DRS_CreateApplicationDelegate NvAPI_DRS_CreateApplication;

        private delegate int NvAPI_DRS_SetSettingDelegate(IntPtr handle, IntPtr profileHandle, ref NvdrsSetting setting);
        private static NvAPI_DRS_SetSettingDelegate NvAPI_DRS_SetSetting;

        private delegate int NvAPI_DRS_SaveSettingsDelegate(IntPtr handle);
        private static NvAPI_DRS_SaveSettingsDelegate NvAPI_DRS_SaveSettings;

        private delegate int NvAPI_DRS_DestroySessionDelegate(IntPtr handle);
        private static NvAPI_DRS_DestroySessionDelegate NvAPI_DRS_DestroySession;

        private static bool _initialized;

        private static void Check(int status)
        {
            if (status != 0)
            {
                throw new Exception($"NVAPI Error: {status}");
            }
        }

        private static void Initialize()
        {
            if (!_initialized)
            {
                NvAPI_Initialize = NvAPI_Delegate<NvAPI_InitializeDelegate>(NvAPI_Initialize_ID);

                Check(NvAPI_Initialize());

                NvAPI_DRS_CreateSession = NvAPI_Delegate<NvAPI_DRS_CreateSessionDelegate>(NvAPI_DRS_CreateSession_ID);
                NvAPI_DRS_LoadSettings = NvAPI_Delegate<NvAPI_DRS_LoadSettingsDelegate>(NvAPI_DRS_LoadSettings_ID);
                NvAPI_DRS_FindProfileByName = NvAPI_Delegate<NvAPI_DRS_FindProfileByNameDelegate>(NvAPI_DRS_FindProfileByName_ID);
                NvAPI_DRS_CreateProfile = NvAPI_Delegate<NvAPI_DRS_CreateProfileDelegate>(NvAPI_DRS_CreateProfile_ID);
                NvAPI_DRS_CreateApplication = NvAPI_Delegate<NvAPI_DRS_CreateApplicationDelegate>(NvAPI_DRS_CreateApplication_ID);
                NvAPI_DRS_SetSetting = NvAPI_Delegate<NvAPI_DRS_SetSettingDelegate>(NvAPI_DRS_SetSetting_ID);
                NvAPI_DRS_SaveSettings = NvAPI_Delegate<NvAPI_DRS_SaveSettingsDelegate>(NvAPI_DRS_SaveSettings_ID);
                NvAPI_DRS_DestroySession = NvAPI_Delegate<NvAPI_DRS_DestroySessionDelegate>(NvAPI_DRS_DestroySession_ID);

                _initialized = true;
            }
        }

        private static uint MakeVersion<T>(uint version) where T : unmanaged
        {
            return (uint)Unsafe.SizeOf<T>() | version << 16;
        }

        public static void SetThreadedOptimization(bool enabled)
        {
            Initialize();

            uint targetValue = (uint)(enabled ? Nvapi.OglThreadControlEnable : Nvapi.OglThreadControlDisable);

            Check(NvAPI_Initialize());

            Check(NvAPI_DRS_CreateSession(out IntPtr handle));

            Check(NvAPI_DRS_LoadSettings(handle));

            IntPtr profileHandle;

            // Check if the profile already exists.

            int status = NvAPI_DRS_FindProfileByName(handle, new NvapiUnicodeString(ProfileName), out profileHandle);

            if (status != 0)
            {
                NvdrsProfile profile = new NvdrsProfile { 
                    Version = MakeVersion<NvdrsProfile>(1), 
                    IsPredefined = 0, 
                    GpuSupport = uint.MaxValue 
                };
                profile.ProfileName.Set(ProfileName);
                Check(NvAPI_DRS_CreateProfile(handle, ref profile, out profileHandle));

                NvdrsApplicationV4 application = new NvdrsApplicationV4
                {
                    Version = MakeVersion<NvdrsApplicationV4>(4),
                    IsPredefined = 0,
                    Flags = 3 // IsMetro, IsCommandLine
                };
                application.AppName.Set("Ryujinx.exe");
                application.UserFriendlyName.Set("Ryujinx");
                application.Launcher.Set("");
                application.FileInFolder.Set("");

                Check(NvAPI_DRS_CreateApplication(handle, profileHandle, ref application));
            }

            NvdrsSetting setting = new NvdrsSetting
            {
                Version = MakeVersion<NvdrsSetting>(1),
                SettingId = Nvapi.OglThreadControlId,
                SettingType = NvdrsSettingType.NvdrsDwordType,
                SettingLocation = NvdrsSettingLocation.NvdrsCurrentProfileLocation,
                IsCurrentPredefined = 0,
                IsPredefinedValid = 0,
                CurrentValue = targetValue,
                PredefinedValue = targetValue
            };

            Check(NvAPI_DRS_SetSetting(handle, profileHandle, ref setting));

            Check(NvAPI_DRS_SaveSettings(handle));

            NvAPI_DRS_DestroySession(handle);
        }

        private static T NvAPI_Delegate<T>(uint id) where T : class
        {
            IntPtr ptr = nvapi_QueryInterface(id);

            if (ptr != IntPtr.Zero)
            {
                return Marshal.GetDelegateForFunctionPointer(ptr, typeof(T)) as T;
            }
            else
            {
                return null;
            }
        }
    }
}