#if !NET_LEGACY && (UNITY_EDITOR || !UNTIY_WEBGL)
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net.WebSockets;
using System.IO;

namespace UnityWebSocket.NoWebGL
{
    public class WebSocket : IWebSocket
    {
        public string Address { get; private set; }

        public WebSocketState ReadyState
        {
            get
            {
                if (socket == null)
                    return WebSocketState.Closed;
                switch (socket.State)
                {
                    case System.Net.WebSockets.WebSocketState.Closed:
                    case System.Net.WebSockets.WebSocketState.None:
                        return WebSocketState.Closed;
                    case System.Net.WebSockets.WebSocketState.CloseReceived:
                    case System.Net.WebSockets.WebSocketState.CloseSent:
                        return WebSocketState.Closing;
                    case System.Net.WebSockets.WebSocketState.Connecting:
                        return WebSocketState.Connecting;
                    case System.Net.WebSockets.WebSocketState.Open:
                        return WebSocketState.Open;
                }
                return WebSocketState.Closed;
            }
        }

        public event EventHandler<OpenEventArgs> OnOpen;
        public event EventHandler<CloseEventArgs> OnClose;
        public event EventHandler<ErrorEventArgs> OnError;
        public event EventHandler<MessageEventArgs> OnMessage;

        private ClientWebSocket socket;
        private bool isOpening => socket.State == System.Net.WebSockets.WebSocketState.Open;

        #region APIs
        public WebSocket(string address)
        {
            this.Address = address;
        }

        public void ConnectAsync()
        {
            if (socket != null)
            {
                HandleError(new Exception("Socket is busy."));
                return;
            }
            socket = new ClientWebSocket();
            Task.Run(ConnectTask);
        }

        public void CloseAsync()
        {
            if (!isOpening) return;
            SendBufferAsync(new SendBuffer(null, WebSocketMessageType.Close));
        }

        public Task SendAsync(byte[] data)
        {
            if (!isOpening) return Task.CompletedTask;
            var buffer = new SendBuffer(data, WebSocketMessageType.Binary);
            SendBufferAsync(buffer);
            return Task.CompletedTask;
        }

        public Task SendAsync(string text)
        {
            if (!isOpening) return Task.CompletedTask; ;
            var data = Encoding.UTF8.GetBytes(text);
            var buffer = new SendBuffer(data, WebSocketMessageType.Text);
            SendBufferAsync(buffer);
            return Task.CompletedTask;
        }
        #endregion


        private async Task ConnectTask()
        {
            Log("Connect Task Begin ...");

            try
            {
                var uri = new Uri(Address);
                await socket.ConnectAsync(uri, CancellationToken.None);
            }
            catch (Exception e)
            {
                HandleError(e);
                HandleClose((ushort)CloseStatusCode.Abnormal, e.Message);
                SocketDispose();
                return;
            }

            HandleOpen();

            Log("Connect Task End !");

            await ReceiveTask();
        }

        class SendBuffer
        {
            public byte[] data;
            public WebSocketMessageType type;
            public SendBuffer(byte[] data, WebSocketMessageType type)
            {
                this.data = data;
                this.type = type;
            }
        }

        private object sendQueueLock = new object();
        private Queue<SendBuffer> sendQueue = new Queue<SendBuffer>();
        private bool isSendTaskRunning;

        private void SendBufferAsync(SendBuffer buffer)
        {
            if (isSendTaskRunning)
            {
                lock (sendQueueLock)
                {
                    if (buffer.type == WebSocketMessageType.Close)
                    {
                        sendQueue.Clear();
                    }
                    sendQueue.Enqueue(buffer);
                }
            }
            else
            {
                isSendTaskRunning = true;
                sendQueue.Enqueue(buffer);
                Task.Run(SendTask);
            }
        }

        private async Task SendTask()
        {
            Log("Send Task Begin ...");

            try
            {
                SendBuffer buffer = null;
                while (sendQueue.Count > 0 && isOpening)
                {
                    lock (sendQueueLock)
                    {
                        buffer = sendQueue.Dequeue();
                    }
                    if (buffer.type == WebSocketMessageType.Close)
                    {
                        Log($"Close Send Begin ...");
                        await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "Normal Closure", CancellationToken.None);
                        Log($"Close Send End !");
                    }
                    else
                    {
                        Log($"Send, type: {buffer.type}, size: {buffer.data.Length}, queue left: {sendQueue.Count}");
                        await socket.SendAsync(new ArraySegment<byte>(buffer.data), buffer.type, true, CancellationToken.None);
                    }
                }
            }
            catch (Exception e)
            {
                HandleError(e);
            }
            finally
            {
                isSendTaskRunning = false;
            }

            Log("Send Task End !");
        }

        private async Task ReceiveTask()
        {
            Log("Receive Task Begin ...");

            string closeReason = "";
            ushort closeCode = 0;
            bool isClosed = false;
            var segment = new ArraySegment<byte>(new byte[8192]);
            var ms = new MemoryStream();

            try
            {
                while (!isClosed)
                {
                    var result = await socket.ReceiveAsync(segment, CancellationToken.None);
                    ms.Write(segment.Array, 0, result.Count);
                    if (!result.EndOfMessage) continue;
                    var data = ms.ToArray();
                    ms.SetLength(0);
                    switch (result.MessageType)
                    {
                        case WebSocketMessageType.Binary:
                            HandleMessage(Opcode.Binary, data);
                            break;
                        case WebSocketMessageType.Text:
                            HandleMessage(Opcode.Text, data);
                            break;
                        case WebSocketMessageType.Close:
                            isClosed = true;
                            closeCode = (ushort)result.CloseStatus;
                            closeReason = result.CloseStatusDescription;
                            break;
                    }
                }
            }
            catch (Exception e)
            {
                HandleError(e);
                closeCode = (ushort)CloseStatusCode.Abnormal;
                closeReason = e.Message;
            }
            finally
            {
                ms.Close();
            }

            HandleClose(closeCode, closeReason);
            SocketDispose();

            Log("Receive Task End !");
        }

        private void SocketDispose()
        {
            sendQueue.Clear();
            socket.Dispose();
            socket = null;
        }

        private void HandleOpen()
        {
            Log("OnOpen");
            OnOpen?.Invoke(this, new OpenEventArgs());
        }

        private void HandleMessage(Opcode opcode, byte[] rawData)
        {
            Log($"OnMessage, type: {opcode}, size: {rawData.Length}");
            OnMessage?.Invoke(this, new MessageEventArgs(opcode, rawData));
        }

        private void HandleClose(ushort code, string reason)
        {
            Log($"OnClose, code: {code}, reason: {reason}");
            OnClose?.Invoke(this, new CloseEventArgs(code, reason));
        }

        private void HandleError(Exception exception)
        {
            Log("OnError, error: " + exception.Message);
            OnError?.Invoke(this, new ErrorEventArgs(exception.Message));
        }

        [System.Diagnostics.Conditional("UNITY_WEB_SOCKET_LOG")]
        private void Log(string msg)
        {
            UnityEngine.Debug.Log($"<color=yellow>[UnityWebSocket]</color>" +
                $"<color=green>[T-{Thread.CurrentThread.ManagedThreadId:D3}]</color>" +
                $"<color=red>[{DateTime.Now.TimeOfDay}]</color>" +
                $" {msg}");
        }
    }
}
#endif
