using System.IO.Ports;

namespace RioJoy.Core.Serial;

/// <summary>
/// <see cref="IRioTransport"/> backed by a physical <see cref="SerialPort"/>.
/// Opens the port at the RIO's 9600 8N1 settings and pulses DTR on open as a
/// board reset/handshake (SETDTR, 50 ms, CLRDTR — port of
/// <c>OpenConnection</c>/<c>SetupConnection</c>, riovjoy2.cpp#L747; see
/// docs/PROTOCOL.md §1).
/// </summary>
public sealed class SerialPortTransport : IRioTransport
{
    /// <summary>Default RIO link bit rate (stock v4.2 firmware).</summary>
    public const int BaudRate = 9600;

    /// <summary>DTR reset-pulse hold time on open.</summary>
    public static readonly TimeSpan DtrPulse = TimeSpan.FromMilliseconds(50);

    /// <summary>Longest <see cref="Dispose"/> will wait for the port to close.</summary>
    public static readonly TimeSpan CloseGracePeriod = TimeSpan.FromSeconds(2);

    private readonly SerialPort _port;
    private readonly Stream _stream;

    /// <param name="portName">COM port name, e.g. "COM1".</param>
    /// <param name="baudRate">
    /// Link rate; default 9600 (stock firmware). The 31250-baud firmware
    /// variant (rio-firmware/, --baud31250) needs an adapter that can make
    /// non-standard rates exactly — FTDI-class USB serial can, classic
    /// 16550 UARTs cannot.
    /// </param>
    public SerialPortTransport(string portName, int baudRate = BaudRate)
    {
        if (string.IsNullOrWhiteSpace(portName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(portName));

        _port = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One)
        {
            Handshake = Handshake.None,
            // The framing state machine handles timing; keep reads non-throwing.
            ReadTimeout = SerialPort.InfiniteTimeout,
            WriteTimeout = SerialPort.InfiniteTimeout,
        };

        _port.Open();

        // DTR reset pulse: assert, hold, release.
        _port.DtrEnable = true;
        Thread.Sleep(DtrPulse);
        _port.DtrEnable = false;

        _stream = _port.BaseStream;
    }

    public string Description => $"{_port.PortName} @ {_port.BaudRate} 8N1";

    public Task<int> ReadAsync(byte[] buffer, CancellationToken cancellationToken) =>
        _stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);

    public Task WriteAsync(byte[] data, CancellationToken cancellationToken) =>
        _stream.WriteAsync(data, 0, data.Length, cancellationToken);

    public void Dispose()
    {
        // Disposing closes and releases the COM port for other owners.
        //
        // net48's SerialStream.Dispose calls FlushFileBuffers, which blocks until
        // the driver drains any buffered TX bytes — indefinitely when the peer has
        // stopped reading (a wedged board, or a virtual-port pair with no reader).
        // Seen in the field as the tray UI freezing on editor close. Abort + clear
        // both queues first so the flush has nothing to wait on, and bound the
        // close so no driver can hang the calling (usually UI) thread.
        try
        {
            _port.DiscardOutBuffer();
            _port.DiscardInBuffer();
        }
        catch
        {
            // Port already gone (unplugged adapter / driver error) — closing below
            // still releases the handle.
        }

        Task close = Compat.TaskCompat.Run(() =>
        {
            try { _port.Dispose(); }
            catch { /* best-effort release */ }
        });

        // If the close is still stuck in the driver despite the purge, give up after
        // the grace period and let it finish on the pool thread; reopening the port
        // may fail until it does, which callers already surface as a status error.
        close.Wait(CloseGracePeriod);
    }
}
