﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Neuralyzer.Core;
using Neuralyzer.Transport;
using Newtonsoft.Json;
using UnityEngine;

namespace Neuralyzer.Components
{
  public class NeuraManager : MonoBehaviour
  {
    public static NeuraManager Instance;

    public RoomStateGen globalState;
    public float deltaTolerance;
    public string localUser;
    public List<string> prefabNames;
    public List<GameObject> prefabs;

    #region Bridge

    public event EventHandler OnConnected;
    public event Action<object, UserJoinedEventArgs> OnUserJoined;
    public event Action<object, ErrorMessageEventArgs> OnErrorEvent;
    public event Action<object, UserLeftEventArgs> OnUserLeft;
    public event Action<object, PropertiesChangedEventArgs> OnPropertiesChanged;
    public event Action<object, List<string>> OnRoomsListChanged;
    public event Action<object, string> OnRoomJoined;
    public event Action<object, string> OnRoomCreated;
    public event EventHandler OnClosed;

    public bool inRoom;
    public string RoomName;
    public List<string> UsersInRoom;
    public List<string> RoomsAvailable;

    #endregion

    private float updateTime = float.MaxValue;
    private float lastUpdate;

    private Dictionary<string, ITrackable> trackers;

    private List<RoomObject> newRoomObjects;
    private RoomStateGen oldStateGen;

    private void Awake()
    {
      Instance = this;
      trackers = new Dictionary<string, ITrackable>();
      newRoomObjects = new List<RoomObject>();
      UsersInRoom = new List<string>();
      RoomsAvailable = new List<string>();
    }

    public IEnumerator Start()
    {
      globalState = new RoomStateGen();
      WaitForEndOfFrame w = new WaitForEndOfFrame();
      NeuraCore.Instance.OnError += ErrorHandler;
      NeuraCore.Instance.OnClose += CloseHandler;
      NeuraCore.Instance.OnOpen += OpenHandler;
      NeuraCore.Instance.OnMessage += MessageHandler;
      InvokeRepeating("RefreshRoomList", 0.1f, 2f);
      while (NeuraCore.Instance.socket == null)
      {
        yield return w;
      }
      updateTime = 1f / NeuraCore.Instance.config.UpdateHz;
    }

    #region Event Handlers

