//                           UNCLASSIFIED
/*-------------------------------------------------------------------------
RAYTHEON PROPRIETARY: THIS DOCUMENT CONTAINS DATA OR INFORMATION
PROPRIETARY TO RAYTHEON COMPANY AND IS RESTRICTED TO USE ONLY BY PERSONS
AUTHORIZED BY RAYTHEON COMPANY IN WRITING TO USE IT. DISCLOSURE TO
UNAUTHORIZED PERSONS WOULD LIKELY CAUSE SUBSTANTIAL COMPETITIVE HARM TO
RAYTHEON COMPANY'S BUSINESS POSITION. NEITHER SAID DOCUMENT NOR ITS
CONTENTS SHALL BE FURNISHED OR DISCLOSED TO OR COPIED OR USED BY PERSONS
OUTSIDE RAYTHEON COMPANY WITHOUT THE EXPRESS WRITTEN APPROVAL OF RAYTHEON
COMPANY.

THIS PROPRIETARY NOTICE IS NOT APPLICABLE IF DELIVERED TO THE U.S.
GOVERNMENT.

UNPUBLISHED WORK - COPYRIGHT RAYTHEON COMPANY.
-------------------------------------------------------------------------*/

using System;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using FpgaMeasurementInstrumentsLib;
using NLog;
using Raytheon.Common;

namespace Raytheon.Instruments
{
   /// <summary>
   /// This class serves as an interface to FPGAs that implement a UDP interface.
   /// It allows for the ability to read and write to registers.
   /// </summary>
   public class CommFpgaEthernet : IFpgaComm, IDisposable
   {
      #region PrivateClassMembers
      private UdpClient _udpClient;
      private readonly int _localPort;
      private readonly int _remotePort;
      private readonly string _remoteAddress;
      private static object _syncObj = new Object();
      private SelfTestResult _selfTestResult;
      private State _state;
      private string _name;

      private readonly ILogger _logger;

      private readonly IConfigurationManager _configurationManager;
      private readonly IConfiguration _configuration;

      #endregion

      #region PrivateFuctions

      /// <summary>
      /// The Finalizer
      /// </summary>
      ~CommFpgaEthernet()
      {
         Dispose(false);
      }

      /// <summary>
      /// Dispose of this object
      /// </summary>
      /// <param name="disposing"></param>
      protected virtual void Dispose(bool disposing)
      {
         if (disposing)
         {
            if (_state == State.Ready)
            {
               _udpClient.Close();

               _state = State.Uninitialized;
            }
         }
      }

      /// <summary>
      /// Read data off of the socket
      /// </summary>
      /// <param name="dataRead">The data that was read</param>
      /// <returns>The number of bytes read</returns>
      private int ReadData(ref byte[] dataRead)
      {
         IPEndPoint remoteIPEndPoint = new IPEndPoint(IPAddress.Any, _localPort);

         dataRead = _udpClient.Receive(ref remoteIPEndPoint);

         int numBytesRead = dataRead.Length;

         return numBytesRead;
      }

      /// <summary>
      /// Send a command message and get a response message
      /// </summary>
      /// <param name="commandMsg">The message to send</param>
      /// <param name="rspMsg">The response</param>
      private void SendCommandGetResponse(FPGACmdMessage commandMsg, ref FPGARspMessage rspMsg)
      {
         uint rxBufferSize = rspMsg.GetEntireMsgLength();
         uint rxNumBytesExpected = rxBufferSize;

         uint dataToSendNumBytes = commandMsg.GetEntireMsgLength();

         // send the command
         byte[] dataToSend = new byte[dataToSendNumBytes];

         // get a pointer to the data
         GCHandle sendPinnedArray = GCHandle.Alloc(dataToSend, GCHandleType.Pinned);
         IntPtr pByte = sendPinnedArray.AddrOfPinnedObject();
         commandMsg.Format(pByte);
         sendPinnedArray.Free();

         // send the data
         int numBytesSent = SendData(dataToSend);

         if (dataToSendNumBytes != numBytesSent)
         {
            throw new Exception("wanted to send: " + dataToSendNumBytes.ToString() + " bytes,  SendData() reported that it sent: " + numBytesSent.ToString());
         }

         // read the response
         byte[] rspBuffer = new byte[rxBufferSize];
         int numBytesRead = ReadData(ref rspBuffer);

         if (numBytesRead != rxNumBytesExpected)
         {
            throw new Exception("received " + numBytesRead.ToString() + " bytes, expected " + rxNumBytesExpected.ToString());
         }

         // populate the rspMsg object
         GCHandle rxPinnedArray = GCHandle.Alloc(rspBuffer, GCHandleType.Pinned);
         IntPtr pBytePtr = rxPinnedArray.AddrOfPinnedObject();
         rspMsg.Parse(pBytePtr);
         rxPinnedArray.Free();
      }

