#if !BESTHTTP_DISABLE_WEBSOCKET

using System;
using System.Text;
using BestHTTP.Extensions;
using BestHTTP.Connections;
using BestHTTP.Logger;
using BestHTTP.PlatformSupport.Memory;

#if !UNITY_WEBGL || UNITY_EDITOR
    using BestHTTP.WebSocket.Frames;
    using BestHTTP.WebSocket.Extensions;
#endif

namespace BestHTTP.WebSocket
{
    public sealed class WebSocket
    {
        /// <summary>
        /// Maximum payload size of a websocket frame. Its default value is 32 KiB.
        /// </summary>
        public static uint MaxFragmentSize = UInt16.MaxValue / 2;

#if !UNITY_WEBGL || UNITY_EDITOR
        public static IExtension[] GetDefaultExtensions()
        {
#if !BESTHTTP_DISABLE_GZIP
            return new IExtension[] { new PerMessageCompression(/*compression level: */           Decompression.Zlib.CompressionLevel.Default,
                                                             /*clientNoContextTakeover: */     false,
                                                             /*serverNoContextTakeover: */     false,
                                                             /*clientMaxWindowBits: */         Decompression.Zlib.ZlibConstants.WindowBitsMax,
                                                             /*desiredServerMaxWindowBits: */  Decompression.Zlib.ZlibConstants.WindowBitsMax,
                                                             /*minDatalengthToCompress: */     PerMessageCompression.MinDataLengthToCompressDefault) };
#else
            return null;
#endif
        }
#endif

        public WebSocketStates State { get { return this.implementation.State; } }

        /// <summary>
        /// The connection to the WebSocket server is open.
        /// </summary>
        public bool IsOpen { get { return this.implementation.IsOpen; } }

        /// <summary>
        /// Data waiting to be written to the wire.
        /// </summary>
        public int BufferedAmount { get { return this.implementation.BufferedAmount; } }

#if !UNITY_WEBGL || UNITY_EDITOR

        /// <summary>
        /// Set to true to start a new thread to send Pings to the WebSocket server
        /// </summary>
        public bool StartPingThread { get; set; }

        /// <summary>
        /// The delay between two Pings in milliseconds. Minimum value is 100ms, default is 10 seconds.
        /// </summary>
        public int PingFrequency { get; set; }

        /// <summary>
        /// If StartPingThread set to true, the plugin will close the connection and emit an OnError event if no
        /// message is received from the server in the given time. Its default value is 2 sec.
        /// </summary>
        public TimeSpan CloseAfterNoMessage { get; set; }

        /// <summary>
        /// The internal HTTPRequest object.
        /// </summary>
        public HTTPRequest InternalRequest { get { return this.implementation.InternalRequest; } }

        /// <summary>
        /// IExtension implementations the plugin will negotiate with the server to use.
        /// </summary>
        public IExtension[] Extensions { get; private set; }

        /// <summary>
        /// Latency calculated from the ping-pong message round-trip times.
        /// </summary>
        public int Latency { get { return this.implementation.Latency; } }

        /// <summary>
        /// When we received the last message from the server.
        /// </summary>
        public DateTime LastMessageReceived { get { return this.implementation.LastMessageReceived; } }

        /// <summary>
        /// When the Websocket Over HTTP/2 implementation fails to connect and EnableImplementationFallback is true, the plugin tries to fall back to the HTTP/1 implementation.
        /// When this happens a new InternalRequest is created and all previous custom modifications (like added headers) are lost. With OnInternalRequestCreated these modifications can be reapplied.
        /// </summary>
        public Action<WebSocket, HTTPRequest> OnInternalRequestCreated;
#endif

        /// <summary>
        /// Called when the connection to the WebSocket server is established.
        /// </summary>
        public OnWebSocketOpenDelegate OnOpen;

        /// <summary>
        /// Called when a new textual message is received from the server.
        /// </summary>
        public OnWebSocketMessageDelegate OnMessage;

        /// <summary>
        /// Called when a new binary message is received from the server.
        /// </summary>
        public OnWebSocketBinaryDelegate OnBinary;

        /// <summary>
        /// Called when a Binary message received. It's a more performant version than the OnBinary event, as the memory will be reused.
        /// </summary>
        public OnWebSocketBinaryNoAllocDelegate OnBinaryNoAlloc;

        /// <summary>
        /// Called when the WebSocket connection is closed.
        /// </summary>
        public OnWebSocketClosedDelegate OnClosed;

        /// <summary>
        /// Called when an error is encountered. The parameter will be the description of the error.
        /// </summary>
        public OnWebSocketErrorDelegate OnError;

#if !UNITY_WEBGL || UNITY_EDITOR
        /// <summary>
        /// Called when an incomplete frame received. No attempt will be made to reassemble these fragments internally, and no reference are stored after this event to this frame.
        /// </summary>
        public OnWebSocketIncompleteFrameDelegate OnIncompleteFrame;
#endif