    private void MessageHandler(object sender, MessageEventArgs e)
    {
      string stData = e.Data ?? Encoding.UTF8.GetString(e.RawData);
      ServerMessage msg = JsonConvert.DeserializeObject<ServerMessage>(stData);
      switch (msg.msgType)
      {
        case "socket:ready":
          //connect to room
          print("connected to server");
          if (OnConnected != null) OnConnected.Invoke(this, new EventArgs());
          break;
        case "room:state:update":
          //handle message
          if (msg.data == null)
          {
            print("empty state update. this should not happen");
            return;
          }
          StateUpdate sup = JsonConvert.DeserializeObject<StateUpdate>(msg.data.ToString());
          if (sup.props != null && sup.props.Count > 0)
          {
            print("Got a properties changed message");
            if (OnPropertiesChanged != null)
              OnPropertiesChanged.Invoke(this, new PropertiesChangedEventArgs {propsChanged = sup.props});
          }
          UpdateLocalState(sup);
          break;
        case "socket:room:joined":
        {
          print("Joined room ");
          inRoom = true;
          UsersInRoom.Add(localUser);
          NeuraCore.Instance.connectionState = ConnectionState.Connected;
          print(msg.data);
          InitialState initState = JsonConvert.DeserializeObject<InitialState>(msg.data.ToString());
          foreach (object participant in initState.participants)
          {
            UserObject user = JsonConvert.DeserializeObject<UserObject>(participant.ToString());
            UsersInRoom.Add(user.name);
          }
          print("Making initial state object");
          var newRoomProps = JsonConvert.DeserializeObject<PropsOnlyState>(initState.state.ToString()).props;
          print("state object created");
          var initStateSUP = new StateUpdate
          {
            props = newRoomProps
          };
          if (OnPropertiesChanged != null)
            OnPropertiesChanged.Invoke(this, new PropertiesChangedEventArgs
            {
              propsChanged = newRoomProps
            });
          UpdateLocalState(initStateSUP);
          if (OnRoomJoined != null) OnRoomJoined.Invoke(this, "");
          RoomName = initState.name;
        }
          break;
        case "room:created":
          RoomCreatedMessage createMsg = JsonConvert.DeserializeObject<RoomCreatedMessage>(msg.data.ToString());
          RoomName = createMsg.roomName;
          print("room " + RoomName + " has been created");
          if (OnRoomCreated != null) OnRoomCreated.Invoke(this, createMsg.roomName);
          NeuraCore.Instance.SendInitialState(JsonConvert.SerializeObject(new
          {
            msgType = "room:state:update",
            data = new StateUpdate
            {
              props = globalState.props
            }
          }));
          break;
        case "room:user:onjoined":
        {
          UserJLMessage jlm = JsonConvert.DeserializeObject<UserJLMessage>(msg.data.ToString());
          UserObject user = JsonConvert.DeserializeObject<UserObject>(jlm.participant.ToString());
          print(user.name + " has joined the room");
          UsersInRoom.Add(user.name);
          if (OnUserJoined != null)
            OnUserJoined.Invoke(this, new UserJoinedEventArgs
            {
              username = user.name
            });
        }
          break;
        case "room:user:onleft":
        {
          UserJLMessage jlm = JsonConvert.DeserializeObject<UserJLMessage>(msg.data.ToString());
          UserObject user = JsonConvert.DeserializeObject<UserObject>(jlm.participant.ToString());
          print(user.name + " has left the room");
          if (UsersInRoom.Contains(user.name))
            UsersInRoom.Remove(user.name);
          print(msg.data);
          if (OnUserLeft != null)
            OnUserLeft.Invoke(this, new UserLeftEventArgs
            {
              username = user.name
            });
        }
          break;
        default:
          print(msg.msgType + "\n " + msg.data);
          break;
      }
    }

    private void OpenHandler(object sender, EventArgs e)
    {
      Debug.Log("Socket connection opened");
    }

    private void CloseHandler(object sender, CloseEventArgs e)
    {
      inRoom = false;
      if (OnClosed != null) OnClosed.Invoke(this, new EventArgs());
      Debug.Log("SocketClosed. Reason: " + e.Reason);
    }

    private void ErrorHandler(object sender, ErrorEventArgs e)
    {
      Debug.LogError(e.Message);
    }

    #endregion

    public void JoinRoom(string roomToJoin, string userName)
    {
      if (NeuraCore.Instance.socket == null || !NeuraCore.Instance.socket.isConnected)
      {
        print("Not connected, trying to auto connect");
        RoomName = roomToJoin;
        localUser = userName;
        OnConnected += AutoJoinRoom;
        Connect();
        return;
      }
      if (inRoom)
      {
        print("Cannot join room when already in one");
        return;
      }
      NeuraCore.Instance.JoinRoom(roomToJoin, userName);
    }

    public void LeaveRoom()
    {
      if (!inRoom)
      {
        print("Cannot leave a room if you aren't in one");
        return;
      }
      RoomName = "";
      UsersInRoom = new List<string>();

      NeuraCore.Instance.Disconnect();
    }

    public bool AddSceneObject(NStateTracker tracker)
    {
      if (trackers.ContainsKey(tracker.id))
        return false;
      trackers.Add(tracker.id, tracker);
      (globalState.objects ?? (globalState.objects = new List<RoomObject>())).Add(tracker.ToRoomObject());
      newRoomObjects.Add(tracker.ToRoomObject());
      return true;
    }

    public void Connect()
    {
      print("Attempting to connect");
      StartCoroutine(NeuraCore.Instance.Connect());
    }

    public void RefreshRoomList()
    {
      StartCoroutine(refreshRooms());
    }

