using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
using UnityEngine;

namespace Neuralyzer.Core
{
  /// <summary>
  /// Handles all interaction with the actual websocket. 
  /// </summary>
  public class NeuraCore : MonoBehaviour
  {
    #region Singleton pattern
    public static NeuraCore Instance
    {
      get
      {
        if (instance)
          return instance;
        GameObject o = Instantiate(Resources.Load("Neuralyzer/Core")) as GameObject;
        if (o != null)
          return instance ?? (instance = o
                   .GetComponent<NeuraCore>());
        return null;
      }
    }
    private static NeuraCore instance;

    #endregion

    [Tooltip("Configuration for your neuralyzer server")]
    public NeuralyzerConfig config;
    [Tooltip("for debugging. do not change, may break your connection")]
    public ConnectionState connectionState;

    #region Internal variables
    internal WebSocket socket;
    private WaitForSeconds tickWait;
    private WaitForEndOfFrame eof;
    private bool isActive;
    private string updateString;
    private string connectionString;
    internal Queue<ErrorEventArgs> errorEventArgs = new Queue<ErrorEventArgs>();
    private float lastPulse = float.MaxValue;
    private float reconnectTime = 0.1f;
    private int reconnectAttempts;
    private Coroutine timeoutCoroutine;
    private Coroutine triggerReconnect;
    private string initialStateString;

    #endregion

    // called when the websocket connection is gracefully closed, or after the connection times out after a hard disconnect
    public event EventHandler<CloseEventArgs> OnClose;
    // called the frame after an error occurs due to some websocket implementations being handled on separate threads, and the ability to call unity functions is desired
    public event EventHandler<ErrorEventArgs> OnError;
    // processed in order after being added to a queue by the socket. this is called when the message is processed NOT when it is received
    public event EventHandler<MessageEventArgs> OnMessage;
    // called when a socket is successfully opened
    public event EventHandler OnOpen;

    /// <summary>
    /// Any message stored here will be picked up during the next client tick and sent to the server, then this will be cleared
    /// </summary>
    /// <param name="updateVal">string to be sent to server. typically json encoded</param>
    public void SetUpdate(string updateVal) // todo: add byte array
    {
      if(!string.IsNullOrEmpty(updateString) && connectionState != ConnectionState.Connected)
        return;
      updateString = updateVal;
    }

    public void SendInitialState(string initialVal)
    {
      if (!string.IsNullOrEmpty(initialStateString))
      {
        print("initial state string not empty: " + initialStateString);
      }
      initialStateString = initialVal;
    }

    public void JoinRoom(string roomName, string userName)
    {
      string jReq = JsonConvert.SerializeObject(new
      {
        msgType = "socket:createOrJoinRoom",
        data = new
        {
          room = roomName,
          name = userName,
          userId = Guid.NewGuid().ToString(),
          deviceType = 0
        }
      });
      socket.SendString(jReq);
      print("join request " + jReq + " sent");
    }

    private void Awake()
    {
      instance = this;
      Application.runInBackground = true;
      if (!config)
      {
        Debug.LogError("Config is required");
      }
      DontDestroyOnLoad(gameObject);
      tickWait = new WaitForSeconds(1 / (float) config.UpdateHz);
      eof = new WaitForEndOfFrame();
    }

    /// <summary>
    /// poll for error processing
    /// this is because errors can come from a different thread
    /// </summary>
    private void Update()
    {
      if (errorEventArgs.Count <= 0) return;
      ErrorEventArgs error = errorEventArgs.Dequeue();
      if (OnError != null) OnError.Invoke(this, error);
    }

    private IEnumerator Start()
    {
      WaitForEndOfFrame w = new WaitForEndOfFrame();
      if (config.ConnectOnStart)
      {
        StartCoroutine(Connect());
      }

      while (socket == null || !socket.isConnected)
      {
        yield return w;
      }

      isActive = true;
      StartCoroutine(Receive());
      StartCoroutine(Send());
    }

