namespace VRio.Core.Device;

/// <summary>
/// The framing shared with the DOSBox-X fork's <c>namedpipe</c> serial
/// backend (its <c>serialnamedpipe.h</c> header comment is the contract's
/// source of truth on that side). A pipe is a plain byte stream, so serial
/// data and modem control lines are multiplexed as typed frames:
///
/// <code>
///   0x00 &lt;len:u8&gt; &lt;len bytes&gt;   serial data, len ≥ 1 (batching allowed)
///   0x01 &lt;lines:u8&gt;             the sender's OWN output lines (bit0 DTR,
///                                bit1 RTS); the receiver applies the
///                                null-modem cross: peer DTR → local DSR,
///                                peer RTS → local CTS
/// </code>
///
/// Each side sends one lines frame immediately on connect; until it arrives
/// the peer's lines are assumed low, and a disconnect drops them low again.
/// Any other frame type is a protocol bug, not line noise — pipes don't drop
/// bytes — so the receiver logs it and drops the connection instead of
/// trying to resync.
/// </summary>
public static class PipeFraming
{
    public const byte DataType = 0x00;
    public const byte LinesType = 0x01;

    /// <summary>Lines-frame bit: the sender's DTR output.</summary>
    public const byte LineDtr = 0x01;

    /// <summary>Lines-frame bit: the sender's RTS output.</summary>
    public const byte LineRts = 0x02;

    /// <summary>Both outputs asserted — what a present, powered device reports.</summary>
    public const byte LinesPresent = LineDtr | LineRts;

    /// <summary>Build a lines frame carrying <paramref name="lines"/>.</summary>
    public static byte[] EncodeLines(byte lines) => new[] { LinesType, lines };
}

/// <summary>
/// Incremental decoder for <see cref="PipeFraming"/>: feed it raw pipe reads,
/// get one <see cref="Data"/> event per complete data frame and one
/// <see cref="Lines"/> event per lines frame. Frames may split across reads
/// at any byte boundary. A malformed stream (unknown type, zero-length data
/// frame) poisons the decoder: <see cref="Feed"/> returns false and
/// <see cref="Violation"/> says why — drop the connection and
/// <see cref="Reset"/> before serving the next one.
/// </summary>
public sealed class PipeFrameDecoder
{
    private enum State { Type, Length, Payload, Lines }

    private readonly byte[] _payload = new byte[byte.MaxValue];
    private State _state;
    private int _fill, _length;

    /// <summary>
    /// A complete data frame's payload as (buffer, count). The buffer is
    /// reused across frames — consume it synchronously.
    /// </summary>
    public event Action<byte[], int>? Data;

    /// <summary>A lines frame's bits (see <see cref="PipeFraming.LineDtr"/>).</summary>
    public event Action<byte>? Lines;

    /// <summary>Why the stream was rejected, or null while it is healthy.</summary>
    public string? Violation { get; private set; }

    /// <summary>Forget any partial frame and clear a violation (new connection).</summary>
    public void Reset()
    {
        _state = State.Type;
        _fill = _length = 0;
        Violation = null;
    }

    /// <summary>
    /// Consume <paramref name="count"/> received bytes from
    /// <paramref name="buffer"/>. Returns false when the stream violates the
    /// framing contract (see <see cref="Violation"/>); a poisoned decoder
    /// keeps returning false until <see cref="Reset"/>.
    /// </summary>
    public bool Feed(byte[] buffer, int count)
    {
        if (Violation is not null)
            return false;

        for (int i = 0; i < count; i++)
        {
            byte b = buffer[i];
            switch (_state)
            {
                case State.Type when b == PipeFraming.DataType:
                    _state = State.Length;
                    break;

                case State.Type when b == PipeFraming.LinesType:
                    _state = State.Lines;
                    break;

                case State.Type:
                    Violation = $"unknown frame type 0x{b:X2}";
                    return false;

                case State.Length when b == 0:
                    Violation = "zero-length data frame";
                    return false;

                case State.Length:
                    _length = b;
                    _fill = 0;
                    _state = State.Payload;
                    break;

                case State.Payload:
                    _payload[_fill++] = b;
                    if (_fill == _length)
                    {
                        _state = State.Type;
                        Data?.Invoke(_payload, _length);
                    }
                    break;

                case State.Lines:
                    _state = State.Type;
                    Lines?.Invoke(b);
                    break;
            }
        }

        return true;
    }
}