    internal void AddUserObject(string id, ITrackable toAdd)
    {
      print("adding object " + id);
      trackers.Add(id, toAdd);
      (globalState.objects ?? (globalState.objects = new List<RoomObject>())).Add(toAdd.ToRoomObject());
      newRoomObjects.Add(toAdd.ToRoomObject());
    }

    internal void RemoveUserObject(string id)
    {
      if (trackers.ContainsKey(id))
      {
        trackers.Remove(id);
      }
      if (globalState.objects.Any(o => o.id == id))
        globalState.objects.Remove(globalState.objects.First(tr => tr.id == id));
    }

    private IEnumerator refreshRooms()
    {
      var request = new WWW(NeuraCore.Instance.config.ConnectionEndpoint.Replace(NeuraCore.Instance.config.ConnectionEndpoint.Contains("wss") ? "wss" : "ws","https") + "/api/rooms");
      yield return request;
      if (request.text != "[]")
      {
        var temp = JsonConvert.DeserializeObject<List<InitialState>>(request.text);
        if (temp != null && temp.Count > 0)
        {
          RoomsAvailable = temp.Select(i => i.name).ToList();
        }
        else
        {
          RoomsAvailable.Clear();
        }
        if (OnRoomsListChanged != null) OnRoomsListChanged.Invoke(this, RoomsAvailable);
      }
      else
      {
        RoomsAvailable.Clear();
      }
    }

    /// <summary>
    /// Updates the local copy of the global state via server delta
    /// </summary>
    /// <param name="serverUpdate">changes to the global state since last server tick</param>
    private void UpdateLocalState(StateUpdate serverUpdate)
    {
      oldStateGen = new RoomStateGen(globalState);

      #region properties update

      foreach (KeyValuePair<string, string> prop in serverUpdate.props)
      {
        print(prop.Key + " : " + prop.Value);
        if (globalState.props.ContainsKey(prop.Key)) //update value or delete with null
        {
          if (prop.Value == null)
          {
            //delete or hide thing?
          }
          else
          {
            globalState.props[prop.Key] = prop.Value;
          }
        }
        else
        {
          globalState.props.Add(prop.Key, prop.Value);
        }
      }

      #endregion

      if (serverUpdate.delete.Count > 0) //delete these object ids from the global state
      {
        foreach (string s in serverUpdate.delete)
        {
          if (globalState.objects.Any(tr => tr.id == s))
            globalState.objects.Remove(globalState.objects.Single(o => o.id == s));
        }
      }
      if (serverUpdate.create.Count > 0) //create these objects and add them to the global state
      {
        foreach (RoomObject pair in serverUpdate.create)
        {
          if (!trackers.ContainsKey(pair.id))
          {
            if (globalState.objects.Contains(pair))
              continue;
            CreateObject(pair); 
          }
          else
          { //if multiple create messages are sent for the same object destroy the oldest ones and keep the newest
            if (globalState.objects.Any(tr => tr.id == pair.id))
              globalState.objects.Remove(globalState.objects.Single(o => o.id == pair.id));
            //Destroy(((/* Your Monobehaviour that implements ITrackable*/) trackers[pair.id]).gameObject);
            trackers.Remove(pair.id);
            CreateObject(pair); 
          }
        }
      }
      if (serverUpdate.update.Count > 0)
      {
        foreach (RoomObject pair in serverUpdate.update)
        {
          if (!trackers.ContainsKey(pair.id))
          {
            Debug.Log("Object created from update. please use the create function instead");
            CreateObject(pair);
            continue;
          }
          //trackers[pair.id].MoveTo(JsonConvert.DeserializeObject<Vector3>(pair.props["position"].ToString()));
          //trackers[pair.id].LookAt(JsonConvert.DeserializeObject<Vector3>(pair.props["lookDirection"].ToString()));
          trackers[pair.id].UpdateFromRoomObject(pair);
        }
      }
    }

