#if UNITY_WEBGL && !UNITY_EDITOR

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;

using BestHTTP.Authentication;

namespace BestHTTP
{
    delegate void OnWebGLRequestHandlerDelegate(int nativeId, int httpStatus, IntPtr pBuffer, int length, int zero);
    delegate void OnWebGLBufferDelegate(int nativeId, IntPtr pBuffer, int length);
    delegate void OnWebGLProgressDelegate(int nativeId, int downloaded, int total);
    delegate void OnWebGLErrorDelegate(int nativeId, string error);
    delegate void OnWebGLTimeoutDelegate(int nativeId);
    delegate void OnWebGLAbortedDelegate(int nativeId);

    internal sealed class WebGLConnection : ConnectionBase
    {
        static Dictionary<int, WebGLConnection> Connections = new Dictionary<int, WebGLConnection>(4);

        int NativeId;
        MemoryStream Stream;

        public WebGLConnection(string serverAddress)
            : base(serverAddress, false)
        {
            XHR_SetLoglevel((byte)HTTPManager.Logger.Level);
        }

        internal override void Abort(HTTPConnectionStates newState)
        {
            State = newState;

            switch (State)
            {
                case HTTPConnectionStates.TimedOut: TimedOutStart = DateTime.UtcNow; break;
            }

            XHR_Abort(this.NativeId);
        }

        protected override void ThreadFunc(object param /*null*/)
        {
            // XmlHttpRequest setup

            this.NativeId = XHR_Create(HTTPRequest.MethodNames[(byte)CurrentRequest.MethodType],
                                       CurrentRequest.CurrentUri.OriginalString,
                                       CurrentRequest.Credentials != null ? CurrentRequest.Credentials.UserName : null,
                                       CurrentRequest.Credentials != null ? CurrentRequest.Credentials.Password : null);
            Connections.Add(NativeId, this);

            CurrentRequest.EnumerateHeaders((header, values) =>
                {
                    if (header != "Content-Length")
                        for (int i = 0; i < values.Count; ++i)
                            XHR_SetRequestHeader(NativeId, header, values[i]);
                }, /*callBeforeSendCallback:*/ true);

            byte[] body = CurrentRequest.GetEntityBody();

            XHR_SetResponseHandler(NativeId, WebGLConnection.OnResponse, WebGLConnection.OnError, WebGLConnection.OnTimeout, WebGLConnection.OnAborted);
            XHR_SetProgressHandler(NativeId, WebGLConnection.OnDownloadProgress, WebGLConnection.OnUploadProgress);

            XHR_SetTimeout(NativeId, (uint)(CurrentRequest.ConnectTimeout.TotalMilliseconds + CurrentRequest.Timeout.TotalMilliseconds));

            XHR_Send(NativeId, body, body != null ? body.Length : 0);
        }

        #region Callback Implementations

        void OnResponse(int httpStatus, byte[] buffer)
        {
            try
            {
                using (MemoryStream ms = new MemoryStream())
                {
                    Stream = ms;

                    XHR_GetStatusLine(NativeId, OnBufferCallback);
                    XHR_GetResponseHeaders(NativeId, OnBufferCallback);

                    if (buffer != null && buffer.Length > 0)
                        ms.Write(buffer, 0, buffer.Length);

                    ms.Seek(0L, SeekOrigin.Begin);

                    SupportedProtocols protocol = CurrentRequest.ProtocolHandler == SupportedProtocols.Unknown ? HTTPProtocolFactory.GetProtocolFromUri(CurrentRequest.CurrentUri) : CurrentRequest.ProtocolHandler;
                    CurrentRequest.Response = HTTPProtocolFactory.Get(protocol, CurrentRequest, ms, CurrentRequest.UseStreaming, false);

                    CurrentRequest.Response.Receive(buffer != null && buffer.Length > 0 ? (int)buffer.Length : -1, true);
                }
            }
            catch (Exception e)
            {
                HTTPManager.Logger.Exception(this.NativeId + " WebGLConnection", "OnResponse", e);

                if (CurrentRequest != null)
                {
                    // Something gone bad, Response must be null!
                    CurrentRequest.Response = null;

                    switch (State)
                    {
                        case HTTPConnectionStates.AbortRequested:
                            CurrentRequest.State = HTTPRequestStates.Aborted;
                            break;
                        case HTTPConnectionStates.TimedOut:
                            CurrentRequest.State = HTTPRequestStates.TimedOut;
                            break;
                        default:
                            CurrentRequest.Exception = e;
                            CurrentRequest.State = HTTPRequestStates.Error;
                            break;
                    }
                }
            }
            finally
            {
                Connections.Remove(NativeId);

                Stream = null;

                if (CurrentRequest != null)
                    lock (HTTPManager.Locker)
                    {
                        State = HTTPConnectionStates.Closed;
                        if (CurrentRequest.State == HTTPRequestStates.Processing)
                        {
                            if (CurrentRequest.Response != null)
                                CurrentRequest.State = HTTPRequestStates.Finished;
                            else
                                CurrentRequest.State = HTTPRequestStates.Error;
                        }
                    }

                LastProcessTime = DateTime.UtcNow;

                if (OnConnectionRecycled != null)
                    RecycleNow();

                XHR_Release(NativeId);
            }
        }