      /// <summary>
      /// Send data out of the socket
      /// </summary>
      /// <param name="dataToSend">The data to send</param>
      /// <returns>The number of bytes sent</returns>
      private int SendData(byte[] dataToSend)
      {
         IPAddress addy = IPAddress.Parse(_remoteAddress);

         IPEndPoint ipEndPoint = new IPEndPoint(addy, _remotePort);

         int numBytesSent = _udpClient.Send(dataToSend, dataToSend.Length, ipEndPoint);

         return numBytesSent;
      }
      #endregion

      #region PublicFuctions

      /// <summary>
      /// CommFpgaEthernet factory constructor
      /// </summary>
      /// <param name="deviceName"></param>
      /// <param name="configurationManager"></param>
      public CommFpgaEthernet(string deviceName, IConfigurationManager configurationManager)
      {
         Name = deviceName;

         _logger = LogManager.GetLogger($"{this.GetType().Name} - {deviceName}");

         _configurationManager = configurationManager;
         _configuration = _configurationManager.GetConfiguration(Name);

         _selfTestResult = SelfTestResult.Unknown;
         _state = State.Uninitialized;

         _localPort = _configuration.GetConfigurationValue("CommFpgaEthernet", "LocalPort", 0);
         _remotePort = _configuration.GetConfigurationValue("CommFpgaEthernet", "RemotePort", 0);
         _remoteAddress = _configuration.GetConfigurationValue("CommFpgaEthernet", "RemoteAddress", "127.0.0.1");
      }


      /// <summary>
      /// 
      /// </summary>
      /// <param name="fpgaName"></param>
      /// <param name="localPort">The port on the local computer</param>
      /// <param name="remotePort">The port that the FPGA is using</param>
      /// <param name="remoteAddress">The address that the FPGA is using</param>
      public CommFpgaEthernet(string deviceName, int localPort, int remotePort, string remoteAddress)
      {
         _name = deviceName;
         _logger = LogManager.GetLogger($"{this.GetType().Name} - {deviceName}");
         _selfTestResult = SelfTestResult.Unknown;
         _state = State.Uninitialized;

         _localPort = localPort;
         _remotePort = remotePort;
         _remoteAddress = remoteAddress;
      }

      /// <summary>
      /// 
      /// </summary>
      /// <returns></returns>
      public bool ClearErrors()
      {
         return false;
      }

      /// <summary>
      /// 
      /// </summary>
      public string DetailedStatus
      {
         get
         {
            return "This is a FPGA Ethernet called " + _name;
         }
      }

      /// <summary>
      /// 
      /// </summary>
      public bool DisplayEnabled
      {
         get
         {
            return false;
         }
         set
         {
            throw new NotImplementedException();
         }
      }

      /// <summary>
      /// Dispose of this object
      /// </summary>
      public void Dispose()
      {
         Dispose(true);

         GC.SuppressFinalize(this);
      }

      /// <summary>
      /// 
      /// </summary>
      public bool FrontPanelEnabled
      {
         get
         {
            return false;
         }
         set
         {
            throw new NotImplementedException();
         }
      }

      /// <summary>
      /// 
      /// </summary>
      public InstrumentMetadata Info
      {
         get
         {
            throw new NotImplementedException();
         }
      }

