// **********************************************************************************************************
//    CommDeviceTcpAsync.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.Reflection;
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 CommDeviceTcpAsync : ICommAsync
   {
      #region PrivateClassMembers

      private uint _defaultReadTimeout;
      private uint _defaultSendTimeout;
      private uint _defaultReadBufferSize;
      private static readonly object _syncObj = new object();
      private TcpClient _tcpClient;
      private TcpListener _tcpListener;
      private NetworkStream _tcpIpStream;
      private int _port;
      private string _remoteAddress;
      private int _remotePort;
      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 CommDeviceTcpAsync(string deviceName, IConfigurationManager configurationManager)
      {
         _name = deviceName;

         // _tcpClient is created in Initialize()
         _tcpClient = null;
         _tcpIpStream = null;

         _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 TCP Async Connection. Attempting to call Shutdown.");
            Shutdown();
         }

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

         _remoteAddress = _configuration.GetConfigurationValue("TcpClient", "RemoteAddress", "127.0.0.1");
         _port = _configuration.GetConfigurationValue("TcpClient", "Port", 0);

         if (string.IsNullOrEmpty(_remoteAddress))
         {
            _tcpListener = new TcpListener(IPAddress.Any, _port);
            _tcpListener.Start();

            _logger.Debug($"{MethodBase.GetCurrentMethod().Name} Started Listening on Port: {_port}");

            Task.Run(async () => await _tcpListener.AcceptTcpClientAsync()).ContinueWith(t =>
            {
               _tcpClient = t.Result;

               _remoteAddress = ((IPEndPoint)_tcpClient.Client.RemoteEndPoint).Address.ToString();
               _remotePort = ((IPEndPoint)_tcpClient.Client.RemoteEndPoint).Port;

               _logger.Debug($"{MethodBase.GetCurrentMethod().Name} Connection Established from Remote Address: {_remoteAddress}:{_remotePort}");

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

               // get the stream
               _tcpIpStream = _tcpClient.GetStream();

               _state = State.Ready;
            });
         }
         else
         {
            _remotePort = _port;
            _tcpClient = new TcpClient(_remoteAddress, _remotePort);

            _logger.Debug($"{MethodBase.GetCurrentMethod().Name} Connected to Remote Address {_remoteAddress}:{_remotePort}");

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

            // get the stream
            _tcpIpStream = _tcpClient.GetStream();

            _state = State.Ready;
         }
      }

      /// <summary>
      /// shuts down the device
      /// </summary>
      public void Shutdown()
      {
         _logger.Debug("Shutting down");
         _tcpClient?.Dispose();
         _tcpClient = null;

         _tcpListener?.Stop();
         _tcpListener = null;

         _tcpIpStream?.Dispose();
         _tcpIpStream = null;

         _state = State.Uninitialized;
      }

      /// <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 (_tcpIpStream == null)
            return 0;

         if (_tcpIpStream.DataAvailable == true)
         {
            _state = State.Busy;
            var bytesRead = await _tcpIpStream.ReadAsync(dataRead, 0, dataRead.Length, token);

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

            _state = State.Ready;
            return (uint)bytesRead;
         }
         else
         {
            return 0;
         }
      }

      /// <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 (_tcpIpStream == null)
            return null;

         if (_tcpIpStream.DataAvailable == true)
         {
            _state = State.Busy;
            var buffer = new byte[_defaultReadBufferSize];
            var bytesRead = await _tcpIpStream.ReadAsync(buffer, 0, buffer.Length, token);
            _state = State.Ready;

            var message = Encoding.UTF8.GetString(buffer, 0, bytesRead);

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

            return message;
         }
         else
         {
            return null;
         }
      }

      /// <summary>
      /// Sets the read timeout
      /// </summary>
      /// <param name="timeoutMs"></param>
      public void SetReadTimeout(uint timeoutMs)
      {
         _logger.Trace($"Setting Reader Timeout: {timeoutMs} Ms");

         _tcpClient.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 (_tcpIpStream == null)
            return 0;

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

         _state = State.Busy;
         await _tcpIpStream.WriteAsync(dataToSend, 0, (int)numBytesToWrite, token);
         _state = State.Ready;
         return numBytesToWrite;
      }

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

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

         _state = State.Busy;
         var buffer = Encoding.UTF8.GetBytes(message);
         await _tcpIpStream.WriteAsync(buffer, 0, buffer.Length, token);
         _state = State.Ready;
      }

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

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

         using (var 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="message"></param>
      /// <param name="timeoutInMs"></param>
      /// <returns></returns>
      public async Task<byte[]> SendCommandGetResponseAsync(byte[] data, CancellationToken cancellationToken = default, int timeoutInMs = 5000)
      {
         if (_tcpIpStream == null)
            return null;

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

         using (var 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)
      {
         _logger.Debug($"Starting continuous reading from {_remoteAddress}:{_remotePort} ...");

         byte[] buffer = new byte[_defaultReadBufferSize];
         while (!cancellationToken.IsCancellationRequested)
         {
            if (_tcpIpStream == null)
            {
               continue;
            }

            uint bytesRead = await ReadAsync(buffer, cancellationToken);
            if (bytesRead > 0)
            {
               string data = Encoding.UTF8.GetString(buffer, 0, (int)bytesRead);

               _logger.Trace($"Message received from {_remoteAddress}:{_remotePort}: {data}");

               dataReceived(data);
               Array.Clear(buffer, 0, (int)bytesRead);
            }
         }

         _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)
      {
         _logger.Debug($"Starting continuous reading from {_remoteAddress}:{_remotePort} ...");

         byte[] buffer = new byte[_defaultReadBufferSize];
         while (!cancellationToken.IsCancellationRequested)
         {
            if (_tcpIpStream == null)
            {
               continue;
            }

            uint bytesRead = await ReadAsync(buffer, cancellationToken);
            if (bytesRead > 0)
            {
               _logger.Trace($"Message received from {_remoteAddress}:{_remotePort}: size {bytesRead}");
               byte[] bufferCopy = new byte[bytesRead];
               Array.Copy(buffer, bufferCopy, bytesRead);
               dataReceived(bufferCopy);
               Array.Clear(buffer, 0, (int)bytesRead);
            }
         }

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

      #endregion
   }
}
