using System.Runtime.InteropServices;
using VRio.Core.Input;

namespace VRio.App;

/// <summary>
/// Polls the first connected XInput (Xbox) controller and converts its state
/// to the Core's normalized <see cref="PadState"/>. Uses the in-box
/// xinput1_4.dll (Windows 8+), falling back to xinput9_1_0.dll, so no
/// redistributable is needed. Polling a disconnected user index is expensive,
/// so while no pad is present the 0–3 scan only runs every ~1 s.
/// </summary>
internal sealed class XInputGamepad
{
    private const int MaxUsers = 4;
    private const uint ErrorSuccess = 0;
    private const int ScanEveryNPolls = 60; // ≈1 s at the 16 ms input tick

    private static bool _tryXInput14 = true;

    private int _user = -1;
    private int _scanCountdown;

    /// <summary>True while a controller is connected and being polled.</summary>
    public bool Connected => _user >= 0;

    /// <summary>XInput user index of the connected controller (0-based).</summary>
    public int UserIndex => _user;

    /// <summary>
    /// Poll the pad. False (with a rest-state snapshot) while disconnected.
    /// </summary>
    public bool TryRead(out PadState state)
    {
        if (_user >= 0)
        {
            if (GetState(_user, out XINPUT_STATE s) == ErrorSuccess)
            {
                state = Convert(s.Gamepad);
                return true;
            }
            _user = -1; // unplugged; fall through to (throttled) rescan
            _scanCountdown = 0;
        }

        if (--_scanCountdown <= 0)
        {
            _scanCountdown = ScanEveryNPolls;
            for (int i = 0; i < MaxUsers; i++)
            {
                if (GetState(i, out XINPUT_STATE s) == ErrorSuccess)
                {
                    _user = i;
                    state = Convert(s.Gamepad);
                    return true;
                }
            }
        }

        state = default;
        return false;
    }

    private static PadState Convert(in XINPUT_GAMEPAD g) => new(
        (PadButtons)g.wButtons,
        Thumb(g.sThumbLX), Thumb(g.sThumbLY),
        Thumb(g.sThumbRX), Thumb(g.sThumbRY),
        g.bLeftTrigger / 255f, g.bRightTrigger / 255f);

    private static float Thumb(short v) => Math.Max(-1f, v / 32767f);

    private static uint GetState(int user, out XINPUT_STATE state)
    {
        if (_tryXInput14)
        {
            try
            {
                return XInputGetState14(user, out state);
            }
            catch (DllNotFoundException)
            {
                _tryXInput14 = false;
            }
        }
        try
        {
            return XInputGetState910(user, out state);
        }
        catch (DllNotFoundException)
        {
            state = default;
            return uint.MaxValue; // no XInput on this system — behaves as "no pad"
        }
    }

    [DllImport("xinput1_4.dll", EntryPoint = "XInputGetState")]
    private static extern uint XInputGetState14(int dwUserIndex, out XINPUT_STATE state);

    [DllImport("xinput9_1_0.dll", EntryPoint = "XInputGetState")]
    private static extern uint XInputGetState910(int dwUserIndex, out XINPUT_STATE state);

    [StructLayout(LayoutKind.Sequential)]
    private struct XINPUT_STATE
    {
        public uint dwPacketNumber;
        public XINPUT_GAMEPAD Gamepad;
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct XINPUT_GAMEPAD
    {
        public ushort wButtons;
        public byte bLeftTrigger;
        public byte bRightTrigger;
        public short sThumbLX;
        public short sThumbLY;
        public short sThumbRX;
        public short sThumbRY;
    }
}
