using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO.Ports;
using System.Collections;
using System.Diagnostics;

namespace iq2dev.com.IQCommLib
{
    public enum IQOperMode
    {
        Boot       = 0,
        IOLMaster  = 1,
        IOLDevice  = 2,  
        IOLMonitor = 3,  
        DirectMode = 0xFD      
    }

    public enum IQPar
    {
        IQParReserved0,
        IQParIQIOperMode,
        IQParBootVersion,
        IQParApplVersion,
        IQParTimeStamp,
        IQParReserved5,
        IQParReserved6,
        IQParReserved7,
        IQParReserved8,
        IQParReserved9,
        IQParReserved10,
        IQParReserved11,
        IQParReserved12,
        IQParReserved13,
        IQParReserved14,
        IQParReserved15,

        IQParMstStackVersion,
        IQParReserved17,
        IQParReserved18,
        IQParReserved19,

        IQParConfigCycleTimeStartup,
        IQParConfigCycleTimePreoperate,
        IQParConfigCycleTimeOperate,
        IQParConfigRestOfCycleTime,
        IQParConfigRevisionID,
        IQParInspectionLevel,
        IQParDeviceVendorID,
        IQParDeviceID,
        IQParDeviceFunctionID,
        IQParDeviceSernum,
        IQParReserved30,
        IQParReserved31,
        IQParReserved32,
        IQParReserved33,
        IQParReserved34,
        IQParReserved35,
        IQParReserved36,
        IQParReserved37,
        IQParReserved38,
        IQParReserved39,

        IQParMasterCycleTime,
        IQParMinCycleTime,
        IQParIsSIOSupported,
        IQParODLengthPreoperate,
        IQParFrameTypeOperate,
        IQParODLengthOperate,
        IQParIsISDUSupported,
        IQParRealRevisionID,
        IQParPDInLength,
        IQParPDOutLength,
        IQParIsInterleaveMode,
        IQParRealDeviceVendorID,
        IQParRealDeviceID,
        IQParRealDeviceFunctionID,
        IQParRealDeviceSernum,
        IQParRealConnectionBaudrate,
        IQParReserved56,
        IQParReserved57,
        IQParReserved58,
        IQParReserved59,

        IQParDSActivState,
        IQParDSDownloadEnable,
        IQParDSUploadEnable,
        IQParReserved63,
        IQParReserved64,
        IQParReserved65,
        IQParReserved66,
        IQParReserved67,
        IQParReserved68,
        IQParReserved69,
        IQParReserved70,
        IQParReserved71,
        IQParReserved72,
        IQParReserved73,
        IQParReserved74,
        IQParReserved75,
        IQParReserved76,
        IQParReserved77,
        IQParReserved78,
        IQParReserved79,
        IQParReserved80,
        IQParReserved81,
        IQParReserved82,
        IQParReserved83,
        IQParReserved84,
        IQParReserved85,
        IQParReserved86,
        IQParReserved87,
        IQParReserved88,
        IQParReserved89,
        IQParReserved90,
        IQParReserved91,
        IQParReserved92,
        IQParReserved93,
        IQParReserved94,
        IQParReserved95,
        IQParReserved96,
        IQParReserved97,
        IQParReserved98,
        IQParReserved99,
        IQParReserved100,
        IQParReserved101,
        IQParReserved102,
        IQParReserved103,
        IQParReserved104,
        IQParReserved105,
        IQParReserved106,
        IQParReserved107,
        IQParReserved108,
        IQParReserved109,
        IQParReserved110,
        IQParReserved111,
        IQParReserved112,
        IQParReserved113,
        IQParReserved114,
        IQParReserved115,
        IQParReserved116,
        IQParReserved117,
        IQParReserved118,
        IQParReserved119,
        IQParReserved120,
        IQParReserved121,
        IQParReserved122,
        IQParReserved123,
        IQParReserved124,
        IQParReserved125,
        IQParReserved126,
        IQParReserved127,

