using BestHTTP.Extensions;
using BestHTTP.Logger;
using BestHTTP.PlatformSupport.Memory;
using BestHTTP.Timings;

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace BestHTTP.Core
{
    public enum RequestEvents
    {
        Upgraded,
        DownloadProgress,
        UploadProgress,
        StreamingData,
        StateChange,
        Resend,
        Headers,
        TimingData
    }

    public
#if CSHARP_7_OR_LATER
        readonly
#endif
        struct RequestEventInfo
    {
        public readonly HTTPRequest SourceRequest;
        public readonly RequestEvents Event;

        public readonly HTTPRequestStates State;

        public readonly long Progress;
        public readonly long ProgressLength;

        public readonly byte[] Data;
        public readonly int DataLength;

        // Timing Data
        public readonly string Name;
        public readonly DateTime Time;
        public readonly TimeSpan Duration;

        // Headers
        public readonly Dictionary<string, List<string>> Headers;

        public RequestEventInfo(HTTPRequest request, RequestEvents @event)
        {
            this.SourceRequest = request;
            this.Event = @event;

            this.State = HTTPRequestStates.Initial;

            this.Progress = this.ProgressLength = 0;

            this.Data = null;
            this.DataLength = 0;

            // TimingData
            this.Name = null;
            this.Time = DateTime.MinValue;
            this.Duration = TimeSpan.Zero;

            // Headers
            this.Headers = null;
        }

        public RequestEventInfo(HTTPRequest request, HTTPRequestStates newState)
        {
            this.SourceRequest = request;
            this.Event = RequestEvents.StateChange;
            this.State = newState;

            this.Progress = this.ProgressLength = 0;
            this.Data = null;
            this.DataLength = 0;

            // TimingData
            this.Name = null;
            this.Time = DateTime.MinValue;
            this.Duration = TimeSpan.Zero;

            // Headers
            this.Headers = null;
        }

        public RequestEventInfo(HTTPRequest request, RequestEvents @event, long progress, long progressLength)
        {
            this.SourceRequest = request;
            this.Event = @event;
            this.State = HTTPRequestStates.Initial;

            this.Progress = progress;
            this.ProgressLength = progressLength;
            this.Data = null;
            this.DataLength = 0;

            // TimingData
            this.Name = null;
            this.Time = DateTime.MinValue;
            this.Duration = TimeSpan.Zero;

            // Headers
            this.Headers = null;
        }

        public RequestEventInfo(HTTPRequest request, byte[] data, int dataLength)
        {
            this.SourceRequest = request;
            this.Event = RequestEvents.StreamingData;
            this.State = HTTPRequestStates.Initial;

            this.Progress = this.ProgressLength = 0;
            this.Data = data;
            this.DataLength = dataLength;

            // TimingData
            this.Name = null;
            this.Time = DateTime.MinValue;
            this.Duration = TimeSpan.Zero;

            // Headers
            this.Headers = null;
        }

        public RequestEventInfo(HTTPRequest request, string name, DateTime time)
        {
            this.SourceRequest = request;
            this.Event = RequestEvents.TimingData;
            this.State = HTTPRequestStates.Initial;

            this.Progress = this.ProgressLength = 0;
            this.Data = null;
            this.DataLength = 0;

            // TimingData
            this.Name = name;
            this.Time = time;
            this.Duration = TimeSpan.Zero;

            // Headers
            this.Headers = null;
        }

        public RequestEventInfo(HTTPRequest request, string name, TimeSpan duration)
        {
            this.SourceRequest = request;
            this.Event = RequestEvents.TimingData;
            this.State = HTTPRequestStates.Initial;

            this.Progress = this.ProgressLength = 0;
            this.Data = null;
            this.DataLength = 0;

            // TimingData
            this.Name = name;
            this.Time = DateTime.Now;
            this.Duration = duration;

            // Headers
            this.Headers = null;
        }

        public RequestEventInfo(HTTPRequest request, Dictionary<string, List<string>> headers)
        {
            this.SourceRequest = request;
            this.Event = RequestEvents.Headers;
            this.State = HTTPRequestStates.Initial;

            this.Progress = this.ProgressLength = 0;
            this.Data = null;
            this.DataLength = 0;

            // TimingData
            this.Name = null;
            this.Time = DateTime.MinValue;
            this.Duration = TimeSpan.Zero;

            // Headers
            this.Headers = headers;
        }

        public override string ToString()
        {
            switch (this.Event)
            {
                case RequestEvents.Upgraded:
                    return string.Format("[RequestEventInfo SourceRequest: {0}, Event: Upgraded]", this.SourceRequest.CurrentUri);
                case RequestEvents.DownloadProgress:
                    return string.Format("[RequestEventInfo SourceRequest: {0}, Event: DownloadProgress, Progress: {1}, ProgressLength: {2}]", this.SourceRequest.CurrentUri, this.Progress, this.ProgressLength);
                case RequestEvents.UploadProgress:
                    return string.Format("[RequestEventInfo SourceRequest: {0}, Event: UploadProgress, Progress: {1}, ProgressLength: {2}]", this.SourceRequest.CurrentUri, this.Progress, this.ProgressLength);
                case RequestEvents.StreamingData:
                    return string.Format("[RequestEventInfo SourceRequest: {0}, Event: StreamingData, DataLength: {1}]", this.SourceRequest.CurrentUri, this.DataLength);
                case RequestEvents.StateChange:
                    return string.Format("[RequestEventInfo SourceRequest: {0}, Event: StateChange, State: {1}]", this.SourceRequest.CurrentUri, this.State);
                case RequestEvents.Resend:
                    return string.Format("[RequestEventInfo SourceRequest: {0}, Event: Resend]", this.SourceRequest.CurrentUri);
                case RequestEvents.Headers:
                    return string.Format("[RequestEventInfo SourceRequest: {0}, Event: Headers]", this.SourceRequest.CurrentUri);
                case RequestEvents.TimingData:
                    if (this.Duration == TimeSpan.Zero)
                        return string.Format("[RequestEventInfo SourceRequest: {0}, Event: TimingData, Name: {1}, Time: {2}]", this.SourceRequest.CurrentUri, this.Name, this.Time);
                    else
                        return string.Format("[RequestEventInfo SourceRequest: {0}, Event: TimingData, Name: {1}, Time: {2}, Duration: {3}]", this.SourceRequest.CurrentUri, this.Name, this.Time, this.Duration);
                default:
                    throw new NotImplementedException(this.Event.ToString());
            }
        }
    }

    class ProgressFlattener
    {
        struct FlattenedProgress
        {
            public HTTPRequest request;
            public OnProgressDelegate onProgress;
            public long progress;
            public long length;
        }

        private FlattenedProgress[] progresses;
        private bool hasProgress;

        public void InsertOrUpdate(RequestEventInfo info, OnProgressDelegate onProgress)
        {
            if (progresses == null)
                progresses = new FlattenedProgress[1];

            hasProgress = true;

            var newProgresss = new FlattenedProgress { request = info.SourceRequest, progress = info.Progress, length = info.ProgressLength, onProgress = onProgress };

            int firstEmptyIdx = -1;
            for (int i = 0; i < progresses.Length; i++)
            {
                var progress = progresses[i];
                if (object.ReferenceEquals(progress.request, info.SourceRequest))
                {
                    progresses[i] = newProgresss;
                    return;
                }

                if (firstEmptyIdx == -1 && progress.request == null)
                    firstEmptyIdx = i;
            }

            if (firstEmptyIdx == -1)
            {
                Array.Resize(ref progresses, progresses.Length + 1);
                progresses[progresses.Length - 1] = newProgresss;
            }
            else
                progresses[firstEmptyIdx] = newProgresss;
        }

        public void DispatchProgressCallbacks()
        {
            if (progresses == null || !hasProgress)
                return;

            for (int i = 0; i < progresses.Length; ++i)
            {
                var @event = progresses[i];
                var source = @event.request;
                if (source != null && @event.onProgress != null)
                {
                    try
                    {
                        @event.onProgress(source, @event.progress, @event.length);
                    }
                    catch (Exception ex)
                    {
                        HTTPManager.Logger.Exception("ProgressFlattener", "DispatchProgressCallbacks", ex, source.Context);
                    }
                }
            }

            Array.Clear(progresses, 0, progresses.Length);
            hasProgress = false;
        }
    }

    public static class RequestEventHelper
    {
        private static ConcurrentQueue<RequestEventInfo> requestEventQueue = new ConcurrentQueue<RequestEventInfo>();

#pragma warning disable 0649
        public static Action<RequestEventInfo> OnEvent;
#pragma warning restore

        // Low frame rate and hight download/upload speed can add more download/upload progress events to dispatch in one frame.
        // This can add higher CPU usage as it might cause updating the UI/do other things unnecessary in the same frame.
        // To avoid this, instead of calling the events directly, we store the last event's data and call download/upload callbacks only once per frame.

        private static ProgressFlattener downloadProgress;
        private static ProgressFlattener uploadProgress;

        public static void EnqueueRequestEvent(RequestEventInfo @event)
        {
            if (HTTPManager.Logger.Level == Loglevels.All)
                HTTPManager.Logger.Information("RequestEventHelper", "Enqueue request event: " + @event.ToString(), @event.SourceRequest.Context);

            requestEventQueue.Enqueue(@event);
        }

        internal static void Clear()
        {
            requestEventQueue.Clear();
        }

        internal static void ProcessQueue()
        {
            RequestEventInfo requestEvent;
            while (requestEventQueue.TryDequeue(out requestEvent))
            {
                HTTPRequest source = requestEvent.SourceRequest;

                if (HTTPManager.Logger.Level == Loglevels.All)
                    HTTPManager.Logger.Information("RequestEventHelper", "Processing request event: " + requestEvent.ToString(), source.Context);

                if (OnEvent != null)
                {
                    try
                    {
                        OnEvent(requestEvent);
                    }
                    catch (Exception ex)
                    {
                        HTTPManager.Logger.Exception("RequestEventHelper", "ProcessQueue", ex, source.Context);
                    }
                }
                
                switch (requestEvent.Event)
                {
                    case RequestEvents.StreamingData:
                        {
                            var response = source.Response;
                            if (response != null)
                                System.Threading.Interlocked.Decrement(ref response.UnprocessedFragments);

                            bool reuseBuffer = true;
                            try
                            {
                                if (source.UseStreaming)
                                    reuseBuffer = source.OnStreamingData(source, response, requestEvent.Data, requestEvent.DataLength);
                            }
                            catch (Exception ex)
                            {
                                HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.StreamingData", ex, source.Context);
                            }

                            if (reuseBuffer)
                                BufferPool.Release(requestEvent.Data);
                            break;
                        }

                    case RequestEvents.DownloadProgress:
                        try
                        {
                            if (source.OnDownloadProgress != null)
                            {
                                if (downloadProgress == null)
                                    downloadProgress = new ProgressFlattener();
                                downloadProgress.InsertOrUpdate(requestEvent, source.OnDownloadProgress);
                            }
                        }
                        catch (Exception ex)
                        {
                            HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.DownloadProgress", ex, source.Context);
                        }
                        break;

                    case RequestEvents.UploadProgress:
                        try
                        {
                            if (source.OnUploadProgress != null)
                            {
                                if (uploadProgress == null)
                                    uploadProgress = new ProgressFlattener();
                                uploadProgress.InsertOrUpdate(requestEvent, source.OnUploadProgress);
                            }
                        }
                        catch (Exception ex)
                        {
                            HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.UploadProgress", ex, source.Context);
                        }
                        break;

#if !UNITY_WEBGL || UNITY_EDITOR
                    case RequestEvents.Upgraded:
                        try
                        {
                            if (source.OnUpgraded != null)
                                source.OnUpgraded(source, source.Response);
                        }
                        catch (Exception ex)
                        {
                            HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.Upgraded", ex, source.Context);
                        }

                        IProtocol protocol = source.Response as IProtocol;
                        if (protocol != null)
                            ProtocolEventHelper.AddProtocol(protocol);
                        break;
#endif

                    case RequestEvents.Resend:
                        source.State = HTTPRequestStates.Initial;
                        
                        var host = HostManager.GetHost(source.CurrentUri.Host);

                        host.Send(source);

                        break;

                    case RequestEvents.Headers:
                        {
                            try
                            {
                                var response = source.Response;
                                if (source.OnHeadersReceived != null && response != null)
                                    source.OnHeadersReceived(source, response, requestEvent.Headers);
                            }
                            catch (Exception ex)
                            {
                                HTTPManager.Logger.Exception("RequestEventHelper", "Process RequestEventQueue - RequestEvents.Headers", ex, source.Context);
                            }
                            break;
                        }

                    case RequestEvents.StateChange:
                        try
                        {
                            RequestEventHelper.HandleRequestStateChange(requestEvent);
                        }
                        catch(Exception ex)
                        {
                            HTTPManager.Logger.Exception("RequestEventHelper", "HandleRequestStateChange", ex, source.Context);
                        }
                        break;

                    case RequestEvents.TimingData:
                        source.Timing.AddEvent(requestEvent.Name, requestEvent.Time, requestEvent.Duration);
                        break;
                }
            }

            uploadProgress?.DispatchProgressCallbacks();
            downloadProgress?.DispatchProgressCallbacks();
        }

        private static bool AbortRequestWhenTimedOut(DateTime now, object context)
        {
            HTTPRequest request = context as HTTPRequest;

            if (request.State >= HTTPRequestStates.Finished)
                return false; // don't repeat

            // Protocols will shut down themselves
            if (request.Response is IProtocol)
                return false;

            if (request.IsTimedOut)
            {
                HTTPManager.Logger.Information("RequestEventHelper", "AbortRequestWhenTimedOut - Request timed out. CurrentUri: " + request.CurrentUri.ToString(), request.Context);
                request.Abort();

                return false; // don't repeat
            }

            return true;  // repeat
        }

        internal static void HandleRequestStateChange(RequestEventInfo @event)
        {
            HTTPRequest source = @event.SourceRequest;

            // Because there's a race condition between setting the request's State in its Abort() function running on Unity's main thread
            //  and the HTTP1/HTTP2 handlers running on an another one.
            // Because of these race conditions cases violating expectations can be:
            //  1.) State is finished but the response null
            //  2.) State is (Connection)TimedOut and the response non-null
            // We have to make sure that no callbacks are called twice and in the request must be in a consistent state!

            //    State        | Request
            //   ---------     +---------
            // 1                  Null
            //   Finished      |   Skip
            //   Timeout/Abort |   Deliver
            //                 
            // 2                 Non-Null
            //   Finished      |    Deliver
            //   Timeout/Abort |    Skip

            switch (@event.State)
            {
                case HTTPRequestStates.Queued:
                    source.QueuedAt = DateTime.UtcNow;
                    if ((!source.UseStreaming && source.UploadStream == null) || source.EnableTimoutForStreaming)
                        BestHTTP.Extensions.Timer.Add(new TimerData(TimeSpan.FromSeconds(1), @event.SourceRequest, AbortRequestWhenTimedOut));
                    break;

                case HTTPRequestStates.ConnectionTimedOut:
                case HTTPRequestStates.TimedOut:
                case HTTPRequestStates.Error:
                case HTTPRequestStates.Aborted:
                    source.Response = null;
                    goto case HTTPRequestStates.Finished;

                case HTTPRequestStates.Finished:

#if !BESTHTTP_DISABLE_CACHING
                    // Here we will try to load content for a failed load. Failed load is a request with ConnectionTimedOut, TimedOut or Error state.
                    // A request with Finished state but response with status code >= 500 also something that we will try to load from the cache.
                    // We have to set what we going to try to load here too (other place is inside IsCachedEntityExpiresInTheFuture) as we don't want to load a cached content for
                    // a request that just finished without any problem!

                    try
                    {
                        bool tryLoad = !source.DisableCache && source.State != HTTPRequestStates.Aborted && (source.State != HTTPRequestStates.Finished || source.Response == null || source.Response.StatusCode >= 500);
                        if (tryLoad && Caching.HTTPCacheService.IsCachedEntityExpiresInTheFuture(source))
                        {
                            HTTPManager.Logger.Information("RequestEventHelper", "IsCachedEntityExpiresInTheFuture check returned true! CurrentUri: " + source.CurrentUri.ToString(), source.Context);

                            PlatformSupport.Threading.ThreadedRunner.RunShortLiving<HTTPRequest>((req) =>
                            {
                                // Disable any other cache activity.
                                req.DisableCache = true;

                                var originalState = req.State;
                                if (Connections.ConnectionHelper.TryLoadAllFromCache("RequestEventHelper", req, req.Context))
                                {
                                    if (req.State != HTTPRequestStates.Finished)
                                        req.State = HTTPRequestStates.Finished;
                                    else
                                        RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, HTTPRequestStates.Finished));
                                }
                                else
                                {
                                    HTTPManager.Logger.Information("RequestEventHelper", "TryLoadAllFromCache failed to load! CurrentUri: " + req.CurrentUri.ToString(), source.Context);

                                    // If for some reason it couldn't load we place back the request to the queue.
                                    RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(req, originalState));
                                }
                            }, source);
                            break;
                        }
                    }
                    catch (Exception ex)
                    {
                        HTTPManager.Logger.Exception("RequestEventHelper", string.Format("HandleRequestStateChange - Cache probe - CurrentUri: \"{0}\" State: {1} StatusCode: {2}", source.CurrentUri, source.State, source.Response != null ? source.Response.StatusCode : 0), ex, source.Context);
                    }
#endif

                    // Dispatch any collected download/upload progress, otherwise they would _after_ the callback!
                    uploadProgress?.DispatchProgressCallbacks();
                    downloadProgress?.DispatchProgressCallbacks();

                    source.Timing.AddEvent(TimingEventNames.Queued_For_Disptach, DateTime.Now, TimeSpan.Zero);
                    source.Timing.AddEvent(TimingEventNames.Finished, DateTime.Now, DateTime.Now - source.Timing.Start);

                    if (source.Callback != null)
                    {
                        try
                        {
                            source.Callback(source, source.Response);

                            source.Timing.AddEvent(TimingEventNames.Callback, DateTime.Now, TimeSpan.Zero);

                            if (HTTPManager.Logger.Level <= Loglevels.Information)
                                HTTPManager.Logger.Information("RequestEventHelper", "Finishing request. Timings: " + source.Timing.ToString(), source.Context);

                        }
                        catch (Exception ex)
                        {
                            HTTPManager.Logger.Exception("RequestEventHelper", "HandleRequestStateChange " + @event.State, ex, source.Context);
                        }
                    }

                    source.Dispose();

                    HostManager.GetHost(source.CurrentUri.Host)
                                .GetHostDefinition(HostDefinition.GetKeyForRequest(source))
                                .TryToSendQueuedRequests();
                    break;
            }
        }
    }
}
