using kcp2k;
using Mirror;
using Mirror.SimpleWeb;
using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;

namespace LightReflectiveMirror
{
    [DefaultExecutionOrder(1001)]
    public partial class LightReflectiveMirrorTransport : Transport
    {
        private void OnConnectedToRelay() => _connectedToRelay = true;
        public bool IsAuthenticated() => _isAuthenticated;
        private void Awake()
        {
            if (clientToServerTransport is LightReflectiveMirrorTransport)
                throw new Exception("Haha real funny... Use a different transport.");

            _directConnectModule = GetComponent<LRMDirectConnectModule>();

            if (_directConnectModule != null)
            {
                if (useNATPunch && !_directConnectModule.SupportsNATPunch())
                {
                    Debug.LogWarning("LRM | NATPunch is turned on but the transport used does not support it. It will be disabled.");
                    useNATPunch = false;
                }
            }

            SetupCallbacks();

            if (connectOnAwake)
                ConnectToRelay();

            InvokeRepeating(nameof(SendHeartbeat), heartBeatInterval, heartBeatInterval);
        }

        private void SetupCallbacks()
        {
            if (_callbacksInitialized)
                return;

            _callbacksInitialized = true;
            clientToServerTransport.OnClientConnected = OnConnectedToRelay;
            clientToServerTransport.OnClientDataReceived = DataReceived;
            clientToServerTransport.OnClientDisconnected = Disconnected;
        }

        void Disconnected()
        {
            _connectedToRelay = false;
            _isAuthenticated = false;
            diconnectedFromRelay?.Invoke();
        }

        public void ConnectToRelay()
        {
            if (!useLoadBalancer)
            {
                if (!_connectedToRelay)
                {
                    Connect(serverIP);
                }
                else
                {
                    Debug.LogWarning("LRM | Already connected to relay!");
                }
            }
            else
            {
                if (!_connectedToRelay)
                {
                    StartCoroutine(RelayConnect());
                }
                else
                {
                    Debug.LogWarning("LRM | Already connected to relay!");
                }
            }
        }

        /// <summary>
        /// Connects to the desired relay
        /// </summary>
        /// <param name="serverIP"></param>
        private void Connect(string serverIP, ushort port = 7777)
        {
            // need to implement custom port
            if (clientToServerTransport is LightReflectiveMirrorTransport)
                throw new Exception("LRM | Client to Server Transport cannot be LRM.");

            SetTransportPort(port);
            

            this.serverIP = serverIP;

            _clientSendBuffer = new byte[clientToServerTransport.GetMaxPacketSize()];
            clientToServerTransport.ClientConnect(serverIP);
        }

        private void DisconnectFromRelay()
        {
            if (IsAuthenticated())
            {
                clientToServerTransport.ClientDisconnect();
            }
        }

        void SendHeartbeat()
        {
            if (_connectedToRelay)
            {
                int pos = 0;
                _clientSendBuffer.WriteByte(ref pos, 200);
                clientToServerTransport.ClientSend(0, new ArraySegment<byte>(_clientSendBuffer, 0, pos));

                if (_NATPuncher != null)
                    _NATPuncher.Send(new byte[] { 0 }, 1, _relayPuncherIP);

                var keys = new List<IPEndPoint>(_serverProxies.GetAllKeys());

                for (int i = 0; i < keys.Count; i++)
                {
                    if (DateTime.Now.Subtract(_serverProxies.GetByFirst(keys[i]).lastInteractionTime).TotalSeconds > 10)
                    {
                        _serverProxies.GetByFirst(keys[i]).Dispose();
                        _serverProxies.Remove(keys[i]);
                    }
                }
            }
        }