        /// <summary>
        /// Logging context of this websocket instance.
        /// </summary>
        public LoggingContext Context { get; private set; }

        /// <summary>
        /// The underlying, real implementation.
        /// </summary>
        private WebSocketBaseImplementation implementation;

        /// <summary>
        /// Creates a WebSocket instance from the given uri.
        /// </summary>
        /// <param name="uri">The uri of the WebSocket server</param>
        public WebSocket(Uri uri)
            :this(uri, string.Empty, string.Empty)
        {
#if (!UNITY_WEBGL || UNITY_EDITOR)
            this.Extensions = WebSocket.GetDefaultExtensions();
#endif
        }

#if !UNITY_WEBGL || UNITY_EDITOR
        public WebSocket(Uri uri, string origin, string protocol)
            :this(uri, origin, protocol, null)
        {
#if (!UNITY_WEBGL || UNITY_EDITOR)
            this.Extensions = WebSocket.GetDefaultExtensions();
#endif
        }
#endif

        /// <summary>
        /// Creates a WebSocket instance from the given uri, protocol and origin.
        /// </summary>
        /// <param name="uri">The uri of the WebSocket server</param>
        /// <param name="origin">Servers that are not intended to process input from any web page but only for certain sites SHOULD verify the |Origin| field is an origin they expect.
        /// If the origin indicated is unacceptable to the server, then it SHOULD respond to the WebSocket handshake with a reply containing HTTP 403 Forbidden status code.</param>
        /// <param name="protocol">The application-level protocol that the client want to use(eg. "chat", "leaderboard", etc.). Can be null or empty string if not used.</param>
        /// <param name="extensions">Optional IExtensions implementations</param>
        public WebSocket(Uri uri, string origin, string protocol
#if !UNITY_WEBGL || UNITY_EDITOR
            , params IExtension[] extensions
#endif
            )

        {
            this.Context = new LoggingContext(this);

#if !UNITY_WEBGL || UNITY_EDITOR
            this.Extensions = extensions;
#endif

            SelectImplementation(uri, origin, protocol);

            // Under WebGL when only the WebSocket protocol is used Setup() isn't called, so we have to call it here.
            HTTPManager.Setup();
        }

        internal WebSocketBaseImplementation SelectImplementation(Uri uri, string origin, string protocol)
        {
#if !UNITY_WEBGL || UNITY_EDITOR
#if !BESTHTTP_DISABLE_ALTERNATE_SSL && !BESTHTTP_DISABLE_HTTP2
            if (HTTPManager.HTTP2Settings.WebSocketOverHTTP2Settings.EnableWebSocketOverHTTP2 && HTTPProtocolFactory.IsSecureProtocol(uri))
            {
                // Try to find a HTTP/2 connection that supports the connect protocol.
                var connectionKey = Core.HostDefinition.GetKeyFor(new UriBuilder("https", uri.Host, uri.Port).Uri
#if !BESTHTTP_DISABLE_PROXY && (!UNITY_WEBGL || UNITY_EDITOR)
                , GetProxy(uri)
#endif
                    );

                var con = BestHTTP.Core.HostManager.GetHost(uri.Host).GetHostDefinition(connectionKey).Find(c => {
                    var httpConnection = c as HTTPConnection;
                    var http2Handler = httpConnection?.requestHandler as Connections.HTTP2.HTTP2Handler;

                    return http2Handler != null && http2Handler.settings.RemoteSettings[Connections.HTTP2.HTTP2Settings.ENABLE_CONNECT_PROTOCOL] != 0;
                });

                if (con != null)
                {
                    HTTPManager.Logger.Information("WebSocket", "Connection with enabled Connect Protocol found!", this.Context);

                    var httpConnection = con as HTTPConnection;
                    var http2Handler = httpConnection?.requestHandler as Connections.HTTP2.HTTP2Handler;

                    this.implementation = new OverHTTP2(this, uri, origin, protocol);
                }
            }
#endif

            if (this.implementation == null)
                this.implementation = new OverHTTP1(this, uri, origin, protocol);
#else
            this.implementation = new WebGLBrowser(this, uri, origin, protocol);
#endif

            return this.implementation;
        }

#if !UNITY_WEBGL || UNITY_EDITOR
        internal void FallbackToHTTP1()
        {
            HTTPManager.Logger.Verbose("WebSocket", "FallbackToHTTP1", this.Context);

            if (this.implementation == null)
                return;

            this.implementation = new OverHTTP1(this, this.implementation.Uri, this.implementation.Origin, this.implementation.Protocol);
            this.implementation.StartOpen();
        }
#endif

