namespace VRio.Core.Protocol;

/// <summary>
/// Streaming receive-side framing state machine, fed raw bytes from the serial
/// port. Identical framing rules to RIOJoy's parser (riojoy/docs/PROTOCOL.md §2)
/// — vRIO just feeds it the opposite direction of traffic (PC → RIO commands):
/// <list type="bullet">
///   <item>Outside a packet, a byte in the command range starts a packet of
///   <c>1 + payloadLen + 1</c> bytes (command + payload + checksum).</item>
///   <item>Outside a packet, any other byte is a control byte (ACK/NAK/…).</item>
///   <item>Mid-packet, a byte with the high bit set aborts the packet and
///   resynchronizes; that byte is discarded.</item>
///   <item>The final byte completes the packet with a real checksum compare.</item>
/// </list>
/// Not thread-safe; drive it from one reader.
/// </summary>
public sealed class PacketParser
{
    // Max framed packet = command(1) + AnalogReply payload(10) + checksum(1) = 12.
    private const int MaxPacketBytes = 16;

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

    // Bytes still expected to complete the current packet (0 = not in a packet).
    private int _remaining;

    // Count of bytes stored in _buffer for the current packet (incl. command).
    private int _count;

    /// <summary>True while a packet is partially received.</summary>
    public bool InPacket => _remaining != 0;

    /// <summary>Discard any in-progress packet and reset to the idle state.</summary>
    public void Reset()
    {
        _remaining = 0;
        _count = 0;
    }

    /// <summary>
    /// Feed one received byte. Returns <see langword="true"/> and sets
    /// <paramref name="ev"/> when this byte produced an event; otherwise the byte
    /// was consumed into an in-progress packet.
    /// </summary>
    public bool Feed(byte ch, out RioRxEvent ev)
    {
        if (_remaining != 0)
        {
            // Mid-packet. A high-bit byte is a framing error → abort and resync.
            if ((ch & 0x80) != 0)
            {
                Reset();
                ev = RioRxEvent.ForFramingError(ch);
                return true;
            }

            _buffer[_count++] = ch;
            _remaining--;

            if (_remaining == 0)
            {
                ev = CompletePacket();
                return true;
            }

            ev = default;
            return false;
        }

        // Not in a packet.
        if (RioCommandTable.IsCommand(ch))
        {
            _count = 0;
            _buffer[_count++] = ch;
            _remaining = RioCommandTable.PayloadLength(ch) + 1; // + checksum byte
            ev = default;
            return false;
        }

        // A byte outside framing: control character (ACK/NAK/RESTART/IDLE) or garbage.
        ev = RioRxEvent.ForControl(ch);
        return true;
    }

    private RioRxEvent CompletePacket()
    {
        // _buffer = [command][payload…][checksum]; _count includes all of them.
        int bodyLen = _count - 1;             // command + payload (exclude checksum)
        byte receivedChecksum = _buffer[bodyLen];
        byte computed = RioChecksum.Compute(_buffer.AsSpan(0, bodyLen));
        bool checksumValid = computed == receivedChecksum;

        var command = (RioCommand)_buffer[0];
        var payload = _buffer.AsSpan(1, bodyLen - 1).ToArray(); // copy out; buffer is reused
        var packet = new RioPacket(command, payload);

        Reset();
        return RioRxEvent.ForPacket(packet, checksumValid);
    }
}