        void DataReceived(ArraySegment<byte> segmentData, int channel)
        {
            try
            {
                var data = segmentData.Array;
                int pos = segmentData.Offset;

                OpCodes opcode = (OpCodes)data.ReadByte(ref pos);

                switch (opcode)
                {
                    case OpCodes.Authenticated:
                        _isAuthenticated = true;
                        break;
                    case OpCodes.AuthenticationRequest:
                        SendAuthKey();
                        break;
                    case OpCodes.GetData:
                        var recvData = data.ReadBytes(ref pos);

                        if (_isServer)
                        {
                            if (_connectedRelayClients.TryGetByFirst(data.ReadInt(ref pos), out int clientID))
                                OnServerDataReceived?.Invoke(clientID, new ArraySegment<byte>(recvData), channel);
                        }

                        if (_isClient)
                            OnClientDataReceived?.Invoke(new ArraySegment<byte>(recvData), channel);
                        break;
                    case OpCodes.ServerLeft:
                        if (_isClient)
                        {
                            _isClient = false;
                            OnClientDisconnected?.Invoke();
                        }
                        break;
                    case OpCodes.PlayerDisconnected:
                        if (_isServer)
                        {
                            int user = data.ReadInt(ref pos);
                            if (_connectedRelayClients.TryGetByFirst(user, out int clientID))
                            {
                                OnServerDisconnected?.Invoke(clientID);
                                _connectedRelayClients.Remove(user);
                            }
                        }
                        break;
                    case OpCodes.RoomCreated:
                        serverId = data.ReadInt(ref pos);
                        break;
                    case OpCodes.ServerJoined:
                        int clientId = data.ReadInt(ref pos);
                        if (_isClient)
                        {
                            OnClientConnected?.Invoke();
                        }
                        if (_isServer)
                        {
                            _connectedRelayClients.Add(clientId, _currentMemberId);
                            OnServerConnected?.Invoke(_currentMemberId);
                            _currentMemberId++;
                        }
                        break;
                    case OpCodes.DirectConnectIP:
                        var ip = data.ReadString(ref pos);
                        int port = data.ReadInt(ref pos);
                        bool attemptNatPunch = data.ReadBool(ref pos);

                        _directConnectEndpoint = new IPEndPoint(IPAddress.Parse(ip), port);

                        if (useNATPunch && attemptNatPunch)
                        {
                            StartCoroutine(NATPunch(_directConnectEndpoint));
                        }

                        if (!_isServer)
                        {
                            if (_clientProxy == null && useNATPunch && attemptNatPunch)
                            {
                                _clientProxy = new SocketProxy(_NATIP.Port - 1);
                                _clientProxy.dataReceived += ClientProcessProxyData;
                            }

                            if (useNATPunch && attemptNatPunch)
                                _directConnectModule.JoinServer("127.0.0.1", _NATIP.Port - 1);
                            else
                                _directConnectModule.JoinServer(ip, port);
                        }

                        break;
                    case OpCodes.RequestNATConnection:
                        if (GetLocalIp() != null && _directConnectModule != null)
                        {
                            byte[] initalData = new byte[150];
                            int sendPos = 0;

                            initalData.WriteBool(ref sendPos, true);
                            initalData.WriteString(ref sendPos, data.ReadString(ref pos));
                            NATPunchtroughPort = data.ReadInt(ref pos);

                            if (_NATPuncher == null)
                            {
                                _NATPuncher = new UdpClient { ExclusiveAddressUse = false };
                                while (true)
                                {
                                    try
                                    {
                                        _NATIP = new IPEndPoint(IPAddress.Parse(GetLocalIp()), UnityEngine.Random.Range(16000, 17000));
                                        _NATPuncher.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
                                        _NATPuncher.Client.Bind(_NATIP);
                                        break;
                                    }
                                    catch {} // Binding port is in use, keep trying :P
                                }
                            }

                            IPAddress serverAddr;

                            if (!IPAddress.TryParse(serverIP, out serverAddr))
                                serverAddr = Dns.GetHostEntry(serverIP).AddressList[0];

                            _relayPuncherIP = new IPEndPoint(IPAddress.Parse(serverIP), NATPunchtroughPort);

                            // Send 3 to lower chance of it being dropped or corrupted when received on server.
                            _NATPuncher.Send(initalData, sendPos, _relayPuncherIP);
                            _NATPuncher.Send(initalData, sendPos, _relayPuncherIP);
                            _NATPuncher.Send(initalData, sendPos, _relayPuncherIP);
                            _NATPuncher.BeginReceive(new AsyncCallback(RecvData), _NATPuncher);
                        }
                        break;
                }
            }
            catch (Exception e) { print(e); }
        }

