using System.IO.Ports;
using VPlasma.Core.Protocol;

namespace VPlasma.Core.Device;

/// <summary>
/// Pumps a real COM port into a <see cref="VPlasmaDevice"/> at the plasma's
/// 9600 8N1 settings. On a single PC, pair it with the game through a
/// virtual null-modem (e.g. com0com): the game's COM2 passthrough opens one
/// end, vPLASMA the other.
///
/// <para>Unlike the RIO, the plasma is a pure listener — the game opens the
/// port with flow control disabled and never reads a byte back — so there is
/// no transmit path and no wire pacing to emulate. Our DTR/RTS are asserted
/// so a host that does check its modem lines sees "display present".</para>
/// </summary>
public sealed class VPlasmaSerialService : IDisposable
{
    private readonly VPlasmaDevice _device;

    private SerialPort? _port;
    private Thread? _reader;
    private volatile bool _running;

    public VPlasmaSerialService(VPlasmaDevice device)
    {
        _device = device ?? throw new ArgumentNullException(nameof(device));
    }

    /// <summary>True while a COM port is open.</summary>
    public bool IsOpen => _port?.IsOpen == true;

    /// <summary>The open port's name, or null.</summary>
    public string? PortName => _port?.PortName;

    /// <summary>Raised after the port opens (true) or closes (false).</summary>
    public event Action<bool>? ConnectionChanged;

    /// <summary>Port-level log lines (open/close/errors).</summary>
    public event Action<string>? Logged;

    /// <summary>Open <paramref name="portName"/> and start listening.</summary>
    public void Open(string portName)
    {
        if (string.IsNullOrWhiteSpace(portName))
            throw new ArgumentException("Port name is required.", nameof(portName));

        Close();

        var port = new SerialPort(portName, PlasmaProtocol.BaudRate, Parity.None, 8, StopBits.One)
        {
            Handshake = Handshake.None,
            // Finite read timeout so the reader thread can notice shutdown.
            ReadTimeout = 200,
            // Assert our modem lines: through a null modem the host sees
            // DSR/CTS high, i.e. "display present".
            DtrEnable = true,
            RtsEnable = true,
        };
        port.Open();

        _port = port;
        _running = true;

        _reader = new Thread(ReadLoop) { IsBackground = true, Name = "vPLASMA serial reader" };
        _reader.Start();

        Logged?.Invoke($"Opened {portName} @ {PlasmaProtocol.BaudRate} 8N1 — listening for the host");
        ConnectionChanged?.Invoke(true);
    }

    /// <summary>Close the port (idempotent).</summary>
    public void Close()
    {
        SerialPort? port = _port;
        if (port is null)
            return;

        _running = false;
        _port = null;
        try { port.Close(); }
        catch (IOException) { }
        port.Dispose();

        _reader?.Join(1000);
        _reader = null;

        Logged?.Invoke("Port closed");
        ConnectionChanged?.Invoke(false);
    }

    private void ReadLoop()
    {
        var buffer = new byte[256];
        while (_running)
        {
            SerialPort? port = _port;
            if (port is null)
                return;

            int n;
            try
            {
                n = port.Read(buffer, 0, buffer.Length);
            }
            catch (TimeoutException)
            {
                continue; // just a poll tick; check _running again
            }
            catch (Exception ex) when (ex is IOException or InvalidOperationException or OperationCanceledException)
            {
                if (_running)
                    Logged?.Invoke($"Port error: {ex.Message}");
                return;
            }

            if (n > 0)
                _device.OnReceived(buffer, n);
        }
    }

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