        void OnBuffer(byte[] buffer)
        {
            if (Stream != null)
            {
                Stream.Write(buffer, 0, buffer.Length);
                Stream.Write(new byte[2] { HTTPResponse.CR, HTTPResponse.LF }, 0, 2);
            }
        }

        void OnDownloadProgress(int down, int total)
        {
            CurrentRequest.Downloaded = down;
            CurrentRequest.DownloadLength = total;
            CurrentRequest.DownloadProgressChanged = true;
        }

        void OnUploadProgress(int up, int total)
        {
            CurrentRequest.Uploaded = up;
            CurrentRequest.UploadLength = total;
            CurrentRequest.UploadProgressChanged = true;
        }

        void OnError(string error)
        {
            HTTPManager.Logger.Information(this.NativeId + " WebGLConnection - OnError", error);

            Connections.Remove(NativeId);

            Stream = null;

            if (CurrentRequest != null)
                lock (HTTPManager.Locker)
                {
                    State = HTTPConnectionStates.Closed;
                    CurrentRequest.State = HTTPRequestStates.Error;
                    CurrentRequest.Exception = new Exception(error);
                }

            LastProcessTime = DateTime.UtcNow;

            if (OnConnectionRecycled != null)
                RecycleNow();

            XHR_Release(NativeId);
        }

        void OnTimeout()
        {
            HTTPManager.Logger.Information(this.NativeId + " WebGLConnection - OnResponse", string.Empty);

            Connections.Remove(NativeId);

            Stream = null;

            if (CurrentRequest != null)
                lock (HTTPManager.Locker)
                {
                    State = HTTPConnectionStates.Closed;
                    CurrentRequest.State = HTTPRequestStates.TimedOut;
                }

            LastProcessTime = DateTime.UtcNow;

            if (OnConnectionRecycled != null)
                RecycleNow();

            XHR_Release(NativeId);
        }

        void OnAborted()
        {
            HTTPManager.Logger.Information(this.NativeId + " WebGLConnection - OnAborted", string.Empty);

            Connections.Remove(NativeId);

            Stream = null;

            if (CurrentRequest != null)
                lock (HTTPManager.Locker)
                {
                    State = HTTPConnectionStates.Closed;
                    CurrentRequest.State = HTTPRequestStates.Aborted;
                }

            LastProcessTime = DateTime.UtcNow;

            if (OnConnectionRecycled != null)
                RecycleNow();

            XHR_Release(NativeId);
        }

        #endregion

        #region WebGL Static Callbacks

        [AOT.MonoPInvokeCallback(typeof(OnWebGLRequestHandlerDelegate))]
        static void OnResponse(int nativeId, int httpStatus, IntPtr pBuffer, int length, int err)
        {
            HTTPManager.Logger.Information("WebGLConnection - OnResponse", string.Format("{0} {1} {2} {3}", nativeId, httpStatus, length, err));

            WebGLConnection conn = null;
            if (!Connections.TryGetValue(nativeId, out conn))
            {
                HTTPManager.Logger.Error("WebGLConnection - OnResponse", "No WebGL connection found for nativeId: " + nativeId.ToString());
                return;
            }

            byte[] buffer = new byte[length];

            // Copy data from the 'unmanaged' memory to managed memory. Buffer will be reclaimed by the GC.
            Marshal.Copy(pBuffer, buffer, 0, length);

            conn.OnResponse(httpStatus, buffer);
        }

        [AOT.MonoPInvokeCallback(typeof(OnWebGLBufferDelegate))]
        static void OnBufferCallback(int nativeId, IntPtr pBuffer, int length)
        {
            WebGLConnection conn = null;
            if (!Connections.TryGetValue(nativeId, out conn))
            {
                HTTPManager.Logger.Error("WebGLConnection - OnBufferCallback", "No WebGL connection found for nativeId: " + nativeId.ToString());
                return;
            }

            byte[] buffer = new byte[length];

            // Copy data from the 'unmanaged' memory to managed memory. Buffer will be reclaimed by the GC.
            Marshal.Copy(pBuffer, buffer, 0, length);

            conn.OnBuffer(buffer);
        }