      /// <summary>
      /// 
      /// </summary>
      public void Initialize()
      {
         Initialize(string.Empty);
      }

      /// <summary>
      /// 
      /// </summary>
      /// <param name="fpga"></param>
      public void Initialize(string fpga)
      {
         //5 second timeout
         const int TIMEOUT = 5000;

         lock (_syncObj)
         {
            if (_state == State.Uninitialized)
            {
               _udpClient = new UdpClient(_localPort);

               _udpClient.Client.ReceiveBufferSize = int.MaxValue;

               _udpClient.Client.SendBufferSize = int.MaxValue;

               _udpClient.Client.SendTimeout = TIMEOUT;

               _udpClient.Client.ReceiveTimeout = TIMEOUT;

               _state = State.Ready;
            }
            else
            {
               throw new Exception("expected the state to be Uninitialized, state was: " + _state.ToString() + " on card " + _name);
            }
         }
      }

      /// <summary>
      /// 
      /// </summary>
      public string Name
      {
         get
         {
            return _name;
         }
         set
         {
            _name = value;
         }
      }

      /// <summary>
      /// 
      /// </summary>
      /// <returns></returns>
      public SelfTestResult PerformSelfTest()
      {
         _selfTestResult = SelfTestResult.Unknown;
         return _selfTestResult;
      }

      /// <summary>
      /// Reads a single register
      /// </summary>
      /// <param name="page">The page of the register to read from</param>
      /// <param name="address">The register address to read from</param>
      /// <returns>The data at the address</returns>
      public uint Read(string fpgaName, uint address)
      {
         // lock up the FPGA resource
         lock (_syncObj)
         {
            FPGACmdMessage cmd = new FPGAReadRegisterCmdMessage((FPGACmdMessage.Page)0, address);

            // this get populated in SendCommandGetResponse()
            FPGARspMessage rsp = new FPGAReadRegisterRspMessage(0, 0);

            SendCommandGetResponse(cmd, ref rsp);

            return rsp.GetData();
         }
      }

      /// <summary>
      /// 
      /// </summary>
      /// <param name="fpgaName"></param>
      /// <param name="address"></param>
      /// <param name="numberOfWordsToRead"></param>
      /// <param name="dataRead"></param>
      /// <param name="shallWeIncrementAddress"></param>
      public void ReadBlock(string fpgaName, uint address, uint numberOfWordsToRead, bool shallWeIncrementAddress, ref uint[] dataRead)
      {
         // lock up the FPGA resource
         lock (_syncObj)
         {
            if (shallWeIncrementAddress)
            {
               FPGACmdMessage cmd = new FPGAReadRegisterCmdBlockIncrMessage((FPGACmdMessage.Page)0, address, numberOfWordsToRead, false);

               // this get populated in SendCommandGetResponse()
               FPGARspMessage rsp = new FPGAReadRegisterBlockIncrRspMessage(0, ref dataRead);

               SendCommandGetResponse(cmd, ref rsp);
            }
            else
            {
               throw new Exception("Not Implemented");
            }
         }
      }

      /// <summary>
      /// 
      /// </summary>
      public void Reset()
      {
         Shutdown();

         Initialize();
      }

      /// <summary>
      /// 
      /// </summary>
      public SelfTestResult SelfTestResult
      {
         get
         {
            return _selfTestResult;
         }
      }

      /// <summary>
      /// 
      /// </summary>
      public void Shutdown()
      {
         // lock up the FPGA resource
         lock (_syncObj)
         {
            if (_state == State.Ready)
            {
               _udpClient.Close();

               _state = State.Uninitialized;
            }
         }
      }

      /// <summary>
      /// 
      /// </summary>
      public State Status
      {
         get
         {
            return _state;
         }
      }

