using System;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using MIN;
using MIN.Abstractions;
using MIN.SerialPort;

namespace MassiveKnob.Plugin.SerialDevice.Worker
{
    public class SerialWorker : IDisposable
    {
        private readonly IMassiveKnobDeviceContext context;
        private readonly ILogger logger;

        private readonly object minProtocolLock = new object();
        private IMINProtocol minProtocol;
        private string lastPortName;
        private int lastBaudRate;
        private bool lastDtrEnable;


        private enum MassiveKnobFrameID
        {
            Handshake = 42,
            HandshakeResponse = 43,
            AnalogInput = 1,
            DigitalInput = 2,
            AnalogOutput = 3,
            DigitalOutput = 4,
            Quit = 62,
            Error = 63
        }


        
        public SerialWorker(IMassiveKnobDeviceContext context, ILogger logger)
        {
            this.context = context;
            this.logger = logger;
        }
        
        
        public void Dispose()
        {
            IMINProtocol instance;

            lock (minProtocolLock)
            {
                instance = minProtocol;
            }

            if (instance != null)
                Task.WaitAny(new [] { instance.QueueFrame((byte) MassiveKnobFrameID.Quit, Array.Empty<byte>()) }, 500);

            Disconnect();
        }
        
        
        public void Connect(string portName, int baudRate, bool dtrEnable)
        {
            lock (minProtocolLock)
            {
                if (portName == lastPortName && baudRate == lastBaudRate && dtrEnable == lastDtrEnable)
                    return;

                lastPortName = portName;
                lastBaudRate = baudRate;
                lastDtrEnable = dtrEnable;

                Disconnect();
                context.Connecting();

                if (string.IsNullOrEmpty(portName) || baudRate == 0)
                    return;

                 
                minProtocol?.Dispose();
                minProtocol = new MINProtocol(new MINSerialTransport(portName, baudRate, dtrEnable: dtrEnable), logger);
                minProtocol.OnConnected += MinProtocolOnOnConnected;
                minProtocol.OnFrame += MinProtocolOnOnFrame;
                minProtocol.Start();
            }
        }


        public void SetAnalogOutput(int analogOutputIndex, byte value)
        {
            IMINProtocol instance;
            
            lock (minProtocolLock)
            {
                instance = minProtocol;
            }
            
            instance?.QueueFrame(
                (byte)MassiveKnobFrameID.AnalogOutput, 
                new [] { (byte)analogOutputIndex, value });
        }

        public void SetDigitalOutput(int digitalOutputIndex, bool on)
        {
            IMINProtocol instance;

            lock (minProtocolLock)
            {
                instance = minProtocol;
            }

            instance?.QueueFrame(
                (byte)MassiveKnobFrameID.DigitalOutput,
                new [] { (byte)digitalOutputIndex, on ? (byte)1 : (byte)0 });
        }


        private void MinProtocolOnOnConnected(object sender, EventArgs e)
        {
            IMINProtocol instance;

            lock (minProtocolLock)
            {
                if (minProtocol != sender as IMINProtocol)
                    return;
                
                instance = minProtocol;
            }
            
            if (instance == null)
                return;

            Task.Run(async () =>
            {
                await instance.Reset();
                await instance.QueueFrame((byte)MassiveKnobFrameID.Handshake, new[] { (byte)'M', (byte)'K' });
            });
        }
        

        private void MinProtocolOnOnFrame(object sender, MINFrameEventArgs e)
        {
            IMINProtocol instance;

            lock (minProtocolLock)
            {
                if (minProtocol != sender as IMINProtocol)
                    return;

                instance = minProtocol;
            }

            if (instance == null)
                return;

            // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault - by design
            switch ((MassiveKnobFrameID)e.Id)
            {
                case MassiveKnobFrameID.HandshakeResponse:
                    if (e.Payload.Length < 4)
                    {
                        logger.LogError("Invalid handshake response length, expected 4, got {length}: {payload}",
                            e.Payload.Length, BitConverter.ToString(e.Payload));
                        
                        Disconnect();
                        return;
                    }

                    var specs = new DeviceSpecs(e.Payload[0], e.Payload[1], e.Payload[2], e.Payload[3]);
                    context.Connected(specs);
                    break;
                
                case MassiveKnobFrameID.AnalogInput:
                    if (e.Payload.Length < 2)
                    {
                        logger.LogError("Invalid analog input payload length, expected 2, got {length}: {payload}",
                            e.Payload.Length, BitConverter.ToString(e.Payload));
                        return;
                    }

                    context.AnalogChanged(e.Payload[0], e.Payload[1]);
                    break;
                
                case MassiveKnobFrameID.DigitalInput:
                    if (e.Payload.Length < 2)
                    {
                        logger.LogError("Invalid digital input payload length, expected 2, got {length}: {payload}",
                            e.Payload.Length, BitConverter.ToString(e.Payload));
                        return;
                    }

                    context.DigitalChanged(e.Payload[0], e.Payload[1] != 0);
                    break;

                case MassiveKnobFrameID.Error:
                    logger.LogError("Error message received from device: {message}", Encoding.ASCII.GetString(e.Payload));
                    break;

                default:
                    logger.LogWarning("Unknown frame ID received: {frameId}", e.Id);
                    break;
            }
        }


