using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using RebootKit.Engine.Network;
using Steamworks;
using Steamworks.Data;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Logger = RebootKit.Engine.Foundation.Logger;

namespace RebootKit.Engine.Steam {
    public class SteamNetworkManager : INetworkManager, ISocketManager, IConnectionManager {
        static readonly Logger s_Logger = new Logger(nameof(SteamNetworkManager));

        public const ulong k_ServerClientID = 0;
        const int k_DefaultPort = 419;

        class Client {
            public ulong ClientID;
            public Connection Connection;
        }

        readonly Dictionary<ulong, Client> m_Clients = new Dictionary<ulong, Client>();

        SocketManager m_SocketManager;
        ConnectionManager m_ConnectionManager;

        float m_StatsRefreshTimer;

        public INetworkManagerDelegate Delegate { get; set; }

        public ulong LocalClientID {
            get {
                return SteamClient.SteamId;
            }
        }

        public NetworkManagerStats Stats { get; } = new NetworkManagerStats();

        // @NOTE: Set this before starting the client to specify which server to connect to.
        public ulong TargetSteamID;

        public SteamNetworkManager() {
            SteamNetworkingUtils.InitRelayNetworkAccess();
            SteamNetworkingUtils.OnDebugOutput += SteamDebugOutput;
            SteamNetworkingUtils.DebugLevel = NetDebugOutput.None;
        }

        public void Dispose() {
            SteamNetworkingUtils.OnDebugOutput -= SteamDebugOutput;

            StopHost();
            Disconnect();
        }

        void SteamDebugOutput(NetDebugOutput output, string msg) {
            s_Logger.Info($"Output: {output} - {msg}");
        }

        //
        // @MARK: Server
        //
        public bool IsServer() {
            return m_SocketManager != null;
        }

        public bool StartHost() {
            try {
                TargetSteamID = SteamClient.SteamId;

                m_SocketManager = SteamNetworkingSockets.CreateRelaySocket<SocketManager>(k_DefaultPort);
                m_SocketManager.Interface = this;

                m_ConnectionManager =
                    SteamNetworkingSockets.ConnectRelay<ConnectionManager>(TargetSteamID, k_DefaultPort);
                m_ConnectionManager.Interface = this;

                SteamFriends.SetRichPresence("connect", LocalClientID.ToString());

                Stats.Reset();

                Delegate?.OnServerStarted();
                Delegate?.OnClientStarted();
            } catch (Exception e) {
                s_Logger.Error($"Failed to create server: {e.Message}");
                m_SocketManager = null;
                return false;
            }

            return true;
        }

        public void StopHost() {
            if (!IsServer()) {
                return;
            }

            s_Logger.Info("Stopping server...");

            SteamFriends.SetRichPresence("connect", null);

            foreach (Client client in m_Clients.Values) {
                client.Connection.Close();
            }

            m_Clients.Clear();

            m_SocketManager.Close();
            m_SocketManager = null;

            Delegate?.OnClientStopped();
            Delegate?.OnServerStopped();
        }

        //
        // @MARK: Client 
        //
        public bool IsClient() {
            return m_ConnectionManager != null;
        }

        public bool StartClient() {
            if (IsClient()) {
                s_Logger.Error("Cannot start client while already connected.");
                return false;
            }

            s_Logger.Info("Connecting to server with steam ID: " + TargetSteamID);
            try {
                m_ConnectionManager =
                    SteamNetworkingSockets.ConnectRelay<ConnectionManager>(TargetSteamID, k_DefaultPort);
                m_ConnectionManager.Interface = this;

                Stats.Reset();

                Delegate?.OnClientStarted();
            } catch (Exception e) {
                s_Logger.Error($"Failed to connect to server with ID {TargetSteamID}: {e.Message}");
                m_ConnectionManager = null;
                return false;
            }

            return true;
        }

        public void Disconnect() {
            if (!IsClient()) {
                return;
            }

            s_Logger.Info("Disconnecting from server...");

            m_ConnectionManager.Close();
            m_ConnectionManager = null;

            Delegate?.OnClientStopped();
        }

        //
        // @MARK: Data Sending/Receiving
        //
        public void Send(ulong clientID, NativeSlice<byte> data, SendMode mode) {
            if (data.Length == 0) {
                s_Logger.Error("Cannot send empty data.");
                return;
            }

            unsafe {
                Send(clientID, (byte*) data.GetUnsafePtr(), data.Length, mode);
            }
        }

