using System.IO.Pipes;

namespace VPlasma.Core.Device;

/// <summary>
/// Serves a <see cref="VPlasmaDevice"/> over the named pipe that the
/// DOSBox-X fork's <c>serial2=namedpipe pipe:vplasma</c> backend connects
/// to — the com0com-free transport. We are the pipe <em>server</em> (the
/// display is always present); DOSBox-X is the client and background-retries
/// while the pipe is missing, so this service listens for the whole app
/// lifetime alongside the COM path.
///
/// <para>Framing is the contract shared with the fork (see VRio.Core's
/// <c>PipeFraming</c>, and <c>serialnamedpipe.h</c> on the fork side):
/// <c>0x00 len bytes</c> carries serial data, <c>0x01 bits</c> carries the
/// sender's DTR/RTS. Like the serial twin, the plasma is a pure listener —
/// we send one lines frame on connect (DTR+RTS high, "display present") and
/// then only ever read. The game may toggle its own lines around writes
/// (DOS console redirection flips RTS per byte); the display doesn't care,
/// so inbound lines frames are consumed silently.</para>
/// </summary>
public sealed class VPlasmaPipeService : IDisposable
{
    /// <summary>The contract pipe name: DOSBox-X connects to <c>\\.\pipe\vplasma</c>.</summary>
    public const string DefaultPipeName = "vplasma";

    // Frame types and line bits, per the shared contract.
    private const byte DataType = 0x00;
    private const byte LinesType = 0x01;
    private const byte LinesPresent = 0x03; // DTR + RTS

    private readonly VPlasmaDevice _device;
    private readonly string _pipeName;

    private NamedPipeServerStream? _pipe;
    private Thread? _server;
    private volatile bool _running;
    private volatile bool _clientConnected;

    public VPlasmaPipeService(VPlasmaDevice device, string pipeName = DefaultPipeName)
    {
        _device = device ?? throw new ArgumentNullException(nameof(device));
        if (string.IsNullOrWhiteSpace(pipeName))
            throw new ArgumentException("Pipe name is required.", nameof(pipeName));
        _pipeName = pipeName;
    }

    /// <summary>The served pipe name (without the <c>\\.\pipe\</c> prefix).</summary>
    public string PipeName => _pipeName;

    /// <summary>True while a client is connected.</summary>
    public bool IsClientConnected => _clientConnected;

    /// <summary>A client connected (true) or went away (false).</summary>
    public event Action<bool>? ClientChanged;

    /// <summary>Pipe-level log lines (listen/connect/errors).</summary>
    public event Action<string>? Logged;

    /// <summary>Start listening (idempotent). Clients may come and go forever.</summary>
    public void Start()
    {
        if (_running)
            return;

        _running = true;
        _server = new Thread(ServerLoop) { IsBackground = true, Name = $"vPLASMA pipe server ({_pipeName})" };
        _server.Start();

        Logged?.Invoke($@"Listening on \\.\pipe\{_pipeName} — DOSBox-X connects when it boots");
    }

    /// <summary>Stop listening and drop any client (idempotent).</summary>
    public void Stop()
    {
        if (!_running)
            return;

        _running = false;
        NamedPipeServerStream? pipe = _pipe;
        _pipe = null;
        try { pipe?.Dispose(); }
        catch (IOException) { }

        // A WaitForConnection pending on the disposed stream can survive the
        // Dispose on net48; a throwaway client connect releases it either way.
        try
        {
            using var poke = new NamedPipeClientStream(".", _pipeName, PipeDirection.Out);
            poke.Connect(100);
        }
        catch (Exception ex) when (ex is IOException or TimeoutException or UnauthorizedAccessException) { }

        _server?.Join(1000);
        _server = null;
        _clientConnected = false;

        Logged?.Invoke("Pipe server stopped");
    }

    private void ServerLoop()
    {
        bool busyLogged = false; // log the name collision once, not per retry

        while (_running)
        {
            NamedPipeServerStream pipe;
            try
            {
                pipe = new NamedPipeServerStream(_pipeName, PipeDirection.InOut, 1,
                    PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
            }
            catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
            {
                // Name already served — e.g. vRIO's built-in glass and the
                // standalone vPLASMA both running. Whoever loses just waits.
                if (!busyLogged)
                {
                    busyLogged = true;
                    Logged?.Invoke($@"\\.\pipe\{_pipeName} is busy ({ex.Message.TrimEnd('.')}) — retrying until it frees up");
                }
                for (int i = 0; i < 20 && _running; i++)
                    Thread.Sleep(100);
                continue;
            }
            busyLogged = false;

            _pipe = pipe;
            try
            {
                pipe.WaitForConnection();

                if (_running)
                {
                    // Our lines frame goes first: display present.
                    pipe.Write(new byte[] { LinesType, LinesPresent }, 0, 2);
                    pipe.Flush();

                    _clientConnected = true;
                    Logged?.Invoke("Pipe client connected — sent lines DTR+RTS (display present)");
                    ClientChanged?.Invoke(true);

                    ReadUntilDisconnect(pipe);
                }
            }
            catch (Exception ex) when (ex is IOException or ObjectDisposedException or InvalidOperationException)
            {
                // Disposed by Stop(), or a client vanished mid-connect.
            }

            bool wasConnected = _clientConnected;
            _clientConnected = false;
            _pipe = null;
            try { pipe.Dispose(); }
            catch (IOException) { }

            if (wasConnected && _running)
            {
                Logged?.Invoke("Pipe client disconnected");
                ClientChanged?.Invoke(false);
            }
        }
    }

    private void ReadUntilDisconnect(NamedPipeServerStream pipe)
    {
        // Inline deframer (the listener-only subset of VRio.Core's
        // PipeFrameDecoder): data payloads go to the device, lines frames are
        // consumed silently, anything else is a protocol bug — log and drop
        // the connection rather than resync.
        const int StateType = 0, StateLength = 1, StatePayload = 2, StateLines = 3;
        int state = StateType, remaining = 0;

        var buffer = new byte[512];
        var payload = new byte[byte.MaxValue];
        int fill = 0;

        while (_running)
        {
            int n;
            try
            {
                n = pipe.Read(buffer, 0, buffer.Length);
            }
            catch (Exception ex) when (ex is IOException or ObjectDisposedException or InvalidOperationException or OperationCanceledException)
            {
                if (_running)
                    Logged?.Invoke($"Pipe error: {ex.Message}");
                return;
            }

            if (n == 0)
                return; // client closed its end

            for (int i = 0; i < n; i++)
            {
                byte b = buffer[i];
                switch (state)
                {
                    case StateType when b == DataType:
                        state = StateLength;
                        break;
                    case StateType when b == LinesType:
                        state = StateLines;
                        break;
                    case StateType:
                        Logged?.Invoke($"Pipe protocol violation: unknown frame type 0x{b:X2} — dropping the connection");
                        return;

                    case StateLength when b == 0:
                        Logged?.Invoke("Pipe protocol violation: zero-length data frame — dropping the connection");
                        return;
                    case StateLength:
                        remaining = b;
                        fill = 0;
                        state = StatePayload;
                        break;

                    case StatePayload:
                        payload[fill++] = b;
                        if (fill == remaining)
                        {
                            state = StateType;
                            _device.OnReceived(payload, remaining);
                        }
                        break;

                    case StateLines:
                        state = StateType; // the display ignores host line state
                        break;
                }
            }
        }
    }

    public void Dispose() => Stop();
}