    public IEnumerator Connect()
    {
      //StartCoroutine(Receive());
      //StartCoroutine(Send());
      if (connectionState == ConnectionState.Disconnected)
      {
        socket = null;
        connectionState = ConnectionState.Connecting;
        Debug.Log("Connecting...");
        socket = new WebSocket(config.ConnectionEndpoint + ":" + config.Port + "/live");
        socket.OnClose += (sender, e) =>
        {
          connectionState = ConnectionState.Disconnected;
          if (timeoutCoroutine != null)
            StopCoroutine(timeoutCoroutine);
          if (e.Reason == "System Timeout")
          {
            triggerReconnect = StartCoroutine(TriggerReconnect());
          }
          if (OnClose != null) OnClose.Invoke(sender, e);
        };
        socket.OnError += (sender, e) =>
        {
          errorEventArgs.Enqueue(e);
        };
        socket.OnMessage += (sender, e) =>
        {
          string stData = e.Data ?? Encoding.UTF8.GetString(e.RawData);
#if UNITY_EDITOR
          Debug.Log(stData);
#endif
          // handle heartbeat
          if (stData == "pulse")
          {
            lastPulse = Time.realtimeSinceStartup;
            reconnectAttempts = 0;
            reconnectTime = 0.1f;
            socket.SendString("blip");
            return;
          }
          if (OnMessage != null) OnMessage.Invoke(sender, e);
        };
        socket.OnOpen += (sender, e) =>
        {
          lastPulse = float.MaxValue;
          timeoutCoroutine = StartCoroutine(TimeoutHandler());
          if (triggerReconnect != null)
            StopCoroutine(triggerReconnect);
          if (OnOpen != null) OnOpen.Invoke(sender, e);
        };
        yield return StartCoroutine(socket.Connect());
#if UNITY_METRO && !UNITY_EDITOR // because messages are received on another thread they need to be processed in a different way than on webgl or in editor
      StartCoroutine(socket.ProcessMessages());
#endif
      }
    }

    public void Disconnect()
    {
      if (socket != null) socket.Close();
    }

    private IEnumerator TriggerReconnect()
    {
      yield return new WaitForEndOfFrame();
      while (config.AutoReconnectCount > reconnectAttempts)
      {
        reconnectAttempts++;
        yield return new WaitForSeconds(reconnectTime);
        if (reconnectTime < 10f)
          reconnectTime *= 10;
        yield return StartCoroutine(Connect());
      }
      Debug.LogError("Full time out, check your network connection");
    }

    private IEnumerator TimeoutHandler()
    {
      WaitForEndOfFrame w = new WaitForEndOfFrame();
      while (Application.isPlaying)
      {
        if (Time.realtimeSinceStartup - lastPulse > config.TimeOut)
        {
          Debug.LogError("System Timeout");
          socket.Close("System Timeout");
          break;
        }
        yield return w;
      }
    }


    private IEnumerator Receive()
    {
      while (isActive)
      {
        while (socket == null || !socket.isConnected)
        {
          yield return eof;
        }
        //receive information as soon as possible
        socket.RecvString();
        yield return eof;
      }
    }

    private IEnumerator Send()
    {
      while (isActive)
      {
        if (!string.IsNullOrEmpty(initialStateString))
        {
          print("initial state being sent to server " + initialStateString);
          socket.SendString(initialStateString);
          initialStateString = "";
          yield return tickWait;
          continue;
        }
        // send out delta state changes every tick
        if (!string.IsNullOrEmpty(updateString) && connectionState == ConnectionState.Connected)
        {
#if UNITY_EDITOR
          //print(updateString);
#endif
          socket.SendString(updateString);
          updateString = "";
        }
        yield return tickWait;
      }
    }

    private void OnApplicationQuit()
    {
      //if there is an active socket close it. without this unity will crash if you try to play in editor after stopping.
      Disconnect();
      StopAllCoroutines();
    }
  }

  public enum ConnectionState : byte
  {
    Disconnected,
    Connecting,
    Connected,
    Disconnecting
  }
}
