using System;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Services;
using MediaBrowser.Model.Text;
using SocketHttpListener.Primitives;

namespace SocketHttpListener.Net
{
    public sealed class HttpListenerRequest
    {
        string[] accept_types;
        Encoding content_encoding;
        long content_length;
        bool cl_set;
        CookieCollection cookies;
        WebHeaderCollection headers;
        string method;
        Stream input_stream;
        Version version;
        QueryParamCollection query_string; // check if null is ok, check if read-only, check case-sensitiveness
        string raw_url;
        Uri url;
        Uri referrer;
        string[] user_languages;
        HttpListenerContext context;
        bool is_chunked;
        bool ka_set;
        bool keep_alive;

        private readonly ITextEncoding _textEncoding;

        internal HttpListenerRequest(HttpListenerContext context, ITextEncoding textEncoding)
        {
            this.context = context;
            _textEncoding = textEncoding;
            headers = new WebHeaderCollection();
            version = HttpVersion.Version10;
        }

        static char[] separators = new char[] { ' ' };

        internal void SetRequestLine(string req)
        {
            string[] parts = req.Split(separators, 3);
            if (parts.Length != 3)
            {
                context.ErrorMessage = "Invalid request line (parts).";
                return;
            }

            method = parts[0];
            foreach (char c in method)
            {
                int ic = (int)c;

                if ((ic >= 'A' && ic <= 'Z') ||
                    (ic > 32 && c < 127 && c != '(' && c != ')' && c != '<' &&
                     c != '<' && c != '>' && c != '@' && c != ',' && c != ';' &&
                     c != ':' && c != '\\' && c != '"' && c != '/' && c != '[' &&
                     c != ']' && c != '?' && c != '=' && c != '{' && c != '}'))
                    continue;

                context.ErrorMessage = "(Invalid verb)";
                return;
            }

            raw_url = parts[1];
            if (parts[2].Length != 8 || !parts[2].StartsWith("HTTP/"))
            {
                context.ErrorMessage = "Invalid request line (version).";
                return;
            }

            try
            {
                version = new Version(parts[2].Substring(5));
                if (version.Major < 1)
                    throw new Exception();
            }
            catch
            {
                context.ErrorMessage = "Invalid request line (version).";
                return;
            }
        }

        void CreateQueryString(string query)
        {
            if (query == null || query.Length == 0)
            {
                query_string = new QueryParamCollection();
                return;
            }

            query_string = new QueryParamCollection();
            if (query[0] == '?')
                query = query.Substring(1);
            string[] components = query.Split('&');
            foreach (string kv in components)
            {
                int pos = kv.IndexOf('=');
                if (pos == -1)
                {
                    query_string.Add(null, WebUtility.UrlDecode(kv));
                }
                else
                {
                    string key = WebUtility.UrlDecode(kv.Substring(0, pos));
                    string val = WebUtility.UrlDecode(kv.Substring(pos + 1));

                    query_string.Add(key, val);
                }
            }
        }