        public unsafe void Send(ulong clientID, byte* data, int length, SendMode mode) {
            if (IsServer()) {
                if (clientID == k_ServerClientID || clientID == LocalClientID) {
                    NativeArray<byte> dataArray = new NativeArray<byte>(length, Allocator.Temp);
                    UnsafeUtility.MemCpy(dataArray.GetUnsafePtr(), data, length);
                    MessageReceived(LocalClientID, dataArray);
                    return;
                }

                if (!m_Clients.TryGetValue(clientID, out Client client)) {
                    s_Logger.Error($"Client with ID {clientID} not found.");
                    return;
                }

                Result sendResult = client.Connection.SendMessage((IntPtr) data, length, GetSteamSendType(mode));
                if (sendResult == Result.OK) {
                    if (mode == SendMode.Reliable) {
                        Stats.ReliableBytesSent += (ulong) length;
                    } else if (mode == SendMode.Unreliable) {
                        Stats.UnreliableBytesSent += (ulong) length;
                    }
                } else {
                    s_Logger.Error($"Failed to send message to clientID, {clientID}. Result: {sendResult}.");
                }
            } else if (IsClient()) {
                if (clientID != k_ServerClientID) {
                    s_Logger.Error($"Client can only send messages to the server (clientID must be {k_ServerClientID.ToString()}).");
                    return;
                }

                Result sendResult = m_ConnectionManager.Connection.SendMessage((IntPtr) data,
                                                                               length,
                                                                               GetSteamSendType(mode));
                if (sendResult == Result.OK) {
                    if (mode == SendMode.Reliable) {
                        Stats.ReliableBytesSent += (ulong) length;
                    } else if (mode == SendMode.Unreliable) {
                        Stats.UnreliableBytesSent += (ulong) length;
                    }
                } else {
                    s_Logger.Error($"Failed to send message to server. Result: {sendResult}. Flushing and retrying...");
                }
            } else {
                s_Logger.Error("Cannot send data, server or client is not started.");
            }
        }

        void MessageReceived(ulong senderID, NativeArray<byte> data) {
            Delegate?.OnMessageReceived(senderID, data);
        }

        public void Tick() {
            m_SocketManager?.Receive();
            m_ConnectionManager?.Receive();

            m_StatsRefreshTimer -= UnityEngine.Time.deltaTime;
            
            if (m_StatsRefreshTimer <= 0f) {
                m_StatsRefreshTimer = 1.0f;

                Stats.ReliableBytesSentPerSecond = Stats.ReliableBytesSent;
                Stats.UnreliableBytesSentPerSecond = Stats.UnreliableBytesSent;
                Stats.BytesReceivedPerSecond = Stats.BytesReceived;

                Stats.ReliableBytesSent = 0;
                Stats.UnreliableBytesSent = 0;
                Stats.BytesReceived = 0;
            }
        }

        //
        // @MARK: IConnectionManager
        //
        void ISocketManager.OnConnecting(Connection connection, ConnectionInfo info) {
            s_Logger.Info($"OnConnecting: {connection.Id} - {info.Identity}");
            connection.Accept();
        }

        void ISocketManager.OnConnected(Connection connection, ConnectionInfo info) {
            if (!m_Clients.ContainsKey(info.Identity.SteamId)) {
                s_Logger.Info($"OnConnected: {connection.Id} - {info.Identity}");

                Client client = new Client {
                    ClientID = info.Identity.SteamId,
                    Connection = connection
                };
                m_Clients.Add(info.Identity.SteamId, client);

                Delegate?.OnClientConnected(client.ClientID);
            } else {
                s_Logger.Error("Failed to add client, already exists: " + info.Identity.SteamId);
                connection.Close();
            }
        }

        void ISocketManager.OnDisconnected(Connection connection, ConnectionInfo info) {
            s_Logger.Info($"OnDisconnected: {connection.Id} - {info.Identity}");

            if (m_Clients.Remove(info.Identity.SteamId)) {
                Delegate?.OnClientDisconnected(info.Identity.SteamId);
            } else {
                s_Logger.Error("Failed to remove client, not found: " + info.Identity.SteamId);
            }
        }

        void ISocketManager.OnMessage(Connection connection,
                                      NetIdentity identity,
                                      IntPtr data,
                                      int size,
                                      long messageNum,
                                      long recvTime,
                                      int channel) {
            NativeArray<byte> dataArray = new NativeArray<byte>(size, Allocator.Temp);
            unsafe {
                UnsafeUtility.MemCpy(dataArray.GetUnsafePtr(), (void*) data, size);
            }

            Stats.BytesReceived += (ulong) size;
            MessageReceived(identity.SteamId, dataArray);
        }

