#if !BESTHTTP_DISABLE_WEBSOCKET && (!UNITY_WEBGL || UNITY_EDITOR)
using System;
using System.IO;
using System.Threading;
using System.Collections.Generic;
using System.Text;
using BestHTTP.Extensions;
using BestHTTP.WebSocket.Frames;
namespace BestHTTP.WebSocket
{
    public sealed class WebSocketResponse : HTTPResponse, IHeartbeat, IProtocol
    {
        #region Public Interface
        /// 
        /// A reference to the original WebSocket instance. Used for accessing extensions.
        /// 
        public WebSocket WebSocket { get; internal set; }
        /// 
        /// Called when a Text message received
        /// 
        public Action OnText;
        /// 
        /// Called when a Binary message received
        /// 
        public Action OnBinary;
        /// 
        /// Called when an incomplete frame received. No attempt will be made to reassemble these fragments.
        /// 
        public Action OnIncompleteFrame;
        /// 
        /// Called when the connection closed.
        /// 
        public Action OnClosed;
        /// 
        /// Indicates whether the connection to the server is closed or not.
        /// 
        public bool IsClosed { get { return closed; } }
        /// 
        /// On what frequency we have to send a ping to the server.
        /// 
        public TimeSpan PingFrequnecy { get; private set; }
        /// 
        /// Maximum size of a fragment's payload data. Its default value is 32767.
        /// 
        public UInt16 MaxFragmentSize { get; private set; }
        /// 
        /// Length of unsent, buffered up data in bytes.
        /// 
        public int BufferedAmount { get { return this._bufferedAmount; } }
        private int _bufferedAmount;
        #endregion
        #region Private Fields
        private List IncompleteFrames = new List();
        private List CompletedFrames = new List();
        private WebSocketFrameReader CloseFrame;
        private object FrameLock = new object();
        private object SendLock = new object();
        private List unsentFrames = new List();
        private AutoResetEvent newFrameSignal = new AutoResetEvent(false);
        private volatile bool sendThreadCreated = false;
        /// 
        /// True if we sent out a Close message to the server
        /// 
        private volatile bool closeSent;
        /// 
        /// True if this WebSocket connection is closed
        /// 
        private volatile bool closed;
        private DateTime lastPing = DateTime.MinValue;
        #endregion
        internal WebSocketResponse(HTTPRequest request, Stream stream, bool isStreamed, bool isFromCache)
            : base(request, stream, isStreamed, isFromCache)
        {
            base.IsClosedManually = true;
            closed = false;
            MaxFragmentSize = UInt16.MaxValue / 2;
        }
        internal void StartReceive()
        {
            if (IsUpgraded)
            {
#if NETFX_CORE
                #pragma warning disable 4014
                    Windows.System.Threading.ThreadPool.RunAsync(ReceiveThreadFunc);
                #pragma warning restore 4014
#else
                ThreadPool.QueueUserWorkItem(ReceiveThreadFunc);
#endif
            }
        }
        internal void CloseStream()
        {
            var conn = HTTPManager.GetConnectionWith(this.baseRequest);
            if (conn != null)
                conn.Abort(HTTPConnectionStates.Closed);
        }
        #region Public interface for interacting with the server
        /// 
        /// It will send the given message to the server in one frame.
        /// 
        public void Send(string message)
        {
            if (message == null)
                throw new ArgumentNullException("message must not be null!");
            byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
            Send(new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Text, data));
        }
        /// 
        /// It will send the given data to the server in one frame.
        /// 
        public void Send(byte[] data)
        {
            if (data == null)
                throw new ArgumentNullException("data must not be null!");
            WebSocketFrame frame = new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Binary, data);
            if (frame.Data != null && frame.Data.Length > this.MaxFragmentSize)
            {
                WebSocketFrame[] additionalFrames = frame.Fragment(this.MaxFragmentSize);
                lock(SendLock)
                {
                    Send(frame);
                    if (additionalFrames != null)
                        for (int i = 0; i < additionalFrames.Length; ++i)
                            Send(additionalFrames[i]);
                }
            }
            else
                Send(frame);
        }
        /// 
        /// Will send count bytes from a byte array, starting from offset.
        /// 
        public void Send(byte[] data, ulong offset, ulong count)
        {
            if (data == null)
                throw new ArgumentNullException("data must not be null!");
            if (offset + count > (ulong)data.Length)
                throw new ArgumentOutOfRangeException("offset + count >= data.Length");
            WebSocketFrame frame = new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Binary, data, offset, count, true, true);
            if (frame.Data != null && frame.Data.Length > this.MaxFragmentSize)
            {
                WebSocketFrame[] additionalFrames = frame.Fragment(this.MaxFragmentSize);
                lock (SendLock)
                {
                    Send(frame);
                    if (additionalFrames != null)
                        for (int i = 0; i < additionalFrames.Length; ++i)
                            Send(additionalFrames[i]);
                }
            }
            else
                Send(frame);
        }
        /// 
        /// It will send the given frame to the server.
        /// 
        public void Send(WebSocketFrame frame)
        {
            if (frame == null)
                throw new ArgumentNullException("frame is null!");
            if (closed)
                return;
            lock (SendLock)
            {
                this.unsentFrames.Add(frame);
                Interlocked.Add(ref this._bufferedAmount, frame.Data != null ? frame.Data.Length : 0);
                if (!sendThreadCreated)
                {
#if NETFX_CORE
#pragma warning disable 4014
                    Windows.System.Threading.ThreadPool.RunAsync(SendThreadFunc);
#pragma warning restore 4014
#else
                    ThreadPool.QueueUserWorkItem(SendThreadFunc);
#endif
                    sendThreadCreated = true;
                }
            }
            HTTPManager.Logger.Information("WebSocketResponse", "Signaling SendThread!");
            newFrameSignal.Set();
        }
        /// 
        /// It will initiate the closing of the connection to the server.
        /// 
        public void Close()
        {
            Close(1000, "Bye!");
        }
        /// 
        /// It will initiate the closing of the connection to the server.
        /// 
        public void Close(UInt16 code, string msg)
        {
            if (closed)
                return;
            Send(new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.ConnectionClose, WebSocket.EncodeCloseData(code, msg)));
        }
        public void StartPinging(int frequency)
        {
            if (frequency < 100)
                throw new ArgumentException("frequency must be at least 100 milliseconds!");
            PingFrequnecy = TimeSpan.FromMilliseconds(frequency);
            HTTPManager.Heartbeats.Subscribe(this);
        }
        #endregion
        #region Private Threading Functions
        private void SendThreadFunc(object param)
        {
            List localFrames = new List();
            try
            {
                while (!closed && !closeSent)
                {
                    if (HTTPManager.Logger.Level <= Logger.Loglevels.Information)
                        HTTPManager.Logger.Information("WebSocketResponse", "SendThread - Waiting...");
                    newFrameSignal.WaitOne();
                    try
                    {
                        lock (SendLock)
                        {
                            // add frames reversed in order
                            for (int i = this.unsentFrames.Count - 1; i >= 0; --i)
                                localFrames.Add(this.unsentFrames[i]);
                            this.unsentFrames.Clear();
                        }
                        if (HTTPManager.Logger.Level <= Logger.Loglevels.Information)
                            HTTPManager.Logger.Information("WebSocketResponse", "SendThread - Wait is over, " + localFrames.Count.ToString() + " new frames!");
                        while (localFrames.Count > 0)
                        {
                            WebSocketFrame frame = localFrames[localFrames.Count - 1];
                            localFrames.RemoveAt(localFrames.Count - 1);
                            if (!closeSent)
                            {
                                byte[] rawData = frame.Get();
                                Stream.Write(rawData, 0, rawData.Length);
                                Stream.Flush();
                                if (frame.Type == WebSocketFrameTypes.ConnectionClose)
                                    closeSent = true;
                            }
                            Interlocked.Add(ref this._bufferedAmount, -frame.Data.Length);
                        }
                    }
                    catch(Exception ex)
                    {
                        if (HTTPUpdateDelegator.IsCreated)
                        {
                            this.baseRequest.Exception = ex;
                            this.baseRequest.State = HTTPRequestStates.Error;
                        }
                        else
                            this.baseRequest.State = HTTPRequestStates.Aborted;
                        closed = true;
                    }
                }
            }
            finally
            {
                sendThreadCreated = false;
                HTTPManager.Logger.Information("WebSocketResponse", "SendThread - Closed!");
            }
        }
        private void ReceiveThreadFunc(object param)
        {
            try
            {
                while (!closed)
                {
                    try
                    {
                        WebSocketFrameReader frame = new WebSocketFrameReader();
                        frame.Read(Stream);
                        // A server MUST NOT mask any frames that it sends to the client.  A client MUST close a connection if it detects a masked frame.
                        // In this case, it MAY use the status code 1002 (protocol error)
                        // (These rules might be relaxed in a future specification.)
                        if (frame.HasMask)
                        {
                            Close(1002, "Protocol Error: masked frame received from server!");
                            continue;
                        }
                        if (!frame.IsFinal)
                        {
                            if (OnIncompleteFrame == null)
                                IncompleteFrames.Add(frame);
                            else
                                lock (FrameLock) CompletedFrames.Add(frame);
                            continue;
                        }
                        switch (frame.Type)
                        {
                            // For a complete documentation and rules on fragmentation see http://tools.ietf.org/html/rfc6455#section-5.4
                            // A fragmented Frame's last fragment's opcode is 0 (Continuation) and the FIN bit is set to 1.
                            case WebSocketFrameTypes.Continuation:
                                // Do an assemble pass only if OnFragment is not set. Otherwise put it in the CompletedFrames, we will handle it in the HandleEvent phase.
                                if (OnIncompleteFrame == null)
                                {
                                    frame.Assemble(IncompleteFrames);
                                    // Remove all incomplete frames
                                    IncompleteFrames.Clear();
                                    // Control frames themselves MUST NOT be fragmented. So, its a normal text or binary frame. Go, handle it as usual.
                                    goto case WebSocketFrameTypes.Binary;
                                }
                                else
                                    lock (FrameLock) CompletedFrames.Add(frame);
                                break;
                            case WebSocketFrameTypes.Text:
                            case WebSocketFrameTypes.Binary:
                                frame.DecodeWithExtensions(WebSocket);
                                lock (FrameLock) CompletedFrames.Add(frame);
                                break;
                            // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in response, unless it already received a Close frame.
                            case WebSocketFrameTypes.Ping:
                                if (!closeSent && !closed)
                                    Send(new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Pong, frame.Data));
                                break;
                            // If an endpoint receives a Close frame and did not previously send a Close frame, the endpoint MUST send a Close frame in response.
                            case WebSocketFrameTypes.ConnectionClose:
                                CloseFrame = frame;
								if (!closeSent)
									Send(new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.ConnectionClose, null));
                                closed = true;
                                break;
                        }
                    }
#if !NETFX_CORE
                    catch (ThreadAbortException)
                    {
                        IncompleteFrames.Clear();
                        this.baseRequest.State = HTTPRequestStates.Aborted;
                        closed = true;
                        newFrameSignal.Set();
                    }
#endif
                    catch (Exception e)
                    {
                        if (HTTPUpdateDelegator.IsCreated)
                        {
                            this.baseRequest.Exception = e;
                            this.baseRequest.State = HTTPRequestStates.Error;
                        }
                        else
                            this.baseRequest.State = HTTPRequestStates.Aborted;
                        closed = true;
                        newFrameSignal.Set();
                    }
                }
            }
            finally
            {
                HTTPManager.Heartbeats.Unsubscribe(this);
            }
        }
        #endregion
        #region Sending Out Events
        /// 
        /// Internal function to send out received messages.
        /// 
        void IProtocol.HandleEvents()
        {
            lock (FrameLock)
            {
                for (int i = 0; i < CompletedFrames.Count; ++i)
                {
                    WebSocketFrameReader frame = CompletedFrames[i];
                    // Bugs in the clients shouldn't interrupt the code, so we need to try-catch and ignore any exception occurring here
                    try
                    {
                        switch (frame.Type)
                        {
                            case WebSocketFrameTypes.Continuation:
                                if (OnIncompleteFrame != null)
                                    OnIncompleteFrame(this, frame);
                                break;
                            case WebSocketFrameTypes.Text:
                                // Any not Final frame is handled as a fragment
                                if (!frame.IsFinal)
                                    goto case WebSocketFrameTypes.Continuation;
                                if (OnText != null)
                                    OnText(this, frame.DataAsText);
                                break;
                            case WebSocketFrameTypes.Binary:
                                // Any not Final frame is handled as a fragment
                                if (!frame.IsFinal)
                                    goto case WebSocketFrameTypes.Continuation;
                                if (OnBinary != null)
                                    OnBinary(this, frame.Data);
                                break;
                        }
                    }
                    catch (Exception ex)
                    {
                        HTTPManager.Logger.Exception("WebSocketResponse", "HandleEvents", ex);
                    }
                }
                CompletedFrames.Clear();
            }//lock (ReadLock)
            // 2015.05.09
            // State checking added because if there is an error the OnClose called first, and then the OnError.
            // Now, when there is an error only the OnError event will be called!
            if (IsClosed && OnClosed != null && baseRequest.State == HTTPRequestStates.Processing)
            {
                try
                {
                    UInt16 statusCode = 0;
                    string msg = string.Empty;
                    // If we received any data, we will get the status code and the message from it
                    if (CloseFrame != null && CloseFrame.Data != null && CloseFrame.Data.Length >= 2)
                    {
                        if (BitConverter.IsLittleEndian)
                            Array.Reverse(CloseFrame.Data, 0, 2);
                        statusCode = BitConverter.ToUInt16(CloseFrame.Data, 0);
                        if (CloseFrame.Data.Length > 2)
                            msg = Encoding.UTF8.GetString(CloseFrame.Data, 2, CloseFrame.Data.Length - 2);
                    }
                    OnClosed(this, statusCode, msg);
                }
                catch (Exception ex)
                {
                    HTTPManager.Logger.Exception("WebSocketResponse", "HandleEvents - OnClosed", ex);
                }
            }
        }
        #endregion
        #region IHeartbeat Implementation
        void IHeartbeat.OnHeartbeatUpdate(TimeSpan dif)
        {
            if (lastPing == DateTime.MinValue)
            {
                lastPing = DateTime.UtcNow;
                return;
            }
            if (DateTime.UtcNow - lastPing >= PingFrequnecy)
            {
                try
                {
                    Send(new WebSocketFrame(this.WebSocket, WebSocketFrameTypes.Ping, Encoding.UTF8.GetBytes(string.Empty)));
                }
                catch
                {
                    this.closed = true;
                    HTTPManager.Heartbeats.Unsubscribe(this);
                }
                lastPing = DateTime.UtcNow;
            }
        }
        #endregion
    }
}
#endif