using BestHTTP.WebSocket;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using MiniJSON;
using UnityEngine.SceneManagement;
using System.Linq;

public class NetHost : MonoBehaviour
{
    public int LocalPlayerID = -1;

    private int nextLocalItemId = 0;

    [Serializable]
    public struct NamedItem
    {
        public string Type;
        public Item Item;
    }
    public HashSet<int> Players = new HashSet<int>();
    // Start is called before the first frame update
    void Start()
    {
        DontDestroyOnLoad(this);

        Connect();
    }

    public NamedItem[] ItemsByType;

    private WebSocket socket;
    public void Connect()
    {
        Clear();
        

        socket = new WebSocket(new System.Uri("wss://blackout.mhack.io/websocket"));
        socket.PingFrequency = 15000;
        socket.StartPingThread = true;

        socket.OnOpen += Opened;
        socket.OnClosed += Closed;
        socket.OnMessage += Message;
        socket.OnError += Error;

        socket.Open();
    }

    private void Error(WebSocket webSocket, string reason)
    {
        Debug.Log("NET: ERROR: " + reason);
    }

    private void Closed(WebSocket webSocket, ushort code, string message)
    {
        Debug.Log("NET: CLOSED: " + message);
        Clear();
    }

    private void Message(WebSocket webSocket, string message)
    {
        Debug.Log("NET: MESSAGE: " + message);

        Dictionary<string, object> json = Json.Deserialize(message) as Dictionary<string, object>;

        if (json != null && json.ContainsKey("type"))
        {
            switch ((string)json["type"])
            {
                case "connected":
                    LocalPlayerID = Convert.ToInt32(json["player_id"]);
                    Debug.Log("NET: local player: " + LocalPlayerID);
                    break;
                case "enter_room":
                    {
                        DestroyRoom();

                        string map = (string)json["map"];
                        if (SceneManager.GetActiveScene().name != map)
                        {
                            SceneManager.LoadScene(map);
                        }
                        List<Dictionary<string, object>> items = json.List("items");

                        foreach (Dictionary<string, object> item in items)
                        {
                            UpdateItem(item);
                        }

                        Item player = SpawnItemLocal("player");

                        player.transform.position = new Vector3(UnityEngine.Random.Range(-4f, 4f), 1, UnityEngine.Random.Range(-4f, 4f));

                        break;
                    }
                case "update_items":
                    {
                        List<Dictionary<string, object>> items = json.List("items");

                        foreach (Dictionary<string, object> item in items)
                        {
                            UpdateItem(item);
                        }
                        break;
                    }
            }
        }
    }

    private List<Item> updatesThisFrame = new List<Item>();

    public void SendItemUpdate(Item item)
    {
        if (item.IsLocal)
        {
            updatesThisFrame.Add(item);
        }
    }

    void LateUpdate()
    {
        if (updatesThisFrame.Count > 0)
        {
            Dictionary<string, object> message = new Dictionary<string, object>();
            message.Add("type", "update_items");
            message.Add("items", updatesThisFrame.Select(i => i.GetItemData()).ToList());

            socket.Send(Json.Serialize(message));

            updatesThisFrame.Clear();
        }
    }

    private void DestroyRoom()
    {
        foreach (Item item in items.Values)
        {
            Destroy(item.gameObject);
        }
        items.Clear();
    }

    private Dictionary<string, Item> items = new Dictionary<string, Item>();

    public Item GetItemByType(string type)
    {
        return ItemsByType.FirstOrDefault(n => n.Type == type).Item;
    }

    private Item SpawnItemLocal(string type)
    {
        Item prefab = GetItemByType(type);
        if (prefab == null) return null;

        Item item = Instantiate<Item>(prefab);
        item.Type = type;
        item.PlayerID = LocalPlayerID;
        item.ItemID = nextLocalItemId++;

        item.gameObject.SendMessage("StartLocal", null, SendMessageOptions.DontRequireReceiver);

        SendItemUpdate(item);

        items[item.Key] = item;

        return item;
    }

    private void UpdateItem(Dictionary<string, object> itemData)
    {
        string key = (string)itemData["key"];

        bool shouldDelete = itemData.ContainsKey("__delete__");

        if (items.ContainsKey(key))
        {
            if (shouldDelete)
            {
                Destroy(items[key].RootTransform.gameObject);
                items.Remove(key);
            }
            else
            {
                items[key].UpdateFromData(itemData);
            }
        }
        else if (!shouldDelete)
        {
            int playerId = itemData.Key("player_id", Convert.ToInt32);
            int itemId = itemData.Key("item_id", Convert.ToInt32);
            string type = (string)itemData["type"];

            Item itemPrefab = GetItemByType(type);

            if (itemPrefab != null)
            {
                Item item = Instantiate<Item>(itemPrefab);
                item.Type = type;
                item.PlayerID = playerId;
                item.ItemID = itemId;

                item.gameObject.SendMessage("StartRemote", null, SendMessageOptions.DontRequireReceiver);

                item.UpdateFromData(itemData);

                items[item.Key] = item;
            }
            else
            {
                Debug.LogError("Can't find Item type " + type);
            }
        }
        
    }

    private void Opened(WebSocket webSocket)
    {
        Debug.Log("NET: OPENED");
    }

    private void Clear()
    {
        if (socket != null)
        {
            socket.OnOpen -= Opened;
            socket.OnClosed -= Closed;
            socket.OnMessage -= Message;
            socket.OnError -= Error;
        }
        socket = null;
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
