// add this component to the NetworkManager
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using UnityEngine;
using UnityEngine.UI;

namespace Mirror.Examples.ListServer
{
    public class ServerStatus
    {
        public string ip;
        //public ushort port; // <- not all transports use a port. assume default port. feel free to also send a port if needed.
        public string title;
        public ushort players;
        public ushort capacity;

        public int lastLatency = -1;
#if !UNITY_WEBGL // Ping isn't known in WebGL builds
        public Ping ping;
#endif
        public ServerStatus(string ip, /*ushort port,*/ string title, ushort players, ushort capacity)
        {
            this.ip = ip;
            //this.port = port;
            this.title = title;
            this.players = players;
            this.capacity = capacity;
#if !UNITY_WEBGL // Ping isn't known in WebGL builds
            ping = new Ping(ip);
#endif
        }
    }

    [RequireComponent(typeof(NetworkManager))]
    public class ListServer : MonoBehaviour
    {
        [Header("Listen Server Connection")]
        public string listServerIp = "127.0.0.1";
        public ushort gameServerToListenPort = 8887;
        public ushort clientToListenPort = 8888;
        public string gameServerTitle = "Deathmatch";

        Telepathy.Client gameServerToListenConnection = new Telepathy.Client();
        Telepathy.Client clientToListenConnection = new Telepathy.Client();

        [Header("UI")]
        public GameObject mainPanel;
        public Transform content;
        public Text statusText;
        public UIServerStatusSlot slotPrefab;
        public Button serverAndPlayButton;
        public Button serverOnlyButton;
        public GameObject connectingPanel;
        public Text connectingText;
        public Button connectingCancelButton;
        int connectingDots = 0;

        // all the servers, stored as dict with unique ip key so we can
        // update them more easily
        // (use "ip:port" if port is needed)
        Dictionary<string, ServerStatus> list = new Dictionary<string, ServerStatus>();

        void Start()
        {
            // examples
            //list["127.0.0.1"] = new ServerStatus("127.0.0.1", "Deathmatch", 3, 10);
            //list["192.168.0.1"] = new ServerStatus("192.168.0.1", "Free for all", 7, 10);
            //list["172.217.22.3"] = new ServerStatus("172.217.22.3", "5vs5", 10, 10);
            //list["172.217.16.142"] = new ServerStatus("172.217.16.142", "Hide & Seek Mod", 0, 10);

            // Update once a second. no need to try to reconnect or read data
            // in each Update call
            // -> calling it more than 1/second would also cause significantly
            //    more broadcasts in the list server.
            InvokeRepeating(nameof(Tick), 0, 1);
        }

        bool IsConnecting() => NetworkClient.active && !ClientScene.ready;
        bool FullyConnected() => NetworkClient.active && ClientScene.ready;

        // should we use the client to listen connection?
        bool UseClientToListen()
        {
            return !NetworkManager.isHeadless && !NetworkServer.active && !FullyConnected();
        }

        // should we use the game server to listen connection?
        bool UseGameServerToListen()
        {
            return NetworkServer.active;
        }

        void Tick()
        {
            TickGameServer();
            TickClient();
        }

        // send server status to list server
        void SendStatus()
        {
            BinaryWriter writer = new BinaryWriter(new MemoryStream());

            // create message
            writer.Write((ushort)NetworkServer.connections.Count);
            writer.Write((ushort)NetworkManager.singleton.maxConnections);
            byte[] titleBytes = Encoding.UTF8.GetBytes(gameServerTitle);
            writer.Write((ushort)titleBytes.Length);
            writer.Write(titleBytes);
            writer.Flush();

            // list server only allows up to 128 bytes per message
            if (writer.BaseStream.Position <= 128)
            {
                // send it
                gameServerToListenConnection.Send(((MemoryStream)writer.BaseStream).ToArray());
            }
            else Debug.LogError("[List Server] List Server will reject messages longer than 128 bytes. Please use a shorter title.");
        }

        void TickGameServer()
        {
            // send server data to listen
            if (UseGameServerToListen())
            {
                // connected yet?
                if (gameServerToListenConnection.Connected)
                {
                    SendStatus();
                }
                // otherwise try to connect
                // (we may have just started the game)
                else if (!gameServerToListenConnection.Connecting)
                {
                    Debug.Log("[List Server] GameServer connecting......");
                    gameServerToListenConnection.Connect(listServerIp, gameServerToListenPort);
                }
            }
            // shouldn't use game server, but still using it?
            else if (gameServerToListenConnection.Connected)
            {
                gameServerToListenConnection.Disconnect();
            }
        }

        void ParseMessage(byte[] bytes)
        {
            // note: we don't use ReadString here because the list server
            //       doesn't know C#'s '7-bit-length + utf8' encoding for strings
            BinaryReader reader = new BinaryReader(new MemoryStream(bytes, false), Encoding.UTF8);
            byte ipBytesLength = reader.ReadByte();
            byte[] ipBytes = reader.ReadBytes(ipBytesLength);
            string ip = new IPAddress(ipBytes).ToString();
            //ushort port = reader.ReadUInt16(); <- not all Transports use a port. assume default.
            ushort players = reader.ReadUInt16();
            ushort capacity = reader.ReadUInt16();
            ushort titleLength = reader.ReadUInt16();
            string title = Encoding.UTF8.GetString(reader.ReadBytes(titleLength));
            //Debug.Log("PARSED: ip=" + ip + /*" port=" + port +*/ " players=" + players + " capacity= " + capacity + " title=" + title);

            // build key
            string key = ip/* + ":" + port*/;

            // find existing or create new one
            if (list.TryGetValue(key, out ServerStatus server))
            {
                // refresh
                server.title = title;
                server.players = players;
                server.capacity = capacity;
            }
            else
            {
                // create
                server = new ServerStatus(ip, /*port,*/ title, players, capacity);
            }

            // save
            list[key] = server;
        }

