// **********************************************************************************************************
//    CommDeviceUdpAsync.cs
//    3/6/2024
//    NGI - Next Generation Interceptor
//    
//    Contract No. HQ0856-21-C-0003/1022000209
//    
//    THIS DOCUMENT DOES NOT CONTAIN TECHNOLOGY OR TECHNICAL DATA CONTROLLED UNDER EITHER THE U.S. 
//    INTERNATIONAL TRAFFIC IN ARMS REGULATIONS OR THE U.S. EXPORT ADMINISTRATION REGULATIONS.
//    
//    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.
//    
//    UNPUBLISHED WORK - COPYRIGHT RAYTHEON COMPANY.
//    
//    DESTRUCTION NOTICE: FOR CLASSIFIED DOCUMENTS FOLLOW THE PROCEDURES IN DOD 5220.22-M, 
//    NATIONAL INDUSTRIAL SECURITY PROGRAM OPERATING MANUAL, FEBRUARY 2006, 
//    INCORPORATING CHANGE 1, MARCH 28, 2013, CHAPTER 5, SECTION 7, OR DODM 5200.01-VOLUME 3, 
//    DOD INFORMATION SECURITY PROGRAM: PROTECTION OF CLASSIFIED INFORMATION, ENCLOSURE 3, 
//    SECTION 17. FOR CONTROLLED UNCLASSIFIED INFORMATION FOLLOW THE PROCEDURES IN DODM 5200.01-VOLUME 4, 
//    INFORMATION SECURITY PROGRAM: CONTROLLED UNCLASSIFIED INFORMATION.
//    
//    CONTROLLED BY: MISSILE DEFENSE AGENCY
//    CONTROLLED BY: GROUND-BASED MIDCOURSE DEFENSE PROGRAM OFFICE
//    CUI CATEGORY: CTI 
//    DISTRIBUTION/DISSEMINATION CONTROL: F 
//    POC: Alex Kravchenko (1118268)
// **********************************************************************************************************
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NLog;
using Raytheon.Common;

namespace Raytheon.Instruments
{
   /// <summary>
   /// A sim communication device
   /// </summary>
   public class CommDeviceUdpAsync : ICommAsync
   {
      #region PrivateClassMembers
      private uint _defaultReadTimeout;
      private uint _defaultSendTimeout;
      private uint _defaultReadBufferSize;
      private static readonly object _syncObj = new object();

      private UdpClient _udpClient;
      private IPEndPoint _remoteEndPoint;

      private int _localPort;
      private int _remotePort;
      private string _remoteAddress;
      private readonly string _name;
      private State _state;

      private readonly ILogger _logger;

      private readonly IConfigurationManager _configurationManager;
      private readonly IConfiguration _configuration;

      #endregion

      public bool ClearErrors() => false;
      public bool FrontPanelEnabled { get => false; set => throw new NotImplementedException(); }
      public bool DisplayEnabled { get => false; set => throw new NotImplementedException(); }
      public string DetailedStatus => $"This is a TCP/IP Device called {_name}";
      public InstrumentMetadata Info => throw new NotImplementedException();
      public State Status => _state;
      public string Name => _name;
      public SelfTestResult PerformSelfTest() => SelfTestResult;
      public SelfTestResult SelfTestResult => SelfTestResult.Unknown;
      public void Open() => Initialize();
      public void Close() => Shutdown();
      public void Reset()
      {
         Close();
         Open();
      }

      #region Private Functions
      /// <summary>
      /// Dispose of the resources contained by this object
      /// </summary>
      public void Dispose()
      {
         try
         {
            lock (_syncObj)
            {
               Dispose(true);
               GC.SuppressFinalize(this);
            }
         }
         catch (Exception err)
         {
            _logger.Error(err.Message + "\r\n" + err.StackTrace);
         }
      }

      /// <summary>
      /// Dispose of the resources contained by this object
      /// </summary>
      /// <param name="disposing"></param>
      protected virtual void Dispose(bool disposing)
      {
         if (disposing)
         {
            // close the socket
            try
            {
               Shutdown();
            }
            catch (Exception err)
            {
               _logger.Error(err.Message + "\r\n" + err.StackTrace);
            }
         }
      }

      #endregion

      #region Public Functions

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

         _state = State.Uninitialized;

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

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

