using System;
using System.Collections.Generic;

#if !BESTHTTP_DISABLE_CACHING
using BestHTTP.Caching;
#endif

using BestHTTP.Core;
using BestHTTP.Extensions;
using BestHTTP.Logger;
using BestHTTP.PlatformSupport.Memory;
using BestHTTP.PlatformSupport.Text;
using Unity.Profiling;

#if !BESTHTTP_DISABLE_COOKIES
using BestHTTP.Cookies;
#endif

using BestHTTP.Connections;

namespace BestHTTP
{
    public enum ShutdownTypes
    {
        Running,
        Gentle,
        Immediate
    }

#if !BESTHTTP_DISABLE_ALTERNATE_SSL && (!UNITY_WEBGL || UNITY_EDITOR)
    public delegate Connections.TLS.AbstractTls13Client TlsClientFactoryDelegate(HTTPRequest request, List<SecureProtocol.Org.BouncyCastle.Tls.ProtocolName> protocols);
#endif

    public delegate System.Security.Cryptography.X509Certificates.X509Certificate ClientCertificateSelector(HTTPRequest request, string targetHost, System.Security.Cryptography.X509Certificates.X509CertificateCollection localCertificates, System.Security.Cryptography.X509Certificates.X509Certificate remoteCertificate, string[] acceptableIssuers);

    /// <summary>
    ///
    /// </summary>
    [BestHTTP.PlatformSupport.IL2CPP.Il2CppEagerStaticClassConstructionAttribute]
    public static partial class HTTPManager
    {
        // Static constructor. Setup default values
        static HTTPManager()
        {
            MaxConnectionPerServer = 6;
            KeepAliveDefaultValue = true;
            MaxPathLength = 255;
            MaxConnectionIdleTime = TimeSpan.FromSeconds(20);

#if !BESTHTTP_DISABLE_COOKIES
#if UNITY_WEBGL && !UNITY_EDITOR
            // Under webgl when IsCookiesEnabled is true, it will set the withCredentials flag for the XmlHTTPRequest
            //  and that's different from the default behavior.
            // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials
            IsCookiesEnabled = false;
#else
            IsCookiesEnabled = true;
#endif
#endif

            CookieJarSize = 10 * 1024 * 1024;
            EnablePrivateBrowsing = false;
            ConnectTimeout = TimeSpan.FromSeconds(20);
            RequestTimeout = TimeSpan.FromSeconds(60);

            // Set the default logger mechanism
            logger = new BestHTTP.Logger.ThreadedLogger();

#if !BESTHTTP_DISABLE_ALTERNATE_SSL && (!UNITY_WEBGL || UNITY_EDITOR)
            UseAlternateSSLDefaultValue = true;
#endif

#if NETFX_CORE
            IOService = new PlatformSupport.FileSystem.NETFXCOREIOService();
#else
            IOService = new PlatformSupport.FileSystem.DefaultIOService();
#endif

#if !BESTHTTP_DISABLE_PROXY && (!UNITY_WEBGL || UNITY_EDITOR)
            ProxyDetector = new Proxies.Autodetect.ProxyDetector();
#endif
        }

#if (!UNITY_WEBGL || UNITY_EDITOR) && !BESTHTTP_DISABLE_ALTERNATE_SSL && !BESTHTTP_DISABLE_HTTP2
        /// <summary>
        /// HTTP/2 settings
        /// </summary>
        public static Connections.HTTP2.HTTP2PluginSettings HTTP2Settings = new Connections.HTTP2.HTTP2PluginSettings();
#endif

#region Global Options

        /// <summary>
        /// The maximum active TCP connections that the client will maintain to a server. Default value is 6. Minimum value is 1.
        /// </summary>
        public static byte MaxConnectionPerServer
        {
            get{ return maxConnectionPerServer; }
            set
            {
                if (value <= 0)
                    throw new ArgumentOutOfRangeException("MaxConnectionPerServer must be greater than 0!");

                bool isGrowing = value > maxConnectionPerServer;
                maxConnectionPerServer = value;

                // If the allowed connections per server is growing, go through all hosts and try to send out queueud requests.
                if (isGrowing)
                    HostManager.TryToSendQueuedRequests();
            }
        }
        private static byte maxConnectionPerServer;

