namespace RioJoy.Core.Protocol;

/// <summary>
/// RIO message commands. The command byte always has its high bit set; the
/// value identifies the message. Faithful port of <c>enum RIOCommand</c> and
/// <c>g_baRIOLengthsA</c> in <c>legacy/riovjoy2.cpp</c> (see docs/PROTOCOL.md §3).
/// </summary>
public enum RioCommand : byte
{
    // PC → RIO
    CheckRequest = 0x80,
    VersionRequest = 0x81,
    AnalogRequest = 0x82,
    ResetRequest = 0x83,
    LampRequest = 0x84,

    // RIO → PC
    CheckReply = 0x85,
    VersionReply = 0x86,
    AnalogReply = 0x87,
    ButtonPressed = 0x88,
    ButtonReleased = 0x89,
    KeyPressed = 0x8A,
    KeyReleased = 0x8B,
    TestModeChange = 0x8C,
}

/// <summary>
/// Single-byte control characters that live <i>outside</i> packet framing
/// (legacy <c>#define ACK_CHAR</c> … <c>IDLE_CHAR</c>, riovjoy2.cpp#L154).
/// </summary>
public enum RioControl : byte
{
    /// <summary>Packet accepted.</summary>
    Ack = 0xFC,

    /// <summary>Packet rejected; resend.</summary>
    Nak = 0xFD,

    /// <summary>Restart; also used as an in-payload "invalid" sentinel.</summary>
    Restart = 0xFE,

    /// <summary>Idle.</summary>
    Idle = 0xFF,
}

/// <summary>
/// Reset targets for <see cref="RioCommand.ResetRequest"/> (docs/PROTOCOL.md §3).
/// </summary>
public enum RioResetTarget : byte
{
    All = 0,
    Throttle = 1,
    LeftPedal = 2,
    RightPedal = 3,
    VerticalJoystick = 4,   // Y
    HorizontalJoystick = 5, // X
}

/// <summary>
/// The <c>statusType</c> field of <see cref="RioCommand.CheckReply"/>
/// (legacy <c>enum RIOStatusType</c>, riovjoy2.cpp#L204).
/// </summary>
public enum RioStatusType : byte
{
    BoardOk = 0,
    BoardMissing = 1,
    BoardBad = 2,
    LampBad = 3,
    RestartCount = 4,
    AbandonCount = 5,
    FullBufferCount = 6,
}

/// <summary>
/// Per-command payload lengths (bytes between the command byte and the
/// checksum byte). Port of <c>g_baRIOLengthsA</c>.
/// </summary>
public static class RioCommandTable
{
    /// <summary>Lowest command byte value (<see cref="RioCommand.CheckRequest"/>).</summary>
    public const byte Base = (byte)RioCommand.CheckRequest;

    // Indexed by (command - Base). Order must match RioCommand.
    private static readonly byte[] PayloadLengths =
    {
        0,  // CheckRequest
        0,  // VersionRequest
        0,  // AnalogRequest
        1,  // ResetRequest
        2,  // LampRequest
        2,  // CheckReply
        2,  // VersionReply
        10, // AnalogReply
        1,  // ButtonPressed
        1,  // ButtonReleased
        2,  // KeyPressed
        2,  // KeyReleased
        1,  // TestModeChange
    };

    /// <summary>Number of defined commands.</summary>
    public static int Count => PayloadLengths.Length;

    /// <summary>Highest valid command byte value (inclusive).</summary>
    public static byte Max => (byte)(Base + Count - 1);

    /// <summary>True if <paramref name="commandByte"/> is a known command.</summary>
    public static bool IsCommand(byte commandByte) =>
        commandByte >= Base && commandByte <= Max;

    /// <summary>
    /// Payload length for a command byte. Throws if the byte is not a known
    /// command — callers framing a stream should guard with
    /// <see cref="IsCommand"/> first.
    /// </summary>
    public static int PayloadLength(byte commandByte)
    {
        if (!IsCommand(commandByte))
            throw new ArgumentOutOfRangeException(
                nameof(commandByte),
                $"0x{commandByte:X2} is not a RIO command (valid 0x{Base:X2}..0x{Max:X2}).");

        return PayloadLengths[commandByte - Base];
    }

    /// <summary>Payload length for a <see cref="RioCommand"/>.</summary>
    public static int PayloadLength(RioCommand command) => PayloadLength((byte)command);
}
