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

namespace iSTP_Embedded
{
    public class EmbeddedProtocol : IDisposable
    {
        public delegate void ProcessingDelegate(EmbeddedPacket packet);
        private ProcessingDelegate LogData;
        private ACK MyAck;

        #region Protocol constants
        public const uint TYPE_FORCE_STOP = 0x01;
        public const uint TYPE_ERROR = 0x02;
        public const uint TYPE_CONFIGURE = 0x04;
        public const uint TYPE_VERSION = 0x05;
        public const uint TYPE_INITIALIZATION = 0x08;
        public const uint TYPE_DATA = 0x10;
        public const uint TYPE_HEALTH_DATA = 0x11;
        public const uint TYPE_COMMENT = 0x40;
        public const uint TYPE_CONTROL = 0x20;
        public const uint TYPE_ACK = 0xFF;

        // from emb platform
        public const uint ID_ERR_CANT_SEND = 0x01;
        public const uint ID_ERR_SR_TOO_HIGH = 0x02;
        public const uint ID_CTRL_CONF_END = 0x04;

        // from PC
        public const uint ID_CONF_REQ = 0x01;
        public const uint ID_CONF_BOARD_PARAM = 0x04;
        public const uint ID_CONF_ENB_HEALTH = 0x07;
        public const uint ID_CONF_SAMPLE_RATE = 0x08;
        public const uint ID_CONF_FIFO_MODE = 0x20;
        public const uint ID_CONF_FIFO_READ_RATE = 0x40;
        public const uint ID_CTRL_START = 0x01;
        public const uint ID_CTRL_STOP = 0x02;
        public const uint ID_INIT_3V3 = 0x01;
        public const uint ID_INIT_5VD = 0x02;
        public const uint ID_INIT_5VA = 0x04;
        public const uint ID_INIT_EXT = 0x08;
        #endregion

        private class LoggerEnabler
        {
            public LoggerEnabler()
            {
                Enabled = false;
            }

            public bool Enabled { get; set; }
        }

        private struct EmbeddedSettings
        {
            public String ConfigFile;
            public UInt16 SampleRate;
        }

        private EmbeddedSettings Settings = new EmbeddedSettings();

        private EmbeddedController _Controller;
        private EventWaitHandle RxCancelEvent;
        private Thread MyRxThread;
        private Object RxSyncObject;
        private EmbeddedPacket LatestPacket;
        private long LastPacketRcvd;
        private EmbeddedPacket SendPacket;
        public delegate void NewPacketHandler(EmbeddedPacket packet);
        public event NewPacketHandler NewPacket;
        private NewPacketHandler MyPacketHandler;
        
        private uint LostPacketCounter;
        private uint LastPacketNumber;
        private bool SampleRateTooHigh;

        private SerialPort Connection;
        private Object SyncObj;
        private static LoggerEnabler LoggingEnabled;
        private ISTPConfiguration EmbConfiguration;
        private EmbeddedHealthData EmbHealth;
        private uint ReceivedPacketCounter;
        private uint DroppedPacketCounter;
        private double CurrentEmbeddedTime = 0d;
        private uint WarningCounter;
        private uint WARNING_STOP = 100;
        FileStream DumpFile;
        private string GlobalEmbConfFileName = string.Empty;

        public int GetPacketInACKmem { get { return (MyAck == null) ? 0 : MyAck.GetNumACK4return; } }
        public string[] GetHealthDataHeaders { get { return EmbConfiguration.GetHealthDataHeaders; } }

        public EmbeddedProtocol()
        {
            Connection = new SerialPort();
            RxCancelEvent = new EventWaitHandle(false, EventResetMode.ManualReset);
            RxSyncObject = new Object();
            SyncObj = new Object();
            LoggingEnabled = new LoggerEnabler();
            LatestPacket = new EmbeddedPacket();
            SendPacket = new EmbeddedPacket();
            LastPacketRcvd = 0;
            MyPacketHandler = new NewPacketHandler(PacketHandler);
            DumpFile = new FileStream("DroppedPackets.bin", FileMode.Append, FileAccess.Write);
            MyAck = new ACK();
        }

        public void Reconfiguration()
        {
            if (GlobalEmbConfFileName == string.Empty)
                GlobalEmbConfFileName = "EmbeddedConfig.txt";

            EmbConfiguration = new ISTPConfiguration(GlobalEmbConfFileName);
            LastConfComPacketTime = DateTime.Now;
            this.Transmit(new EmbeddedPacket(0, TYPE_CONFIGURE, ID_CONF_REQ, null));
            int i = 0;
            //wait max 10s
            while (i++ < 100 && !EmbConfiguration.ConfigurationComplete)
            {
                Thread.Sleep(100);
            }

        }

