using RioJoy.Core.Calibration;
using RioJoy.Core.Mapping;

namespace RioJoy.Core.Hid;

/// <summary>
/// Builds the fixed 25-byte input report consumed by the RioGamepad driver
/// (see <c>driver/RioGamepad/Public.h</c>). Layout:
/// <list type="bullet">
///   <item>bytes 0–11: axes X,Y,Z,Rx,Ry,Rz — unsigned 16-bit little-endian</item>
///   <item>byte 12: low nibble = hat (0–3; 0x0F = centered/null)</item>
///   <item>bytes 13–24: 96 button bits (button N at byte 13 + (N-1)/8, bit (N-1)%8)</item>
/// </list>
/// The instance holds current state so successive axis/button/hat updates compose
/// into one report, mirroring how a gamepad reports its whole state each time.
/// </summary>
public sealed class RioHidReport
{
    /// <summary>Report size in bytes (must match the driver's RIO_REPORT_SIZE).</summary>
    public const int Size = 25;

    /// <summary>Number of buttons in the report.</summary>
    public const int ButtonCount = 96;

    /// <summary>Logical maximum for an axis (HID descriptor logical max).</summary>
    public const int AxisMax = 32767;

    private const int HatByte = 12;
    private const int ButtonsOffset = 13;
    private const byte HatCentered = 0x0F;

    private readonly byte[] _buffer = new byte[Size];

    public RioHidReport()
    {
        // Center the axes and center the hat at rest.
        SetAxis(JoyAxis.X, AxisOutputs.Center);
        SetAxis(JoyAxis.Y, AxisOutputs.Center);
        SetAxis(JoyAxis.Z, AxisOutputs.Center);
        SetAxis(JoyAxis.Rx, AxisOutputs.Center);
        SetAxis(JoyAxis.Ry, AxisOutputs.Center);
        SetAxis(JoyAxis.Rz, AxisOutputs.Center);
        SetHat(RioHat.Centered);
    }

    /// <summary>Copy of the current report bytes (length <see cref="Size"/>).</summary>
    public byte[] ToArray() => (byte[])_buffer.Clone();

    /// <summary>Set an axis value (clamped to 0..<see cref="AxisMax"/>), little-endian.</summary>
    public void SetAxis(JoyAxis axis, int value)
    {
        ushort v = (ushort)RioJoy.Core.Compat.Net48Math.Clamp(value, 0, AxisMax);
        int offset = (int)axis * 2;
        _buffer[offset] = (byte)(v & 0xFF);
        _buffer[offset + 1] = (byte)(v >> 8);
    }

    /// <summary>Set the POV hat position (<see cref="RioHat.Centered"/> = null).</summary>
    public void SetHat(RioHat position)
    {
        byte nibble = position == RioHat.Centered ? HatCentered : (byte)((int)position & 0x0F);
        _buffer[HatByte] = (byte)((_buffer[HatByte] & 0xF0) | nibble);
    }

    /// <summary>
    /// Set a button's state. <paramref name="button"/> is the 1-based button
    /// number (1..96), matching the legacy vJoy <c>SetBtn</c> numbering.
    /// </summary>
    public void SetButton(int button, bool pressed)
    {
        if (button < 1 || button > ButtonCount)
            throw new ArgumentOutOfRangeException(nameof(button), $"Button must be 1..{ButtonCount}.");

        int bit = button - 1;
        int index = ButtonsOffset + (bit / 8);
        byte mask = (byte)(1 << (bit % 8));
        if (pressed)
            _buffer[index] |= mask;
        else
            _buffer[index] &= (byte)~mask;
    }
}