        internal void FinishInitialization()
        {
            string host = UserHostName;
            if (version > HttpVersion.Version10 && (host == null || host.Length == 0))
            {
                context.ErrorMessage = "Invalid host name";
                return;
            }

            string path;
            Uri raw_uri = null;
            if (MaybeUri(raw_url.ToLowerInvariant()) && Uri.TryCreate(raw_url, UriKind.Absolute, out raw_uri))
                path = raw_uri.PathAndQuery;
            else
                path = raw_url;

            if ((host == null || host.Length == 0))
                host = UserHostAddress;

            if (raw_uri != null)
                host = raw_uri.Host;

            int colon = host.LastIndexOf(':');
            if (colon >= 0)
                host = host.Substring(0, colon);

            string base_uri = String.Format("{0}://{1}:{2}",
                (IsSecureConnection) ? (IsWebSocketRequest ? "wss" : "https") : (IsWebSocketRequest ? "ws" : "http"),
                                host, LocalEndPoint.Port);

            if (!Uri.TryCreate(base_uri + path, UriKind.Absolute, out url))
            {
                context.ErrorMessage = WebUtility.HtmlEncode("Invalid url: " + base_uri + path);
                return; return;
            }

            CreateQueryString(url.Query);

            if (version >= HttpVersion.Version11)
            {
                string t_encoding = Headers["Transfer-Encoding"];
                is_chunked = (t_encoding != null && String.Compare(t_encoding, "chunked", StringComparison.OrdinalIgnoreCase) == 0);
                // 'identity' is not valid!
                if (t_encoding != null && !is_chunked)
                {
                    context.Connection.SendError(null, 501);
                    return;
                }
            }

            if (!is_chunked && !cl_set)
            {
                if (String.Compare(method, "POST", StringComparison.OrdinalIgnoreCase) == 0 ||
                    String.Compare(method, "PUT", StringComparison.OrdinalIgnoreCase) == 0)
                {
                    context.Connection.SendError(null, 411);
                    return;
                }
            }

            if (String.Compare(Headers["Expect"], "100-continue", StringComparison.OrdinalIgnoreCase) == 0)
            {
                var output = (ResponseStream)context.Connection.GetResponseStream(true);

                var _100continue = _textEncoding.GetASCIIEncoding().GetBytes("HTTP/1.1 100 Continue\r\n\r\n");

                output.InternalWrite(_100continue, 0, _100continue.Length);
            }
        }

        static bool MaybeUri(string s)
        {
            int p = s.IndexOf(':');
            if (p == -1)
                return false;

            if (p >= 10)
                return false;

            return IsPredefinedScheme(s.Substring(0, p));
        }

        //
        // Using a simple block of if's is twice as slow as the compiler generated
        // switch statement.   But using this tuned code is faster than the
        // compiler generated code, with a million loops on x86-64:
        //
        // With "http": .10 vs .51 (first check)
        // with "https": .16 vs .51 (second check)
        // with "foo": .22 vs .31 (never found)
        // with "mailto": .12 vs .51  (last check)
        //
        //
        static bool IsPredefinedScheme(string scheme)
        {
            if (scheme == null || scheme.Length < 3)
                return false;

            char c = scheme[0];
            if (c == 'h')
                return (scheme == "http" || scheme == "https");
            if (c == 'f')
                return (scheme == "file" || scheme == "ftp");

            if (c == 'n')
            {
                c = scheme[1];
                if (c == 'e')
                    return (scheme == "news" || scheme == "net.pipe" || scheme == "net.tcp");
                if (scheme == "nntp")
                    return true;
                return false;
            }
            if ((c == 'g' && scheme == "gopher") || (c == 'm' && scheme == "mailto"))
                return true;

            return false;
        }

        internal static string Unquote(String str)
        {
            int start = str.IndexOf('\"');
            int end = str.LastIndexOf('\"');
            if (start >= 0 && end >= 0)
                str = str.Substring(start + 1, end - 1);
            return str.Trim();
        }