        /// <summary>
        /// Default value of a HTTP request's IsKeepAlive value. Default value is true. If you make rare request to the server it should be changed to false.
        /// </summary>
        public static bool KeepAliveDefaultValue { get; set; }

#if !BESTHTTP_DISABLE_CACHING
        /// <summary>
        /// Set to true, if caching is prohibited.
        /// </summary>
        public static bool IsCachingDisabled { get; set; }
#endif

        /// <summary>
        /// How many time must be passed to destroy that connection after a connection finished its last request. Its default value is 20 seconds.
        /// </summary>
        public static TimeSpan MaxConnectionIdleTime { get; set; }

#if !BESTHTTP_DISABLE_COOKIES
        /// <summary>
        /// Set to false to disable all Cookie. It's default value is true.
        /// </summary>
        public static bool IsCookiesEnabled { get; set; }
#endif

        /// <summary>
        /// Size of the Cookie Jar in bytes. It's default value is 10485760 (10 MB).
        /// </summary>
        public static uint CookieJarSize { get; set; }

        /// <summary>
        /// If this property is set to true, then new cookies treated as session cookies and these cookies are not saved to disk. Its default value is false;
        /// </summary>
        public static bool EnablePrivateBrowsing { get; set; }

        /// <summary>
        /// Global, default value of the HTTPRequest's ConnectTimeout property. If set to TimeSpan.Zero or lower, no connect timeout logic is executed. Default value is 20 seconds.
        /// </summary>
        public static TimeSpan ConnectTimeout { get; set; }

        /// <summary>
        /// Global, default value of the HTTPRequest's Timeout property. Default value is 60 seconds.
        /// </summary>
        public static TimeSpan RequestTimeout { get; set; }

        /// <summary>
        /// By default the plugin will save all cache and cookie data under the path returned by Application.persistentDataPath.
        /// You can assign a function to this delegate to return a custom root path to define a new path.
        /// <remarks>This delegate will be called on a non Unity thread!</remarks>
        /// </summary>
        public static System.Func<string> RootCacheFolderProvider { get; set; }

#if !BESTHTTP_DISABLE_PROXY && (!UNITY_WEBGL || UNITY_EDITOR)

        public static Proxies.Autodetect.ProxyDetector ProxyDetector {
            get => _proxyDetector;
            set {
                _proxyDetector?.Detach();
                _proxyDetector = value;
            }
        }
        private static Proxies.Autodetect.ProxyDetector _proxyDetector;

        /// <summary>
        /// The global, default proxy for all HTTPRequests. The HTTPRequest's Proxy still can be changed per-request. Default value is null.
        /// </summary>
        public static Proxy Proxy { get; set; }
#endif

        /// <summary>
        /// Heartbeat manager to use less threads in the plugin. The heartbeat updates are called from the OnUpdate function.
        /// </summary>
        public static HeartbeatManager Heartbeats
        {
            get
            {
                if (heartbeats == null)
                    heartbeats = new HeartbeatManager();
                return heartbeats;
            }
        }
        private static HeartbeatManager heartbeats;

        /// <summary>
        /// A basic BestHTTP.Logger.ILogger implementation to be able to log intelligently additional informations about the plugin's internal mechanism.
        /// </summary>
        public static BestHTTP.Logger.ILogger Logger
        {
            get
            {
                // Make sure that it has a valid logger instance.
                if (logger == null)
                {
                    logger = new ThreadedLogger();
                    logger.Level = Loglevels.None;
                }

                return logger;
            }

            set { logger = value; }
        }
        private static BestHTTP.Logger.ILogger logger;

#if !BESTHTTP_DISABLE_ALTERNATE_SSL && (!UNITY_WEBGL || UNITY_EDITOR)

        public static TlsClientFactoryDelegate TlsClientFactory;