        public void setController(EmbeddedController controller)
        {
            this._Controller = controller;
        }

        public void SetDataLogger(ProcessingDelegate pd)
        {
            LogData = pd;
        }

        public void ConfigureConnection(string portName,
                                        string baudRate,
                                        string dataBits,
                                        string stopBits,
                                        string parity,
                                        string handShake)
        {
            Connection.PortName = portName;
            Connection.BaudRate = Convert.ToInt32(baudRate);
            Connection.DataBits = Convert.ToInt32(dataBits);

            // Stop bits
            if (stopBits == "1")
                this.Connection.StopBits = StopBits.One;
            else if (stopBits == "1.5")
                this.Connection.StopBits = StopBits.OnePointFive;
            else if (stopBits == "2")
                this.Connection.StopBits = StopBits.Two;
            else
                this.Connection.StopBits = StopBits.None;

            // Parity bits
            if (parity == "None")
                this.Connection.Parity = Parity.None;
            else if (parity == "Odd")
                this.Connection.Parity = Parity.Odd;
            else if (parity == "Even")
                this.Connection.Parity = Parity.Even;
            else if (parity == "Mark")
                this.Connection.Parity = Parity.Mark;
            else
                this.Connection.Parity = Parity.Space;

            // Flow control
            if (handShake == "RTS")
                this.Connection.Handshake = Handshake.RequestToSend;
            else
                this.Connection.Handshake = Handshake.None; 
        }

        public String ConfigFile
        {
            set { this.Settings.ConfigFile = value; }
        }

        /// <summary>
        /// Incoming packets reading thread
        /// </summary>
        private void RxThread()
        {
            EmbeddedPacket.ReadResult Result;
            while (true)
            {
                try
                {
                    // Try to receive packet
                    Result = LatestPacket.ReadFromStream(Connection.BaseStream, RxCancelEvent, RxSyncObject);
                }
                catch (Exception)
                {
                    // Log error and exit RxThread on error
                    //Logger.Log(Logger.MessageType.Error, e);
                    Connection.Close();
                    MyRxThread = null;
                    break;
                }

                // if new Packet was received remember time of arrival and call Event
                if (Result == EmbeddedPacket.ReadResult.PacketReceived)
                {
                    lock (RxSyncObject)
                    {
                        LastPacketRcvd = DateTime.Now.Ticks;
                    }
                    if (NewPacket != null)
                    {
                        NewPacket(LatestPacket);
                    }
                }
                else if (Result == EmbeddedPacket.ReadResult.CksError)
                {
                    DroppedPacketCounter++;
                    LatestPacket.DumpPacket(DumpFile);
                    DebugConsole.Write("!!Packet dropped due to wrong CKS!!\n");
                }
                // Rx was Stopped, exit thread
                else
                {
                    break;
                }
            }
        }

        public void ClearStreamBuffer()
        {
            this.Connection.BaseStream.Flush();
        }

        public Boolean Connect() 
        {
            try
            { 
                // Connect
                Connection.Open();
                Connection.WriteTimeout = 1000;

                // Reset Cancel event
                RxCancelEvent.Reset();

                // Reset Packet Counter
                LatestPacket.MsgNumber = 0;
                SendPacket.MsgNumber = 0;

                // Raise this application's priority
                using (Process p = Process.GetCurrentProcess())
                    p.PriorityClass = ProcessPriorityClass.High;

                // Start Rx Thread
                MyRxThread = new Thread(this.RxThread);
                MyRxThread.Name = "iSTP reading thread";
                MyRxThread.Priority = ThreadPriority.AboveNormal;
                MyRxThread.Start();

                return true;
            }
            catch (Exception ex)
            {
                DebugConsole.WriteLine(ex);
                return false;
            }
            
        }

        public Boolean Disconnect() 
        {
            bool retVal = false;
            if (MyRxThread != null)
            {
                RxCancelEvent.Set();
                if (Thread.CurrentThread.ManagedThreadId != MyRxThread.ManagedThreadId)
                {
                    MyRxThread.Join(1000);
                }
                MyRxThread = null;
            }

            try
            {
                Connection.Close();
                retVal = true;
            }
            catch (Exception ex)
            {
                DebugConsole.WriteLine(ex);
            }
            
            // Reset this application's priority
            using (Process p = Process.GetCurrentProcess())
                p.PriorityClass = ProcessPriorityClass.Normal;
            
            return retVal;
        }