        private void Disconnect()
        {
            lock (minProtocolLock)
            {
                minProtocol?.Dispose();
                minProtocol = null;
            }
            
            context.Disconnected();
        }



        /*
                void SafeCloseSerialPort()
                {
                    try
                    {
                        serialPort?.Dispose();
                    }
                    catch
                    {
                        // ignored
                    }

                    serialPort = null;
                    context.Connecting();
                }


                while (serialPort == null && !cancellationToken.IsCancellationRequested)
                {
                    if (!TryConnect(ref serialPort, settings, out specs))
                    {
                        SafeCloseSerialPort();
                        Thread.Sleep(500);
                    }
                    else
                        break;
                }

                if (cancellationToken.IsCancellationRequested)
                {
                    SafeCloseSerialPort();
                    break;
                }

                var processingMessage = false;

                Debug.Assert(serialPort != null, nameof(serialPort) + " != null");
                serialPort.DataReceived += (sender, args) =>
                {
                    if (args.EventType != SerialData.Chars || processingMessage)
                        return;

                    var senderPort = (SerialPort)sender;
                    processingMessage = true;
                    try
                    {
                        var message = (char)senderPort.ReadByte();
                        ProcessMessage(senderPort, message);
                    }
                    finally
                    {
                        processingMessage = false;
                    }
                };


                context.Connected(specs);
                try
                {
                    // This is where sending data to the hardware would be implemented
                    while (serialPort.IsOpen && !cancellationToken.IsCancellationRequested)
                    {
                        Thread.Sleep(10);
                    }
                }
                catch
                {
                    // ignored
                }

                context.Disconnected();
                SafeCloseSerialPort();

                if (!cancellationToken.IsCancellationRequested)
                    Thread.Sleep(500);
            }
        }


        private static bool TryConnect(ref SerialPort serialPort, ConnectionSettings settings, out DeviceSpecs specs)
        {
            try
            {
                serialPort = new SerialPort(settings.PortName, settings.BaudRate)
                {
                    Encoding = Encoding.ASCII,
                    ReadTimeout = 1000, 
                    WriteTimeout = 1000,
                    DtrEnable = settings.DtrEnable
                };
                serialPort.Open();

                // Send handshake
                serialPort.Write(new[] { 'H', 'M', 'K', 'B' }, 0, 4);

                // Wait for reply
                var response = serialPort.ReadByte();

                if ((char) response == 'H')
                {
                    specs = new DeviceSpecs(serialPort.ReadByte(), serialPort.ReadByte(), serialPort.ReadByte(), serialPort.ReadByte());
                    if (specs.AnalogInputCount > -1 && specs.DigitalInputCount > -1 && specs.AnalogOutputCount > -1 && specs.DigitalOutputCount > -1)
                        return true;
                }
                else
                    CheckForError(serialPort, (char)response);

                specs = default;
                return false;
            }
            catch
            {
                specs = default;
                return false;
            }
        }


        private void ProcessMessage(SerialPort serialPort, char message)
        {
            switch (message)
            {
                case 'V':
                    var knobIndex = (byte)serialPort.ReadByte();
                    var volume = (byte)serialPort.ReadByte();

                    if (knobIndex < 255 && volume <= 100)
                        context.AnalogChanged(knobIndex, volume);

                    break;
            }
        }


        private static void CheckForError(SerialPort serialPort, char message)
        {
            if (message != 'E')
                return;

            var length = serialPort.ReadByte();
            if (length <= 0)
                return;

            var buffer = new byte[length];
            var bytesRead = 0;
            
            while (bytesRead < length)
                bytesRead += serialPort.Read(buffer, bytesRead, length - bytesRead);

            var errorMessage = Encoding.ASCII.GetString(buffer);
            Debug.Print(errorMessage);
        }


        private readonly struct ConnectionSettings
        {
            public readonly string PortName;
            public readonly int BaudRate;
            public readonly bool DtrEnable;

            public ConnectionSettings(string portName, int baudRate, bool dtrEnable)
            {
                PortName = portName;
                BaudRate = baudRate;
                DtrEnable = dtrEnable;
            }
        }
        */
    }
}
