using System;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

public class ArduinoConnection : IDisposable
{
    public const int BaudRate = 115200;

    // Must match what your sketch replies to "REQUEST_ID" (Form2.cs: EXPECTED_DEVICE_ID)
    // public const string ExpectedDeviceId = "EARS_READER";
    public const string ExpectedDeviceId = "N_EARS_ESP32C3";

    private const int HeartbeatTimeoutMs   = 10000;
    private const int HeartbeatCheckMs     = 2000;
    private const int MaxReconnectAttempts = 5;
    private const int ResetSettleMs        = 1500;  // board reboots when DTR asserts

    private readonly object _gate = new object();
    private SerialPort _port;
    private Thread _reader;
    private volatile bool _reading;

    private DateTime _lastData = DateTime.MinValue;
    private uint _heartbeatId;
    private bool _autoReconnect;
    private int _reconnectAttempts;
    private DateTime _nextReconnect = DateTime.MinValue;

    /// <summary>Raised on the GTK main thread for every complete line from the device.</summary>
    public event Action<string> LineReceived;
    /// <summary>Raised on the GTK main thread with human-readable status text.</summary>
    public event Action<string> Log;
    /// <summary>Raised on the GTK main thread when the link goes up or down.</summary>
    public event Action<bool> ConnectionChanged;

    public string PortName { get; private set; }

    public bool IsOpen
    {
        get { lock (_gate) return _port != null && _port.IsOpen; }
    }

    public static string[] ListPorts()
    {
        return SerialPort.GetPortNames()
                         .Distinct()
                         .OrderBy(p => p, StringComparer.Ordinal)
                         .ToArray();
    }

    // ---------- public API ----------

    public bool Connect(string portName)
    {
        if (string.IsNullOrEmpty(portName))
        {
            Emit(Log, "No port selected");
            return false;
        }

        ClosePort(silent: true);

        if (!OpenPort(portName))
            return false;

        _autoReconnect = false;
        _reconnectAttempts = 0;
        StartHeartbeat();

        Emit(Log, $"Connected to {portName} @ {BaudRate}");
        Emit(ConnectionChanged, true);

        // Give the board time to finish booting before the first command.
        Task.Delay(ResetSettleMs).ContinueWith(_ => Send("MODE3"));
        return true;
    }

    public void Disconnect()
    {
        _autoReconnect = false;
        StopHeartbeat();
        ClosePort(silent: false);
    }

    public void Send(string command)
    {
        try
        {
            lock (_gate)
            {
                if (_port == null || !_port.IsOpen)
                {
                    Emit(Log, $"Cannot send '{command}' — not connected");
                    return;
                }
                _port.Write(command + "\n");
            }
            Emit(Log, $"Sent: {command}");
        }
        catch (Exception ex)
        {
            Emit(Log, $"Send failed: {ex.Message}");
        }
    }

    /// <summary>
    /// Probes every port for a device answering REQUEST_ID with the expected ID.
    /// Returns the port name, or null. Runs off the UI thread; the caller
    /// connects on the main thread so no GLib call happens from a worker.
    /// </summary>
    public Task<string> DetectPortAsync()
    {
        return Task.Run(() =>
        {
            foreach (string p in ListPorts())
            {
                if (IsOpen && p == PortName) continue;

                Emit(Log, $"Probing {p}...");
                string id;
                if (Probe(p, out id))
                {
                    Emit(Log, $"Found {ExpectedDeviceId} on {p}");
                    return p;
                }
                if (id != null)
                    Emit(Log, $"  {p}: reported '{id}' (not a match)");
            }
            return (string)null;
        });
    }

    // ---------- port plumbing ----------

    private bool OpenPort(string portName)
    {
        try
        {
            lock (_gate)
            {
                _port = new SerialPort(portName, BaudRate)
                {
                    ReadTimeout  = 500,
                    WriteTimeout = 1000,
                    NewLine      = "\n",
                    DtrEnable    = true,
                    RtsEnable    = true,
                };
                _port.Open();
            }

            PortName = portName;
            _lastData = DateTime.Now;
            StartReader();
            return true;
        }
        catch (Exception ex)
        {
            Emit(Log, $"Open failed on {portName}: {ex.Message}");
            lock (_gate)
            {
                _port?.Dispose();
                _port = null;
            }
            return false;
        }
    }

    private void ClosePort(bool silent)
    {
        _reading = false;

        Thread t = _reader;
        _reader = null;
        if (t != null && t.IsAlive && t != Thread.CurrentThread)
            t.Join(1000);

        lock (_gate)
        {
            try { if (_port != null && _port.IsOpen) _port.Close(); }
            catch (Exception ex) { Emit(Log, $"Close error: {ex.Message}"); }
            _port?.Dispose();
            _port = null;
        }

        if (!silent)
        {
            Emit(Log, "Disconnected");
            Emit(ConnectionChanged, false);
        }
    }

