using System.IO.Pipes;

namespace RioJoy.Core.Serial;

/// <summary>
/// <see cref="IRioTransport"/> over a local named pipe, for driving the vRIO
/// device emulator without com0com. vRIO serves <c>\\.\pipe\vrio</c> for the
/// whole app lifetime (the device is always present); we connect as the
/// client — the same role the DOSBox-X fork's <c>namedpipe</c> serial backend
/// plays. Framing per <see cref="PipeFraming"/>: writes wrap in data frames,
/// reads unwrap them, and modem lines travel in-band as lines frames.
///
/// <para>On connect this replays <see cref="SerialPortTransport"/>'s board
/// reset over the pipe: a lines frame asserting DTR, the
/// <see cref="SerialPortTransport.DtrPulse"/> hold, then a lines frame
/// releasing it (RTS stays low throughout, matching the COM path's
/// RtsEnable default). The in-band frames keep the pulse's exact position
/// in the byte stream, which is why the contract multiplexes control onto
/// the data pipe instead of using a second one.</para>
///
/// <para>The peer's lines frames (vRIO asserts DTR+RTS on connect — "board
/// present") are decoded and tracked but nothing consumes them: the COM path
/// runs Handshake.None and never reads DSR/CTS either. A peer disconnect or
/// framing violation surfaces as a 0-byte read — the transport-closed signal
/// <see cref="RioSerialLink"/> already understands, same as a yanked
/// adapter.</para>
///
/// <para>Writes are not paced: host→RIO traffic is tiny stop-and-wait
/// commands, and the com0com path never emulated baud timing on this
/// direction either. (vRIO paces its own RIO→host TX, where the analog
/// stream would otherwise burst.)</para>
/// </summary>
public sealed class NamedPipeTransport : IRioTransport
{
    /// <summary>How long to wait for the pipe server before giving up.</summary>
    public static readonly TimeSpan DefaultConnectTimeout = TimeSpan.FromSeconds(2);

    private readonly NamedPipeClientStream _pipe;
    private readonly string _pipeName;
    private readonly PipeFrameDecoder _decoder = new();

    // Decoded data bytes not yet handed to a reader (a pipe read may carry
    // more payload than the caller's buffer holds). Only the receive loop
    // touches these — IRioTransport has a single reader by contract.
    private readonly Queue<byte> _decoded = new();
    private readonly byte[] _raw = new byte[512];
    private volatile bool _closed;
    private byte _peerLines;

    /// <param name="pipeName">Pipe name without the <c>\\.\pipe\</c> prefix, e.g. "vrio".</param>
    /// <param name="connectTimeout">Server wait; default <see cref="DefaultConnectTimeout"/>.</param>
    public NamedPipeTransport(string pipeName, TimeSpan? connectTimeout = null)
    {
        if (string.IsNullOrWhiteSpace(pipeName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(pipeName));
        _pipeName = pipeName;

        _decoder.Data += (buffer, count) =>
        {
            for (int i = 0; i < count; i++)
                _decoded.Enqueue(buffer[i]);
        };
        _decoder.Lines += lines => _peerLines = lines;

        _pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous);
        try
        {
            _pipe.Connect((int)(connectTimeout ?? DefaultConnectTimeout).TotalMilliseconds);
        }
        catch (TimeoutException)
        {
            _pipe.Dispose();
            throw new TimeoutException($@"No pipe server at \\.\pipe\{pipeName} — is vRIO running?");
        }
        catch
        {
            _pipe.Dispose();
            throw;
        }

        // DTR reset pulse, in-band: assert, hold, release (port of the COM
        // path's SETDTR/CLRDTR — see SerialPortTransport and PROTOCOL.md §1).
        //
        // Queued as overlapped writes, never awaited here: the pipe's buffers
        // default to 0 bytes, so a synchronous write blocks until the peer
        // reads it — and vRIO's own on-connect lines frame is written before
        // it starts reading, a write-first/write-first deadlock if we blocked
        // too. Pipe writes complete in issue order, so the assert→release
        // edge keeps its exact position in the byte stream; both frames
        // drain once the two readers are up. (When vRIO is already reading —
        // the steady case — the 50 ms hold arrives in real time too; during
        // the startup rendezvous the device may see a shorter pulse, which
        // still carries both edges.)
        try
        {
            byte[] assert = PipeFraming.EncodeLines(PipeFraming.LineDtr);
            Observe(_pipe.WriteAsync(assert, 0, assert.Length, CancellationToken.None));
            Thread.Sleep(SerialPortTransport.DtrPulse);
            byte[] release = PipeFraming.EncodeLines(0);
            Observe(_pipe.WriteAsync(release, 0, release.Length, CancellationToken.None));
        }
        catch
        {
            _pipe.Dispose();
            throw;
        }
    }

    // Swallow a queued write's eventual fault (e.g. the peer vanished before
    // draining it) — on net40 an unobserved task exception kills the process.
    private static void Observe(Task task) =>
        task.ContinueWith(t => { _ = t.Exception; }, TaskContinuationOptions.ExecuteSynchronously);

    public string Description => $@"\\.\pipe\{_pipeName}";

    /// <summary>The peer's last lines frame (bit0 its DTR → our DSR, bit1 its RTS → our CTS).</summary>
    public byte PeerLines => _peerLines;

    /// <summary>Why the peer's stream was rejected, or null (diagnostic; see <see cref="PipeFrameDecoder.Violation"/>).</summary>
    public string? Violation => _decoder.Violation;

    public async Task<int> ReadAsync(byte[] buffer, CancellationToken cancellationToken)
    {
        while (_decoded.Count == 0)
        {
            if (_closed)
                return 0;

            int n;
            try
            {
                n = await _pipe.ReadAsync(_raw, 0, _raw.Length, cancellationToken).ConfigureAwait(false);
            }
            catch (Exception ex) when (_closed && ex is IOException or ObjectDisposedException)
            {
                return 0; // Dispose broke the pending read — normal teardown
            }

            if (n == 0)
            {
                _closed = true; // server went away — unplugged cable
                return 0;
            }

            if (!_decoder.Feed(_raw, n))
            {
                _closed = true; // framing violation: not line noise on a pipe, drop the link
                return 0;
            }
        }

        int count = Math.Min(buffer.Length, _decoded.Count);
        for (int i = 0; i < count; i++)
            buffer[i] = _decoded.Dequeue();
        return count;
    }

    public Task WriteAsync(byte[] data, CancellationToken cancellationToken)
    {
        // Empty input frames to an empty array; the 0-byte write is a no-op.
        byte[] framed = PipeFraming.EncodeData(data);
        return _pipe.WriteAsync(framed, 0, framed.Length, cancellationToken);
    }

    public void Dispose()
    {
        // Closing the handle aborts a pending overlapped read (the receive
        // loop's read ignores cancellation, same as the COM path — teardown
        // relies on this). Pipes have no FlushFileBuffers hang to guard
        // against, so no grace-period dance like SerialPortTransport's.
        _closed = true;
        try { _pipe.Dispose(); }
        catch (IOException) { }
    }
}
