﻿using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using UnityEngine;

#if WINDOWS_UWP
using Windows.Networking.Sockets;
using Windows.Storage.Streams;
#else
using WebSocketSharp;
#endif

public interface IWebSocket
{
    void Connect();
    void OnOpen(Action callback);
    void OnClose(Action<WebSocketReason> callback);
    void OnError(Action<WebSocketReason> callback);
    void OnMessage(Action<string> callback);
    void Close();

    void Send(string data);
}

public class WebSocketReason
{
    public WebSocketReason(string reason)
    {
        Reason = reason;
    }

    public string Reason { get; private set; }
}

#if WINDOWS_UWP

public class UWPWebSocket : IWebSocket
{
    private readonly string _uri;
    private readonly MessageWebSocket _socket;
    private readonly DataWriter _writer;
    private Action<WebSocketReason> _errorCallback = r => { };
    private Action _openCallback = () => { };

    public UWPWebSocket(string uri)
    {
        _uri = uri;
        _socket = new MessageWebSocket();
        _socket.Control.MessageType = SocketMessageType.Utf8;
        _writer = new DataWriter(_socket.OutputStream);
    }

    public async void Connect()
    {
        await _socket.ConnectAsync(new Uri(_uri));
        _openCallback();
    }
    public void Close()
    {
        _socket.Close(1000, "Closed due to user request");
    }

    public void OnOpen(Action callback)
    {
        _openCallback += callback;
    }

    public void OnClose(Action<WebSocketReason> callback)
    {
        _socket.Closed += (sender, args) => callback(new WebSocketReason(args.Reason));
    }

    public void OnError(Action<WebSocketReason> callback)
    {
        _errorCallback += callback;
    }

    public void OnMessage(Action<string> callback)
    {
        _socket.MessageReceived += (sender, args) =>
        {
            string readString;
            try
            {
                using (var reader = args.GetDataReader())
                {
                    reader.UnicodeEncoding = UnicodeEncoding.Utf8;
                    readString = reader.ReadString(reader.UnconsumedBufferLength);
                }
            }
            catch (Exception e)
            {
                _errorCallback(new WebSocketReason(e.Message));
                return;
            }

            callback(readString);
        };
    }

    public async void Send(string data)
    {
        _writer.WriteString(data);

        try
        {
            // Send the data as one complete message.
            await _writer.StoreAsync();
        }
        catch (Exception ex)
        {
            _errorCallback(new WebSocketReason(ex.Message));
        }
    }
}

#else
public class WebSocketSharpWebSocket : IWebSocket
{
    private readonly string _uri;
    private readonly WebSocket _ws;

    public WebSocketSharpWebSocket(string uri)
    {
        _uri = uri;
        _ws = new WebSocket(uri);
    }

    public void Connect()
    {
        Thread.Sleep(500);
        _ws.Connect();
    }

    public void Close()
    {
        _ws.Close();
    }

    public void OnOpen(Action callback)
    {
        _ws.OnOpen += (sender, args) => callback();
    }

    public void OnClose(Action<WebSocketReason> callback)
    {
        _ws.OnClose += (sender, args) => callback(new WebSocketReason(args.Reason));
    }

    public void OnError(Action<WebSocketReason> callback)
    {
        _ws.OnError += (sender, args) => callback(new WebSocketReason(args.Message));
    }

    public void OnMessage(Action<string> callback)
    {
        _ws.OnMessage += (sender, args) => callback(args.Data);
    }

    public void Send(string data)
    {
        _ws.Send(data);
    }
}
#endif

public static class WebSocketFactory
{
    public static IWebSocket Create(string uri)
    {
#if WINDOWS_UWP
        return new UWPWebSocket(uri);
#else
        return new WebSocketSharpWebSocket(uri);
# endif
    }
}

public abstract class SignalRHub
{
    public abstract string HubName { get; }

}

public class SignalRClient : MonoBehaviour
{
    private IWebSocket _ws;
    private string _escapedConnectionToken;
    private bool _isSignalRStarted;

    private Dictionary<string, UnTypedActionContainer> _actionMap = new Dictionary<string, UnTypedActionContainer>();
    private Queue<Action> _waitingActions = new Queue<Action>();

    public string SignalRHostAndPath = "localhost:29281/signalr";
    public string[] HubNames = new string[0];

    private int _tid;
    private string _signalRSchemaVersion = "1.3";

    public bool IsStarted { get { return _ws != null; } }

    public string EscapedConnectionData
    {
        get
        {
            var hubDefinitions = HubNames.Select(n => new { name = n }).ToArray();
            var serialized = JsonConvert.SerializeObject(hubDefinitions);

            return Uri.EscapeUriString(serialized);
        }
    }

    private class NegotiateResponse
    {
        public string Url;
        public string ConnectionToken;
        public Guid ConnectionId;
    }

    public IEnumerator Start()
    {
        _tid = UnityEngine.Random.Range(1, 2000000000);
        yield return StartCoroutine(NegotiateRequest());

        if (_escapedConnectionToken == null) yield break;

        yield return StartCoroutine(StartSignalR());

        if (!_isSignalRStarted) yield break;

        yield return StartCoroutine(OpenWebSocket());
    }

    public void Update()
    {
        while (_waitingActions.Any())
        {
            var action = _waitingActions.Dequeue();
            action();
        }
    }