        public static Connections.TLS.AbstractTls13Client DefaultTlsClientFactory(HTTPRequest request, List<SecureProtocol.Org.BouncyCastle.Tls.ProtocolName> protocols)
        {
            // http://tools.ietf.org/html/rfc3546#section-3.1
            // -It is RECOMMENDED that clients include an extension of type "server_name" in the client hello whenever they locate a server by a supported name type.
            // -Literal IPv4 and IPv6 addresses are not permitted in "HostName".

            // User-defined list has a higher priority
            List<SecureProtocol.Org.BouncyCastle.Tls.ServerName> hostNames = null;

            // If there's no user defined one and the host isn't an IP address, add the default one
            if (!request.CurrentUri.IsHostIsAnIPAddress())
            {
                hostNames = new List<SecureProtocol.Org.BouncyCastle.Tls.ServerName>(1);
                hostNames.Add(new SecureProtocol.Org.BouncyCastle.Tls.ServerName(0, System.Text.Encoding.UTF8.GetBytes(request.CurrentUri.Host)));
            }

            return new Connections.TLS.DefaultTls13Client(request, hostNames, protocols);
        }

        /// <summary>
        /// The default value for the HTTPRequest's UseAlternateSSL property.
        /// </summary>
        public static bool UseAlternateSSLDefaultValue { get; set; }
#endif

#if !NETFX_CORE
        public static Func<HTTPRequest, System.Security.Cryptography.X509Certificates.X509Certificate, System.Security.Cryptography.X509Certificates.X509Chain, System.Net.Security.SslPolicyErrors, bool> DefaultCertificationValidator;
        public static ClientCertificateSelector ClientCertificationProvider;
#endif

        /// <summary>
        /// TCP Client's send buffer size.
        /// </summary>
        public static int? SendBufferSize;

        /// <summary>
        /// TCP Client's receive buffer size.
        /// </summary>
        public static int? ReceiveBufferSize;

        /// <summary>
        /// An IIOService implementation to handle filesystem operations.
        /// </summary>
        public static PlatformSupport.FileSystem.IIOService IOService;

        /// <summary>
        /// On most systems the maximum length of a path is around 255 character. If a cache entity's path is longer than this value it doesn't get cached. There no platform independent API to query the exact value on the current system, but it's
        /// exposed here and can be overridden. It's default value is 255.
        /// </summary>
        internal static int MaxPathLength { get; set; }

        /// <summary>
        /// User-agent string that will be sent with each requests.
        /// </summary>
        public static string UserAgent = "BestHTTP/2 v2.8.5";

        /// <summary>
        /// It's true if the application is quitting and the plugin is shutting down itself.
        /// </summary>
        public static bool IsQuitting { get { return _isQuitting; } private set { _isQuitting = value; } }
        private static volatile bool _isQuitting;
#endregion

#region Manager variables

        private static bool IsSetupCalled;

#endregion

#region Public Interface

        public static void Setup()
        {
            if (IsSetupCalled)
                return;
            IsSetupCalled = true;
            IsQuitting = false;

            HTTPManager.Logger.Information("HTTPManager", "Setup called! UserAgent: " + UserAgent);

            HTTPUpdateDelegator.CheckInstance();

#if !BESTHTTP_DISABLE_CACHING
            HTTPCacheService.CheckSetup();
#endif

#if !BESTHTTP_DISABLE_COOKIES
            Cookies.CookieJar.SetupFolder();
            Cookies.CookieJar.Load();
#endif

            HostManager.Load();
        }

        public static HTTPRequest SendRequest(string url, OnRequestFinishedDelegate callback)
        {
            return SendRequest(new HTTPRequest(new Uri(url), HTTPMethods.Get, callback));
        }

        public static HTTPRequest SendRequest(string url, HTTPMethods methodType, OnRequestFinishedDelegate callback)
        {
            return SendRequest(new HTTPRequest(new Uri(url), methodType, callback));
        }

        public static HTTPRequest SendRequest(string url, HTTPMethods methodType, bool isKeepAlive, OnRequestFinishedDelegate callback)
        {
            return SendRequest(new HTTPRequest(new Uri(url), methodType, isKeepAlive, callback));
        }

        public static HTTPRequest SendRequest(string url, HTTPMethods methodType, bool isKeepAlive, bool disableCache, OnRequestFinishedDelegate callback)
        {
            return SendRequest(new HTTPRequest(new Uri(url), methodType, isKeepAlive, disableCache, callback));
        }