    private void StartReader()
    {
        _reading = true;
        _reader = new Thread(ReaderLoop)
        {
            IsBackground = true,
            Name = "arduino-reader"
        };
        _reader.Start();
    }

    private void ReaderLoop()
    {
        var pending = new StringBuilder();
        var chunk = new byte[4096];

        while (_reading)
        {
            SerialPort p;
            lock (_gate) p = _port;

            if (p == null || !p.IsOpen) { Thread.Sleep(100); continue; }

            try
            {
                int n = p.Read(chunk, 0, chunk.Length);
                if (n <= 0) continue;

                _lastData = DateTime.Now;
                pending.Append(Encoding.ASCII.GetString(chunk, 0, n));

                string buffered = pending.ToString();
                int nl;
                while ((nl = buffered.IndexOf('\n')) >= 0)
                {
                    string line = buffered.Substring(0, nl).Trim();
                    buffered = buffered.Substring(nl + 1);
                    if (line.Length > 0)
                        Emit(LineReceived, line);
                }
                pending.Clear();
                pending.Append(buffered);
            }
            catch (TimeoutException) { /* normal when idle */ }
            catch (Exception ex)
            {
                if (_reading) Emit(Log, $"Read error: {ex.Message}");
                Thread.Sleep(200);
            }
        }
    }

    private bool Probe(string portName, out string deviceId)
    {
        deviceId = null;
        SerialPort test = null;
        try
        {
            test = new SerialPort(portName, BaudRate)
            {
                ReadTimeout  = 500,
                WriteTimeout = 1000,
                NewLine      = "\n",
                DtrEnable    = true,
                RtsEnable    = true,
            };
            test.Open();
            Thread.Sleep(ResetSettleMs);
            test.DiscardInBuffer();
            test.Write("REQUEST_ID\n");

            DateTime deadline = DateTime.Now.AddSeconds(3);
            while (DateTime.Now < deadline)
            {
                try
                {
                    string line = test.ReadLine().Trim();
                    if (line.StartsWith("DEVICE_ID:"))
                    {
                        deviceId = line.Substring("DEVICE_ID:".Length).Trim();
                        return deviceId == ExpectedDeviceId;
                    }
                }
                catch (TimeoutException) { }
            }
        }
        catch (Exception ex)
        {
            Emit(Log, $"  {portName}: {ex.Message}");
        }
        finally
        {
            try { test?.Close(); } catch { }
            test?.Dispose();
        }
        return false;
    }

    // ---------- heartbeat / auto-reconnect (runs on the GTK main loop) ----------

    private void StartHeartbeat()
    {
        StopHeartbeat();
        _heartbeatId = GLib.Timeout.Add(HeartbeatCheckMs, OnHeartbeatTick);
    }

    private void StopHeartbeat()
    {
        if (_heartbeatId != 0)
        {
            GLib.Source.Remove(_heartbeatId);
            _heartbeatId = 0;
        }
    }

    private bool OnHeartbeatTick()
    {
        if (_autoReconnect && !IsOpen)
        {
            if (_reconnectAttempts >= MaxReconnectAttempts)
            {
                _autoReconnect = false;
                Log?.Invoke($"Reconnection failed after {MaxReconnectAttempts} attempts. Check USB cable, port and power, then reconnect manually.");
                StopHeartbeat();
                return false;
            }

            if (DateTime.Now >= _nextReconnect)
            {
                _reconnectAttempts++;
                Log?.Invoke($"Reconnect attempt {_reconnectAttempts}/{MaxReconnectAttempts} on {PortName}...");

                if (OpenPort(PortName))
                {
                    _autoReconnect = false;
                    _reconnectAttempts = 0;
                    Log?.Invoke("Reconnected");
                    ConnectionChanged?.Invoke(true);
                    Task.Delay(ResetSettleMs).ContinueWith(_ => Send("MODE3"));
                }
                else
                {
                    _nextReconnect = DateTime.Now.AddSeconds(2);
                }
            }
            return true;
        }

        if (!IsOpen) return true;

        if ((DateTime.Now - _lastData).TotalMilliseconds >= HeartbeatTimeoutMs)
        {
            Log?.Invoke("Heartbeat timeout — no device activity");
            ClosePort(silent: true);
            ConnectionChanged?.Invoke(false);
            _autoReconnect = true;
            _reconnectAttempts = 0;
            _nextReconnect = DateTime.Now.AddSeconds(2);
        }
        return true;
    }

    // ---------- helpers ----------

    private static void Emit<T>(Action<T> handler, T arg)
    {
        Action<T> h = handler;
        if (h == null) return;
        Gtk.Application.Invoke((s, e) => h(arg));
    }

    public void Dispose()
    {
        _autoReconnect = false;
        StopHeartbeat();
        ClosePort(silent: true);
    }
}