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,
        IQParProductInfo,
        IQParFDSpeed,
        IQParIOLPower,
        IQParGadgetID,
        IQParSWToolID,
        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,
        IQParRebootDevice,
        IQParResetParameters,

        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,
        IQParEMC_MseqCounter,
        IQParEMCCycleTime,
        IQParEMCMstState,
        IQParEMC_CKSErrCounter,
        IQParEMC_NoRspErrCounter,
        IQParEMC_RspErrCounter,
        IQParEMC_AvaitedTstRsp,
        IQParReserved77,
        IQParReserved78,
        IQParReserved79,
        IQParReserved80,
        IQParReserved81,
        IQParReserved82,
        IQParReserved83,
        IQParReserved84,
        IQParReserved85,
        IQParReserved86,
        IQParReserved87,
        IQParReserved88,
        IQParReserved89,
        IQParSetIsduCashedMode,
        IQParReserved91,
        IQParReserved92,
        IQParReserved93,
        IQParReserved94,
        IQParReserved95,
        IQParReserved96,
        IQParReserved97,
        IQParReserved98,
        IQParReserved99,
        IQParSetSendInvalidMSeq,//enable/disable sending invalid m-sequence
        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,
        IQParDiagnoseData,

        IQParDevStackVersion,
        IQParGenDevDirPage,
        IQParGenDevSupportedBaudrate,
        IQParGenDevODRspMoment,
        IQParGenDevUploadReqFlag,
        IQParGenDevISDUSetLength,
        IQParGenDevMstCycTime,
        IQParReserved135,
        IQParReserved136,
        IQParReserved137,
        IQParReserved138,
        IQParReserved139,
        IQParReserved140,
        IQParReserved141,
        IQParReserved142,
        IQParReserved143,
        IQParDevEMCMstTestMode,
        IQParDevEMCMstTestCounters,
        IQParReserved146
    }
        
    public enum IQCommand
    {
        WriteIQPar    = 0x11,
        ReadIQPar     = 0x12,

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

        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 class IQMstStatus2 : IQMstStatus
    {
        public bool IsIsduCaching = false;
        public bool IsIsduCacheEmpty = 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 void openComport(String port)
        {
            lock (SyncObj)
            {
                try
                {
                    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();
                }
                catch(Exception)
                {
                    serialPort = null;
                    throw;
                }
            }
        }

        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 = "";
                    if (recvBuf[3] == 0x56)
                    {
                        var isduType = recvBuf[4] == 1 ? "read" : "write";
                        UInt16 index = (UInt16)(((recvBuf[5] << 8) & 0xFF00) + recvBuf[6]);
                        byte subindex = recvBuf[7];
                        UInt16 errorCode = (UInt16)(((recvBuf[8] << 8) & 0xFF00) + recvBuf[9]);
                        throw new IQCommException(errorCode, $"ISDU {isduType}, index: {index}, subindex: {subindex} cached operation failed" +
                            $"with 0x" + String.Format("{0:X04}", errorCode));
                    }
                    else
                    {
                        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);
        }
    }
    



}