    private IEnumerator NegotiateRequest()
    {
        var requestUriString = "http://" + SignalRHostAndPath + "/negotiate" +
                               "?connectionData=" + EscapedConnectionData +
                               "&clientProtocol=" + _signalRSchemaVersion;

        Debug.Log("SignalR Negotiation: " + requestUriString);

        var negotiateRequest = new WWW(requestUriString);
        yield return negotiateRequest;

        if (negotiateRequest.error != null)
        {
            Debug.LogWarning("Error negotiating signalR:" + negotiateRequest.error);
            yield break;
        }

        var payload = negotiateRequest.text;

        var negotiateResponse = JsonConvert.DeserializeObject<NegotiateResponse>(payload);
        _escapedConnectionToken = Uri.EscapeDataString(negotiateResponse.ConnectionToken);
    }

    private IEnumerator StartSignalR()
    {
        // start
        var startUri = "http://" + SignalRHostAndPath + "/start?transport=webSockets" +
                       "&clientProtocol=" + _signalRSchemaVersion +
                       "&connectionToken=" + _escapedConnectionToken +
                       "&connectionData=" + EscapedConnectionData +
                       "&_=" + UnityEngine.Random.Range(1, 1000000000);
        Debug.Log("SignalR Start: " + startUri);

        var startRequest = new WWW(startUri);
        yield return startRequest;

        if (startRequest.error != null)
        {
            Debug.LogWarning("Error starting SignalR:" + startRequest.error);
            yield break;
        }
        _isSignalRStarted = true;
        Debug.Log("startWWW returned: " + startRequest.text);
    }

    private IEnumerator OpenWebSocket()
    {
        var query = "?transport=webSockets" +
                    "&connectionToken=" + _escapedConnectionToken +
                    "&connectionData=" + EscapedConnectionData +
                    "&tid=" + _tid;

        Debug.Log("Opening, query is: signalr/connect" + query);

        _ws = _ws == null
            ? WebSocketFactory.Create("ws://" + SignalRHostAndPath + "/connect" + query)
            : WebSocketFactory.Create("ws://" + SignalRHostAndPath + "/reconnect" + query);

        _ws.OnClose(_ws_OnClose);
        _ws.OnError(_ws_OnError);
        _ws.OnMessage(_ws_OnMessage);
        _ws.OnOpen(_ws_OnOpen);

        bool isComplete = false;
        _ws.OnOpen(() => isComplete = true);
        _ws.OnError(reason => isComplete = true);

        _ws.Connect();

        yield return new WaitUntil(() => isComplete);
    }

    void OnDestroy()
    {
        if (_ws != null)
        {
            _ws.Close();
            _escapedConnectionToken = null;
            _actionMap.Clear();
            _ws = null;
        }
    }

    public void SendToServer(string hub, string method, object[] arguments)
    {
        Debug.Log(string.Format("Sending message to {0}.{1}({2} args)", hub, method, arguments.Length));
        var payload = new SignalRMessage
        {
            H = hub,
            M = method,
            A = arguments,
            I = UnityEngine.Random.Range(1, 1000000000)
        };

        var wsPacket = JsonConvert.SerializeObject(payload);
        _ws.Send(wsPacket);
    }

    void _ws_OnOpen()
    {
        Debug.Log("Opened Connection");
    }

    //
    // This seems to be retriving the last frame containing the Identifier
    void _ws_OnMessage(string data)
    {
        var received = JsonConvert.DeserializeObject<MessageWrapper>(data);

        if (received.M == null)
            return;

        foreach (var msg in received.M)
        {
            var hub = msg.H;
            var message = msg.M;

            Debug.Log(string.Format("Got a message for {0}.{1}({2} args)", hub, message, msg.A.Length));

            var key = ActionKey(hub, message);
            var action = _actionMap.ContainsKey(key) ? _actionMap[key] : null;
            if (action != null)
            {
                action.Action(msg.A);
            }
        }
    }

    private static string ActionKey(string hub, string message)
    {
        return (hub + "|" + message).ToLowerInvariant();
    }

    void _ws_OnError(WebSocketReason webSocketReason)
    {
        Debug.LogWarning("WebSocket error: " + webSocketReason.Reason);
    }

    void _ws_OnClose(WebSocketReason webSocketReason)
    {
        Debug.Log(webSocketReason.Reason);
    }

    public void On(string hub, string method, Action callback)
    {
        if (!HubNames.Contains(hub))
        {
            Debug.LogWarning("Not registered for hub: " + hub);
        }

        _actionMap.Add(ActionKey(hub, method), new UnTypedActionContainer
        {
            Action = arguments =>
            {
                if (arguments.Length == -0)
                {
                    _waitingActions.Enqueue(callback);
                }
            }
        });
    }

    public void On<T>(string hub, string method, Action<T> callback) where T : class
    {
        _actionMap.Add(ActionKey(hub, method), new UnTypedActionContainer
        {
            Action = arguments =>
            {
                if (arguments.Length == 1)
                {
                    var arg1 = (T)arguments[0];
                    _waitingActions.Enqueue(() => callback(arg1));
                }
            }
        });
    }

    public void On<T1, T2>(string hub, string method, Action<T1, T2> callback)
    {
        _actionMap.Add(ActionKey(hub, method), new UnTypedActionContainer
        {
            Action = arguments =>
            {
                if (arguments.Length == 2)
                {
                    var arg1 = (T1)arguments[0];
                    var arg2 = (T2)arguments[1];

                    _waitingActions.Enqueue(() => callback(arg1, arg2));
                }
            }
        });
    }
}

internal class UnTypedActionContainer
{
    public Action<object[]> Action { get; set; }
}

class MessageWrapper
{
    public string C;
    public SignalRMessage[] M;
}

class SignalRMessage
{
    public string H;
    public string M;
    public object[] A;
    public int I;
}
