using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;

namespace PSLauncher.Core.Health.OpenVRInterop;

/// <summary>
/// P/Invoke minimal vers <c>openvr_api.dll</c> (SDK Valve OpenVR, BSD-3).
/// On NE bundle PAS la DLL : on la résout dynamiquement via la copie installée
/// par SteamVR (chemin canonique <c>%LocalAppData%\openvr\openvrpaths.vrpath</c>),
/// donc :
///   - SteamVR pas installé → la DLL n'est pas trouvée et init renvoie une erreur claire,
///   - SteamVR mis à jour → on profite automatiquement de la dernière openvr_api.dll
///     sans avoir à respinner un build du launcher.
///
/// Le SDK OpenVR expose une "flat C API" : quelques fonctions globales pour
/// init/shutdown, et une <c>GetGenericInterface("FnTable:IVRSystem_022")</c>
/// qui renvoie un pointeur sur une struct C de pointeurs de fonction (vtable).
/// Cette classe expose juste ce qu'on consomme dans <see cref="OpenVrService"/> :
/// présence de la runtime, présence d'un HMD, et la vtable IVRSystem.
/// </summary>
public static class OpenVRNative
{
    private const string DllName = "openvr_api";

    /// <summary>
    /// EVRApplicationType passé à VR_InitInternal2. <c>Background = 4</c> : on
    /// veut LIRE l'état de SteamVR sans déclencher de rendu, sans devenir une
    /// scene application, sans monopoliser le compositor.
    /// </summary>
    public const int VRApplicationBackground = 4;

    /// <summary>Indices de slots dans la vtable IVRSystem_022. Stables depuis SteamVR 1.10 (2019).</summary>
    public static class IVRSystemSlot
    {
        public const int GetTrackedDeviceActivityLevel        = 16;
        public const int GetTrackedDeviceIndexForControllerRole = 18;
        public const int GetTrackedDeviceClass                = 20;
        public const int IsTrackedDeviceConnected             = 21;
        public const int GetBoolTrackedDeviceProperty         = 22;
        public const int GetFloatTrackedDeviceProperty        = 23;
        public const int GetStringTrackedDeviceProperty       = 28;
    }

    /// <summary>
    /// Indices ETrackedDeviceProperty les plus utiles pour notre usage. Liste
    /// complète dans <c>openvr.h</c> ; on en garde un sous-ensemble pour ne pas
    /// dupliquer 800 enums dont 99% inutiles.
    /// </summary>
    public static class DeviceProperty
    {
        public const int Prop_DeviceIsWireless_Bool             = 1003;
        public const int Prop_DeviceIsCharging_Bool             = 1004;
        public const int Prop_DeviceProvidesBatteryStatus_Bool  = 1026; // varie par version, fallback safe sur GetBool
        public const int Prop_DeviceBatteryPercentage_Float     = 1029;
        public const int Prop_SerialNumber_String               = 1002;
        public const int Prop_TrackingSystemName_String         = 1000;
        public const int Prop_ModelNumber_String                = 1001;
    }

    /// <summary>Classe OpenVR d'un device tracké. 1=HMD, 2=Controller, 3=GenericTracker, 4=TrackingReference.</summary>
    public enum ETrackedDeviceClass
    {
        Invalid           = 0,
        HMD               = 1,
        Controller        = 2,
        GenericTracker    = 3,
        TrackingReference = 4,
        DisplayRedirect   = 5,
    }

    public enum ETrackedControllerRole
    {
        Invalid    = 0,
        LeftHand   = 1,
        RightHand  = 2,
        OptOut     = 3,
        Treadmill  = 4,
        Stylus     = 5,
    }

    /// <summary>
    /// Niveau d'activité du device. Idle = en veille, n'envoie plus de pose.
    /// Standby = posé mais réveille rapidement. UserInteraction = en main.
    /// </summary>
    public enum EDeviceActivityLevel
    {
        Unknown                  = -1,
        Idle                     = 0,
        UserInteraction          = 1,
        UserInteraction_Timeout  = 2,
        Standby                  = 3,
        Idle_Timeout             = 4,
    }