        internal void AddHeader(string header)
        {
            int colon = header.IndexOf(':');
            if (colon == -1 || colon == 0)
            {
                context.ErrorMessage = "Bad Request";
                context.ErrorStatus = 400;
                return;
            }

            string name = header.Substring(0, colon).Trim();
            string val = header.Substring(colon + 1).Trim();
            string lower = name.ToLowerInvariant();
            headers.SetInternal(name, val);
            switch (lower)
            {
                case "accept-language":
                    user_languages = val.Split(','); // yes, only split with a ','
                    break;
                case "accept":
                    accept_types = val.Split(','); // yes, only split with a ','
                    break;
                case "content-length":
                    try
                    {
                        //TODO: max. content_length?
                        content_length = Int64.Parse(val.Trim());
                        if (content_length < 0)
                            context.ErrorMessage = "Invalid Content-Length.";
                        cl_set = true;
                    }
                    catch
                    {
                        context.ErrorMessage = "Invalid Content-Length.";
                    }

                    break;
                case "content-type":
                    {
                        var contents = val.Split(';');
                        foreach (var content in contents)
                        {
                            var tmp = content.Trim();
                            if (tmp.StartsWith("charset"))
                            {
                                var charset = tmp.GetValue("=");
                                if (charset != null && charset.Length > 0)
                                {
                                    try
                                    {

                                        // Support upnp/dlna devices - CONTENT-TYPE: text/xml ; charset="utf-8"\r\n
                                        charset = charset.Trim('"');
                                        var index = charset.IndexOf('"');
                                        if (index != -1) charset = charset.Substring(0, index);

                                        content_encoding = Encoding.GetEncoding(charset);
                                    }
                                    catch
                                    {
                                        context.ErrorMessage = "Invalid Content-Type header: " + charset;
                                    }
                                }

                                break;
                            }
                        }
                    }
                    break;
                case "referer":
                    try
                    {
                        referrer = new Uri(val);
                    }
                    catch
                    {
                        referrer = new Uri("http://someone.is.screwing.with.the.headers.com/");
                    }
                    break;
                case "cookie":
                    if (cookies == null)
                        cookies = new CookieCollection();

                    string[] cookieStrings = val.Split(new char[] { ',', ';' });
                    Cookie current = null;
                    int version = 0;
                    foreach (string cookieString in cookieStrings)
                    {
                        string str = cookieString.Trim();
                        if (str.Length == 0)
                            continue;
                        if (str.StartsWith("$Version"))
                        {
                            version = Int32.Parse(Unquote(str.Substring(str.IndexOf('=') + 1)));
                        }
                        else if (str.StartsWith("$Path"))
                        {
                            if (current != null)
                                current.Path = str.Substring(str.IndexOf('=') + 1).Trim();
                        }
                        else if (str.StartsWith("$Domain"))
                        {
                            if (current != null)
                                current.Domain = str.Substring(str.IndexOf('=') + 1).Trim();
                        }
                        else if (str.StartsWith("$Port"))
                        {
                            if (current != null)
                                current.Port = str.Substring(str.IndexOf('=') + 1).Trim();
                        }
                        else
                        {
                            if (current != null)
                            {
                                cookies.Add(current);
                            }
                            current = new Cookie();
                            int idx = str.IndexOf('=');
                            if (idx > 0)
                            {
                                current.Name = str.Substring(0, idx).Trim();
                                current.Value = str.Substring(idx + 1).Trim();
                            }
                            else
                            {
                                current.Name = str.Trim();
                                current.Value = String.Empty;
                            }
                            current.Version = version;
                        }
                    }
                    if (current != null)
                    {
                        cookies.Add(current);
                    }
                    break;
            }
        }

        // returns true is the stream could be reused.
        internal bool FlushInput()
        {
            if (!HasEntityBody)
                return true;

            int length = 2048;
            if (content_length > 0)
                length = (int)Math.Min(content_length, (long)length);

            byte[] bytes = new byte[length];
            while (true)
            {
                // TODO: test if MS has a timeout when doing this
                try
                {
                    var task = InputStream.ReadAsync(bytes, 0, length);
                    var result = Task.WaitAll(new [] { task }, 1000);
                    if (!result)
                    {
                        return false;
                    }
                    if (task.Result <= 0)
                    {
                        return true;
                    }
                }
                catch (ObjectDisposedException e)
                {
                    input_stream = null;
                    return true;
                }
                catch
                {
                    return false;
                }
            }
        }

        public string[] AcceptTypes
        {
            get { return accept_types; }
        }

        public int ClientCertificateError
        {
            get
            {
                HttpConnection cnc = context.Connection;
                //if (cnc.ClientCertificate == null)
                //    throw new InvalidOperationException("No client certificate");
                //int[] errors = cnc.ClientCertificateErrors;
                //if (errors != null && errors.Length > 0)
                //    return errors[0];
                return 0;
            }
        }

        public Encoding ContentEncoding
        {
            get
            {
                if (content_encoding == null)
                    content_encoding = _textEncoding.GetDefaultEncoding();
                return content_encoding;
            }
        }

        public long ContentLength64
        {
            get { return content_length; }
        }

