using System.Net.Sockets;

namespace InnovEnergy.Lib.Channels.V2;

public class TcpChannel : IChannel<IReadOnlyList<Byte>>
{
    private Int32   TimeoutMs { get; }
    private String  Hostname  { get; }
    private UInt16  Port      { get; }
    private Byte[]  Buffer    { get; }

    private Socket? Socket    { get; set; }

    public TcpChannel(String hostname, 
                      UInt16 port,
                    TimeSpan timeout = default,
                       Int32 bufferSize = 8192)
    {
        if (bufferSize <= 0)
            throw new ArgumentOutOfRangeException(nameof(bufferSize));

        Hostname  = hostname;
        Port      = port;
        TimeoutMs = (Int32)timeout.TotalMilliseconds;
        Buffer    = new Byte[bufferSize];
    }


    public async Task<IReadOnlyList<Byte>> Read()
    {
        try
        {
            await Open();
            
            var nRead = await Socket!.ReceiveAsync(Buffer, SocketFlags.None);

            if (nRead <= 0)
                throw new Exception("TCP Connection was closed");

            // copy from buffer, so we can reuse it          
            var data = new Byte[nRead];
            Array.Copy(Buffer, data, nRead);
            return data;
        }
        catch
        {
            Close();
            throw;
        }
    }

    private async Task Open()
    {
        if (Socket is not null)
            return;
        
        Socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
        {
            Blocking          = true,
            NoDelay           = true,
            LingerState       = new LingerOption(false, 0),
            ReceiveBufferSize = Buffer.Length,
            SendBufferSize    = Buffer.Length,
            ReceiveTimeout    = TimeoutMs,
            SendTimeout       = TimeoutMs,
        };

        var cts = new CancellationTokenSource();
        cts.CancelAfter(TimeoutMs);

        try
        {
            await Socket.ConnectAsync(Hostname, Port, cts.Token).AsTask();
        }
        catch 
        {
            Socket = null!;    
            throw;
        }
    }

    public async Task Write(IReadOnlyList<Byte> tx)
    {
        try
        {
            await Open();
            await Socket!.SendAsync(tx as Byte[] ?? tx.ToArray(), SocketFlags.None);
        }
        catch
        {
            Close();
            throw;
        }
    }

    private void Close()
    {
        if (Socket is null)
            return;

        try
        {
            Socket.Dispose();
        }
        finally
        {
            Socket = null;
        }
    }
}