    // ATTENTION : OpenVR renvoie des bool C++ (1 octet). Sans MarshalAs(I1), le
    // marshaller .NET lit 4 octets (BOOL Win32) et obtient des valeurs random ;
    // c'est ce qui faisait passer VR_IsHmdPresent à "true" même casque débranché,
    // déclenchant ensuite une init qui plante quand SteamVR n'est pas lancé.
    [DllImport(DllName, EntryPoint = "VR_IsRuntimeInstalled", CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool VR_IsRuntimeInstalled();

    [DllImport(DllName, EntryPoint = "VR_IsHmdPresent", CallingConvention = CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public static extern bool VR_IsHmdPresent();

    [DllImport(DllName, EntryPoint = "VR_InitInternal2", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
    public static extern uint VR_InitInternal2(out int peError, int eApplicationType, [MarshalAs(UnmanagedType.LPStr)] string? pStartupInfo);

    [DllImport(DllName, EntryPoint = "VR_ShutdownInternal", CallingConvention = CallingConvention.Cdecl)]
    public static extern void VR_ShutdownInternal();

    [DllImport(DllName, EntryPoint = "VR_GetGenericInterface", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
    public static extern IntPtr VR_GetGenericInterface([MarshalAs(UnmanagedType.LPStr)] string pchInterfaceVersion, out int peError);

    [DllImport(DllName, EntryPoint = "VR_GetVRInitErrorAsEnglishDescription", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
    public static extern IntPtr VR_GetVRInitErrorAsEnglishDescription(int error);

    /// <summary>
    /// Récupère un pointeur de fonction depuis la vtable IVRSystem (struct
    /// contiguë de IntPtr à <paramref name="vtablePtr"/>) au slot demandé.
    /// </summary>
    public static IntPtr GetVTableSlot(IntPtr vtablePtr, int slotIndex)
    {
        unsafe
        {
            return ((IntPtr*)vtablePtr)[slotIndex];
        }
    }

    public static string GetInitErrorDescription(int error)
    {
        try
        {
            var ptr = VR_GetVRInitErrorAsEnglishDescription(error);
            return ptr == IntPtr.Zero ? $"unknown ({error})" : Marshal.PtrToStringAnsi(ptr) ?? $"unknown ({error})";
        }
        catch { return $"error code {error}"; }
    }

    // ============= DLL RESOLUTION =============

    private static int _resolverInstalled;

    /// <summary>
    /// Installe (idempotent) un <see cref="DllImportResolver"/> qui, lorsque
    /// .NET demande "openvr_api", le résout vers la DLL de l'install SteamVR
    /// active. Pas d'effet de bord si SteamVR est absent — la DllImport
    /// échouera ensuite comme d'habitude et le service catch.
    /// </summary>
    public static void EnsureResolverInstalled()
    {
        if (Interlocked.Exchange(ref _resolverInstalled, 1) == 1) return;
        try
        {
            NativeLibrary.SetDllImportResolver(typeof(OpenVRNative).Assembly, ResolveOpenVrDll);
        }
        catch
        {
            // Déjà installé ailleurs, ou plateforme qui ne supporte pas. On laisse
            // le DllImport faire son travail (cherchera dans le PATH).
        }
    }

    private static IntPtr ResolveOpenVrDll(string libraryName, System.Reflection.Assembly assembly, DllImportSearchPath? searchPath)
    {
        if (!string.Equals(libraryName, DllName, StringComparison.OrdinalIgnoreCase))
            return IntPtr.Zero;

        var path = LocateOpenVrDll();
        if (path is null) return IntPtr.Zero;

        try { return NativeLibrary.Load(path); }
        catch { return IntPtr.Zero; }
    }

    /// <summary>
    /// Cherche openvr_api.dll dans cet ordre :
    /// 1. <c>%LocalAppData%\openvr\openvrpaths.vrpath</c> (canon écrit par SteamVR au setup).
    /// 2. Dossier Steam standard : <c>C:\Program Files (x86)\Steam\steamapps\common\SteamVR\bin\win64\</c>.
    /// </summary>
    private static string? LocateOpenVrDll()
    {
        // 1. Lire openvrpaths.vrpath (JSON)
        try
        {
            var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
            var pathsFile = Path.Combine(localAppData, "openvr", "openvrpaths.vrpath");
            if (File.Exists(pathsFile))
            {
                var json = File.ReadAllText(pathsFile, Encoding.UTF8);
                using var doc = JsonDocument.Parse(json);
                if (doc.RootElement.TryGetProperty("runtime", out var runtimes) && runtimes.ValueKind == JsonValueKind.Array)
                {
                    foreach (var elt in runtimes.EnumerateArray())
                    {
                        var dir = elt.GetString();
                        if (string.IsNullOrEmpty(dir)) continue;
                        var dll = Path.Combine(dir, "bin", "win64", "openvr_api.dll");
                        if (File.Exists(dll)) return dll;
                    }
                }
            }
        }
        catch { /* fallback */ }

        // 2. Chemin Steam standard
        try
        {
            var steamPath = @"C:\Program Files (x86)\Steam\steamapps\common\SteamVR\bin\win64\openvr_api.dll";
            if (File.Exists(steamPath)) return steamPath;
        }
        catch { }

        return null;
    }

    // ============= DELEGATES POUR LES SLOTS UTILISÉS =============

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate EDeviceActivityLevel GetTrackedDeviceActivityLevelFn(uint deviceIndex);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate uint GetTrackedDeviceIndexForControllerRoleFn(ETrackedControllerRole role);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate ETrackedDeviceClass GetTrackedDeviceClassFn(uint deviceIndex);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public delegate bool IsTrackedDeviceConnectedFn(uint deviceIndex);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    [return: MarshalAs(UnmanagedType.I1)]
    public delegate bool GetBoolTrackedDevicePropertyFn(uint deviceIndex, int property, out int peError);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate float GetFloatTrackedDevicePropertyFn(uint deviceIndex, int property, out int peError);

    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
    public delegate uint GetStringTrackedDevicePropertyFn(uint deviceIndex, int property, IntPtr buffer, uint bufferSize, out int peError);
}