        [AOT.MonoPInvokeCallback(typeof(OnWebGLProgressDelegate))]
        static void OnDownloadProgress(int nativeId, int downloaded, int total)
        {
            HTTPManager.Logger.Information(nativeId + " OnDownloadProgress", downloaded.ToString() + " / " + total.ToString());

            WebGLConnection conn = null;
            if (!Connections.TryGetValue(nativeId, out conn))
            {
                HTTPManager.Logger.Error("WebGLConnection - OnDownloadProgress", "No WebGL connection found for nativeId: " + nativeId.ToString());
                return;
            }

            conn.OnDownloadProgress(downloaded, total);
        }

        [AOT.MonoPInvokeCallback(typeof(OnWebGLProgressDelegate))]
        static void OnUploadProgress(int nativeId, int uploaded, int total)
        {
            HTTPManager.Logger.Information(nativeId + " OnUploadProgress", uploaded.ToString() + " / " + total.ToString());

            WebGLConnection conn = null;
            if (!Connections.TryGetValue(nativeId, out conn))
            {
                HTTPManager.Logger.Error("WebGLConnection - OnUploadProgress", "No WebGL connection found for nativeId: " + nativeId.ToString());
                return;
            }

            conn.OnUploadProgress(uploaded, total);
        }

        [AOT.MonoPInvokeCallback(typeof(OnWebGLErrorDelegate))]
        static void OnError(int nativeId, string error)
        {
            WebGLConnection conn = null;
            if (!Connections.TryGetValue(nativeId, out conn))
            {
                HTTPManager.Logger.Error("WebGLConnection - OnError", "No WebGL connection found for nativeId: " + nativeId.ToString() + " Error: " + error);
                return;
            }

            conn.OnError(error);
        }

        [AOT.MonoPInvokeCallback(typeof(OnWebGLTimeoutDelegate))]
        static void OnTimeout(int nativeId)
        {
            WebGLConnection conn = null;
            if (!Connections.TryGetValue(nativeId, out conn))
            {
                HTTPManager.Logger.Error("WebGLConnection - OnTimeout", "No WebGL connection found for nativeId: " + nativeId.ToString());
                return;
            }

            conn.OnTimeout();
        }

        [AOT.MonoPInvokeCallback(typeof(OnWebGLAbortedDelegate))]
        static void OnAborted(int nativeId)
        {
            WebGLConnection conn = null;
            if (!Connections.TryGetValue(nativeId, out conn))
            {
                HTTPManager.Logger.Error("WebGLConnection - OnAborted", "No WebGL connection found for nativeId: " + nativeId.ToString());
                return;
            }

            conn.OnAborted();
        }

        #endregion

        #region WebGL Interface

        [DllImport("__Internal")]
        private static extern int XHR_Create(string method, string url, string userName, string passwd);

        /// <summary>
        /// Is an unsigned long representing the number of milliseconds a request can take before automatically being terminated. A value of 0 (which is the default) means there is no timeout.
        /// </summary>
        [DllImport("__Internal")]
        private static extern void XHR_SetTimeout(int nativeId, uint timeout);

        [DllImport("__Internal")]
        private static extern void XHR_SetRequestHeader(int nativeId, string header, string value);

        [DllImport("__Internal")]
        private static extern void XHR_SetResponseHandler(int nativeId, OnWebGLRequestHandlerDelegate onresponse, OnWebGLErrorDelegate onerror, OnWebGLTimeoutDelegate ontimeout, OnWebGLAbortedDelegate onabort);

        [DllImport("__Internal")]
        private static extern void XHR_SetProgressHandler(int nativeId, OnWebGLProgressDelegate onDownloadProgress, OnWebGLProgressDelegate onUploadProgress);

        [DllImport("__Internal")]
        private static extern void XHR_Send(int nativeId, [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.U1, SizeParamIndex = 2)] byte[] body, int length);

        [DllImport("__Internal")]
        private static extern void XHR_GetResponseHeaders(int nativeId, OnWebGLBufferDelegate callback);

        [DllImport("__Internal")]
        private static extern void XHR_GetStatusLine(int nativeId, OnWebGLBufferDelegate callback);

        [DllImport("__Internal")]
        private static extern void XHR_Abort(int nativeId);

        [DllImport("__Internal")]
        private static extern void XHR_Release(int nativeId);

        [DllImport("__Internal")]
        private static extern void XHR_SetLoglevel(int logLevel);

        #endregion
    }
}

#endif