using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Tesses.WebServer
{
    //This file contains modified code from https://github.com/dajuric/simple-http
    /// 
    /// Delegate executed when a file is about to be read from a body stream.
    /// 
    /// Field name.
    /// name of the file.
    /// Content type.
    /// Stream to be populated.
    public delegate Stream OnFile(string fieldName, string fileName, string contentType);
    public static class DajuricSimpleHttpExtensions
    {
       /* Thanks to you we fixed this
        public static void Print(string text,[CallerLineNumber] int lineNumber = 0)
        {
            Console.WriteLine($"[LINE {lineNumber}] {text}");
        }*/
        static void Deconstruct(this KeyValuePair tuple, out T1 key, out T2 value)
        {
            key = tuple.Key;
            value = tuple.Value;
        }
        const string BYTES_RANGE_HEADER = "Range";
        static bool ParseForm(this ServerContext ctx)
        {
            //Print("Enter ParseForm(this ServerContext ctx)");
            var args = ctx.QueryParams;
            string content_type = ctx.RequestHeaders.GetFirst("Content-Type");
            if (content_type != "application/x-www-form-urlencoded")
                return false;
            //Print("Before BodyAsString");
            var str = ctx.BodyAsString();
            //Print("After BodyAsString");   
            if (str == null)
                return false;
            //Print("Before For Loop");
            foreach (var pair in str.Split('&'))
            {
                var nameValue = pair.Split('=');
                if (nameValue.Length != (1 + 1))
                    continue;
                //Print($"Before Add: {nameValue[0]}: {nameValue[1]}");
                args.Add(nameValue[0], WebUtility.UrlDecode(nameValue[1]));
                 //Print($"After Add: {nameValue[0]}: {nameValue[1]}");
              
            }
            return true;
        }
        private static string BodyAsString(this ServerContext ctx)
        {
            
            string str = null;
            using (var reader = new StreamReader(ctx.GetRequestStream()))
            {
                //Print("Before ReadToEnd");
              
                str = reader.ReadToEnd();
                 //Print("After ReadToEnd");
              
            }
            return str;
        }
        
        public static async Task SendStreamAsync(this ServerContext ctx, Stream strm, string contentType = "application/octet-stream")
        {
            //ctx.StatusCode = 200;
            int start = 0, end = (int)strm.Length - 1;
            if (ctx.RequestHeaders.ContainsKey(BYTES_RANGE_HEADER) && strm.CanSeek)
            {
                if (ctx.RequestHeaders[BYTES_RANGE_HEADER].Count > 1)
                {
                    throw new NotSupportedException("Multiple 'Range' headers are not supported.");
                }
                var range = ctx.RequestHeaders[BYTES_RANGE_HEADER][0].Replace("bytes=", String.Empty)
                                   .Split(new string[] { "-" }, StringSplitOptions.RemoveEmptyEntries)
                                   .Select(x => Int32.Parse(x))
                                   .ToArray();
                start = (range.Length > 0) ? range[0] : 0;
                end = (range.Length > 1) ? range[1] : (int)(strm.Length - 1);
                var hdrs = ctx.ResponseHeaders;
                hdrs.Add("Accept-Ranges", "bytes");
                hdrs.Add("Content-Range", "bytes " + start + "-" + end + "/" + strm.Length);
                ctx.StatusCode = 206;
            }
            ctx.ResponseHeaders.Add("Content-Length", (end - start + 1).ToString());
            ctx.ResponseHeaders.Add("Content-Type", contentType);
            await ctx.WriteHeadersAsync();
            if (!ctx.Method.Equals("HEAD", StringComparison.Ordinal))
            {
                try
                {
                    if(strm.CanSeek)
                    strm.Position = start;
                    strm.CopyTo(ctx.NetworkStream, Math.Min(8 * 1024 * 1024, end - start + 1));
                }
                finally
                {
                    strm.Close();
                    ctx.NetworkStream.Close();
                }
            }
        }
        static Dictionary ParseMultipartForm(ServerContext serverCtx, OnFile onFile)
        {
            var args = serverCtx.QueryParams;
            string content_type=serverCtx.RequestHeaders.GetFirst("Content-Type");
            if (content_type.StartsWith("multipart/form-data",StringComparison.Ordinal) == false)
                throw new InvalidDataException("Not 'multipart/form-data'.");
            var boundary = Regex.Match(content_type, "boundary=(.+)").Groups[1].Value;
            boundary = "--" + boundary;
            var files = new Dictionary();
            var inputStream = new BufferedStream(serverCtx.GetRequestStream());
             //Print("Before ParseUntillBoundaryEnd");
              
            parseUntillBoundaryEnd(inputStream, new MemoryStream(), boundary);
            //Print("After ParseUntillBoundaryEnd");
            while (true)
            {
                //Print("Before ParseSection");
                var (n, v, fn, ct) = parseSection(inputStream, "\r\n" + boundary, onFile);
                //Print("After ParseSection");
                if (String.IsNullOrEmpty(n)) break;
                v.Position = 0;
                if (!String.IsNullOrEmpty(fn))
                    files.Add(n, new HttpFile(fn, v, ct));
                else
                    args.Add(n, readAsString(v));
            }
            return files;
        }
        private static (string Name, Stream Value, string FileName, string ContentType) parseSection(Stream source, string boundary, OnFile onFile)
        {
            //Print("Before ReadContentDisposition");
            var (n, fn, ct) = readContentDisposition(source);
            //Print("After ReadContentDisposition");
            source.ReadByte(); source.ReadByte(); //\r\n (empty row)
            var dst = String.IsNullOrEmpty(fn) ? new MemoryStream() : onFile(n, fn, ct);
            if (dst == null)
                throw new ArgumentException(nameof(onFile), "The on-file callback must return a stream.");
            //Print("Before ParseUntillBodyEnd");
            parseUntillBoundaryEnd(source, dst, boundary);
            //Print("Before ParseUntillBodyEnd");
            return (n, dst, fn, ct);
        }
        private static (string Name, string FileName, string ContentType) readContentDisposition(Stream stream)
        {
            const string UTF_FNAME = "utf-8''";
            var l = readLine(stream);
            if (String.IsNullOrEmpty(l))
                return (null, null, null);
            //(regex matches are taken from NancyFX) and modified
            var n = Regex.Match(l, @"name=""?(?[^\""]*)").Groups["n"].Value;
            var f = Regex.Match(l, @"filename\*?=""?(?[^\"";]*)").Groups["f"]?.Value;
            string cType = null;
            if (!String.IsNullOrEmpty(f))
            {
                if (f.StartsWith(UTF_FNAME))
                    f = Uri.UnescapeDataString(f.Substring(UTF_FNAME.Length));
                l = readLine(stream);
                cType = Regex.Match(l, "Content-Type: (?.+)").Groups["cType"].Value;
            }
            return (n, f, cType);
        }
        private static void parseUntillBoundaryEnd(Stream source, Stream destination, string boundary)
        {
            var checkBuffer = new byte[boundary.Length]; //for boundary checking
            int b, i = 0;
            while ((b = source.ReadByte()) != -1)
            {
                if (i == boundary.Length) //boundary found -> go to the end of line
                {
                    if (b == '\n') break;
                    continue;
                }
                if (b == boundary[i]) //start filling the check buffer
                {
                    checkBuffer[i] = (byte)b;
                    i++;
                }
                else
                {
                    var idx = 0;
                    while (idx < i) //write the buffer data to stream
                    {
                        destination.WriteByte(checkBuffer[idx]);
                        idx++;
                    }
                    i = 0;
                    destination.WriteByte((byte)b); //write the current byte
                }
            }
        }
        private static string readLine(Stream stream)
        {
            var sb = new StringBuilder();
            int b;
            while ((b = stream.ReadByte()) != -1 && b != '\n')
                sb.Append((char)b);
            if (sb.Length > 0 && sb[sb.Length - 1] == '\r')
                sb.Remove(sb.Length - 1, 1);
            return sb.ToString();
        }
        private static string readAsString(Stream stream)
        {
            var sb = new StringBuilder();
            int b;
            while ((b = stream.ReadByte()) != -1)
                sb.Append((char)b);
            return sb.ToString();
        
        }
        /// 
        /// Parses body of the request including form and multi-part form data.
        /// 
        /// HTTP request.
        /// Key-value pairs populated by the form data by this function.
        /// Name-file pair collection.
        public static Dictionary ParseBody(this ServerContext ctx)
        {
            
            return ctx.ParseBody( (n, fn, ct) => new MemoryStream());
        }
        /// 
        /// Parses body of the request including form and multi-part form data.
        /// 
        /// HTTP request.
        /// Key-value pairs populated by the form data by this function.
        /// 
        /// Function called if a file is about to be parsed. The stream is attached to a corresponding .
        /// By default,  is used, but for large files, it is recommended to open  directly.
        /// 
        /// Name-file pair collection.
        public static Dictionary ParseBody(this ServerContext request, OnFile onFile)
        {
            if (request == null)
                throw new ArgumentNullException(nameof(request));
            if (!request.RequestHeaders.ContainsKey("Content-Type"))
                throw new ArgumentNullException("request.RequestHeaders[\"Content-Type\"]");
            if (onFile == null)
                throw new ArgumentNullException(nameof(onFile));
            var files = new Dictionary();
            string content_type = request.RequestHeaders.GetFirst("Content-Type");
            if (content_type.StartsWith("application/x-www-form-urlencoded",StringComparison.Ordinal))
            {
                ParseForm(request);
            }
            else if (content_type.StartsWith("multipart/form-data",StringComparison.Ordinal))
            {
                files = ParseMultipartForm(request, onFile);
            }
            else
                throw new NotSupportedException("The body content-type is not supported.");
            return files;
        }
    }
    /// 
    /// HTTP file data container.
    /// 
    public class HttpFile : IDisposable
    {
        /// 
        /// Creates new HTTP file data container.
        /// 
        /// File name.
        /// Data.
        /// Content type.
        internal HttpFile(string fileName, Stream value, string contentType)
        {
            Value = value;
            FileName = fileName;
            ContentType = contentType;
        }
        /// 
        /// Gets the name of the file.
        /// 
        public string FileName { get; private set; }
        /// 
        /// Gets the data.
        /// If a stream is created  it will be closed when this HttpFile object is disposed.
        /// 
        public Stream Value { get; private set; }
        /// 
        /// Content type.
        /// 
        public string ContentType { get; private set; }
        /// 
        /// Saves the data into a file.
        /// Directory path will be auto created if does not exists.
        /// 
        /// File path with name.
        /// True to overwrite the existing file, false otherwise.
        /// True if the file is saved/overwritten, false otherwise.
        public bool Save(string fileName, bool overwrite = false)
        {
            if (File.Exists(Path.GetFullPath(fileName)))
                return false;
            var dir = Path.GetDirectoryName(Path.GetFullPath(fileName));
            Directory.CreateDirectory(dir);
            Value.Position = 0;
            using (var outStream = File.OpenWrite(fileName))
                Value.CopyTo(outStream);
            return true;
        }
        /// 
        /// Disposes the current instance.
        /// 
        public void Dispose()
        {
            if (Value != null)
            {
                Value?.Dispose();
                Value = null;
            }
        }
        /// 
        /// Disposes the current instance.
        /// 
        ~HttpFile()
        {
            Dispose();
        }
    }
    /// 
    /// Route server, Based on SimpleHTTP (Used most of the Route Source)
    /// 
    public delegate bool ShouldProcessFunc(ServerContext ctx);
    public delegate Task HttpActionAsync(ServerContext ctx);
    public delegate void HttpAction(ServerContext ctx);
    public class RouteServer : Server
    {
        public List<(ShouldProcessFunc ShouldProcessFunc, HttpActionAsync Action)> Methods = new List<(ShouldProcessFunc ShouldProcessFunc, HttpActionAsync Action)>();
        public override async Task GetAsync(ServerContext ctx)
        {
            await Process(ctx);
        }
        public override async Task PostAsync(ServerContext ctx)
        {
            await Process(ctx);
        }
        public override async Task OtherAsync(ServerContext ctx)
        { 
            await Process(ctx);
        }
        public override async Task OptionsAsync(ServerContext ctx)
        {
            await Process(ctx);
        }
        private async Task Process(ServerContext ctx)
        {
            foreach(var (shouldProcessFunc,action) in Methods)
            {
                if(!shouldProcessFunc(ctx))
                {
                    ctx.ResetQueryParms();
                    continue;
                }
                await action(ctx);
                return;
            }
        }
        /// 
        /// Adds the specified action to the route collection.
        /// The order of actions defines the priority.
        /// 
        /// Function defining whether the specified action should be executed or not.
        /// Action executed if the specified pattern matches the URL path.
        public void Add(ShouldProcessFunc shouldProcess, HttpActionAsync action)
        {
            Methods.Add((shouldProcess, action));
        }
        /// 
        /// Adds the specified action to the route collection.
        /// The order of actions defines the priority.
        /// 
        /// 
        /// String url 
        /// 
        /// Action executed if the specified pattern matches the URL path.
        /// HTTP method (GET, POST, DELETE, HEAD).
        public void Add(string url,HttpActionAsync action,string method="GET")
        {
            Add((e) =>
            {
                if (!e.Method.Equals( method,StringComparison.Ordinal))
                    return false;
                return e.UrlPath.Equals(url,StringComparison.Ordinal);
            },
                action);
        }
        /// 
        /// Adds the specified action to the route collection.
        /// The order of actions defines the priority.
        /// 
        /// 
        /// String url 
        /// 
        /// Action executed if the specified pattern matches the URL path.
        /// HTTP method (GET, POST, DELETE, HEAD).
        public void Add(string url, HttpAction action, string method = "GET")
        {
            Add((e) =>
            {
                if (!e.Method.Equals(method, StringComparison.Ordinal))
                    return false;
                return e.UrlPath.Equals(url, StringComparison.Ordinal);
            },
                action);
        }
        /// 
        /// Adds the specified action to the route collection.
        /// The order of actions defines the priority.
        /// 
        /// Function defining whether the specified action should be executed or not.
        /// Action executed if the specified pattern matches the URL path.
        public void Add(ShouldProcessFunc shouldProcess,HttpAction action)
        {
            Methods.Add((shouldProcess, (e) =>
            {
                action(e);
                return Task.FromResult(true);
            }
            ));
        }
    }
}