using iq2dev.com.iqCommLib;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;

namespace iq2dev.com.IQCommLib;

public class ProtocolCommunicator : IProtocolCommunicator, IDisposable
{
    #region Fields

    ITransmitComm _transmitComm;

    #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)
    {
        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);
    }

    public void Reconnect()
    {
        _transmitComm.Disconnect();
        Thread.Sleep(1000);
        _transmitComm.Connect();
    }

    #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 IQGadgetID ReadID()
    {
        IQGadgetID gadgetID = IQGadgetID.ReservedID;
        try
        {
            gadgetID = (IQGadgetID)readIQCommPar(IQPar.IQParGadgetID)[0];
        }
        catch (IQCommException e)
        {
            if (e.ErrCode == 0x1501)
            {
                gadgetID = IQGadgetID.IQInterface;
            }
            else
            {
                throw;
            }
        }
        return gadgetID;
    }

    public uint GetMasterCycleTime()
    {
        var masterCycleTimeByteArray = readIQCommPar(IQPar.IQParMasterCycleTime).GetRange(0, 2);
        masterCycleTimeByteArray.Reverse();
        var masterCycleTime = BitConverter.ToUInt16(masterCycleTimeByteArray.ToArray());
        return masterCycleTime;
    }

    public uint GetODLengthOperate()
    {
        var oDLengthOperate = readIQCommPar(IQPar.IQParODLengthOperate)[0];
        return oDLengthOperate;
    }

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

        SendRecvCommand(IQCommChannel.IOLink, IQCommand.WriteIQPar, send_data);
    }

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

        return SendRecvCommand(IQCommChannel.IOLink, IQCommand.ReadIQPar, data);
    }

    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>());
    }

    public IsduCachedInfo ReadIsduCachedInfo()
    {
        return new IsduCachedInfo(readIQCommPar(IQPar.IQParSetIsduCashedMode));
    }

    public List<byte> ReadDiagnoseInformation()
    {
        return readIQCommPar(IQPar.IQParDiagnoseData);
    }

    #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 List<byte> startODWriteCashed(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);
        return SendRecvCommand(IQCommChannel.IOLink, IQCommand.ODWriteCashedReq, 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();
    }

    public List<byte> WriteODCashed(UInt16 index, byte subIndex, List<byte> data)
    {
        return startODWriteCashed(index, subIndex, data);
    }

    public 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

    // Track whether Dispose has been called.
    private bool disposed = false;

    // Implement IDisposable.
    // Do not make this method virtual.
    // A derived class should not be able to override this method.
    public void Dispose()
    {
        Dispose(disposing: true);
        // This object will be cleaned up by the Dispose method.
        // Therefore, you should call GC.SuppressFinalize to
        // take this object off the finalization queue
        // and prevent finalization code for this object
        // from executing a second time.
        GC.SuppressFinalize(this);
    }

    // Dispose(bool disposing) executes in two distinct scenarios.
    // If disposing equals true, the method has been called directly
    // or indirectly by a user's code. Managed and unmanaged resources
    // can be disposed.
    // If disposing equals false, the method has been called by the
    // runtime from inside the finalizer and you should not reference
    // other objects. Only unmanaged resources can be disposed.
    protected virtual void Dispose(bool disposing)
    {
        // Check to see if Dispose has already been called.
        if (!this.disposed)
        {
            // If disposing equals true, dispose all managed
            // and unmanaged resources.
            if (disposing)
            {
                // Dispose managed resources.
                _transmitComm.Disconnect();
            }

            // Note disposing has been done.
            disposed = true;
        }
    }

    // Use C# finalizer syntax for finalization code.
    // This finalizer will run only if the Dispose method
    // does not get called.
    // It gives your base class the opportunity to finalize.
    // Do not provide finalizer in types derived from this class.
    ~ProtocolCommunicator()
    {
        // Do not re-create Dispose clean-up code here.
        // Calling Dispose(disposing: false) is optimal in terms of
        // readability and maintainability.
        Dispose(disposing: false);
    }

}
