// **********************************************************************************************************
//    CommDeviceSerialAsync.cs
//    4/3/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 NLog;
using Raytheon.Common;
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
using System.Reflection;
using System.IO.Ports;

namespace Raytheon.Instruments
{
	/// <summary>
	/// A sim communication device
	/// </summary>
	public class CommDeviceSerialAsync : ICommAsync
	{
		#region PrivateClassMembers

		private uint _defaultReadTimeout;
		private uint _defaultSendTimeout;
		private uint _defaultReadBufferSize;
		private static readonly object _syncObj = new object();

		private SerialPort _serialPort;

		private readonly string _comPortName;
		private readonly int _baudRate;
		private readonly Parity _parity;
		private readonly int _dataBits;
		private readonly StopBits _stopBits;

		private readonly string _name;
		private State _state;

		/// <summary>
		/// NLog logger
		/// </summary>
		private readonly ILogger _logger;
		/// <summary>
		/// Raytheon configuration
		/// </summary>
		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 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="name"></param>
		/// <param name="configurationManager"></param>
		public CommDeviceSerialAsync(string name, IConfigurationManager configurationManager, ILogger logger)
		{
			_name = name;
			_configurationManager = configurationManager;
			_configuration = _configurationManager.GetConfiguration(name);
			_logger = logger;

			_comPortName = _configuration.GetConfigurationValue("CommDeviceSerialAsync", "COMPortName", "COM15");
			_baudRate = _configuration.GetConfigurationValue("CommDeviceSerialAsync", "BaudRate", 115200);
			_parity = _configuration.GetConfigurationValue("CommDeviceSerialAsync", "Parity", Parity.None);
			_dataBits = _configuration.GetConfigurationValue("CommDeviceSerialAsync", "DataBits", 8);
			_stopBits = _configuration.GetConfigurationValue("CommDeviceSerialAsync", "StopBits", StopBits.One);

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

			_state = State.Uninitialized;
		}

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

			_serialPort = new SerialPort(_comPortName, _baudRate, _parity, _dataBits, _stopBits);

			Open();
		}

		/// <summary>
		/// Opens COM serial port for communications
		/// </summary>
		public void Open()
		{
			try
			{
				_serialPort.Open();
				_state = State.Ready;
			}
			catch (Exception ex)
			{
				_logger.Error(ex, ex.Message);
				throw;
			}
		}

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

			_serialPort.Close();

			_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)
		{
			var bytesRead = await _serialPort.BaseStream.ReadAsync(dataRead, 0, dataRead.Length, token);
			return (uint)bytesRead;
		}

		/// <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)
		{
			var data = await ReadLineAsync(token);
			return data;
		}

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

			_logger.Trace($"Setting Reader Timeout: {timeoutMs} Ms");
			_serialPort.ReadTimeout = (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 (_serialPort == null || !_serialPort.IsOpen)
				return 0;

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

			await _serialPort.BaseStream.WriteAsync(dataToSend, 0, (int)numBytesToWrite, token);
			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 (_serialPort == null || !_serialPort.IsOpen)
				return;

			_logger.Trace($"Writing message to ({_comPortName}), message: {message}");
			await WriteLineAsync(message, token);
		}

		/// <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 (_serialPort == null || !_serialPort.IsOpen)
				return null;

			_logger.Trace($"Sending command waiting for response from ({_comPortName}), 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 token = default, int timeoutInMs = 5000)
		{
			if (_serialPort == null || !_serialPort.IsOpen)
				return null;

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

			await WriteAsync(data, (uint)data.Length, token);
			_serialPort.ReadTimeout = timeoutInMs;
			var response = new byte[data.Length];
			await ReadAsync(response, token);
			return response;
		}

		/// <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 (_serialPort == null || !_serialPort.IsOpen)
				return;

			_logger.Debug($"Starting continuous reading from {_comPortName} ...");

			while (!cancellationToken.IsCancellationRequested)
			{
				var data = await ReadAsync(cancellationToken);
				dataReceived?.Invoke(data);
			}

			_logger.Debug($"Finished continuous reading from {_comPortName} ...");
		}

		/// <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 (_serialPort == null || !_serialPort.IsOpen)
				return;

			_logger.Debug($"Starting continuous reading from {_comPortName} ...");

			while (!cancellationToken.IsCancellationRequested)
			{
				var data = new byte[_defaultReadBufferSize]; // Adjust buffer size as needed
				var bytesRead = await ReadAsync(data, cancellationToken);
				Array.Resize(ref data, (int)bytesRead);
				dataReceived?.Invoke(data);
			}

			_logger.Debug($"Finished continuous reading from {_comPortName} ...");
		}

		#endregion

		#region private functions
		/// <summary>
		/// reads line async
		/// </summary>
		/// <param name="cancellationToken"></param>
		/// <returns></returns>
		private async Task<string> ReadLineAsync(CancellationToken cancellationToken = default)
		{
			try
			{
				cancellationToken.ThrowIfCancellationRequested();
				var line = await Task.Run(() => _serialPort.ReadLine(), cancellationToken);
				return line;
			}
			catch (OperationCanceledException ex)
			{
				_logger.Error(ex, ex.Message);
				return null;
			}
		}

		/// <summary>
		/// writes line async
		/// </summary>
		/// <param name="message"></param>
		/// <param name="cancellationToken"></param>
		/// <returns></returns>
		private async Task WriteLineAsync(string message, CancellationToken cancellationToken = default)
		{
			try
			{
				cancellationToken.ThrowIfCancellationRequested();
				await Task.Run(() => _serialPort.WriteLine(message), cancellationToken);
			}
			catch (OperationCanceledException ex)
			{
				_logger.Error(ex, ex.Message);
			}
		}

		#endregion
	}
}