        IQParDevStackVersion,
        IQParGenDevDirPage,
        IQParGenDevSupportedBaudrate,
        IQParGenDevODRspMoment,
        IQParGenDevUploadReqFlag,
        IQParGenDevISDUSetLength,
        IQParGenDevMstCycTime,
        IQParReserved135
    }
        
    public enum IQCommand
    {
        WriteIQPar    = 0x11,
        ReadIQPar     = 0x12,

        SetOperMode   = 0x21,
        GetMstStatus  = 0x22,
        SetPDOut      = 0x23,        
        ODWriteReq    = 0x24,
        ODWriteRsp    = 0x25,
        ODReadReq     = 0x26,
        ODReadRsp     = 0x27,        
        GetEvent      = 0x28,                

        GetDevStatus  = 0x31,
        SetPDIn       = 0x32,
        GetODReq      = 0x33,
        SendEvent     = 0x34,
        AddISDU       = 0x35,
        RemoveISDU    = 0x36,
        RemoveAllISDU = 0x37,
        GetISDUVal    = 0x38,
        SetISDUVal    = 0x39,
        GetISDU       = 0x3A,
            
        GetTrace = 0x42,

        WriteDM = 0x7D,
        ReadDM  = 0xFD
    }
    
    public enum IQCommChannel
    {
        Error = 0x00,
        IOLink = 0x40,
        IQInterface = 0xC0
    }

    public class IQSettings : ICloneable
    {
        public const String AutoPort = "auto";

        private String comport = AutoPort;
        private int baudrate = 115200;
        public int CommTimeoutMS = 500;

        public int StatusTimerTickMS = 50;
        public int RequestTimeoutMS = 1000;

        //public bool IsAutoReconnect = false;

        public String Comport
        {
            get { return comport; }
            set { comport = value; }
        }

        public int Baudrate
        {
            get { return baudrate; }
            set { baudrate = value; }
        }

        public object Clone()
        {
            return this.MemberwiseClone();
        }
    }

    public enum IOLCommBaudrate
    {
        COM1 = 0,
        COM2 = 1,
        COM3 = 2
    }

    public enum IQMstMode
    {
        Inactive = 0,
        DO = 1,
        DI = 2,
        Auto = 3,
        Preoperate = 4,
        Operate = 5
    }

    public enum IQMstState
    {
        Inactive = 0,
        DO = 1,
        DI = 2,
        CheckFault = 3,
        Preoperate = 4,
        Operate = 5
    }

    public class IQMstStatus
    {
        public IQMstState state = IQMstState.Inactive;
        public bool isPDOutInvalid = false;
        public bool isPDInInvalid = false;
        public bool isEvent = false;
        public bool isODActive = false;
        public bool isNewPDOutSet = false;
    }

    public enum IQDevState
    {
        Inactive = 0,
        SIO = 1,
        Startup = 2,
        Preoperate = 3,
        Operate = 4
    }

    public class IQDevStatus
    {
        public IQDevState state = IQDevState.Inactive;
        public bool isPDOutInvalid = false;
        public bool isPDInInvalid = false;
        public bool isEvent = false;
        public bool isODReq = false;
        public bool isNewPDInSet = false;
    }

    public enum IQODReqType
    {
        Write    = 0,
        WriteErr = 1,
        Read     = 2,
        ReadErr  = 3
    }

    public class IQODRequest
    {
        public IQODReqType Type  = IQODReqType.Read;
        public UInt16      Index = 0;
        public byte        SubIndex = 0;
        public List<byte>  Data = new List<byte>();
        public UInt16      Error = 0;
    }

    public enum IOLEventInstance
    {
        Unknown = 0,
        PL = 1,
        DL = 2,
        AL = 3,
        Application = 4        
    }

    public enum IOLEventSource
    {
        Remote = 0,
        Local = 8
    }

    public enum IOLEventType
    {
        Reserved = 0,
        Notification = 0x10,
        Warning = 0x20,
        Error = 0x30
    }

    public enum IOLEventMode
    {
        Reserved = 0,
        SingleShot = 0x40,
        Disappears = 0x80,
        Appears = 0xC0
    }