      /// <summary>
      /// initialize instrument
      /// </summary>
      public void Initialize()
      {
         if (_state != State.Uninitialized)
         {
            _logger.Warn("Reinitialization of existing UDP Async Connection. Attempting to call Shutdown.");
            Shutdown();
         }

         _defaultReadTimeout = _configuration.GetConfigurationValue<uint>("UdpClient", "ReadTimeout", 25);
         _defaultSendTimeout = _configuration.GetConfigurationValue<uint>("UdpClient", "SendTimeout", 5000);
         _defaultReadBufferSize = _configuration.GetConfigurationValue<uint>("UdpClient", "BufferSize", 1024);

         _localPort = _configuration.GetConfigurationValue("UdpClient", "LocalPort", 0);

         _remoteAddress = _configuration.GetConfigurationValue("UdpClient", "RemoteAddress", "127.0.0.1");
         _remotePort = _configuration.GetConfigurationValue("UdpClient", "RemotePort", 0);

         _udpClient = new UdpClient();

         if (string.IsNullOrEmpty(_remoteAddress))
         {
            _logger.Debug($"Initializing as UDP Server. Listening on port: {_localPort}");
            _udpClient.Client.Bind(new IPEndPoint(IPAddress.Any, _localPort));
         }
         else
         {
            _logger.Debug($"Initializing as UDP Client. Ready to Talk to: {_remoteAddress}:{_remotePort}");
            // get the remote endpoint
            _remoteEndPoint = new IPEndPoint(IPAddress.Parse(_remoteAddress), _remotePort);
         }

         // set timeouts
         _udpClient.Client.SendTimeout = (int)_defaultSendTimeout;
         _udpClient.Client.ReceiveTimeout = (int)_defaultReadTimeout;

         _state = State.Ready;
      }

      /// <summary>
      /// shuts down the device
      /// </summary>
      public void Shutdown()
      {
         _logger.Debug("Shutting Down...");
         _state = State.Uninitialized;
         _udpClient?.Dispose();
         _udpClient = null;
      }

      /// <summary>
      /// Read data from the device asynchronously.
      /// </summary>
      /// <param name="dataRead">The buffer to put the data in</param>
      /// <returns>The number of bytes read</returns>
      public async Task<uint> ReadAsync(byte[] dataRead, CancellationToken token = default)
      {
         if (_udpClient == null)
            return 0;

         var received = await _udpClient.ReceiveAsync();
         Array.Copy(received.Buffer, dataRead, Math.Min(dataRead.Length, received.Buffer.Length));

         UpdateRemoteAddressAndPort(received.RemoteEndPoint);

         _logger.Trace($"Reading Data, bytes received: {received.Buffer?.Length}");

         return (uint)received.Buffer.Length;
      }

      /// <summary>
      /// Read string from the device asynchronously.
      /// </summary>
      /// <param name="dataRead">The buffer to put the data in</param>
      /// <returns>The number of bytes read</returns>
      public async Task<string> ReadAsync(CancellationToken token = default)
      {
         if (_udpClient == null)
            return null;

         var received = await _udpClient.ReceiveAsync();

         UpdateRemoteAddressAndPort(received.RemoteEndPoint);

         var data = Encoding.UTF8.GetString(received.Buffer);

         _logger.Trace($"Reading Data, message received: {data}");

         return data;
      }

      /// <summary>
      /// Sets the read timeout
      /// </summary>
      /// <param name="timeoutMs"></param>
      public void SetReadTimeout(uint timeoutMs)
      {
         if (_udpClient == null)
            return;

         _logger.Trace($"Setting Reader Timeout: {timeoutMs} Ms");

         _udpClient.Client.ReceiveTimeout = (int)timeoutMs;
      }

      /// <summary>
      /// Write data to the device asynchronously
      /// </summary>
      /// <param name="dataToSend"></param>
      /// <param name="numBytesToWrite"></param>
      /// <returns></returns>
      public async Task<uint> WriteAsync(byte[] dataToSend, uint numBytesToWrite, CancellationToken token = default)
      {
         if (_udpClient == null || _remoteEndPoint == null)
            return 0;

         _logger.Trace($"Writing message to ({_remoteAddress}:{_remotePort}), bytes: {dataToSend?.Length}");

         _state = State.Busy;
         await _udpClient.SendAsync(dataToSend, (int)numBytesToWrite, _remoteEndPoint);
         _state = State.Ready;
         return numBytesToWrite;
      }

      /// <summary>
      /// Write string data to the device asynchronously
      /// </summary>
      /// <param name="message"></param>
      /// <param name="token"></param>
      /// <returns></returns>
      public async Task WriteAsync(string message, CancellationToken token = default)
      {
         if (_udpClient == null || _remoteEndPoint == null)
            return;

         _logger.Trace($"Writing message to ({_remoteAddress}:{_remotePort}), message: {message}");

         _state = State.Busy;
         var dataToSend = Encoding.UTF8.GetBytes(message);
         await _udpClient.SendAsync(dataToSend, dataToSend.Length, _remoteEndPoint);
         _state = State.Ready;
      }