        //
        // @MARK: ISocketManager
        //
        void IConnectionManager.OnConnecting(ConnectionInfo info) { }

        void IConnectionManager.OnConnected(ConnectionInfo info) {
            Delegate?.OnClientConnected(info.Identity.SteamId);
        }

        void IConnectionManager.OnDisconnected(ConnectionInfo info) {
            Delegate?.OnClientDisconnected(info.Identity.SteamId);
        }

        void IConnectionManager.OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel) {
            NativeArray<byte> dataArray = new NativeArray<byte>(size, Allocator.Temp);
            unsafe {
                UnsafeUtility.MemCpy(dataArray.GetUnsafePtr(), (void*) data, size);
            }

            Stats.BytesReceived += (ulong) size;
            MessageReceived(k_ServerClientID, dataArray);
        }

        //
        // @MARK: Utilities
        //
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        static SendType GetSteamSendType(SendMode mode) {
            return mode switch {
                SendMode.Reliable => SendType.Reliable,
                SendMode.Unreliable => SendType.Unreliable,
                _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null)
            };
        }
    }

//     class SteamNetworkManager : INetworkManager {
//         static readonly Logger s_Logger = new Logger(nameof(SteamNetworkManager));
//
//         const int k_DefaultPort = 420;
//
//         SocketManager m_SocketManager;
//         ConnectionManager m_ConnectionManager;
//         IDisposable m_TickDisposable;
//
//         SteamId m_HostSteamID;
//
//         public void Initialize(INetworkManagerEventHandler eventHandler) {
//             m_TickDisposable = Observable.EveryUpdate()
//                                              .Subscribe(_ => Tick());
//
//             SteamNetworkingUtils.DebugLevel = NetDebugOutput.Debug;
//
//             SteamNetworkingSockets.OnConnectionStatusChanged += OnConnectionStatusChanged;
//             SteamNetworkingUtils.OnDebugOutput += OnSteamNetworkDebugOutput;
//
// #if !RR_LOCALHOST_ONLY
//             SteamNetworkingUtils.InitRelayNetworkAccess();
// #endif
//         }
//
//         public void Shutdown() {
//             Disconnect();
//
//             SteamNetworkingUtils.OnDebugOutput -= OnSteamNetworkDebugOutput;
//             SteamNetworkingSockets.OnConnectionStatusChanged -= OnConnectionStatusChanged;
//
//             m_TickDisposable.Dispose();
//         }
//
//         public bool IsServer() {
//             return m_SocketManager != null;
//         }
//
//         public bool IsClient() {
//             return m_ConnectionManager != null;
//         }
//
//         public bool StartServer() {
//             Disconnect();
//
//             s_Logger.Info("Creating server...");
//
//             try {
//                 m_HostSteamID = SteamNetworkingSockets.Identity.SteamId;
//
// #if RR_LOCALHOST_ONLY
//                 m_SocketManager = SteamNetworkingSockets.CreateNormalSocket(NetAddress.LocalHost(2137), new ServerCallbacks());
//                 m_ConnectionManager = SteamNetworkingSockets.ConnectNormal(NetAddress.LocalHost(2137), new ClientCallbacks());
// #else
//                 m_SocketManager = SteamNetworkingSockets.CreateRelaySocket<SocketManager>(k_DefaultPort);
//                 m_SocketManager.Interface = new ServerCallbacks();
//                 m_ConnectionManager =
//  SteamNetworkingSockets.ConnectRelay<ConnectionManager>(m_HostSteamID, k_DefaultPort);
//                 m_ConnectionManager.Interface = new ClientCallbacks();
// #endif
//             } catch (Exception e) {
//                 s_Logger.Error($"Failed to create server: {e.Message}");
//                 m_SocketManager = null;
//                 m_ConnectionManager = null;
//
//                 return false;
//             }
//
//             return true;
//         }
//
//         public void StopServer() {
//             Disconnect();
//         }
//
//         public bool Connect(ulong serverID) {
//             if (IsServer()) {
//                 s_Logger.Error("Cannot connect to a server while running as a server.");
//                 return false;
//             }
//
//             Disconnect();
//
//             s_Logger.Info("Connecting to server with steam ID: " + serverID);
//             try {
// #if RR_LOCALHOST_ONLY
//                 m_ConnectionManager =
//                     SteamNetworkingSockets.ConnectNormal(NetAddress.LocalHost(2137), new ClientCallbacks());
// #else
//                 m_ConnectionManager = SteamNetworkingSockets.ConnectRelay<ConnectionManager>(serverID, k_DefaultPort);
//                 m_ConnectionManager.Interface = new ClientCallbacks();
// #endif
//
//                 m_HostSteamID = serverID;
//             } catch (Exception e) {
//                 s_Logger.Error($"Failed to connect to server with ID {serverID}: {e.Message}");
//                 m_ConnectionManager = null;
//
//                 return false;
//             }
//
//             return true;
//         }
//
//         public void Disconnect() {
//             if (m_ConnectionManager != null) {
//                 s_Logger.Info("Disconnecting from the server...");
//                 m_ConnectionManager.Close();
//                 m_ConnectionManager = null;
//             }
//
//             if (m_SocketManager != null) {
//                 s_Logger.Info("Shutting down the server...");
//                 m_SocketManager.Close();
//                 m_SocketManager = null;
//             }
//         }
//         public void Send(ulong clientID, NetworkMessage message, SendMode mode) {
//             if (!IsServer()) {
//                 return;
//             }
//
//             foreach (Connection connection in m_SocketManager.Connected) {
//                 connection.SendMessage(new byte[] {
//                     0xDE,
//                     0xAD,
//                     0xBE,
//                     0xEF
//                 }, SendType.Reliable);
//             }
//         }
//
//         public void Broadcast(NetworkMessage message, SendMode mode) {
//             if (!IsServer()) {
//                 return;
//             }
//
//             foreach (Connection connection in m_SocketManager.Connected) {
//                 connection.SendMessage(new byte[] {
//                     0xDE,
//                     0xAD,
//                     0xBE,
//                     0xEF
//                 }, SendType.Reliable);
//             }
//         }
//
//         void OnSteamNetworkDebugOutput(NetDebugOutput level, string message) {
//             LogLevel logLevel = level switch {
//                 NetDebugOutput.Debug => LogLevel.Debug,
//                 NetDebugOutput.Msg => LogLevel.Info,
//                 NetDebugOutput.Warning => LogLevel.Warning,
//                 NetDebugOutput.Error => LogLevel.Error,
//                 _ => LogLevel.Info
//             };
//
//             s_Logger.Log(logLevel, message);
//         }
//
//         void Tick() {
//             m_SocketManager?.Receive();
//             m_ConnectionManager?.Receive();
//         }
//
//         void OnConnectionStatusChanged(Connection connection, ConnectionInfo info) {
//             s_Logger.Info($"OnConnectionStatusChanged: {connection.Id} - {info.Identity} - Status: {info.State}");
//         }
//
//         class ServerCallbacks : ISocketManager {
//             public void OnConnecting(Connection connection, ConnectionInfo data) {
//                 s_Logger.Info($"OnConnecting: {connection.Id} - {data.Identity}");
//
//                 connection.Accept();
//             }
//
//             public void OnConnected(Connection connection, ConnectionInfo data) {
//                 s_Logger.Info($"OnConnected: {connection.Id} - {data.Identity}");
//
//                 connection.SendMessage(new byte[] {
//                     0xBE,
//                     0xFE,
//                     0x00,
//                     0x00
//                 }, 0, 4, 0);
//             }
//
//             public void OnDisconnected(Connection connection, ConnectionInfo data) {
//                 s_Logger.Info($"OnDisconnected: {connection.Id} - {data.Identity}");
//             }
//
//             public void OnMessage(Connection connection,
//                                   NetIdentity identity,
//                                   IntPtr data,
//                                   int size,
//                                   long messageNum,
//                                   long recvTime,
//                                   int channel) {
//                 byte[] buffer = new byte[size];
//                 System.Runtime.InteropServices.Marshal.Copy(data, buffer, 0, size);
//
//                 s_Logger.Info($"OnMessage: {connection.Id} - {identity} - Size: {size} - Channel: {channel}");
//             }
//         }
//
//         class ClientCallbacks : IConnectionManager {
//             public void OnConnected(ConnectionInfo info) {
//                 s_Logger.Info("ConnectionOnConnected");
//             }
//
//             public void OnConnecting(ConnectionInfo info) {
//                 s_Logger.Info("ConnectionOnConnecting");
//             }
//
//             public void OnDisconnected(ConnectionInfo info) {
//                 s_Logger.Info("ConnectionOnDisconnected");
//             }
//
//             public void OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel) {
//                 byte[] buffer = new byte[size];
//                 System.Runtime.InteropServices.Marshal.Copy(data, buffer, 0, size);
//
//                 s_Logger.Info($"OnMessage: Size: {size} - Channel: {channel}");
//             }
//         }
//     }
}