using System.Diagnostics;
using System.IO.Ports;
using System.Runtime.InteropServices;
using VRio.Core.Protocol;

namespace VRio.Core.Device;

/// <summary>
/// Pumps a <see cref="VRioDevice"/> over a real COM port at the RIO's
/// 9600 8N1 settings. On a single PC, pair it with RIOJoy through a virtual
/// null-modem (e.g. com0com): vRIO opens one end, RIOJoy the other.
///
/// <para>Outbound bytes are paced at the wire rate: one byte per 10-bit frame
/// time (~1.04 ms at 9600 8N1). A virtual null-modem has no UART, so an
/// unpaced multi-byte write lands at the host back-to-back in microseconds —
/// a burst no real board could produce, and a timing tell that has tripped up
/// hosts tuned to hardware. A writer thread schedules each byte against a
/// monotonic slot deadline (<c>slot = max(prevSlot + period, now)</c>), so
/// the stream averages the true baud rate without bursting after idle.</para>
///
/// <para>A write fault never kills the writer — a real UART streams into an
/// unterminated line rather than blocking. If the virtual wire's far side
/// stops draining (peer end closed, write timeout), the stalled byte and the
/// queued backlog are dropped and transmission resumes with the next fresh
/// packet once the host reads again.</para>
///
/// <para>RIOJoy pulses DTR for 50 ms when it opens its end (the board-reset
/// handshake); through a null modem that arrives here as a DSR blip, which is
/// surfaced via <see cref="HostHandshake"/> so the UI can show that a host
/// connected.</para>
/// </summary>
public sealed class VRioSerialService : IDisposable
{
    /// <summary>RIO link bit rate (must match RIOJoy's transport).</summary>
    public const int BaudRate = 9600;

    // One byte on the wire is 10 bits (start + 8 data + stop) at 9600 baud.
    private static readonly long BytePeriodTicks = Stopwatch.Frequency * 10 / BaudRate;

    // Below this remaining wait (~1.8 ms) Thread.Sleep(1) would overshoot the
    // slot even at 1 ms timer resolution, so the pacer spins the remainder.
    private static readonly long SpinThresholdTicks = Stopwatch.Frequency * 18 / 10_000;

    private readonly VRioDevice _device;
    private readonly object _txGate = new();
    private readonly Queue<byte> _txQueue = new();

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

    public VRioSerialService(VRioDevice device)
    {
        _device = device ?? throw new ArgumentNullException(nameof(device));
        _device.Transmit += Write;
    }

    /// <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>
    /// The host's DTR line changed (RIOJoy pulses it high for 50 ms when it
    /// opens the port — the board-reset handshake). The argument is the new
    /// line state as seen on our DSR pin.
    /// </summary>
    public event Action<bool>? HostHandshake;

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

    /// <summary>Open <paramref name="portName"/> and start serving the device.</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, BaudRate, Parity.None, 8, StopBits.One)
        {
            Handshake = Handshake.None,
            // Finite read timeout so the reader thread can notice shutdown.
            ReadTimeout = 200,
            WriteTimeout = 2000,
            // Assert our modem lines: through a null modem the host sees DSR/CTS
            // high, i.e. "board present".
            DtrEnable = true,
            RtsEnable = true,
        };
        port.PinChanged += OnPinChanged;
        port.Open();

        _port = port;
        _running = true;
        lock (_txGate) _txQueue.Clear();

        // 1 ms system timer resolution while the port is open, so the pacer's
        // Thread.Sleep(1) actually sleeps ~1 ms instead of the 15.6 ms default.
        _timerResolutionRaised = timeBeginPeriod(1) == 0;

        _reader = new Thread(ReadLoop) { IsBackground = true, Name = "vRIO serial reader" };
        _reader.Start();
        _writer = new Thread(WriteLoop) { IsBackground = true, Name = "vRIO serial writer" };
        _writer.Start();

