using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Raytheon.Instruments.LSPS
{
   /// <summary>
	/// Class for TCP client communication 
	/// </summary>
   internal class TcpClientSock
   {
      private Socket sock_;
      private string remoteAddress_;
      private int remotePort_;
      IPEndPoint remoteEP_;
      IPAddress ipAddress_ = null;
      private static object _syncObj = new object();

      /// <summary>
		/// Constructor
		/// </summary>
		/// <param name="remoteAddress"></param>
		/// <param name="remotePort"></param>
      public TcpClientSock(string remoteAddress, int remotePort)
      {
         remoteAddress_ = remoteAddress;
         remotePort_ = remotePort;

         // if remoteAddress is a hostname
         if (!IPAddress.TryParse(remoteAddress_, out ipAddress_))
         {
            string preferredSubnet = "";

            IPHostEntry host = Dns.GetHostEntry(remoteAddress_);
            foreach (IPAddress ip in host.AddressList)
            {
               AddressFamily af = ip.AddressFamily;
               if (af == AddressFamily.InterNetwork)
               {
                  if (preferredSubnet != String.Empty)
                  {
                     if (Regex.IsMatch(ip.ToString(), preferredSubnet, RegexOptions.IgnoreCase))
                        ipAddress_ = ip;
                  }
                  else
                     ipAddress_ = ip;

                  if (ipAddress_ != null)
                     break;
               }
            }
         }

         if (ipAddress_ != null)
         {
            remoteEP_ = new IPEndPoint(ipAddress_, remotePort_);
         }
         else
            throw new Exception("Unable to connect to " + remoteAddress_);

         sock_ = new Socket(ipAddress_.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
      }

      /// <summary>
		/// Connect to remote host
		/// </summary>
		/// <returns></returns>
      public void Connect()
      {
         lock (_syncObj)
         {
            try
            {
               if (!sock_.Connected && IsRemoteHostAlive())
                  sock_.Connect(remoteEP_);
            }
            catch (ObjectDisposedException)
            {
               sock_ = new Socket(ipAddress_.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
               if (IsRemoteHostAlive())
                  sock_.Connect(remoteEP_);
            }
            catch (Exception) { throw; }
         }
      }

      /// <summary>
		/// Disconnect from remote host
		/// </summary>
		/// <returns></returns>
      public void Disconnect()
      {
         lock (_syncObj)
         {
            if (sock_.Connected)
            {
               sock_.Shutdown(SocketShutdown.Both);
               sock_.Disconnect(true);
               sock_.Close();
            }
         }
      }

      /// <summary>
		/// Ping if remote host is alive
		/// </summary>
		/// <returns>true/false</returns>
      bool IsRemoteHostAlive()
      {
         bool isRemoteHostAlive = true;

         //Do a ping test to see if the server is reachable
         try
         {
            Ping pingTest = new Ping();
            PingReply reply = pingTest.Send(ipAddress_);
            if (reply.Status != IPStatus.Success)
               isRemoteHostAlive = false;
         }
         catch (PingException)
         {
            isRemoteHostAlive = false;
         }

         //See if the tcp state is ok
         if (sock_.Poll(5000, SelectMode.SelectRead) && (sock_.Available == 0))
         {
            isRemoteHostAlive = false;
         }

         return isRemoteHostAlive;
      }

      /// <summary>
		/// Send Command and Get Response
		/// </summary>
		/// <param name="command"></param>
		/// <param name="responseTimeOutMs"></param>
      /// <param name="responseBufSize"></param>
		/// <returns>response in ASCII format</returns>
      public string SendCommandAndGetResponse(string command, int responseTimeOutMs = 5000, int responseBufSize = 1024)
      {
         byte[] bytes = new byte[responseBufSize];
         sock_.ReceiveTimeout = responseTimeOutMs;

         int bytesRec = 0;

         byte[] msg = Encoding.ASCII.GetBytes(command);

         lock (_syncObj)
         {
            sock_.Send(msg);

            try
            {
               bytesRec = sock_.Receive(bytes);
            }
            catch (SocketException ex)
            {
               throw new Exception("SocketException Error Code: " + ex.ErrorCode + $" ({((SocketError)ex.ErrorCode).ToString()})");
            }
         }

         return Encoding.ASCII.GetString(bytes, 0, bytesRec);
      }

      /// <summary>
		/// Read data from the device.
		/// </summary>
		/// <param name="dataBuf"></param>
      /// <param name="responseTimeOutMs"></param>
		/// <returns>The number of bytes read</returns>
      public int Read(ref byte[] dataBuf, int responseTimeOutMs = 5000)
      {
         int bytesRec = 0;
         sock_.ReceiveTimeout = responseTimeOutMs;
         lock (_syncObj)
         {
            try
            {
               bytesRec = sock_.Receive(dataBuf);
            }
            catch (SocketException)
            {
               bytesRec = 0;
            }
         }

         return bytesRec;
      }

      /// <summary>
		/// Write data to device.
		/// </summary>
		/// <param name="dataBuf"></param>
		/// <returns>The number of bytes written</returns>
      public int Write(byte[] dataBuf)
      {
         int bytesWritten = 0;
         lock (_syncObj)
         {
            try
            {
               bytesWritten = sock_.Send(dataBuf);
            }
            catch (SocketException)
            {
               bytesWritten = 0;
            }
         }

         return bytesWritten;
      }

      /// <summary>
		/// Write data to device.
		/// </summary>
		/// <param name="data"></param>
		/// <returns>The number of bytes written</returns>
      public int Write(string data)
      {
         int bytesWritten = 0;
         byte[] msg = Encoding.ASCII.GetBytes(data);

         lock (_syncObj)
         {
            try
            {
               bytesWritten = sock_.Send(msg);
            }
            catch (SocketException)
            {
               bytesWritten = 0;
            }
         }

         return bytesWritten;
      }

      /// <summary>
		/// Clear Hardware Receiver Buffer of residual data
		/// </summary>
		/// <returns>none</returns>
      public void ClearReceiveBuffer()
      {
         byte[] bytes = new byte[1024];
         sock_.ReceiveTimeout = 100;

         int bytesRec = 0;

         lock (_syncObj)
         {
            do
            {
               try
               {
                  bytesRec = sock_.Receive(bytes);
               }
               catch (SocketException)
               {
                  bytesRec = 0;
               }
            } while (bytesRec > 0);
         }
      }
   }
}
