using System;
using System.Threading;
using CommandMessenger;

namespace CommandMessengerTests
{
    public class TransferSpeed 
    {        
        private CmdMessenger _cmdMessenger;
        readonly Enumerator _command;      
        public bool RunLoop { get; set; }
        private int _receivedItemsCount;                        // Counter of number of command items received
        private int _receivedBytesCount;                        // Counter of number of command bytes received
        long _beginTime;                                        // Start time, 1st item of sequence received 
        long _endTime;                                          // End time, last item of sequence received 
        private volatile bool _receiveSeriesFinished;           // Indicates if plain text float series has been fully received
        private volatile bool _sendSeriesFinished;
        private readonly systemSettings _systemSettings;
        const int SeriesLength = 10000;                         // Number of items we like to receive from the Arduino
        private const float SeriesBase = 1.00001F;              // Base of values to return: SeriesBase * (0..SeriesLength-1)
        private float _minimalBps;
        public TransferSpeed(systemSettings systemSettings, Enumerator command)
        {
            _systemSettings = systemSettings;
            _command = command;
            DefineCommands();
        }

        // ------------------ Command Callbacks -------------------------
        private void DefineCommands()
        {
            _command.Add(new[]
               {
                "RequestReset"                   ,   // Command Request reset
                "RequestResetAcknowledge"        ,   // Command to acknowledge reset
        
                "RequestSeries"    ,   // Command Request to send series in plain text
                "ReceiveSeries"    ,   // Command to send an item in plain text
                "DoneReceiveSeries",   // Command to tell that sending series in plain text is done
      
                "PrepareSendSeries",   // Command to tell other side to prepare for receiving a series of text float commands
                "SendSeries"       ,   // Command to send a series of text float commands
                "AckSendSeries"        // Command to acknowledge the send series of text float commands

             });
        }

        // ------------------ Command Callbacks -------------------------
        private void AttachCommandCallBacks()
        {
            _cmdMessenger.Attach(_command["RequestResetAcknowledge"], OnRequestResetAcknowledge);
            _cmdMessenger.Attach(_command["ReceiveSeries"], OnReceiveSeries);
            _cmdMessenger.Attach(_command["DoneReceiveSeries"], OnDoneReceiveSeries);
            _cmdMessenger.Attach(_command["AckSendSeries"], OnAckSendSeries);
        }


        // ------------------ Test functions -------------------------

        public void RunTests()
        {
            // Wait a bit before starting the test
            Thread.Sleep(1000);

            // Open Connection
            Common.StartTestSet("Benchmarking transfer speeds");
            SetUpConnection();

            // Stop logging commands as this may degrade performance
            Common.LogCommands(false);

            // Test acknowledgments
            //*** Benchmark 1: receive series of float data
            SetupReceiveSeries();

            //*** Benchmark 2: queued send series of float data
            SetupQueuedSendSeries();

            //*** Benchmark 3: direct send series of float data
            DirectSendSeries();

            // Start logging commands again
            Common.LogCommands(true);

            // Close connection
            CloseConnection();
            Common.EndTestSet();
        }

        public void SetUpConnection()
        {
            try
            {
                _cmdMessenger = Common.Connect(_systemSettings);
                AttachCommandCallBacks();
                if (!Common.Connected)
                {
                    Common.TestNotOk("Not connected after opening connection");
                }               
            }
            catch (Exception)
            {
            }
        }

        public void CloseConnection()
        {
            try
            {
                Common.Disconnect();
            }
            catch (Exception)
            {
            }
        }

        private void WaitAndClear()
        {
            var requestResetCommand = new SendCommand(_command["RequestReset"], _command["RequestResetAcknowledge"], 1000);
            var requestResetAcknowledge = _cmdMessenger.SendCommand(requestResetCommand, SendQueue.ClearQueue,ReceiveQueue.ClearQueue);

            Common.WriteLine(!requestResetAcknowledge.Ok ? "No Wait OK received" : "Wait received");
            // Wait another second to see if
            Thread.Sleep(1000);
            // Clear queues again to be very sure
            _cmdMessenger.ClearReceiveQueue();
            _cmdMessenger.ClearSendQueue();
        }


        // Called when a RequestResetAcknowledge comes in   
        private void OnRequestResetAcknowledge(ReceivedCommand receivedcommand)
        {
            // This function should not be called because OnRequestResetAcknowledge should
            // be handled by the requestResetAcknowledge synchronous command in WaitAndClear()
            // if it happens, we will do another WaitAndClear() and hope that works 
            WaitAndClear();
        }

        // *** Benchmark 1 ***
        private void SetupReceiveSeries()
        {
            Common.StartTest("Calculating speed in receiving series of float data");

            WaitAndClear();

            _receiveSeriesFinished = false;
            _receivedItemsCount = 0;
            _receivedBytesCount = 0;
            _minimalBps = _systemSettings.MinReceiveSpeed;

            // Send command requesting a series of 100 float values send in plain text form           
            var commandPlainText = new SendCommand(_command["RequestSeries"]);
            commandPlainText.AddArgument(SeriesLength);
            commandPlainText.AddArgument(SeriesBase);
            
            // Send command 
            _cmdMessenger.SendCommand(commandPlainText);

            // Now wait until all values have arrived
            while (!_receiveSeriesFinished)
            {
                Thread.Sleep(100);
            }
        }

        // Callback function To receive the plain text float series from the Arduino
        void OnReceiveSeries(ReceivedCommand arguments)
        {

            if (_receivedItemsCount % (SeriesLength / 10) == 0)
                Common.WriteLine(_receivedItemsCount+ " Received value: " + arguments.ReadFloatArg());
            if (_receivedItemsCount == 0)
            {
                // Received first value, start stopwatch
                _beginTime = Millis;
            }

            _receivedItemsCount++;
            _receivedBytesCount += CountBytesInCommand(arguments, false);

        }

