using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace iq2dev.com.IQCommLib
{
    public class ProtocolCommunicator
    {
        #region Fields

        ITransmitComm? _transmitComm = null;

        #endregion

        public ProtocolCommunicator(ITransmitComm transmitComm)
        {
            _transmitComm = transmitComm;
        }

        public List<byte> SendRecvCommand(IQCommChannel channel, IQCommand command, List<byte> transmitData)
        {
            return SendRecvCommand(channel, (byte)command, transmitData);
        }

        public List<byte> SendRecvCommand(IQCommChannel channel, byte command, List<byte> transmitData)
        {
            if (_transmitComm == null)
            {
                throw new NullReferenceException($"Communication is not initialized");
            }

            List<byte> sendBytes = new List<byte>();

            // check data bytes length
            if (transmitData.Count > 254)
            {
                throw new IQCommException("send bytes number is too large");
            }

            // pack send message
            sendBytes.Add((byte)command);
            sendBytes.AddRange(transmitData);

            return _transmitComm.SendRecv(channel, sendBytes);
        }

        #region IQ Interface

        public static List<byte> ReadFirmwareVersion(ITransmitComm transmitCom)
        {
            List<byte> sendBytes = new List<byte>();
            sendBytes.Add((byte)IQCommand.ReadIQPar);
            sendBytes.Add((byte)IQPar.IQParApplVersion);
            return transmitCom.SendRecv(IQCommChannel.IOLink, sendBytes);
        }

        public void writeIQCommPar(IQPar number, List<byte> data)
        {
            List<byte> send_data = new List<byte>();
            send_data.Add((byte)number);
            send_data.AddRange(data);
            UInt16 errorCode = 0;

            try
            {
                SendRecvCommand(IQCommChannel.IOLink, IQCommand.WriteIQPar, send_data);
            }
            catch (Exception exp)
            {
                throw new IQCommException(errorCode, number.ToString() + " can not be written, " + exp.Message);
            }
        }

        public List<byte> readIQCommPar(IQPar number)
        {
            List<byte> data = new List<byte>();
            data.Add((byte)number);
            UInt16 errorCode = 0;

            try
            {
                return SendRecvCommand(IQCommChannel.IOLink, IQCommand.ReadIQPar, data);
            }
            catch (Exception exp)
            {
                throw new IQCommException(errorCode, number.ToString() + " can not be read, " + exp.Message);
            }
        }

        public void writeDirectMode(List<byte> data)
        {
            SendRecvCommand(IQCommChannel.IOLink, IQCommand.WriteDM, data);
        }

        public List<byte> readDirectMode()
        {
            return SendRecvCommand(IQCommChannel.IOLink, IQCommand.ReadDM, new List<byte>());
        }

        #endregion

        #region Master

        public IQMstStatus getMstStatus(List<byte> pdIn)
        {
            IQMstStatus status = new IQMstStatus();
            byte bstatus;

            if (pdIn == null)
                throw new IQCommException("process data input list is null");

            // get status byte and pd in
            List<byte> recvData = SendRecvCommand(IQCommChannel.IOLink, IQCommand.GetMstStatus, new List<byte>());
            bstatus = recvData[0];
            recvData.RemoveAt(0);
            pdIn.Clear();
            pdIn.AddRange(recvData);

            // parse status byte
            status.state = (IQMstState)(bstatus & 0x07);
            status.isPDOutInvalid = (bstatus & 0x08) == 0x08;
            status.isPDInInvalid = (bstatus & 0x10) == 0x10;
            status.isEvent = (bstatus & 0x20) == 0x20;
            status.isODActive = (bstatus & 0x40) == 0x40;
            status.isNewPDOutSet = (bstatus & 0x80) == 0x80;

            return status;
        }

        public void setPDOut(List<byte> pdOut)
        {
            SendRecvCommand(IQCommChannel.IOLink, IQCommand.SetPDOut, pdOut);
        }

        public void setOperMode(IQMstMode mode, int timeoutSeconds)
        {
            List<byte> data = new List<byte>();
            data.Insert(0, (byte)(mode));
            SendRecvCommand(IQCommChannel.IOLink, IQCommand.SetOperMode, data);

            Stopwatch timer = new Stopwatch();
            timer.Start();
            while (timer.Elapsed.TotalSeconds < timeoutSeconds)
            {
                var newPDIn = new List<byte>();
                IQMstStatus newIQMstStatus = getMstStatus(newPDIn);

                switch (newIQMstStatus.state)
                {
                    case IQMstState.DI:
                    {
                        if (mode == IQMstMode.DI)
                        {
                            return;
                        }
                        break;
                    }
                    case IQMstState.DO:
                    {
                        if (mode == IQMstMode.DO)
                        {
                            return;
                        }
                        break;
                    }
                    case IQMstState.Inactive:
                    {
                        if (mode == IQMstMode.Inactive)
                        {
                            return;
                        }
                        break;
                    }
                    case IQMstState.Operate:
                    {
                        if ((mode == IQMstMode.Operate) || (mode == IQMstMode.Auto))
                        {
                            return;
                        }
                        break;
                    }
                    case IQMstState.Preoperate:
                    {
                        if (mode == IQMstMode.Preoperate)
                        {
                            return;
                        }
                        break;
                    }
                }
            }
            timer.Stop();
            throw new Exception("Timeout. Cannot change mode");
        }

        public void setOperMode(IQMstMode mode)
        {
            setOperMode(mode, 5);
        }

        public void startODWrite(UInt16 index, byte subIndex, List<byte> data)
        {
            List<byte> send_data = new List<byte>();
            send_data.Add((byte)(index & 0x00FF));        // add index low            
            send_data.Add((byte)((index >> 8) & 0x00FF)); // add index high
            send_data.Add(subIndex);                      // add subIndex
            send_data.AddRange(data);
            SendRecvCommand(IQCommChannel.IOLink, IQCommand.ODWriteReq, send_data);
        }

        public void getODWriteResponse()
        {
            List<byte> data = new List<byte>();
            List<byte> recvData = SendRecvCommand(IQCommChannel.IOLink, IQCommand.ODWriteRsp, data);

            ValidateIfErrRsp(recvData);
        }

        public void startODRead(UInt16 index, byte subIndex)
        {
            List<byte> data = new List<byte>();
            data.Insert(0, (byte)(index & 0x00FF));        // add index low
            data.Insert(1, (byte)((index >> 8) & 0x00FF)); // add index high
            data.Insert(2, subIndex);                      // add subIndex
            SendRecvCommand(IQCommChannel.IOLink, IQCommand.ODReadReq, data);
        }

        public List<byte> getODReadResponse()
        {
            List<byte> data = new List<byte>();
            List<byte> recvData = SendRecvCommand(IQCommChannel.IOLink, IQCommand.ODReadRsp, data);

            ValidateIfErrRsp(recvData);
            recvData.RemoveAt(0);
            return recvData;
        }

        public IOLEvent readEvent()
        {
            List<byte> _event = SendRecvCommand(IQCommChannel.IOLink, IQCommand.GetEvent, new List<byte>());
            IOLEvent Event = new IOLEvent();
            Event.parse(_event);
            return Event;
        }

        private void ValidateIfErrRsp(List<byte> recvData)
        {
            string errMessage = "";
            if ((recvData.Count > 0) && (recvData[0] != 0))
            {
                UInt16 errorCode = (UInt16)(((recvData[0] << 8) & 0xFF00) + recvData[1]);
                if (IOLinkErrorCodes.description.ContainsKey(errorCode))
                    errMessage += IOLinkErrorCodes.description[errorCode];
                errMessage = "IO-Link ISDU error 0x" + String.Format("{0:X04}", errorCode) + " (" + errMessage + ")";
                throw new Exception(errMessage);
            }
        }

        public List<byte> ReadOD(UInt16 index, byte subIndex)
        {
            startODRead(index, subIndex);
            WaitODRsp();
            return getODReadResponse();
        }

        public void WriteOD(UInt16 index, byte subIndex, List<byte> data)
        {
            startODWrite(index, subIndex, data);
            WaitODRsp();
            getODWriteResponse();
        }

        void WaitODRsp()
        {
            var newPDIn = new List<byte>();
            Stopwatch timer = new Stopwatch();
            timer.Start();
            while (timer.Elapsed.TotalSeconds < 5)
            {
                IQMstStatus newIQMstStatus = getMstStatus(newPDIn);
                if (newIQMstStatus.isODActive == false)
                {
                    return;
                }
            }
            timer.Stop();
            throw new Exception("ISDU timeout");
        }

        #endregion

        #region Device

        public IQDevStatus getDevStatus(List<byte> pdOut)
        {
            IQDevStatus status = new IQDevStatus();
            byte bstatus;

            if (pdOut == null)
                throw new IQCommException("process data input list is null");

            // get status byte and pd in
            List<byte> recvData = SendRecvCommand(IQCommChannel.IOLink, IQCommand.GetDevStatus, new List<byte>());
            bstatus = recvData[0];
            recvData.RemoveAt(0);
            pdOut.Clear();
            pdOut.AddRange(recvData);

            // parse status byte
            status.state = (IQDevState)(bstatus & 0x07);
            status.isPDOutInvalid = (bstatus & 0x08) == 0x08;
            status.isPDInInvalid = (bstatus & 0x10) == 0x10;
            status.isEvent = (bstatus & 0x20) == 0x20;
            status.isODReq = (bstatus & 0x40) == 0x40;
            status.isNewPDInSet = (bstatus & 0x80) == 0x80;

            return status;
        }

        public void setPDIn(bool IsInvalid, List<byte> pdIn)
        {
            List<byte> data = new List<byte>(pdIn);
            data.Insert(0, (byte)(IsInvalid ? 1 : 0));
            SendRecvCommand(IQCommChannel.IOLink, IQCommand.SetPDIn, data);
        }

        public IQODRequest getODRequest()
        {
            List<byte> recvData = SendRecvCommand(IQCommChannel.IOLink, IQCommand.GetODReq, new List<byte>());

            IQODRequest iqODRequest = new IQODRequest();

            iqODRequest.Type = (IQODReqType)recvData[0];
            iqODRequest.Index = (UInt16)(recvData[2] * 256 + recvData[1]);
            iqODRequest.SubIndex = recvData[3];
            recvData.RemoveRange(0, 4);

            if (iqODRequest.Type == IQODReqType.WriteErr || iqODRequest.Type == IQODReqType.ReadErr)
            {
                iqODRequest.Error = (UInt16)(recvData[0] * 256 + recvData[1]);
                recvData.RemoveRange(0, 2);
            }

            iqODRequest.Data = recvData;

            return iqODRequest;
        }

        public void sendEvent(IOLEvent Event)
        {
            SendRecvCommand(IQCommChannel.IOLink, IQCommand.SendEvent, Event.pack());
        }

        public void addISDU(List<byte> isdu)
        {
            UInt16 errorCode = 0;
            try
            {
                SendRecvCommand(IQCommChannel.IOLink, IQCommand.AddISDU, isdu);
            }
            catch (Exception exp)
            {
                throw new IQCommException(errorCode, "Can not add ISDU, " + exp.Message);
            }
        }

        public List<byte> GetISDU(UInt16 address)
        {
            UInt16 errorCode = 0;
            List<byte> data = new List<byte>();
            data.Add((byte)(address & 0xFF));
            data.Add((byte)((address >> 8) & 0xFF));
            try
            {
                return SendRecvCommand(IQCommChannel.IOLink, IQCommand.GetISDU, data);
            }
            catch (Exception exp)
            {
                throw new IQCommException(errorCode, "Can not add ISDU, " + exp.Message);
            }
        }

        public void SetISDUValue(UInt16 index, byte subindex, List<byte> value)
        {
            List<byte> data = new List<byte>();
            data.Add((byte)(index & 0xFF));
            data.Add((byte)((index >> 8) & 0xFF));
            data.Add(subindex);
            data.AddRange(value);
            SendRecvCommand(IQCommChannel.IOLink, IQCommand.SetISDUVal, data);
        }

        public List<byte> GetISDUValue(UInt16 index, byte subindex)
        {
            List<byte> data = new List<byte>();
            data.Add((byte)(index & 0xFF));
            data.Add((byte)((index >> 8) & 0xFF));
            data.Add(subindex);
            return SendRecvCommand(IQCommChannel.IOLink, IQCommand.GetISDUVal, data);
        }

        public void removeAllISDU()
        {
            SendRecvCommand(IQCommChannel.IOLink, IQCommand.RemoveAllISDU, new List<byte>() { });
        }

        #endregion
    }
}