    public class IOLEvent
    {
        public IOLEventInstance Instance = IOLEventInstance.Unknown;
        public IOLEventSource Source = IOLEventSource.Remote;
        public IOLEventType Type = IOLEventType.Notification;
        public IOLEventMode Mode = IOLEventMode.SingleShot;
        public UInt16 Code = 0;

        public override string ToString()
        {
            return Instance.ToString() + ", " + Source.ToString() + ", " + Type.ToString() + ", " + Mode.ToString() + ", Code " + String.Format("{0:X04}", Code);
        }

        public void parse(List<byte> _event)
        {
            byte eventQualifier = _event[0];
                        
            int instance = eventQualifier & 0x07;
            if (instance == 0 || instance > 4)
                Instance = IOLEventInstance.Unknown;
            else
                Instance = (IOLEventInstance)instance;

            Source = (IOLEventSource)(eventQualifier & 0x08);
            Type = (IOLEventType)(eventQualifier & 0x30);
            Mode = (IOLEventMode)(eventQualifier & 0xC0);

            Code = (UInt16)(_event[1] * 256 + _event[2]);
        }

        public List<byte> pack()
        {
            List<byte> _event = new List<byte>();
            _event.Add((byte)((byte)Instance | (byte)Source | (byte)Type | (byte)Mode));
            _event.Add(BitConverter.GetBytes(Code)[1]);
            _event.Add(BitConverter.GetBytes(Code)[0]);
            return _event;
        }
    }

    public class IQCommException : Exception
    {
        public UInt16 ErrCode = 0;
        public IQCommException() : base() { }
        public IQCommException(String message) : base(message) { }
        public IQCommException(UInt16 _ErrCode, String message) : base(message) { ErrCode = _ErrCode; }
    }

    public class IQComport : ITransmitComm
    {
        private object SyncObj = new object();

        public readonly static int[] ComBaudrates = new int[] { 115200 };

        private SerialPort? serialPort = null;

        public IQSettings IQSettings = new IQSettings();

        public String? ConnectedSerialPort { get { return serialPort != null ? serialPort.PortName : null; } }

        public List<String> getComports()
        {
            String[] _ports = SerialPort.GetPortNames();
            List<String> ports = new List<String>(_ports);
            ports.Insert(0, IQSettings.AutoPort);
            ports.Sort();
            return ports;
        }

        public bool openComport(String port)
        {
            lock (SyncObj)
            {
                serialPort = new SerialPort(port, IQSettings.Baudrate, Parity.None, 8, StopBits.One);
                serialPort.ReadTimeout = IQSettings.CommTimeoutMS;
                serialPort.WriteTimeout = IQSettings.CommTimeoutMS;
                serialPort.Open();
                serialPort.DiscardInBuffer();
                serialPort.DiscardOutBuffer();
                return true;
            }
        }

        public void closeComport()
        {
            lock (SyncObj)
            {
                if (serialPort != null)
                {
                    serialPort.DiscardInBuffer();
                    serialPort.DiscardOutBuffer();
                    serialPort.Close();
                    serialPort = null;
                }
            }
        }

        public String Connect()
        {
            // auto port connect
            if (IQSettings.Comport == IQSettings.AutoPort)
            {
                foreach (String port in SerialPort.GetPortNames())
                {
                    try
                    {
                        IQSettings.Comport = port;
                        Connect();
                        IQSettings.Comport = IQSettings.AutoPort;
                        return port;
                    }
                    catch (Exception) { IQSettings.Comport = IQSettings.AutoPort; }
                }
                throw new IQCommException("IQ Interface not found");
            }
            // defined port connect
            else
            {
                try
                {
                    openComport(IQSettings.Comport);
                    ProtocolCommunicator.ReadFirmwareVersion(this);
                    return IQSettings.Comport;
                }
                catch (Exception e)
                {
                    closeComport();
                    throw new IQCommException("IQ Interface can not be connected to the port " + IQSettings.Comport + ": " + e.Message);
                }                                
            }
        }