        private void OnDoneReceiveSeries(ReceivedCommand receivedcommand)
        {
            float bps = CalcTransferSpeed();

            if (bps > _minimalBps)
            {
                Common.TestOk("Embedded system is receiving data as fast as expected. Measured: " + bps + " bps, expected " + _minimalBps);
            }
            else
            {
                Common.TestNotOk("Embedded system is receiving data not as fast as expected. Measured: " + bps + " bps, expected " + _minimalBps);
            }


            _receiveSeriesFinished = true;

            Common.EndTest();
        }

        private float CalcTransferSpeed()
        {
            Common.WriteLine("Benchmark results");
            // Received all values, stop stopwatch
            _endTime = Millis;
            var deltaTime = (_endTime - _beginTime);
            Common.WriteLine(
                deltaTime +
                " milliseconds per " +
                _receivedItemsCount +
                " items = is " +
                (float)deltaTime / (float)_receivedItemsCount +
                " ms/item, " +
                (float)1000 * _receivedItemsCount / (float)deltaTime +
                " Hz"
            );

            float bps = (float)8 * 1000 * _receivedBytesCount / (float)deltaTime;
            Common.WriteLine(
                deltaTime +
                " milliseconds per " +
                _receivedItemsCount +
                " bytes = is " +
                (float)deltaTime / (float)_receivedBytesCount +
                " ms/byte, " +
                (float)1000 * _receivedBytesCount / (float)deltaTime +
                " bytes/sec, " +
                bps +
                " bps"
            );
            return bps;
        }


        // *** Benchmark 2 ***
        // Setup queued send series
        private void SetupQueuedSendSeries()
        {
            Common.StartTest("Calculating speed in sending queued series of float data");
            WaitAndClear();

            _minimalBps = _systemSettings.MinSendSpeed;
            _sendSeriesFinished = false;
            var prepareSendSeries = new SendCommand(_command["PrepareSendSeries"]);
            prepareSendSeries.AddArgument(SeriesLength);
            _cmdMessenger.SendCommand(prepareSendSeries, SendQueue.WaitForEmptyQueue, ReceiveQueue.WaitForEmptyQueue);

            // Prepare
            _receivedBytesCount = 0;
            _beginTime = Millis;

            // Now queue all commands
            for (var sendItemsCount = 0; sendItemsCount < SeriesLength; sendItemsCount++)
            {
                var sendSeries = new SendCommand(_command["SendSeries"]);
                sendSeries.AddArgument(sendItemsCount * SeriesBase);

                _cmdMessenger.QueueCommand(sendSeries);
                _receivedBytesCount += CountBytesInCommand(sendSeries, _cmdMessenger.PrintLfCr);

                if (sendItemsCount % (SeriesLength / 10) == 0)
                    Common.WriteLine("Send value: " + sendItemsCount * SeriesBase);
            }

            // Now wait until receiving party acknowledges that values have arrived
            while (!_sendSeriesFinished)
            {
                Thread.Sleep(100);
            }
        }

        private void OnAckSendSeries(ReceivedCommand receivedcommand)
        {
            float bps = CalcTransferSpeed();

            if (bps > _minimalBps)
            {
                Common.TestOk("Embedded system is receiving data as fast as expected. Measured: " + bps + " bps, expected " + _minimalBps);
            }
            else
            {
                Common.TestNotOk("Embedded system is receiving data not as fast as expected. Measured: " + bps + " bps, expected " + _minimalBps);
            }


            Common.EndTest();
            _sendSeriesFinished = true;
        }

        // *** Benchmark 3 ***
        private void DirectSendSeries()
        {
            Common.StartTest("Calculating speed in individually sending a series of float data");
            WaitAndClear();

            _minimalBps = _systemSettings.MinDirectSendSpeed; 
            _sendSeriesFinished = false;

            var prepareSendSeries = new SendCommand(_command["PrepareSendSeries"]);
            prepareSendSeries.AddArgument(SeriesLength);
            // We need to to send the prepareSendSeries by bypassing the queue or it might be sent after the directly send commands later on
            _cmdMessenger.SendCommand(prepareSendSeries, SendQueue.WaitForEmptyQueue, ReceiveQueue.WaitForEmptyQueue,UseQueue.BypassQueue);

            // Prepare
            _receivedBytesCount = 0;
            _beginTime = Millis;
            
            // Now send all commands individually and bypass the queue
             for (var sendItemsCount = 0; sendItemsCount < SeriesLength; sendItemsCount++)
            {
                var sendSeries = new SendCommand(_command["SendSeries"]);
                sendSeries.AddArgument(sendItemsCount * SeriesBase);

                _cmdMessenger.SendCommand(sendSeries, SendQueue.Default, ReceiveQueue.Default, UseQueue.BypassQueue);
                _receivedBytesCount += CountBytesInCommand(sendSeries, _cmdMessenger.PrintLfCr);
     
                if (sendItemsCount%(SeriesLength/10) == 0)
                {
                    Common.WriteLine("Send value: " + sendItemsCount*SeriesBase);
                }
            }
            _endTime = Millis;
            // Now wait until receiving party acknowledges that values have arrived
            while (!_sendSeriesFinished)
            {
                Thread.Sleep(100);
            }
        }

        // Tools
        private static int CountBytesInCommand(Command command, bool printLfCr)
        {
            var bytes = command.CommandString().Length; // Command + command separator
            if (printLfCr) bytes += 2; // Add  bytes for carriage return ('\r') and /or a newline  ('\n')
            return bytes;
        }

        // Return Milliseconds since 1970
        public static long Millis { get { return (long)((DateTime.Now.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds); } }
    }
}