        /// <summary>
        /// Start the opening process.
        /// </summary>
        public void Open()
        {
            this.implementation.StartOpen();
        }

        /// <summary>
        /// It will send the given message to the server in one frame.
        /// </summary>
        public void Send(string message)
        {
            if (!IsOpen)
                return;

            this.implementation.Send(message);
        }

        /// <summary>
        /// It will send the given data to the server in one frame.
        /// </summary>
        public void Send(byte[] buffer)
        {
            if (!IsOpen)
                return;

            this.implementation.Send(buffer);
        }

        /// <summary>
        /// Will send count bytes from a byte array, starting from offset.
        /// </summary>
        public void Send(byte[] buffer, ulong offset, ulong count)
        {
            if (!IsOpen)
                return;

            this.implementation.Send(buffer, offset, count);
        }

        /// <summary>
        /// Will send the data in one or more binary frame and takes ownership over it calling BufferPool.Release when sent.
        /// </summary>
        public void SendAsBinary(BufferSegment data)
        {
            if (!IsOpen)
            {
                BufferPool.Release(data);
                return;
            }

            this.implementation.SendAsBinary(data);
        }

        /// <summary>
        /// Will send data as a text frame and takes owenership over the memory region releasing it to the BufferPool as soon as possible.
        /// </summary>
        public void SendAsText(BufferSegment data)
        {
            if (!IsOpen)
            {
                BufferPool.Release(data);
                return;
            }

            this.implementation.SendAsText(data);
        }

#if !UNITY_WEBGL || UNITY_EDITOR
        /// <summary>
        /// It will send the given frame to the server.
        /// </summary>
        public void Send(WebSocketFrame frame)
        {
            if (!IsOpen)
                return;

            this.implementation.Send(frame);
        }
#endif

        /// <summary>
        /// It will initiate the closing of the connection to the server.
        /// </summary>
        public void Close()
        {
            if (State >= WebSocketStates.Closing)
                return;

            this.implementation.StartClose(1000, "Bye!");
        }

        /// <summary>
        /// It will initiate the closing of the connection to the server sending the given code and message.
        /// </summary>
        public void Close(UInt16 code, string message)
        {
            if (!IsOpen)
                return;

            this.implementation.StartClose(code, message);
        }

#if !BESTHTTP_DISABLE_PROXY && (!UNITY_WEBGL || UNITY_EDITOR)
        internal Proxy GetProxy(Uri uri)
        {
            // WebSocket is not a request-response based protocol, so we need a 'tunnel' through the proxy
            HTTPProxy proxy = HTTPManager.Proxy as HTTPProxy;
            if (proxy != null && proxy.UseProxyForAddress(uri))
                proxy = new HTTPProxy(proxy.Address,
                                      proxy.Credentials,
                                      false, /*turn on 'tunneling'*/
                                      false, /*sendWholeUri*/
                                      proxy.NonTransparentForHTTPS);

            return proxy;
        }
#endif

#if !UNITY_WEBGL || UNITY_EDITOR

        public static BufferSegment EncodeCloseData(UInt16 code, string message)
        {
            //If there is a body, the first two bytes of the body MUST be a 2-byte unsigned integer
            // (in network byte order) representing a status code with value /code/ defined in Section 7.4 (http://tools.ietf.org/html/rfc6455#section-7.4). Following the 2-byte integer,
            // the body MAY contain UTF-8-encoded data with value /reason/, the interpretation of which is not defined by this specification.
            // This data is not necessarily human readable but may be useful for debugging or passing information relevant to the script that opened the connection.
            int msgLen = Encoding.UTF8.GetByteCount(message);
            using (var ms = new BufferPoolMemoryStream(2 + msgLen))
            {
                byte[] buff = BitConverter.GetBytes(code);
                if (BitConverter.IsLittleEndian)
                    Array.Reverse(buff, 0, buff.Length);

                ms.Write(buff, 0, buff.Length);

                buff = Encoding.UTF8.GetBytes(message);
                ms.Write(buff, 0, buff.Length);

                buff = ms.ToArray();

                return buff.AsBuffer(buff.Length);
            }
        }

        internal static string GetSecKey(object[] from)
        {
            const int keysLength = 16;
            byte[] keys = BufferPool.Get(keysLength, true);
            int pos = 0;

            for (int i = 0; i < from.Length; ++i)
            {
                byte[] hash = BitConverter.GetBytes((Int32)from[i].GetHashCode());

                for (int cv = 0; cv < hash.Length && pos < keysLength; ++cv)
                    keys[pos++] = hash[cv];
            }

            var result = Convert.ToBase64String(keys, 0, keysLength);
            BufferPool.Release(keys);

            return result;
        }
#endif
    }
}

#endif
