#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Emby.Server.Implementations.Services;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using IRequest = MediaBrowser.Model.Services.IRequest;
using MimeTypes = MediaBrowser.Model.Net.MimeTypes;
namespace Emby.Server.Implementations.HttpServer
{
    /// 
    /// Class HttpResultFactory.
    /// 
    public class HttpResultFactory : IHttpResultFactory
    {
        // Last-Modified and If-Modified-Since must follow strict date format,
        // see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
        private const string HttpDateFormat = "ddd, dd MMM yyyy HH:mm:ss \"GMT\"";
        // We specifically use en-US culture because both day of week and month names require it
        private static readonly CultureInfo _enUSculture = new CultureInfo("en-US", false);
        /// 
        /// The logger.
        /// 
        private readonly ILogger _logger;
        private readonly IFileSystem _fileSystem;
        private readonly IJsonSerializer _jsonSerializer;
        private readonly IStreamHelper _streamHelper;
        /// 
        /// Initializes a new instance of the  class.
        /// 
        public HttpResultFactory(ILoggerFactory loggerfactory, IFileSystem fileSystem, IJsonSerializer jsonSerializer, IStreamHelper streamHelper)
        {
            _fileSystem = fileSystem;
            _jsonSerializer = jsonSerializer;
            _streamHelper = streamHelper;
            _logger = loggerfactory.CreateLogger("HttpResultFactory");
        }
        /// 
        /// Gets the result.
        /// 
        /// The content.
        /// Type of the content.
        /// The response headers.
        /// System.Object.
        public object GetResult(IRequest requestContext, byte[] content, string contentType, IDictionary responseHeaders = null)
        {
            return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
        }
        public object GetResult(string content, string contentType, IDictionary responseHeaders = null)
        {
            return GetHttpResult(null, content, contentType, true, responseHeaders);
        }
        public object GetResult(IRequest requestContext, Stream content, string contentType, IDictionary responseHeaders = null)
        {
            return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
        }
        public object GetResult(IRequest requestContext, string content, string contentType, IDictionary responseHeaders = null)
        {
            return GetHttpResult(requestContext, content, contentType, true, responseHeaders);
        }
        public object GetRedirectResult(string url)
        {
            var responseHeaders = new Dictionary();
            responseHeaders[HeaderNames.Location] = url;
            var result = new HttpResult(Array.Empty(), "text/plain", HttpStatusCode.Redirect);
            AddResponseHeaders(result, responseHeaders);
            return result;
        }
        /// 
        /// Gets the HTTP result.
        /// 
        private IHasHeaders GetHttpResult(IRequest requestContext, Stream content, string contentType, bool addCachePrevention, IDictionary responseHeaders = null)
        {
            var result = new StreamWriter(content, contentType);
            if (responseHeaders == null)
            {
                responseHeaders = new Dictionary();
            }
            if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string expires))
            {
                responseHeaders[HeaderNames.Expires] = "0";
            }
            AddResponseHeaders(result, responseHeaders);
            return result;
        }
        /// 
        /// Gets the HTTP result.
        /// 
        private IHasHeaders GetHttpResult(IRequest requestContext, byte[] content, string contentType, bool addCachePrevention, IDictionary responseHeaders = null)
        {
            string compressionType = null;
            bool isHeadRequest = false;
            if (requestContext != null)
            {
                compressionType = GetCompressionType(requestContext, content, contentType);
                isHeadRequest = string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
            }
            IHasHeaders result;
            if (string.IsNullOrEmpty(compressionType))
            {
                var contentLength = content.Length;
                if (isHeadRequest)
                {
                    content = Array.Empty();
                }
                result = new StreamWriter(content, contentType, contentLength);
            }
            else
            {
                result = GetCompressedResult(content, compressionType, responseHeaders, isHeadRequest, contentType);
            }
            if (responseHeaders == null)
            {
                responseHeaders = new Dictionary();
            }
            if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
            {
                responseHeaders[HeaderNames.Expires] = "0";
            }
            AddResponseHeaders(result, responseHeaders);
            return result;
        }
        /// 
        /// Gets the HTTP result.
        /// 
        private IHasHeaders GetHttpResult(IRequest requestContext, string content, string contentType, bool addCachePrevention, IDictionary responseHeaders = null)
        {
            IHasHeaders result;
            var bytes = Encoding.UTF8.GetBytes(content);
            var compressionType = requestContext == null ? null : GetCompressionType(requestContext, bytes, contentType);
            var isHeadRequest = requestContext == null ? false : string.Equals(requestContext.Verb, "head", StringComparison.OrdinalIgnoreCase);
            if (string.IsNullOrEmpty(compressionType))
            {
                var contentLength = bytes.Length;
                if (isHeadRequest)
                {
                    bytes = Array.Empty();
                }
                result = new StreamWriter(bytes, contentType, contentLength);
            }
            else
            {
                result = GetCompressedResult(bytes, compressionType, responseHeaders, isHeadRequest, contentType);
            }
            if (responseHeaders == null)
            {
                responseHeaders = new Dictionary();
            }
            if (addCachePrevention && !responseHeaders.TryGetValue(HeaderNames.Expires, out string _))
            {
                responseHeaders[HeaderNames.Expires] = "0";
            }
            AddResponseHeaders(result, responseHeaders);
            return result;
        }
        /// 
        /// Gets the optimized result.
        /// 
        /// 
        public object GetResult(IRequest requestContext, T result, IDictionary responseHeaders = null)
            where T : class
        {
            if (result == null)
            {
                throw new ArgumentNullException(nameof(result));
            }
            if (responseHeaders == null)
            {
                responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase);
            }
            responseHeaders[HeaderNames.Expires] = "0";
            return ToOptimizedResultInternal(requestContext, result, responseHeaders);
        }
        private string GetCompressionType(IRequest request, byte[] content, string responseContentType)
        {
            if (responseContentType == null)
            {
                return null;
            }
            // Per apple docs, hls manifests must be compressed
            if (!responseContentType.StartsWith("text/", StringComparison.OrdinalIgnoreCase) &&
                responseContentType.IndexOf("json", StringComparison.OrdinalIgnoreCase) == -1 &&
                responseContentType.IndexOf("javascript", StringComparison.OrdinalIgnoreCase) == -1 &&
                responseContentType.IndexOf("xml", StringComparison.OrdinalIgnoreCase) == -1 &&
                responseContentType.IndexOf("application/x-mpegURL", StringComparison.OrdinalIgnoreCase) == -1)
            {
                return null;
            }
            if (content.Length < 1024)
            {
                return null;
            }
            return GetCompressionType(request);
        }
        private static string GetCompressionType(IRequest request)
        {
            var acceptEncoding = request.Headers[HeaderNames.AcceptEncoding].ToString();
            if (string.IsNullOrEmpty(acceptEncoding))
            {
                //if (_brotliCompressor != null && acceptEncoding.IndexOf("br", StringComparison.OrdinalIgnoreCase) != -1)
                //    return "br";
                if (acceptEncoding.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1)
                    return "deflate";
                if (acceptEncoding.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
                    return "gzip";
            }
            return null;
        }
        /// 
        /// Returns the optimized result for the IRequestContext.
        /// Does not use or store results in any cache.
        /// 
        /// 
        /// 
        /// 
        public object ToOptimizedResult(IRequest request, T dto)
        {
            return ToOptimizedResultInternal(request, dto);
        }
        private object ToOptimizedResultInternal(IRequest request, T dto, IDictionary responseHeaders = null)
        {
            // TODO: @bond use Span and .Equals
            var contentType = request.ResponseContentType?.Split(';')[0].Trim().ToLowerInvariant();
            switch (contentType)
            {
                case "application/xml":
                case "text/xml":
                case "text/xml; charset=utf-8": //"text/xml; charset=utf-8" also matches xml
                    return GetHttpResult(request, SerializeToXmlString(dto), contentType, false, responseHeaders);
                case "application/json":
                case "text/json":
                    return GetHttpResult(request, _jsonSerializer.SerializeToString(dto), contentType, false, responseHeaders);
                default:
                    break;
            }
            var isHeadRequest = string.Equals(request.Verb, "head", StringComparison.OrdinalIgnoreCase);
            var ms = new MemoryStream();
            var writerFn = RequestHelper.GetResponseWriter(HttpListenerHost.Instance, contentType);
            writerFn(dto, ms);
            ms.Position = 0;
            if (isHeadRequest)
            {
                using (ms)
                {
                    return GetHttpResult(request, Array.Empty(), contentType, true, responseHeaders);
                }
            }
            return GetHttpResult(request, ms, contentType, true, responseHeaders);
        }
        private IHasHeaders GetCompressedResult(byte[] content,
            string requestedCompressionType,
            IDictionary responseHeaders,
            bool isHeadRequest,
            string contentType)
        {
            if (responseHeaders == null)
            {
                responseHeaders = new Dictionary(StringComparer.OrdinalIgnoreCase);
            }
            content = Compress(content, requestedCompressionType);
            responseHeaders[HeaderNames.ContentEncoding] = requestedCompressionType;
            responseHeaders[HeaderNames.Vary] = HeaderNames.AcceptEncoding;
            var contentLength = content.Length;
            if (isHeadRequest)
            {
                var result = new StreamWriter(Array.Empty(), contentType, contentLength);
                AddResponseHeaders(result, responseHeaders);
                return result;
            }
            else
            {
                var result = new StreamWriter(content, contentType, contentLength);
                AddResponseHeaders(result, responseHeaders);
                return result;
            }
        }
        private byte[] Compress(byte[] bytes, string compressionType)
        {
            if (string.Equals(compressionType, "deflate", StringComparison.OrdinalIgnoreCase))
            {
                return Deflate(bytes);
            }
            if (string.Equals(compressionType, "gzip", StringComparison.OrdinalIgnoreCase))
            {
                return GZip(bytes);
            }
            throw new NotSupportedException(compressionType);
        }
        private static byte[] Deflate(byte[] bytes)
        {
            // In .NET FX incompat-ville, you can't access compressed bytes without closing DeflateStream
            // Which means we must use MemoryStream since you have to use ToArray() on a closed Stream
            using (var ms = new MemoryStream())
            using (var zipStream = new DeflateStream(ms, CompressionMode.Compress))
            {
                zipStream.Write(bytes, 0, bytes.Length);
                zipStream.Dispose();
                return ms.ToArray();
            }
        }
        private static byte[] GZip(byte[] buffer)
        {
            using (var ms = new MemoryStream())
            using (var zipStream = new GZipStream(ms, CompressionMode.Compress))
            {
                zipStream.Write(buffer, 0, buffer.Length);
                zipStream.Dispose();
                return ms.ToArray();
            }
        }
        private static string SerializeToXmlString(object from)
        {
            using (var ms = new MemoryStream())
            {
                var xwSettings = new XmlWriterSettings();
                xwSettings.Encoding = new UTF8Encoding(false);
                xwSettings.OmitXmlDeclaration = false;
                using (var xw = XmlWriter.Create(ms, xwSettings))
                {
                    var serializer = new DataContractSerializer(from.GetType());
                    serializer.WriteObject(xw, from);
                    xw.Flush();
                    ms.Seek(0, SeekOrigin.Begin);
                    using (var reader = new StreamReader(ms))
                    {
                        return reader.ReadToEnd();
                    }
                }
            }
        }
        /// 
        /// Pres the process optimized result.
        /// 
        private object GetCachedResult(IRequest requestContext, IDictionary responseHeaders, StaticResultOptions options)
        {
            bool noCache = (requestContext.Headers[HeaderNames.CacheControl].ToString()).IndexOf("no-cache", StringComparison.OrdinalIgnoreCase) != -1;
            AddCachingHeaders(responseHeaders, options.CacheDuration, noCache, options.DateLastModified);
            if (!noCache)
            {
                if (!DateTime.TryParseExact(requestContext.Headers[HeaderNames.IfModifiedSince], HttpDateFormat, _enUSculture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var ifModifiedSinceHeader))
                {
                    _logger.LogDebug("Failed to parse If-Modified-Since header date: {0}", requestContext.Headers[HeaderNames.IfModifiedSince]);
                    return null;
                }
                if (IsNotModified(ifModifiedSinceHeader, options.CacheDuration, options.DateLastModified))
                {
                    AddAgeHeader(responseHeaders, options.DateLastModified);
                    var result = new HttpResult(Array.Empty(), options.ContentType ?? "text/html", HttpStatusCode.NotModified);
                    AddResponseHeaders(result, responseHeaders);
                    return result;
                }
            }
            return null;
        }
        public Task