        public void Disconnect()
        {
            lock (SyncObj)
            {
                closeComport();                
            }
        }

        #region ITransmitComm

        public List<byte> SendRecv(IQCommChannel channel, List<byte> transmitData)
        {
            if (serialPort == null)
            {
                throw new NullReferenceException($"Serial port is not initialized");
            }
            List<byte> recvData = new List<byte>();
            lock (SyncObj)
            {
                transmitData.Insert(0, (byte)channel);
                transmitData.Insert(0, Convert.ToByte(transmitData.Count));
                transmitData[1] = Convert.ToByte(transmitData[1] | get6bitChecksum(transmitData.ToArray()));
                byte command = transmitData[2];

                // pack message
                serialPort.Write(transmitData.ToArray(), 0, transmitData.Count);

                //get message size
                int messageSize = serialPort.ReadByte();
                if (messageSize < 0)
                {
                    throw new IQCommException("The end of the stream has been read");
                }


                byte[] recvBuf = new byte[1 + messageSize];
                recvBuf[0] = Convert.ToByte(messageSize);

                //[Svitlychnyi]Do not use Read methods from SerialPort class:
                //It is not wait, until complete data is read, because this method use Streaming and conflict with buffering inside SerialPort. It takes a lot of time to find out
                int readBytes = serialPort.Read(recvBuf, 1, messageSize);
                while (readBytes < messageSize)
                {
                    readBytes += serialPort.Read(recvBuf, readBytes + 1, messageSize - readBytes);
                }

                // check checksum
                byte cks = (byte)(recvBuf[1] & 0x3F);
                if (cks != get6bitChecksum(recvBuf))
                    throw new IQCommException("response checksum error");

                // check error channel
                if ((recvBuf[1] & 0xC0) == (byte)(IQCommChannel.Error))
                {
                    String errMessage = "";
                    UInt16 errorCode = (UInt16)(((recvBuf[3] << 8) & 0xFF00) + recvBuf[4]);
                    if (IQCommErrorCodes.description.ContainsKey(errorCode))
                        errMessage += IQCommErrorCodes.description[errorCode];
                    throw new IQCommException(errorCode, "IQ comm error 0x" + String.Format("{0:X04}", errorCode) + " (" + errMessage + ")");
                }
                // check confirm channel
                else if ((recvBuf[1] & 0xC0) != (byte)(channel))
                {
                    throw new IQCommException("iq comm channel is not confirmed within response");
                }

                if (recvBuf[2] != (0x80 | command))
                {
                    throw new IQCommException("IQI command confirmation is absent");
                }

                // collect response data
                for (int index = 3; index <= recvBuf[0]; index++)
                {
                    recvData.Add(recvBuf[index]);
                }
            }

            return recvData;
        }

        #endregion ITransmitComm

        public static byte get6bitChecksum(byte[] data)
        {
            byte ucChkSum1;
            byte ucChkSum2;
            byte ucChkSum3;
            byte ucStart = 0x52;
            byte ucCounter;
            byte i;

            if (data.Length != data[0] + 1)
            {
                throw new ArgumentException("Wrong input data length");
            }

            // Get telegram length:
            ucCounter = (byte)(data[0] - 1);

            ucStart ^= (byte)(data[1] & 0xC0);
            i = 2;
            while (ucCounter > 0)
            {
                // First xor all telegram characters:
                ucStart ^= data[i];
                i++;
                ucCounter--;
            }

            // Compress 8bit result to 6 bit
            ucChkSum1 = (byte)(ucStart ^ (ucStart << 4));
            ucChkSum1 ^= (byte)(ucChkSum1 >> 2);
            ucChkSum2 = (byte)(ucStart ^ (ucStart >> 1));
            ucChkSum3 = (byte)((ucChkSum1 & 0x30) | ((ucChkSum2 & 0x40) >> 3) | ((ucChkSum2 & 0x10) >> 2) |
                               ((ucChkSum2 & 0x04) >> 1) | (ucChkSum2 & 0x01));

            return (ucChkSum3);
        }
    }
    



}