        public void Initialize(string configDumpFileName)
        {
            this.GlobalEmbConfFileName = configDumpFileName;
            this.NewPacket -= MyPacketHandler; // to prevent double execution of the packet handler in subsequent test runs
            this.NewPacket += MyPacketHandler;            
            if (EmbConfiguration != null)
                EmbConfiguration = null;
            EmbConfiguration = new ISTPConfiguration(configDumpFileName);
            EmbHealth = new EmbeddedHealthData();
            EmbeddedPacket RequestConfig = new EmbeddedPacket(0, TYPE_CONFIGURE, ID_CONF_REQ, null);
            LastConfComPacketTime = DateTime.Now;
            this.Connection.BaseStream.Flush();
            this.Transmit(RequestConfig);
            SampleRateTooHigh = false;
        }

        /// <summary>
        /// New Packet Handler function
        /// </summary>
        /// <param name="newPacket">New packet object</param>
        private void PacketHandler(EmbeddedPacket newPacket)
        {
            lock (SyncObj)
            {
                switch (newPacket.MsgType)
                {
                    case TYPE_FORCE_STOP:
                        // not defined/implemented
                        break;
                    case TYPE_ERROR:
                        if (newPacket.MsgID == ID_ERR_CANT_SEND)
                            LostPacketCounter += newPacket.MsgNumber;
                        if (newPacket.MsgID == ID_ERR_SR_TOO_HIGH)
                            SampleRateTooHigh = true;
                        break;
                    case TYPE_CONFIGURE:
                        DebugConsole.WriteLine("{0} CONFIGURATION", newPacket.MsgNumber);
                        EmbConfiguration.DigestConfPacket(newPacket);
                        LastConfComPacketTime = DateTime.Now;
                        break;
                    case TYPE_VERSION:
                        DebugConsole.WriteLine("{0} VERSION", newPacket.MsgNumber);
                        EmbConfiguration.DigestVersionPacket(newPacket);
                        break;
                    case TYPE_INITIALIZATION:
                        // not defined/implemented
                        break;
                    case TYPE_DATA:
                        lock (LoggingEnabled)
                        {
							if (LogData != null && LoggingEnabled.Enabled)
	                        {
	                            CurrentEmbeddedTime = newPacket.MsgTimeStamp;
	                            LastDataPacketTime = DateTime.Now;
	                            if ((newPacket.MsgNumber - LastPacketNumber) > 1)
	                            {
	                                LostPacketCounter += newPacket.MsgNumber - LastPacketNumber - 1; //Increment by the number of packets we've lost
                                    DebugConsole.WriteLine(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.f") + ": {0} packets lost starting with packet # {1}", newPacket.MsgNumber - LastPacketNumber - 1, LastPacketNumber + 1);
	                            }
	                            ReceivedPacketCounter++;
                        
	                            LastPacketNumber = newPacket.MsgNumber;                        
	                            LogData(newPacket);
	                        }
						}
                        break;

                    case TYPE_HEALTH_DATA:
                        EmbHealth.UpdateData(newPacket);
                        break;

                    case TYPE_COMMENT:
                        DebugConsole.WriteLine("{0} COMMENT", newPacket.MsgNumber);
                        EmbConfiguration.DigestCommentPacket(newPacket);
                        LastConfComPacketTime = DateTime.Now;
                        break;
                    case TYPE_CONTROL:
                        if (newPacket.MsgID == ID_CTRL_CONF_END)
                            DebugConsole.WriteLine("{0} CTRL_CONF_END", newPacket.MsgNumber);
                        if (newPacket.MsgID == ID_CTRL_CONF_END)
                            EmbConfiguration.Finish(newPacket);
                        break;
                    case TYPE_ACK:
                        MyAck.FindACK(newPacket, ref this.WarningCounter);
                        break;
                }
            } // end-of-lock SyncObj
        }

        public void ClearMemory()
        {
            this.MyAck.ClearMemory();
            this.WarningCounter = 0;
        }

        private bool Transmit(EmbeddedPacket txPacket, bool SaveForACK = true)
        {
            bool retVal = false;
            if (this.Connection.IsOpen)
            {
                retVal = txPacket.WriteToStream(this.Connection.BaseStream);
            }

            if(SaveForACK)
                MyAck.SaveMsgForAck(txPacket);

            return retVal;
        }

        public ISTPConfiguration EmbeddedConfiguration { get { return EmbConfiguration; } }

        public EmbeddedHealthData EmbeddedHealth { get { return EmbHealth; } }

        public void ResetRxStats()
        {
            ReceivedPacketCounter = 0;
            DroppedPacketCounter = 0;
            LostPacketCounter = 0;
        }

        public bool Initialization(uint id)
        {
            return (Transmit(new EmbeddedPacket(0, TYPE_INITIALIZATION, id, null)));
        }

        public bool StopLogging()
        {
            return (Transmit(new EmbeddedPacket(0, TYPE_CONTROL, ID_CTRL_STOP, null)));
        }

        public bool StartLogging()
        {
            LastPacketNumber = 0;
            LastDataPacketTime = DateTime.Now;
            CurrentEmbeddedTime = 0.0f;
            return( Transmit(new EmbeddedPacket(0, TYPE_CONTROL, ID_CTRL_START, null)));
        }

        public void EnableDataProcessing(bool enable)
        {
            lock (LoggingEnabled)
            {
                LoggingEnabled.Enabled = enable;
            }
        }

        public bool SetFIFOmode(bool isEnabled)
        {
            uint id = isEnabled ? (0x1 << 16) | ID_CONF_FIFO_MODE : ID_CONF_FIFO_MODE;
            return Transmit(new EmbeddedPacket(0, TYPE_CONFIGURE, id, null));
        }

        public void RequestSampleRate(UInt16 rate)
        {
            this.Settings.SampleRate = rate;
            UInt32 ID = ID_CONF_SAMPLE_RATE | (((uint)rate) << 16);
            Transmit(new EmbeddedPacket(0, TYPE_CONFIGURE, ID, null));
        }

        public void RequestFifoReadRate(UInt16 rate)
        {
            UInt32 ID = ID_CONF_FIFO_READ_RATE | (((uint)rate) << 16);
            Transmit(new EmbeddedPacket(0, TYPE_CONFIGURE, ID, null));
        }

        public void EnableHealthDataTransmission(bool enable)
        {
            uint id = (enable ? 0x01u : 0x00u) << 8 |
                      ID_CONF_ENB_HEALTH;

            Transmit(new EmbeddedPacket(0, TYPE_CONFIGURE, id, null));
        }

        public void ConfigureBoardParameter(int axis, int board, int paramIndex, int optionIndex)
        {
            uint id = (((uint) axis & 0x0f)        << 28) |
                      (((uint) board & 0x0f)       << 24) |
                      (((uint) paramIndex & 0xff)  << 16) |
                      (((uint) optionIndex & 0xff) <<  8) |
                      ( ID_CONF_BOARD_PARAM);
            Transmit(new EmbeddedPacket(0, TYPE_CONFIGURE, id, null));
            EmbConfiguration.ChangeSelectedParama(axis, board, paramIndex, optionIndex);
        }

        public uint LostPackets
        { get { return (LostPacketCounter); } }

        public uint DroppedPackets
        { get { return (DroppedPacketCounter); } }

        public uint ReceivedPackets
        { get { return (ReceivedPacketCounter); } }

        public bool SampleRateOK
        { get { return (!SampleRateTooHigh); } }

        public void Dispose()
        {
            DumpFile.Close();
            if (Connection.IsOpen)
                Disconnect();            
            if (Connection != null)
                Connection.Dispose();
        }

        public DateTime LastDataPacketTime { get; set; }

        public DateTime LastConfComPacketTime { get; set; }

        public double EmbeddedTime { get { return CurrentEmbeddedTime; } }

        internal void SetSimSensorCount(int count)
        {
            uint id = (((uint)count & 0xff) << 16) |
                      (0x10);
            Transmit(new EmbeddedPacket(0, TYPE_CONFIGURE, id, null));
        }

        public void ResendMSG_ACK()
        {
            EmbeddedPacket[] EP = MyAck.GetACKmemory;
            if (EP.Length > 0)
            {
                foreach (EmbeddedPacket ep in EP)
                {
                    Transmit(ep, false);
                }
            }
        }

        public bool CheckTime_ACK()
        {
            bool _res = false;

            if ((DateTime.UtcNow - MyAck.GetLastMSGtime) > MyAck.GetWaitingTime)
                _res = true;

            return _res;
        }
    }
}
