using System.IO.Ports;
using System.Text;
using CliWrap.Buffered;
using InnovEnergy.Lib.Utils;

namespace InnovEnergy.App.BmsTunnel;


public class BmsTunnel : IDisposable
{
    private SerialPort SerialPort { get; }
    
    public String Tty  { get; }
    public Byte   Node { get; set; }

    private const Int32    BaudRate = 115200;
    private const Int32    DataBits = 8;
    private const Parity   Parity   = System.IO.Ports.Parity.Even;
    private const StopBits StopBits = System.IO.Ports.StopBits.One;

    private const Int32 CrcLength = 2; 
    
    private const Byte TunnelCode = 0x41;
    private const String CrcError = "?? CRC FAILED";

    public BmsTunnel(String tty, Byte node)
    {
        Tty  = tty;
        Node = node;

        StopSerialStarter();

        SerialPort = new SerialPort(Tty, BaudRate, Parity, DataBits, StopBits);
        SerialPort.ReadTimeout = 100;
        
        SerialPort.Open();
    }


    private IEnumerable<Byte> Header
    {
        get
        {
            yield return Node;
            yield return TunnelCode;
        }
    }
    
    private static IEnumerable<Byte> NewLine
    {
        get
        {
            yield return 0x0D;
        }
    }


    public IEnumerable<String> SendCommand(String command)
    {
        var reply = SendSingleCommand(command);

        while (!reply.StartsWith("??"))
        {
            yield return reply;
            
            if (reply.EndsWith("chars answered. Ready."))
                yield break;
            
            reply = GetMore();
        }

        if (reply == CrcError)
        {
            yield return "";
            yield return CrcError.Substring(3);
        }
    }

    private String GetMore() => SendSingleCommand("");


    private String SendSingleCommand(String command)
    {
        var payload = Header
                     .Concat(CommandToBytes(command))
                     .ToList();

        var crc = CalcCrc(payload);
        
        payload.AddRange(crc);
  
        SerialPort.Write(payload.ToArray(), 0, payload.Count);

        var response = Enumerable
                      .Range(0, 255)
                      .Select(ReadByte)
                      .TakeWhile(b => b >= 0)
                      .Select(Convert.ToByte)
                      .ToArray();

        if (!CheckCrc(response))
        {
            // TODO: this should go into outer loop instead of returning magic value CrcError
            
            //Console.WriteLine(BitConverter.ToString(response).Replace("-", " "));
            return CrcError;
        }

        return response
              .Skip(2)
              .TakeWhile(b => b != 0x0D)
              .ToArray()
              .Apply(Encoding.ASCII.GetString);

        Int32 ReadByte<T>(T _)
        {
            try
            {
                return SerialPort.ReadByte();
            }
            catch (TimeoutException)
            {
                return -1;
            }
        }
    }
    
    private static IReadOnlyList<Byte> CalcCrc(IEnumerable<Byte> data)
    {
        UInt16 crc = 0xFFFF;

        foreach (var b in data)
        {
            crc ^= b;

            for (var bit = 0; bit < 8; bit++)
            {
                var bit0 = (crc & 0x0001) != 0;
                crc >>= 1;
                if (bit0) crc ^= 0xA001;
            }
        }

        var hi = 0xFF & crc;
        var lo = (crc >> 8) & 0xFF;

        return new[] {(Byte) hi, (Byte) lo}; // big endian
    }

    private static Boolean CheckCrc(IReadOnlyList<Byte> data)
    {
        var expectedCrc = data.SkipLast(CrcLength).Apply(CalcCrc);
        var actualCrc   = data.TakeLast(CrcLength);

        return actualCrc.SequenceEqual(expectedCrc);
    }

    private static IEnumerable<Byte> CommandToBytes(String command)
    {
        if (command == "")
            return Enumerable.Empty<Byte>();

        return command
              .Apply(Encoding.ASCII.GetBytes)
              .Concat(NewLine);
    }

    private void StopSerialStarter()
    {
        CliPrograms.StopTty
                   .WithArguments(Tty)
                   .ExecuteBufferedAsync()
                   .Task
                   .Wait(3000);
    }

    private void StartSerialStarter()
    {
        CliPrograms.StartTty
                   .WithArguments(Tty)
                   .ExecuteBufferedAsync()
                   .Task
                   .Wait(3000);
    }

    public void Dispose()
    {
        SerialPort.Dispose();
        StartSerialStarter();
    }
}