using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Kernel;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace MediaBrowser.Common.Net.Handlers
{
    /// 
    /// Class BaseHandler
    /// 
    public abstract class BaseHandler : IHttpServerHandler
        where TKernelType : IKernel
    {
        /// 
        /// Initializes the specified kernel.
        /// 
        /// The kernel.
        public void Initialize(IKernel kernel)
        {
            Kernel = (TKernelType)kernel;
        }
        /// 
        /// Gets or sets the kernel.
        /// 
        /// The kernel.
        protected TKernelType Kernel { get; private set; }
        /// 
        /// Gets the URL suffix used to determine if this handler can process a request.
        /// 
        /// The URL suffix.
        protected virtual string UrlSuffix
        {
            get
            {
                var name = GetType().Name;
                const string srch = "Handler";
                if (name.EndsWith(srch, StringComparison.OrdinalIgnoreCase))
                {
                    name = name.Substring(0, name.Length - srch.Length);
                }
                return "api/" + name;
            }
        }
        /// 
        /// Handleses the request.
        /// 
        /// The request.
        /// true if XXXX, false otherwise
        public virtual bool HandlesRequest(HttpListenerRequest request)
        {
            var name = '/' + UrlSuffix.TrimStart('/');
            var url = Kernel.WebApplicationName + name;
            return request.Url.LocalPath.EndsWith(url, StringComparison.OrdinalIgnoreCase);
        }
        /// 
        /// Gets or sets the compressed stream.
        /// 
        /// The compressed stream.
        private Stream CompressedStream { get; set; }
        /// 
        /// Gets a value indicating whether [use chunked encoding].
        /// 
        /// null if [use chunked encoding] contains no value, true if [use chunked encoding]; otherwise, false.
        public virtual bool? UseChunkedEncoding
        {
            get
            {
                return null;
            }
        }
        /// 
        /// The original HttpListenerContext
        /// 
        /// The HTTP listener context.
        protected HttpListenerContext HttpListenerContext { get; set; }
        /// 
        /// The _query string
        /// 
        private NameValueCollection _queryString;
        /// 
        /// The original QueryString
        /// 
        /// The query string.
        public NameValueCollection QueryString
        {
            get
            {
                // HttpListenerContext.Request.QueryString is not decoded properly
                return _queryString;
            }
        }
        /// 
        /// The _requested ranges
        /// 
        private List> _requestedRanges;
        /// 
        /// Gets the requested ranges.
        /// 
        /// The requested ranges.
        protected IEnumerable> RequestedRanges
        {
            get
            {
                if (_requestedRanges == null)
                {
                    _requestedRanges = new List>();
                    if (IsRangeRequest)
                    {
                        // Example: bytes=0-,32-63
                        var ranges = HttpListenerContext.Request.Headers["Range"].Split('=')[1].Split(',');
                        foreach (var range in ranges)
                        {
                            var vals = range.Split('-');
                            long start = 0;
                            long? end = null;
                            if (!string.IsNullOrEmpty(vals[0]))
                            {
                                start = long.Parse(vals[0]);
                            }
                            if (!string.IsNullOrEmpty(vals[1]))
                            {
                                end = long.Parse(vals[1]);
                            }
                            _requestedRanges.Add(new KeyValuePair(start, end));
                        }
                    }
                }
                return _requestedRanges;
            }
        }
        /// 
        /// Gets a value indicating whether this instance is range request.
        /// 
        /// true if this instance is range request; otherwise, false.
        protected bool IsRangeRequest
        {
            get
            {
                return HttpListenerContext.Request.Headers.AllKeys.Contains("Range");
            }
        }
        /// 
        /// Gets a value indicating whether [client supports compression].
        /// 
        /// true if [client supports compression]; otherwise, false.
        protected bool ClientSupportsCompression
        {
            get
            {
                var enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
                return enc.Equals("*", StringComparison.OrdinalIgnoreCase) ||
                    enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 ||
                    enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1;
            }
        }
        /// 
        /// Gets the compression method.
        /// 
        /// The compression method.
        private string CompressionMethod
        {
            get
            {
                var enc = HttpListenerContext.Request.Headers["Accept-Encoding"] ?? string.Empty;
                if (enc.IndexOf("deflate", StringComparison.OrdinalIgnoreCase) != -1 || enc.Equals("*", StringComparison.OrdinalIgnoreCase))
                {
                    return "deflate";
                }
                if (enc.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1)
                {
                    return "gzip";
                }
                return null;
            }
        }
        /// 
        /// Processes the request.
        /// 
        /// The CTX.
        /// Task.
        public virtual async Task ProcessRequest(HttpListenerContext ctx)
        {
            HttpListenerContext = ctx;
            ctx.Response.AddHeader("Access-Control-Allow-Origin", "*");
            ctx.Response.KeepAlive = true;
            try
            {
                await ProcessRequestInternal(ctx).ConfigureAwait(false);
            }
            catch (InvalidOperationException ex)
            {
                HandleException(ctx.Response, ex, 422);
                throw;
            }
            catch (ResourceNotFoundException ex)
            {
                HandleException(ctx.Response, ex, 404);
                throw;
            }
            catch (FileNotFoundException ex)
            {
                HandleException(ctx.Response, ex, 404);
                throw;
            }
            catch (DirectoryNotFoundException ex)
            {
                HandleException(ctx.Response, ex, 404);
                throw;
            }
            catch (UnauthorizedAccessException ex)
            {
                HandleException(ctx.Response, ex, 401);
                throw;
            }
            catch (ArgumentException ex)
            {
                HandleException(ctx.Response, ex, 400);
                throw;
            }
            catch (Exception ex)
            {
                HandleException(ctx.Response, ex, 500);
                throw;
            }
            finally
            {
                DisposeResponseStream();
            }
        }
        /// 
        /// Appends the error message.
        /// 
        /// The response.
        /// The ex.
        /// The status code.
        private void HandleException(HttpListenerResponse response, Exception ex, int statusCode)
        {
            response.StatusCode = statusCode;
            response.Headers.Add("Status", statusCode.ToString(new CultureInfo("en-US")));
            response.Headers.Remove("Age");
            response.Headers.Remove("Expires");
            response.Headers.Remove("Cache-Control");
            response.Headers.Remove("Etag");
            response.Headers.Remove("Last-Modified");
            response.ContentType = "text/plain";
            //Logger.ErrorException("Error processing request", ex);
            
            if (!string.IsNullOrEmpty(ex.Message))
            {
                response.AddHeader("X-Application-Error-Code", ex.Message);
            }
            var bytes = Encoding.UTF8.GetBytes(ex.Message);
            var stream = CompressedStream ?? response.OutputStream;
            // This could fail, but try to add the stack trace as the body content
            try
            {
                stream.Write(bytes, 0, bytes.Length);
            }
            catch (Exception ex1)
            {
                //Logger.ErrorException("Error dumping stack trace", ex1);
            }
        }
        /// 
        /// Processes the request internal.
        /// 
        /// The CTX.
        /// Task.
        private async Task ProcessRequestInternal(HttpListenerContext ctx)
        {
            var responseInfo = await GetResponseInfo().ConfigureAwait(false);
            // Let the client know if byte range requests are supported or not
            if (responseInfo.SupportsByteRangeRequests)
            {
                ctx.Response.Headers["Accept-Ranges"] = "bytes";
            }
            else if (!responseInfo.SupportsByteRangeRequests)
            {
                ctx.Response.Headers["Accept-Ranges"] = "none";
            }
            if (responseInfo.IsResponseValid && responseInfo.SupportsByteRangeRequests && IsRangeRequest)
            {
                // Set the initial status code
                // When serving a range request, we need to return status code 206 to indicate a partial response body
                responseInfo.StatusCode = 206;
            }
            ctx.Response.ContentType = responseInfo.ContentType;
            if (responseInfo.Etag.HasValue)
            {
                ctx.Response.Headers["ETag"] = responseInfo.Etag.Value.ToString("N");
            }
            var isCacheValid = true;
            // Validate If-Modified-Since
            if (ctx.Request.Headers.AllKeys.Contains("If-Modified-Since"))
            {
                DateTime ifModifiedSince;
                if (DateTime.TryParse(ctx.Request.Headers["If-Modified-Since"], out ifModifiedSince))
                {
                    isCacheValid = IsCacheValid(ifModifiedSince.ToUniversalTime(), responseInfo.CacheDuration,
                                                responseInfo.DateLastModified);
                }
            }
            // Validate If-None-Match
            if (isCacheValid &&
                (responseInfo.Etag.HasValue || !string.IsNullOrEmpty(ctx.Request.Headers["If-None-Match"])))
            {
                Guid ifNoneMatch;
                if (Guid.TryParse(ctx.Request.Headers["If-None-Match"] ?? string.Empty, out ifNoneMatch))
                {
                    if (responseInfo.Etag.HasValue && responseInfo.Etag.Value == ifNoneMatch)
                    {
                        responseInfo.StatusCode = 304;
                    }
                }
            }
            LogResponse(ctx, responseInfo);
            if (responseInfo.IsResponseValid)
            {
                await OnProcessingRequest(responseInfo).ConfigureAwait(false);
            }
            if (responseInfo.IsResponseValid)
            {
                await ProcessUncachedRequest(ctx, responseInfo).ConfigureAwait(false);
            }
            else
            {
                if (responseInfo.StatusCode == 304)
                {
                    AddAgeHeader(ctx.Response, responseInfo);
                    AddExpiresHeader(ctx.Response, responseInfo);
                }
                ctx.Response.StatusCode = responseInfo.StatusCode;
                ctx.Response.SendChunked = false;
            }
        }
        /// 
        /// The _null task result
        /// 
        private readonly Task _nullTaskResult = Task.FromResult(true);
        /// 
        /// Called when [processing request].
        /// 
        /// The response info.
        /// Task.
        protected virtual Task OnProcessingRequest(ResponseInfo responseInfo)
        {
            return _nullTaskResult;
        }
        /// 
        /// Logs the response.
        /// 
        /// The CTX.
        /// The response info.
        private void LogResponse(HttpListenerContext ctx, ResponseInfo responseInfo)
        {
            // Don't log normal 200's
            if (responseInfo.StatusCode == 200)
            {
                return;
            }
            var log = new StringBuilder();
            log.AppendLine(string.Format("Url: {0}", ctx.Request.Url));
            log.AppendLine("Headers: " + string.Join(",", ctx.Response.Headers.AllKeys.Select(k => k + "=" + ctx.Response.Headers[k])));
            var msg = "Http Response Sent (" + responseInfo.StatusCode + ") to " + ctx.Request.RemoteEndPoint;
            if (Kernel.Configuration.EnableHttpLevelLogging)
            {
                //Logger.LogMultiline(msg, LogSeverity.Debug, log);
            }
        }
        /// 
        /// Processes the uncached request.
        /// 
        /// The CTX.
        /// The response info.
        /// Task.
        private async Task ProcessUncachedRequest(HttpListenerContext ctx, ResponseInfo responseInfo)
        {
            var totalContentLength = GetTotalContentLength(responseInfo);
            // By default, use chunked encoding if we don't know the content length
            var useChunkedEncoding = UseChunkedEncoding == null ? (totalContentLength == null) : UseChunkedEncoding.Value;
            // Don't force this to true. HttpListener will default it to true if supported by the client.
            if (!useChunkedEncoding)
            {
                ctx.Response.SendChunked = false;
            }
            // Set the content length, if we know it
            if (totalContentLength.HasValue)
            {
                ctx.Response.ContentLength64 = totalContentLength.Value;
            }
            var compressResponse = responseInfo.CompressResponse && ClientSupportsCompression;
            // Add the compression header
            if (compressResponse)
            {
                ctx.Response.AddHeader("Content-Encoding", CompressionMethod);
                ctx.Response.AddHeader("Vary", "Accept-Encoding");
            }
            // Don't specify both last modified and Etag, unless caching unconditionally. They are redundant
            // https://developers.google.com/speed/docs/best-practices/caching#LeverageBrowserCaching
            if (responseInfo.DateLastModified.HasValue && (!responseInfo.Etag.HasValue || responseInfo.CacheDuration.Ticks > 0))
            {
                ctx.Response.Headers[HttpResponseHeader.LastModified] = responseInfo.DateLastModified.Value.ToString("r");
                AddAgeHeader(ctx.Response, responseInfo);
            }
            // Add caching headers
            ConfigureCaching(ctx.Response, responseInfo);
            // Set the status code
            ctx.Response.StatusCode = responseInfo.StatusCode;
            if (responseInfo.IsResponseValid)
            {
                // Finally, write the response data
                var outputStream = ctx.Response.OutputStream;
                if (compressResponse)
                {
                    if (CompressionMethod.Equals("deflate", StringComparison.OrdinalIgnoreCase))
                    {
                        CompressedStream = new DeflateStream(outputStream, CompressionLevel.Fastest, true);
                    }
                    else
                    {
                        CompressedStream = new GZipStream(outputStream, CompressionLevel.Fastest, true);
                    }
                    outputStream = CompressedStream;
                }
                await WriteResponseToOutputStream(outputStream, responseInfo, totalContentLength).ConfigureAwait(false);
            }
            else
            {
                ctx.Response.SendChunked = false;
            }
        }
        /// 
        /// Configures the caching.
        /// 
        /// The response.
        /// The response info.
        private void ConfigureCaching(HttpListenerResponse response, ResponseInfo responseInfo)
        {
            if (responseInfo.CacheDuration.Ticks > 0)
            {
                response.Headers[HttpResponseHeader.CacheControl] = "public, max-age=" + Convert.ToInt32(responseInfo.CacheDuration.TotalSeconds);
            }
            else if (responseInfo.Etag.HasValue)
            {
                response.Headers[HttpResponseHeader.CacheControl] = "public";
            }
            else
            {
                response.Headers[HttpResponseHeader.CacheControl] = "no-cache, no-store, must-revalidate";
                response.Headers[HttpResponseHeader.Pragma] = "no-cache, no-store, must-revalidate";
            }
            AddExpiresHeader(response, responseInfo);
        }
        /// 
        /// Adds the expires header.
        /// 
        /// The response.
        /// The response info.
        private void AddExpiresHeader(HttpListenerResponse response, ResponseInfo responseInfo)
        {
            if (responseInfo.CacheDuration.Ticks > 0)
            {
                response.Headers[HttpResponseHeader.Expires] = DateTime.UtcNow.Add(responseInfo.CacheDuration).ToString("r");
            }
            else if (!responseInfo.Etag.HasValue)
            {
                response.Headers[HttpResponseHeader.Expires] = "-1";
            }
        }
        /// 
        /// Adds the age header.
        /// 
        /// The response.
        /// The response info.
        private void AddAgeHeader(HttpListenerResponse response, ResponseInfo responseInfo)
        {
            if (responseInfo.DateLastModified.HasValue)
            {
                response.Headers[HttpResponseHeader.Age] = Convert.ToInt32((DateTime.UtcNow - responseInfo.DateLastModified.Value).TotalSeconds).ToString(CultureInfo.InvariantCulture);
            }
        }
        /// 
        /// Writes the response to output stream.
        /// 
        /// The stream.
        /// The response info.
        /// Length of the content.
        /// Task.
        protected abstract Task WriteResponseToOutputStream(Stream stream, ResponseInfo responseInfo, long? contentLength);
        /// 
        /// Disposes the response stream.
        /// 
        protected virtual void DisposeResponseStream()
        {
            if (CompressedStream != null)
            {
                try
                {
                    CompressedStream.Dispose();
                }
                catch (Exception ex)
                {
                    //Logger.ErrorException("Error disposing compressed stream", ex);
                }
            }
            try
            {
                //HttpListenerContext.Response.OutputStream.Dispose();
                HttpListenerContext.Response.Close();
            }
            catch (Exception ex)
            {
                //Logger.ErrorException("Error disposing response", ex);
            }
        }
        /// 
        /// Determines whether [is cache valid] [the specified if modified since].
        /// 
        /// If modified since.
        /// Duration of the cache.
        /// The date modified.
        /// true if [is cache valid] [the specified if modified since]; otherwise, false.
        private bool IsCacheValid(DateTime ifModifiedSince, TimeSpan cacheDuration, DateTime? dateModified)
        {
            if (dateModified.HasValue)
            {
                DateTime lastModified = NormalizeDateForComparison(dateModified.Value);
                ifModifiedSince = NormalizeDateForComparison(ifModifiedSince);
                return lastModified <= ifModifiedSince;
            }
            DateTime cacheExpirationDate = ifModifiedSince.Add(cacheDuration);
            if (DateTime.UtcNow < cacheExpirationDate)
            {
                return true;
            }
            return false;
        }
        /// 
        /// When the browser sends the IfModifiedDate, it's precision is limited to seconds, so this will account for that
        /// 
        /// The date.
        /// DateTime.
        private DateTime NormalizeDateForComparison(DateTime date)
        {
            return new DateTime(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Kind);
        }
        /// 
        /// Gets the total length of the content.
        /// 
        /// The response info.
        /// System.Nullable{System.Int64}.
        protected virtual long? GetTotalContentLength(ResponseInfo responseInfo)
        {
            return null;
        }
        /// 
        /// Gets the response info.
        /// 
        /// Task{ResponseInfo}.
        protected abstract Task GetResponseInfo();
        /// 
        /// Gets a bool query string param.
        /// 
        /// The name.
        /// true if XXXX, false otherwise
        protected bool GetBoolQueryStringParam(string name)
        {
            var val = QueryString[name] ?? string.Empty;
            return val.Equals("1", StringComparison.OrdinalIgnoreCase) || val.Equals("true", StringComparison.OrdinalIgnoreCase);
        }
        /// 
        /// The _form values
        /// 
        private Hashtable _formValues;
        /// 
        /// Gets a value from form POST data
        /// 
        /// The name.
        /// Task{System.String}.
        protected async Task GetFormValue(string name)
        {
            if (_formValues == null)
            {
                _formValues = await GetFormValues(HttpListenerContext.Request).ConfigureAwait(false);
            }
            if (_formValues.ContainsKey(name))
            {
                return _formValues[name].ToString();
            }
            return null;
        }
        /// 
        /// Extracts form POST data from a request
        /// 
        /// The request.
        /// Task{Hashtable}.
        private async Task GetFormValues(HttpListenerRequest request)
        {
            var formVars = new Hashtable();
            if (request.HasEntityBody)
            {
                if (request.ContentType.IndexOf("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) != -1)
                {
                    using (var requestBody = request.InputStream)
                    {
                        using (var reader = new StreamReader(requestBody, request.ContentEncoding))
                        {
                            var s = await reader.ReadToEndAsync().ConfigureAwait(false);
                            var pairs = s.Split('&');
                            foreach (var pair in pairs)
                            {
                                var index = pair.IndexOf('=');
                                if (index != -1)
                                {
                                    var name = pair.Substring(0, index);
                                    var value = pair.Substring(index + 1);
                                    formVars.Add(name, value);
                                }
                            }
                        }
                    }
                }
            }
            return formVars;
        }
    }
    /// 
    /// Class ResponseInfo
    /// 
    public class ResponseInfo
    {
        /// 
        /// Gets or sets the type of the content.
        /// 
        /// The type of the content.
        public string ContentType { get; set; }
        /// 
        /// Gets or sets the etag.
        /// 
        /// The etag.
        public Guid? Etag { get; set; }
        /// 
        /// Gets or sets the date last modified.
        /// 
        /// The date last modified.
        public DateTime? DateLastModified { get; set; }
        /// 
        /// Gets or sets the duration of the cache.
        /// 
        /// The duration of the cache.
        public TimeSpan CacheDuration { get; set; }
        /// 
        /// Gets or sets a value indicating whether [compress response].
        /// 
        /// true if [compress response]; otherwise, false.
        public bool CompressResponse { get; set; }
        /// 
        /// Gets or sets the status code.
        /// 
        /// The status code.
        public int StatusCode { get; set; }
        /// 
        /// Gets or sets a value indicating whether [supports byte range requests].
        /// 
        /// true if [supports byte range requests]; otherwise, false.
        public bool SupportsByteRangeRequests { get; set; }
        /// 
        /// Initializes a new instance of the  class.
        /// 
        public ResponseInfo()
        {
            CacheDuration = TimeSpan.FromTicks(0);
            CompressResponse = true;
            StatusCode = 200;
        }
        /// 
        /// Gets a value indicating whether this instance is response valid.
        /// 
        /// true if this instance is response valid; otherwise, false.
        public bool IsResponseValid
        {
            get
            {
                return StatusCode >= 200 && StatusCode < 300;
            }
        }
    }
}