        Logged?.Invoke($"Opened {portName} @ {BaudRate} 8N1 (TX paced at the wire rate) — waiting 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;
        lock (_txGate) Monitor.PulseAll(_txGate); // wake the writer so it can exit
        _port = null;
        port.PinChanged -= OnPinChanged;
        try { port.Close(); }
        catch (IOException) { }
        port.Dispose();

        _reader?.Join(1000);
        _reader = null;
        _writer?.Join(1000);
        _writer = null;
        lock (_txGate) _txQueue.Clear();

        if (_timerResolutionRaised)
        {
            timeEndPeriod(1);
            _timerResolutionRaised = false;
        }

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

    private void OnPinChanged(object sender, SerialPinChangedEventArgs e)
    {
        // RIOJoy raises then drops DTR on open; either edge means a host is there.
        if (e.EventType != SerialPinChange.DsrChanged)
            return;

        bool high = false;
        try { high = _port?.DsrHolding ?? false; }
        catch (Exception ex) when (ex is IOException or InvalidOperationException) { }
        HostHandshake?.Invoke(high);
    }

    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);
        }
    }

    // The device's Transmit handler: queue the frame for the paced writer so
    // the caller (UI click, reader thread mid-reply) never blocks on the port.
    private void Write(byte[] data)
    {
        if (!_running)
            return; // device poked while offline — drop silently

        lock (_txGate)
        {
            foreach (byte b in data)
                _txQueue.Enqueue(b);
            Monitor.Pulse(_txGate);
        }
    }

    private void WriteLoop()
    {
        var one = new byte[1];
        long slot = Stopwatch.GetTimestamp();
        bool txHealthy = true; // log stall/recovery transitions, not every byte

        while (_running)
        {
            lock (_txGate)
            {
                while (_txQueue.Count == 0)
                {
                    if (!_running)
                        return;
                    Monitor.Wait(_txGate, 200); // timed, so a missed pulse can't wedge shutdown
                }
                one[0] = _txQueue.Dequeue();
            }

            // This byte's wire slot: one frame after the previous byte, or now
            // if the line has been idle (no burst "catch-up" debt).
            slot = Math.Max(slot + BytePeriodTicks, Stopwatch.GetTimestamp());
            PaceUntil(slot);

            SerialPort? port = _port;
            if (port is null)
                return;
            try
            {
                port.Write(one, 0, 1);
                if (!txHealthy)
                {
                    txHealthy = true;
                    Logged?.Invoke("TX recovered — host is draining the wire again");
                }
            }
            catch (Exception ex) when (ex is IOException or InvalidOperationException or TimeoutException)
            {
                if (!_running)
                    return;
                // A real UART cannot wedge: it shifts bits onto the line whether
                // or not anyone is listening. A failed write means the virtual
                // wire's far side stopped draining (peer end closed), so the
                // queued backlog is already stale — drop it and keep serving
                // fresh traffic; writes land again once the host reads its end.
                int dropped;
                lock (_txGate)
                {
                    dropped = _txQueue.Count;
                    _txQueue.Clear();
                }
                if (txHealthy)
                {
                    txHealthy = false;
                    Logged?.Invoke($"TX stalled ({ex.Message.TrimEnd('.')}) — dropped {dropped + 1} stale byte(s), writer stays alive");
                }
                continue;
            }

            // If the wait overshot its slot, pace the next byte from the
            // actual emission instead: a UART can never put two frames closer
            // than the frame time, so a stall must not cause a catch-up burst.
            long now = Stopwatch.GetTimestamp();
            if (now > slot)
                slot = now;
        }
    }

    private static void PaceUntil(long slotTicks)
    {
        while (true)
        {
            long remaining = slotTicks - Stopwatch.GetTimestamp();
            if (remaining <= 0)
                return;
            if (remaining > SpinThresholdTicks)
                Thread.Sleep(1);
            else
                Thread.SpinWait(64);
        }
    }

    [DllImport("winmm.dll")]
    private static extern uint timeBeginPeriod(uint uMilliseconds);

    [DllImport("winmm.dll")]
    private static extern uint timeEndPeriod(uint uMilliseconds);

    public void Dispose()
    {
        _device.Transmit -= Write;
        Close();
    }
}