        public void SetTransportPort(ushort port)
        {
            if (clientToServerTransport is KcpTransport kcp)
                kcp.Port = port;

            if (clientToServerTransport is TelepathyTransport telepathy)
                telepathy.port = port;

            if (clientToServerTransport is SimpleWebTransport swt)
                swt.port = port;
        }

        public void UpdateRoomInfo(string newServerName = null, string newServerData = null, bool? newServerIsPublic = null, int? newPlayerCap = null)
        {
            if (_isServer)
            {
                int pos = 0;

                _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.UpdateRoomData);

                if (!string.IsNullOrEmpty(newServerName))
                {
                    _clientSendBuffer.WriteBool(ref pos, true);
                    _clientSendBuffer.WriteString(ref pos, newServerName);
                }
                else
                    _clientSendBuffer.WriteBool(ref pos, false);

                if (!string.IsNullOrEmpty(newServerData))
                {
                    _clientSendBuffer.WriteBool(ref pos, true);
                    _clientSendBuffer.WriteString(ref pos, newServerData);
                }
                else
                    _clientSendBuffer.WriteBool(ref pos, false);

                if (newServerIsPublic != null)
                {
                    _clientSendBuffer.WriteBool(ref pos, true);
                    _clientSendBuffer.WriteBool(ref pos, newServerIsPublic.Value);
                }
                else
                    _clientSendBuffer.WriteBool(ref pos, false);

                if (newPlayerCap != null)
                {
                    _clientSendBuffer.WriteBool(ref pos, true);
                    _clientSendBuffer.WriteInt(ref pos, newPlayerCap.Value);
                }
                else
                    _clientSendBuffer.WriteBool(ref pos, false);

                clientToServerTransport.ClientSend(0, new ArraySegment<byte>(_clientSendBuffer, 0, pos));
            }
        }

        Room GetServerForID(int serverID)
        {
            for(int i = 0; i < relayServerList.Count; i++)
            {
                if(relayServerList[i].serverId == serverID)
                    return relayServerList[i];
            }

            throw new Exception("LRM | An attempt was made to connect to a server which does not exist!");
        }

        void SendAuthKey()
        {
            int pos = 0;
            _clientSendBuffer.WriteByte(ref pos, (byte)OpCodes.AuthenticationResponse);
            _clientSendBuffer.WriteString(ref pos, authenticationKey);
            clientToServerTransport.ClientSend(0, new ArraySegment<byte>(_clientSendBuffer, 0, pos));
        }

        public enum OpCodes
        {
            Default = 0, RequestID = 1, JoinServer = 2, SendData = 3, GetID = 4, ServerJoined = 5, GetData = 6, CreateRoom = 7, ServerLeft = 8, PlayerDisconnected = 9, RoomCreated = 10,
            LeaveRoom = 11, KickPlayer = 12, AuthenticationRequest = 13, AuthenticationResponse = 14, Authenticated = 17, UpdateRoomData = 18, ServerConnectionData = 19, RequestNATConnection = 20,
            DirectConnectIP = 21
        }

        private static string GetLocalIp()
        {
            var host = Dns.GetHostEntry(Dns.GetHostName());
            foreach (var ip in host.AddressList)
            {
                if (ip.AddressFamily == AddressFamily.InterNetwork)
                {
                    return ip.ToString();
                }
            }

            return null;
        }
    }

    [Serializable]
    public struct Room
    {
        public string serverName;
        public int maxPlayers;
        public int serverId;
        public string serverData;
        public int hostId;
        public List<int> clients;

        public RelayAddress relayInfo;

        /// <summary>
        /// This variable is only available on the client
        /// </summary>
        public int currentPlayers { get => clients.Count + 1; }
    }

    [Serializable]
    public struct RelayAddress
    {
        public ushort Port;
        public ushort EndpointPort;
        public string Address;
    }

}