      /// <summary>
      /// Write to a single register
      /// </summary>
      /// <param name="page">The page of the register to read from</param>
      /// <param name="address">The address to write to</param>
      /// <param name="value">The data to write</param>
      public void Write(string fpgaName, uint address, uint value)
      {
         // lock up the FPGA resource
         lock (_syncObj)
         {
            FPGACmdMessage cmd = new FPGAWriteRegisterCmdMessage((FPGACmdMessage.Page)0, address, value);

            // this get populated in SendCommandGetResponse()
            FPGARspMessage rsp = new FPGAWriteRegisterRspMessage(0, 0);

            SendCommandGetResponse(cmd, ref rsp);

            if (rsp.GetAddress() != address)
            {
               throw new Exception("Command write on address: " + address.ToString("X8") + ", received address: " + rsp.GetAddress().ToString("X8"));
            }
            else if (rsp.GetData() != 0xace0beef)
            {
               throw new Exception("Command write on address: " + address.ToString("X8") + " returned value: " + rsp.GetData().ToString("X8"));
            }
         }
      }

      /// <summary>
      /// 
      /// </summary>
      /// <param name="fpgaName"></param>
      /// <param name="address"></param>
      /// <param name="numberOfBytes"></param>
      /// <param name="data"></param>
      public void WriteBlock(string fpgaName, uint address, uint numberOfWordsToWrite, uint[] data, bool shallWeIncrementAddress)
      {
         // lock up the FPGA resource
         lock (_syncObj)
         {
            FPGACmdMessage cmd = null;

            if (shallWeIncrementAddress)
            {
               cmd = new FPGAWriteRegisterCmdBlockIncrMessage((FPGACmdMessage.Page)0, address, data, false);
            }
            else
            {
               cmd = new FPGAWriteRegisterCmdBlockMessage((FPGACmdMessage.Page)0, address, data, false);
            }

            // this get populated in SendCommandGetResponse()
            FPGARspMessage rsp = new FPGAWriteRegisterRspMessage(0, 0);

            SendCommandGetResponse(cmd, ref rsp);

            if (rsp.GetAddress() != address)
            {
               throw new Exception("Command write on address: " + address.ToString("X8") + ", received address: " + rsp.GetAddress().ToString("X8"));
            }
            else if (rsp.GetData() != 0xace0beef)
            {
               throw new Exception("Command write on address: " + address.ToString("X8") + " returned value: " + rsp.GetData().ToString("X8"));
            }
         }
      }

      /// <summary>
      /// 
      /// </summary>
      /// <param name="page"></param>
      /// <param name="address"></param>
      /// <param name="data"></param>
      /// <param name="incrementAddress"></param>
      /// <param name="byteSwap"></param>
      public void WriteRegister(uint page, uint address, uint[] data, bool incrementAddress = true, bool byteSwap = true)
      {
         // lock up the FPGA resource
         lock (_syncObj)
         {
            if (page > (uint)FPGACmdMessage.Page.PAGE3)
            {
               throw new Exception("input parameter 'page' is out of range: " + page.ToString());
            }

            FPGACmdMessage cmd;

            if (incrementAddress)
            {
               cmd = new FPGAWriteRegisterCmdBlockIncrMessage((FPGACmdMessage.Page)page, address, data, byteSwap);
            }
            else
            {
               cmd = new FPGAWriteRegisterCmdBlockMessage((FPGACmdMessage.Page)page, address, data, byteSwap);
            }


            // this get populated in SendCommandGetResponse()
            FPGARspMessage rsp = new FPGAWriteRegisterRspMessage(0, 0);

            SendCommandGetResponse(cmd, ref rsp);

            if (rsp.GetAddress() != address)
            {
               throw new Exception("Command write on address: " + address.ToString("X8") + ", received address: " + rsp.GetAddress().ToString("X8"));
            }
            else if (rsp.GetData() != 0xace0beef)
            {
               throw new Exception("Command write on address: " + address.ToString("X8") + " returned value: " + rsp.GetData().ToString("X8"));
            }
         }
      }

      /// <summary>
      /// Loads firmware
      /// </summary>
      /// <param name="fpgaName"></param>
      public void LoadFirmware(string fpgaName)
      {
         Initialize(fpgaName);
      }
      #endregion
   }
}