      /// <summary>
      /// Send Command and Get Response asynchronously
      /// </summary>
      /// <param name="message"></param>
      /// <param name="cancellationToken"></param>
      /// <param name="timeoutInMs"></param>
      /// <returns></returns>
      public async Task<string> SendCommandGetResponseAsync(string message, CancellationToken cancellationToken = default, int timeoutInMs = 5000)
      {
         if (_udpClient == null)
            return null;

         _logger.Trace($"Sending command waiting for response from ({_remoteAddress}:{_remotePort}), message: {message}");

         using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutInMs)))
         {
            if (cancellationToken == default)
            {
               cancellationToken = cts.Token;
            }
            await WriteAsync(message, cancellationToken);
            string readResponse = await ReadAsync(cancellationToken);
            _logger.Trace($"Received response: {readResponse}");

            return readResponse;
         }
      }

      /// <summary>
      /// Send Command and Get Response asynchronously
      /// </summary>
      /// <param name="data"></param>
      /// <param name="cancellationToken"></param>
      /// <param name="timeoutInMs"></param>
      /// <returns></returns>
      public async Task<byte[]> SendCommandGetResponseAsync(byte[] data, CancellationToken cancellationToken = default, int timeoutInMs = 5000)
      {
         if (_udpClient == null)
            return null;

         _logger.Trace($"Sending command waiting for response from ({_remoteAddress}:{_remotePort}), message length: {data.Length}");

         using (CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(timeoutInMs)))
         {
            if (cancellationToken == default)
            {
               cancellationToken = cts.Token;
            }
            await WriteAsync(data, (uint)data.Length, cancellationToken);
            byte[] buffer = new byte[_defaultReadBufferSize];
            uint bytesRead = await ReadAsync(buffer, cancellationToken);
            _logger.Trace($"Received response of size: {bytesRead}");

            return buffer;
         }
      }

      /// <summary>
      /// keeps reading until canceled via token,
      /// received messages sent to dataReceived function
      /// </summary>
      /// <param name="cancellationToken"></param>
      /// <param name="dataReceived"></param>
      /// <returns></returns>
      public async Task KeepReadingAsync(CancellationToken cancellationToken, Action<string> dataReceived)
      {
         if (_udpClient == null)
            return;

         _logger.Debug($"Starting continuous reading from port: {_localPort} ...");

         while (!cancellationToken.IsCancellationRequested)
         {
            if (_udpClient == null)
               break;

            var received = await ReceiveAsync(_udpClient, cancellationToken);

            if (received != null && received.Buffer != null)
            {
               UpdateRemoteAddressAndPort(received.RemoteEndPoint);
               _logger.Trace($"Incoming Data from {_remoteAddress}:{_remotePort}: size {received.Buffer.Length}");
               string data = Encoding.UTF8.GetString(received.Buffer, 0, received.Buffer.Length);
               dataReceived(data);
            }
         }

         _logger.Debug($"Finished continuous reading from {_remoteAddress}:{_remotePort} ...");
      }

      /// <summary>
      /// keeps reading until canceled via token,
      /// received messages sent to dataReceived function
      /// </summary>
      /// <param name="cancellationToken"></param>
      /// <param name="dataReceived"></param>
      /// <returns></returns>
      public async Task KeepReadingAsync(CancellationToken cancellationToken, Action<byte[]> dataReceived)
      {
         if (_udpClient == null)
            return;

         _logger.Debug($"Starting continuous reading from port: {_localPort} ...");

         while (!cancellationToken.IsCancellationRequested)
         {
            if (_udpClient == null)
               break;

            var received = await ReceiveAsync(_udpClient, cancellationToken);
            if (received != null && received.Buffer != null)
            {
               UpdateRemoteAddressAndPort(received.RemoteEndPoint);
               _logger.Trace($"Incoming Data from {_remoteAddress}:{_remotePort}: size {received.Buffer.Length}");
               dataReceived(received.Buffer);
            }
         }

         _logger.Debug($"Finished continuous reading from {_remoteAddress}:{_remotePort} ...");
      }

      #endregion

      #region Private Functions
      /// <summary>
      /// Update client information for logging
      /// </summary>
      /// <param name="remoteEndPoint"></param>
      private void UpdateRemoteAddressAndPort(IPEndPoint remoteEndPoint)
      {
         if (remoteEndPoint == null)
            return;

         if (_remotePort == 0 || string.IsNullOrEmpty(_remoteAddress))
         {
            _remotePort = remoteEndPoint.Port;
            _remoteAddress = remoteEndPoint.Address.ToString();
         }

         if (_remoteEndPoint == null || _remoteEndPoint.Port != remoteEndPoint.Port)
         {
            // get the remote endpoint
            _remoteEndPoint = remoteEndPoint;

            _logger.Debug($"Starting to receive data from {_remoteAddress}:{_remotePort}");
         }
      }

      /// <summary>
      /// ReceiveAsyc with cancellation token implementation
      /// </summary>
      /// <param name="client"></param>
      /// <param name="breakToken"></param>
      /// <returns></returns>
      private Task<UdpReceiveResult> ReceiveAsync(UdpClient client, CancellationToken breakToken) => breakToken.IsCancellationRequested
            ? Task.Run(() => new UdpReceiveResult())
            : Task<UdpReceiveResult>.Factory.FromAsync
         ((callback, state) => client.BeginReceive(callback, state), (ar) =>
         {
            if (breakToken.IsCancellationRequested)
               return new UdpReceiveResult();

            IPEndPoint remoteEP = null;
            var buffer = client.EndReceive(ar, ref remoteEP);
            return new UdpReceiveResult(buffer, remoteEP);
         }, null);

      #endregion
   }
}