        public string ContentType
        {
            get { return headers["content-type"]; }
        }

        public CookieCollection Cookies
        {
            get
            {
                // TODO: check if the collection is read-only
                if (cookies == null)
                    cookies = new CookieCollection();
                return cookies;
            }
        }

        public bool HasEntityBody
        {
            get { return (content_length > 0 || is_chunked); }
        }

        public QueryParamCollection Headers
        {
            get { return headers; }
        }

        public string HttpMethod
        {
            get { return method; }
        }

        public Stream InputStream
        {
            get
            {
                if (input_stream == null)
                {
                    if (is_chunked || content_length > 0)
                        input_stream = context.Connection.GetRequestStream(is_chunked, content_length);
                    else
                        input_stream = Stream.Null;
                }

                return input_stream;
            }
        }

        public bool IsAuthenticated
        {
            get { return false; }
        }

        public bool IsLocal
        {
            get { return RemoteEndPoint.IpAddress.Equals(IpAddressInfo.Loopback) || RemoteEndPoint.IpAddress.Equals(IpAddressInfo.IPv6Loopback) || LocalEndPoint.IpAddress.Equals(RemoteEndPoint.IpAddress); }
        }

        public bool IsSecureConnection
        {
            get { return context.Connection.IsSecure; }
        }

        public bool KeepAlive
        {
            get
            {
                if (ka_set)
                    return keep_alive;

                ka_set = true;
                // 1. Connection header
                // 2. Protocol (1.1 == keep-alive by default)
                // 3. Keep-Alive header
                string cnc = headers["Connection"];
                if (!String.IsNullOrEmpty(cnc))
                {
                    keep_alive = (0 == String.Compare(cnc, "keep-alive", StringComparison.OrdinalIgnoreCase));
                }
                else if (version == HttpVersion.Version11)
                {
                    keep_alive = true;
                }
                else
                {
                    cnc = headers["keep-alive"];
                    if (!String.IsNullOrEmpty(cnc))
                        keep_alive = (0 != String.Compare(cnc, "closed", StringComparison.OrdinalIgnoreCase));
                }
                return keep_alive;
            }
        }

        public IpEndPointInfo LocalEndPoint
        {
            get { return context.Connection.LocalEndPoint; }
        }

        public Version ProtocolVersion
        {
            get { return version; }
        }

        public QueryParamCollection QueryString
        {
            get { return query_string; }
        }

        public string RawUrl
        {
            get { return raw_url; }
        }

        public IpEndPointInfo RemoteEndPoint
        {
            get { return context.Connection.RemoteEndPoint; }
        }

        public Guid RequestTraceIdentifier
        {
            get { return Guid.Empty; }
        }

        public Uri Url
        {
            get { return url; }
        }

        public Uri UrlReferrer
        {
            get { return referrer; }
        }

        public string UserAgent
        {
            get { return headers["user-agent"]; }
        }

        public string UserHostAddress
        {
            get { return LocalEndPoint.ToString(); }
        }

        public string UserHostName
        {
            get { return headers["host"]; }
        }

        public string[] UserLanguages
        {
            get { return user_languages; }
        }

        public string ServiceName
        {
            get
            {
                return null;
            }
        }

        private bool _websocketRequestWasSet;
        private bool _websocketRequest;

        /// <summary>
        /// Gets a value indicating whether the request is a WebSocket connection request.
        /// </summary>
        /// <value>
        /// <c>true</c> if the request is a WebSocket connection request; otherwise, <c>false</c>.
        /// </value>
        public bool IsWebSocketRequest
        {
            get
            {
                if (!_websocketRequestWasSet)
                {
                    _websocketRequest = method == "GET" &&
                                        version > HttpVersion.Version10 &&
                                        headers.Contains("Upgrade", "websocket") &&
                                        headers.Contains("Connection", "Upgrade");

                    _websocketRequestWasSet = true;
                }

                return _websocketRequest;
            }
        }

        public Task<ICertificate> GetClientCertificateAsync()
        {
            return Task.FromResult<ICertificate>(null);
        }
    }
}
