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://cells.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);
                        }

                        List<Dictionary<string, object>> tags = FindObjectsOfType<ServerTag>().Select(t => t.GetJSON()).ToList();

                        Dictionary<string, object> tagMessage = new Dictionary<string, object>();
                        tagMessage.Add("type", "server_tags");
                        tagMessage.Add("tags", tags);

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

                        //Item player = SpawnItemLocal("player", p =>
                        //{
                        //    p.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;
                    }
                case "take_ownership":
                    {
                        List<Dictionary<string, object>> items = json.List("items");

                        foreach (Dictionary<string, object> item in items)
                        {
                            TakeOwnership(item);
                        }
                        break;
                    }
                case "item_command":
                    {
                        List<string> itemKeys = json.ListString("item_keys");

                        Dictionary<string, object> details = json.Obj("details");

                        string command = "" + json["command"];

                        foreach (string key in itemKeys)
                        {
                            if (items.ContainsKey(key) && items[key].IsLocal)
                            {
                                items[key].RootTransform.SendMessage(command, details, SendMessageOptions.DontRequireReceiver);
                            }
                        }
                        break;
                    }
            }
        }
    }

    private List<Item> updatesThisFrame = new List<Item>();
    private List<Dictionary<string, object>> manualUpdatesThisFrame = new List<Dictionary<string, object>>();

    public void SendItemUpdate(Item item, bool manual = false)
    {
        if (item.IsLocal)
        {
            if (manual)
            {
                manualUpdatesThisFrame.Add(item.GetItemData());
            }
            else
            {
                updatesThisFrame.Add(item);
            }
        }
    }

    void LateUpdate()
    {
        if (updatesThisFrame.Count > 0 || manualUpdatesThisFrame.Count > 0)
        {
            List<Dictionary<string, object>> updates = new List<Dictionary<string, object>>();
            updates.AddRange(updatesThisFrame.Select(i => i.GetItemData()));
            updates.AddRange(manualUpdatesThisFrame);

            Dictionary<string, object> message = new Dictionary<string, object>();
            message.Add("type", "update_items");
            message.Add("items", updates);

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

            updatesThisFrame.ForEach(
                i =>
                {
                    i.Details.Remove("__change_ownership_old_key__");
                }
            );
            updatesThisFrame.Clear();
            manualUpdatesThisFrame.Clear();
        }
    }

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

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

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

    public Item SpawnItemLocal(string type, Action<Item> setupBeforeInitializing = null)
    {
        Item prefab = GetItemByType(type);
        if (prefab == null) return null;

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

        if (setupBeforeInitializing != null) setupBeforeInitializing(item);

        item.RootTransform.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"];

        if (itemData.ContainsKey("__change_ownership_old_key__"))
        {
            string oldKey = (string)itemData["__change_ownership_old_key__"];

            if (oldKey != key)
            {
                itemData.Remove("__change_ownership_old_key__");

                if (items.ContainsKey(oldKey))
                {
                    int playerId = itemData.Key("player_id", Convert.ToInt32);

                    if (playerId != LocalPlayerID)
                    {
                        int itemId = itemData.Key("item_id", Convert.ToInt32);

                        Item item = items[oldKey];

                        items.Remove(oldKey);

                        item.PlayerID = playerId;
                        item.ItemID = itemId;

                        items[item.Key] = item;
                    }
                }
                else
                {
                    //Debug.Log("Received rename key but didn't have the original");
                }
            }
            else
            {
                //Debug.Log("Received rename key but already renamed");
            }
        }

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

        if (items.ContainsKey(key))
        {
            if (shouldDelete)
            {
                DeleteItemInternal(items[key], key, false);
                
            }
            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)
            {
                if (itemId < 0)
                {
                    if (playerId != LocalPlayerID)
                    {
                        Debug.Log("Info about non-spawned, non-owned item?");
                        return;
                    }
                    itemId = nextLocalItemId++;
                }

                Item item = Instantiate<Item>(itemPrefab);
                item.Type = type;
                item.PlayerID = playerId;

                item.ItemID = itemId;

                item.UpdateFromData(itemData);
                items[item.Key] = item;

                if (item.IsLocal)
                {
                    item.RootTransform.SendMessage("StartLocal", null, SendMessageOptions.DontRequireReceiver);
                    SendItemUpdate(item);
                }
                else
                {
                    item.RootTransform.SendMessage("StartRemote", null, SendMessageOptions.DontRequireReceiver);
                }
            }
            else
            {
                Debug.LogError("Can't find Item type " + type);
            }
        }
        
    }

    public void DestroyItem(Item item)
    {
        if (item.IsLocal)
        {
            item.Details["__delete__"] = true;
            DeleteItemInternal(item);
        }
    }

    private void DeleteItemInternal(Item item, string useKey = null, bool sendUpdate = true)
    {        
        items.Remove(useKey == null ? item.Key : useKey);

        if (sendUpdate)
        {
            SendItemUpdate(item, true);
        }

        if (item.ManualDelete)
        {
            item.RootTransform.SendMessage("ItemDestroy", null, SendMessageOptions.DontRequireReceiver);
        }
        else
        {
            Destroy(item.RootTransform.gameObject);
        }
    }

    public void SendItemCommand(string itemKey, string command, Dictionary<string, object> details = null)
    {
        SendItemsCommand(new List<string> { itemKey }, command, details);
    }

    public void SendItemsCommand(List<string> itemKeys, string command, Dictionary<string, object> details = null)
    {
        if (details == null) details = new Dictionary<string, object>();

        Dictionary<string, object> message = new Dictionary<string, object>();
        message["type"] = "item_command";
        message["command"] = command;
        message["details"] = details;
        message["item_keys"] = itemKeys;

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

    private void TakeOwnership(Dictionary<string, object> itemData)
    {
        int playerId = itemData.Key("player_id", Convert.ToInt32);
        if (playerId != LocalPlayerID) return;

        string key = (string)itemData["key"];

        if (items.ContainsKey(key))
        {
            Item item = items[key];
            items.Remove(key);

            item.PlayerID = LocalPlayerID;
            item.ItemID = nextLocalItemId++;

            item.Details["__change_ownership_old_key__"] = key;

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

            SendItemUpdate(item);
        }
    }

    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()
    {
        
    }
}