        void TickClient()
        {
            // receive client data from listen
            if (UseClientToListen())
            {
                // connected yet?
                if (clientToListenConnection.Connected)
                {
                    // receive latest game server info
                    while (clientToListenConnection.GetNextMessage(out Telepathy.Message message))
                    {
                        // connected?
                        if (message.eventType == Telepathy.EventType.Connected)
                            Debug.Log("[List Server] Client connected!");
                        // data message?
                        else if (message.eventType == Telepathy.EventType.Data)
                            ParseMessage(message.data);
                        // disconnected?
                        else if (message.eventType == Telepathy.EventType.Connected)
                            Debug.Log("[List Server] Client disconnected.");
                    }

#if !UNITY_WEBGL // Ping isn't known in WebGL builds
                    // ping again if previous ping finished
                    foreach (ServerStatus server in list.Values)
                    {
                        if (server.ping.isDone)
                        {
                            server.lastLatency = server.ping.time;
                            server.ping = new Ping(server.ip);
                        }
                    }
#endif
                }
                // otherwise try to connect
                // (we may have just joined the menu/disconnect from game server)
                else if (!clientToListenConnection.Connecting)
                {
                    Debug.Log("[List Server] Client connecting...");
                    clientToListenConnection.Connect(listServerIp, clientToListenPort);
                }
            }
            // shouldn't use client, but still using it? (e.g. after joining)
            else if (clientToListenConnection.Connected)
            {
                clientToListenConnection.Disconnect();
                list.Clear();
            }

            // refresh UI afterwards
            OnUI();
        }

        // instantiate/remove enough prefabs to match amount
        public static void BalancePrefabs(GameObject prefab, int amount, Transform parent)
        {
            // instantiate until amount
            for (int i = parent.childCount; i < amount; ++i)
            {
                Instantiate(prefab, parent, false);
            }

            // delete everything that's too much
            // (backwards loop because Destroy changes childCount)
            for (int i = parent.childCount-1; i >= amount; --i)
                Destroy(parent.GetChild(i).gameObject);
        }

        void OnUI()
        {
            // only show while client not connected and server not started
            if (!NetworkManager.singleton.isNetworkActive || IsConnecting())
            {
                mainPanel.SetActive(true);

                // status text
                if (clientToListenConnection.Connecting)
                {
                    //statusText.color = Color.yellow;
                    statusText.text = "Connecting...";
                }
                else if (clientToListenConnection.Connected)
                {
                    //statusText.color = Color.green;
                    statusText.text = "Connected!";
                }
                else
                {
                    //statusText.color = Color.gray;
                    statusText.text = "Disconnected";
                }

                // instantiate/destroy enough slots
                BalancePrefabs(slotPrefab.gameObject, list.Count, content);

                // refresh all members
                for (int i = 0; i < list.Values.Count; ++i)
                {
                    UIServerStatusSlot slot = content.GetChild(i).GetComponent<UIServerStatusSlot>();
                    ServerStatus server = list.Values.ToList()[i];
                    slot.titleText.text = server.title;
                    slot.playersText.text = server.players + "/" + server.capacity;
                    slot.latencyText.text = server.lastLatency != -1 ? server.lastLatency.ToString() : "...";
                    slot.addressText.text = server.ip;
                    slot.joinButton.interactable = !IsConnecting();
                    slot.joinButton.gameObject.SetActive(server.players < server.capacity);
                    slot.joinButton.onClick.RemoveAllListeners();
                    slot.joinButton.onClick.AddListener(() => {
                        NetworkManager.singleton.networkAddress = server.ip;
                        NetworkManager.singleton.StartClient();
                    });
                }

                // server buttons
                serverAndPlayButton.interactable = !IsConnecting();
                serverAndPlayButton.onClick.RemoveAllListeners();
                serverAndPlayButton.onClick.AddListener(() => {
                    NetworkManager.singleton.StartHost();
                });

                serverOnlyButton.interactable = !IsConnecting();
                serverOnlyButton.onClick.RemoveAllListeners();
                serverOnlyButton.onClick.AddListener(() => {
                    NetworkManager.singleton.StartServer();
                });
            }
            else mainPanel.SetActive(false);

            // show connecting panel while connecting
            if (IsConnecting())
            {
                connectingPanel.SetActive(true);

                // . => .. => ... => ....
                connectingDots = ((connectingDots + 1) % 4);
                connectingText.text = "Connecting" + new string('.', connectingDots);

                // cancel button
                connectingCancelButton.onClick.RemoveAllListeners();
                connectingCancelButton.onClick.AddListener(NetworkManager.singleton.StopClient);
            }
            else connectingPanel.SetActive(false);
        }

        // disconnect everything when pressing Stop in the Editor
        void OnApplicationQuit()
        {
            if (gameServerToListenConnection.Connected)
                gameServerToListenConnection.Disconnect();
            if (clientToListenConnection.Connected)
                clientToListenConnection.Disconnect();
        }
    }
}
