#nullable enable
using System;
using System.Collections.Generic;
using IGP.Multiplayer.Core;
using IGP.Multiplayer.Models;
using IGP.UnitySDK;

namespace IGP.Multiplayer
{
    internal sealed class IGPNetworkTransportConfig
    {
        public string Host { get; set; } = string.Empty;
        public int Port { get; set; }
        public string Token { get; set; } = string.Empty;
        public string RoomId { get; set; } = string.Empty;
        public string PlayerId { get; set; } = string.Empty;
        public int PayloadMaxBytes { get; set; }
        public int FrameMaxBytes { get; set; }
        public bool UseEnvelopeV1 { get; set; }
    }

    // The application chooses whether an instance backs its reliable or
    // unreliable lane. The transport itself only owns connection and I/O.
    internal interface IIGPNetworkTransport
    {
        IGPRealtimeTransport Transport { get; }
        bool IsConnected { get; }
        bool IsAlive { get; }
        bool IsWritable { get; }
        int NegotiatedEnvelopeVersion { get; }
        IGPRTTStats? AppRTTStats { get; }

        event Action<IGPTransportMessageReceived> MessageReceived;
        event Action<IGPTransportConnectionChanged> ConnectionChanged;
        event Action<IGPTransportFault> Faulted;

        void Connect(IGPNetworkTransportConfig config);
        void Disconnect();
        void Tick();
        IGPTransportSendResult TrySendMessages(IReadOnlyList<Message> messages, bool isControl);
    }

    internal static class IGPNetworkTransportFactory
    {
        public static IIGPNetworkTransport CreateKcp(IGPKcpClient? client = null)
        {
            return new IGPKcpNetworkTransport(client);
        }

        public static IIGPNetworkTransport CreateUdp(IGPUdpClient client)
        {
            return new IGPUdpNetworkTransport(client);
        }

        public static IIGPNetworkTransport CreateTcp(IGPTcpClient? client = null)
        {
            return new IGPTcpNetworkTransport(client);
        }
    }

    internal sealed class IGPKcpNetworkTransport : IIGPNetworkTransport
    {
        private readonly IGPKcpClient client;

        public IGPKcpNetworkTransport(IGPKcpClient? client = null)
        {
            this.client = client ?? new IGPKcpClient();
        }

        public bool IsConnected => client.IsConnected;
        public bool IsAlive => client.IsAlive;
        public bool IsWritable => client.IsWritable;
        public IGPRealtimeTransport Transport => IGPRealtimeTransport.Kcp;
        public int NegotiatedEnvelopeVersion => client.UsesEnvelopeV1 ? 1 : 0;
        public IGPRTTStats AppRTTStats => client.AppRTTStats;

        public event Action<IGPTransportMessageReceived> MessageReceived
        {
            add => client.MessageReceived += value;
            remove => client.MessageReceived -= value;
        }

        public event Action<IGPTransportConnectionChanged> ConnectionChanged
        {
            add => client.ConnectionChanged += value;
            remove => client.ConnectionChanged -= value;
        }

        public event Action<IGPTransportFault> Faulted
        {
            add => client.Faulted += value;
            remove => client.Faulted -= value;
        }

        public void Connect(IGPNetworkTransportConfig config)
        {
            if (string.IsNullOrWhiteSpace(config.Host) ||
                config.Port <= 0 ||
                string.IsNullOrWhiteSpace(config.Token))
            {
                throw new IGPSDKException("Invalid hosted KCP descriptor");
            }

            client.Connect(config.Host, config.Port, config.RoomId, config.PlayerId, config.Token);
        }

        public void Disconnect()
        {
            client.Disconnect();
        }

        public void Tick()
        {
            client.Tick();
        }

        public IGPTransportSendResult TrySendMessages(IReadOnlyList<Message> messages, bool isControl)
        {
            return client.TrySendMessages(messages, isControl);
        }
    }

    internal sealed class IGPUdpNetworkTransport : IIGPNetworkTransport
    {
        private readonly IGPUdpClient client;