    /// <summary>
    /// Creates a local copy of a remote object
    /// </summary>
    /// <param name="newObj"></param>
    private void CreateObject(RoomObject newObj)
    {
      ITrackable tracker;
      GameObject go;
      if (prefabNames.Contains((string) newObj.props["prefab"]))
      {
        go = Instantiate<GameObject>(prefabs[prefabNames.IndexOf((string) newObj.props["prefab"])]);
        tracker = go.GetComponent<ITrackable>();
      }
      else
      {
        go = new GameObject("Prefab " + (string) newObj.props["prefab"] + " not found");
        tracker = go.AddComponent<NStateTracker>();
      }
      tracker.id = newObj.id;
      tracker.prefab = (string) newObj.props["prefab"];
      tracker.UpdateFromRoomObject(newObj);
      trackers.Add(tracker.id, tracker);
      globalState.objects.Add(newObj);
    }

    /// <summary>
    /// Upload any changes in objects owned by the local player, in scene properties, or scene objects
    /// </summary>
    private void UploadGlobalState()
    {
      lastUpdate = Time.realtimeSinceStartup;
      if (!inRoom) return;
      List<RoomObject> localObjectsUpdate = new List<RoomObject>();

      StateUpdate diffState = null;
      if (globalState.objects != null)
      {
        foreach (RoomObject pair in globalState.objects)
        {
          if (!trackers[pair.id].isLocal) continue;
          if (!CompareDictionaries(pair.props, trackers[pair.id].ToRoomObject().props))
          {
            localObjectsUpdate.Add(trackers[pair.id].ToRoomObject());
          }
        }

        //update the local copy of the global state with the new values for player controlled and scene objects
        for (int i = 0; i < localObjectsUpdate.Count; i++)
        {
          globalState.objects[globalState.objects.IndexOf(localObjectsUpdate[i])] = localObjectsUpdate[i];
        }

        if (localObjectsUpdate.Count != 0 || newRoomObjects.Count != 0)
        {
          diffState = new StateUpdate {update = localObjectsUpdate};
          if (newRoomObjects.Count > 0)
            diffState.create = newRoomObjects;


          newRoomObjects = new List<RoomObject>();
        }
      }
      else
      {
        print("globalState has no objects");
      }
      if (globalState.props.Count > 0)
      {
        foreach (KeyValuePair<string, string> stateProp in globalState.props)
        {
          //print(stateProp.Key + " : " + stateProp.Value );
          if (oldStateGen != null && oldStateGen.props.ContainsKey(stateProp.Key))
          {
            if (oldStateGen.props[stateProp.Key] != stateProp.Value)
              (diffState ?? (diffState = new StateUpdate())).UpdateProperty(stateProp.Key, stateProp.Value);
          }
          else
          {
            (diffState ?? (diffState = new StateUpdate())).UpdateProperty(stateProp.Key, stateProp.Value);
          }
        }
      }

      if (diffState == null) return;
      //Serialize the updates to json and pass them to the NeuraCore to be sent to the server
      NeuraCore.Instance.SetUpdate(JsonConvert.SerializeObject(new
      {
        msgType = "room:state:update",
        data = diffState
      }));
      //Find all local owned and scene objects that have changed since the last tick
      oldStateGen = new RoomStateGen(globalState);
    }

    private void Update()
    {
      if (lastUpdate + updateTime < Time.realtimeSinceStartup)
      {
        UploadGlobalState();
      }
    }

    private void AutoJoinRoom(object sender, EventArgs e)
    {
      print("auto joining room");
      NeuraCore.Instance.JoinRoom(RoomName, localUser);
      OnConnected -= AutoJoinRoom;
    }

    private bool CompareDictionaries(Dictionary<string, object> d1, Dictionary<string, object> d2)
    {
      if (d1 == null)
      {
        return d2 == null;
      }
      if (d2 == null)
        return false;
      if (d1.Count == d2.Count)
      {
        foreach (var pair in d1)
        {
          if (!d2.ContainsKey(pair.Key))
          {
            return false;
          }
          if (pair.Value.ToString() != d2[pair.Key].ToString())
          {
            return false;
          }
        }
        return true;
      }
      return false;
    }
  }
}