        public static HTTPRequest SendRequest(HTTPRequest request)
        {
            if (!IsSetupCalled)
                Setup();

            if (request.IsCancellationRequested || IsQuitting)
                return request;

#if !BESTHTTP_DISABLE_CACHING
            // If possible load the full response from cache.
            if (Caching.HTTPCacheService.IsCachedEntityExpiresInTheFuture(request))
            {
                DateTime started = DateTime.Now;
                PlatformSupport.Threading.ThreadedRunner.RunShortLiving<HTTPRequest>((req) =>
                {
                    if (Connections.ConnectionHelper.TryLoadAllFromCache("HTTPManager", req, req.Context))
                    {
                        req.Timing.Add("Full Cache Load", DateTime.Now - started);
                        req.State = HTTPRequestStates.Finished;
                    }
                    else
                    {
                        // If for some reason it couldn't load we place back the request to the queue.

                        request.State = HTTPRequestStates.Queued;
                        RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(request, RequestEvents.Resend));
                    }
                }, request);
            }
            else
#endif
            {
                request.State = HTTPRequestStates.Queued;
                RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(request, RequestEvents.Resend));
            }

            return request;
        }

#endregion

#region Internal Helper Functions

        /// <summary>
        /// Will return where the various caches should be saved.
        /// </summary>
        public static string GetRootCacheFolder()
        {
            try
            {
                if (RootCacheFolderProvider != null)
                    return RootCacheFolderProvider();
            }
            catch(Exception ex)
            {
                HTTPManager.Logger.Exception("HTTPManager", "GetRootCacheFolder", ex);
            }

#if NETFX_CORE
            return Windows.Storage.ApplicationData.Current.LocalFolder.Path;
#else
            return UnityEngine.Application.persistentDataPath;
#endif
        }

#if UNITY_EDITOR
#if UNITY_2019_3_OR_NEWER
        [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.SubsystemRegistration)]
#endif
        public static void ResetSetup()
        {
            IsSetupCalled = false;
            BufferedReadNetworkStream.ResetNetworkStats();
            HTTPManager.Logger.Information("HTTPManager", "Reset called!");
        }
#endif

#endregion

#region MonoBehaviour Events (Called from HTTPUpdateDelegator)

        /// <summary>
        /// Update function that should be called regularly from a Unity event(Update, LateUpdate). Callbacks are dispatched from this function.
        /// </summary>
        public static void OnUpdate()
        {
            using (var _ = new ProfilerMarker(nameof(RequestEventHelper)).Auto())
                RequestEventHelper.ProcessQueue();

            using (var _ = new ProfilerMarker(nameof(ConnectionEventHelper)).Auto())
                ConnectionEventHelper.ProcessQueue();

            using (var _ = new ProfilerMarker(nameof(ProtocolEventHelper)).Auto())
                ProtocolEventHelper.ProcessQueue();

            using (var _ = new ProfilerMarker(nameof(PluginEventHelper)).Auto())
                PluginEventHelper.ProcessQueue();

            using (var _ = new ProfilerMarker(nameof(Timer)).Auto())
                BestHTTP.Extensions.Timer.Process();

            if (heartbeats != null)
            {
                using (var _ = new ProfilerMarker(nameof(HeartbeatManager)).Auto())
                    heartbeats.Update();
            }

            using (var _ = new ProfilerMarker(nameof(BufferPool)).Auto())
                BufferPool.Maintain();

            using (var _ = new ProfilerMarker(nameof(StringBuilderPool)).Auto())
                StringBuilderPool.Maintain();
        }

        public static void OnQuit()
        {
            HTTPManager.Logger.Information("HTTPManager", "OnQuit called!");

            IsQuitting = true;

            AbortAll();

#if !BESTHTTP_DISABLE_CACHING
            HTTPCacheService.SaveLibrary();
#endif

#if !BESTHTTP_DISABLE_COOKIES
            CookieJar.Persist();
#endif

            OnUpdate();

            HostManager.Clear();

            Heartbeats.Clear();
        }

        public static void AbortAll()
        {
            HTTPManager.Logger.Information("HTTPManager", "AbortAll called!");

            // This is an immediate shutdown request!

            RequestEventHelper.Clear();
            ConnectionEventHelper.Clear();
            PluginEventHelper.Clear();
            ProtocolEventHelper.Clear();

            HostManager.Shutdown();

            ProtocolEventHelper.CancelActiveProtocols();
        }

#endregion
    }
}