        public IGPUdpNetworkTransport(IGPUdpClient client)
        {
            this.client = client;
        }

        public bool IsConnected => client.IsConnected;
        public bool IsAlive => client.IsConnected;
        public bool IsWritable => client.IsConnected;
        public IGPRealtimeTransport Transport => IGPRealtimeTransport.Udp;
        public int NegotiatedEnvelopeVersion => 0;
        public IGPRTTStats? AppRTTStats => null;

        public event Action<IGPTransportMessageReceived> MessageReceived
        {
            add => client.MessageReceived += value;
            remove => client.MessageReceived -= value;
        }

        public event Action<IGPTransportConnectionChanged> ConnectionChanged
        {
            add => client.ConnectionChanged += value;
            remove => client.ConnectionChanged -= value;
        }

        public event Action<IGPTransportFault> Faulted
        {
            add => client.Faulted += value;
            remove => client.Faulted -= value;
        }

        public void Connect(IGPNetworkTransportConfig config)
        {
            if (string.IsNullOrWhiteSpace(config.Host) ||
                config.Port <= 0 ||
                string.IsNullOrWhiteSpace(config.Token) ||
                config.PayloadMaxBytes <= 0)
            {
                throw new IGPSDKException("Invalid hosted UDP descriptor");
            }

            client.Connect(
                config.Host,
                config.Port,
                config.RoomId,
                config.PlayerId,
                config.Token,
                config.PayloadMaxBytes,
                config.UseEnvelopeV1);
        }

        public void Disconnect()
        {
            client.Disconnect();
        }

        public void Tick()
        {
            client.Tick();
        }

        public IGPTransportSendResult TrySendMessages(IReadOnlyList<Message> messages, bool isControl)
        {
            if (!client.IsConnected)
            {
                return IGPTransportSendResult.Unavailable;
            }

            foreach (var message in messages)
            {
                if (!client.TrySendMessage(message))
                {
                    return IGPTransportSendResult.WouldBlock;
                }
            }

            return IGPTransportSendResult.Accepted;
        }
    }

    internal sealed class IGPTcpNetworkTransport : IIGPNetworkTransport
    {
        private readonly IGPTcpClient client;

        public IGPTcpNetworkTransport(IGPTcpClient? client = null)
        {
            this.client = client ?? new IGPTcpClient();
        }

        public IGPRealtimeTransport Transport => IGPRealtimeTransport.Tcp;
        public bool IsConnected => client.IsConnected;
        public bool IsAlive => client.IsAlive;
        public bool IsWritable => client.IsWritable;
        public int NegotiatedEnvelopeVersion => client.NegotiatedEnvelopeVersion;
        public IGPRTTStats AppRTTStats => client.AppRTTStats;

        public event Action<IGPTransportMessageReceived> MessageReceived
        {
            add => client.MessageReceived += value;
            remove => client.MessageReceived -= value;
        }

        public event Action<IGPTransportConnectionChanged> ConnectionChanged
        {
            add => client.ConnectionChanged += value;
            remove => client.ConnectionChanged -= value;
        }

        public event Action<IGPTransportFault> Faulted
        {
            add => client.Faulted += value;
            remove => client.Faulted -= value;
        }

        public void Connect(IGPNetworkTransportConfig config)
        {
            if (string.IsNullOrWhiteSpace(config.Host) ||
                config.Port <= 0 ||
                string.IsNullOrWhiteSpace(config.Token) ||
                config.FrameMaxBytes <= 0)
            {
                throw new IGPSDKException("Invalid hosted TCP descriptor");
            }

            client.Connect(
                config.Host,
                config.Port,
                config.RoomId,
                config.PlayerId,
                config.Token,
                config.FrameMaxBytes);
        }

        public void Disconnect()
        {
            client.Disconnect();
        }

        public void Tick()
        {
            client.Tick();
        }

        public IGPTransportSendResult TrySendMessages(IReadOnlyList<Message> messages, bool isControl)
        {
            return client.TrySendMessages(messages, isControl);
        }
    }
}
