using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Logging;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Text;
using SocketHttpListener.Primitives;

namespace SocketHttpListener.Net
{
    sealed class HttpConnection
    {
        const int BufferSize = 8192;
        ISocket sock;
        Stream stream;
        EndPointListener epl;
        MemoryStream ms;
        byte[] buffer;
        HttpListenerContext context;
        StringBuilder current_line;
        ListenerPrefix prefix;
        RequestStream i_stream;
        Stream o_stream;
        bool chunked;
        int reuses;
        bool context_bound;
        bool secure;
        int s_timeout = 300000; // 90k ms for first request, 15k ms from then on
        IpEndPointInfo local_ep;
        HttpListener last_listener;
        int[] client_cert_errors;
        ICertificate cert;
        Stream ssl_stream;

        private ILogger _logger;
        private readonly ICryptoProvider _cryptoProvider;
        private readonly IMemoryStreamFactory _memoryStreamFactory;
        private readonly ITextEncoding _textEncoding;
        private readonly IStreamFactory _streamFactory;

        private HttpConnection(ILogger logger, ISocket sock, EndPointListener epl, bool secure, ICertificate cert, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding)
        {
            _logger = logger;
            this.sock = sock;
            this.epl = epl;
            this.secure = secure;
            this.cert = cert;
            _cryptoProvider = cryptoProvider;
            _memoryStreamFactory = memoryStreamFactory;
            _textEncoding = textEncoding;
            _streamFactory = streamFactory;
        }

        private async Task InitStream()
        {
            if (secure == false)
            {
                stream = _streamFactory.CreateNetworkStream(sock, false);
            }
            else
            {
                //ssl_stream = epl.Listener.CreateSslStream(new NetworkStream(sock, false), false, (t, c, ch, e) =>
                //{
                //    if (c == null)
                //        return true;
                //    var c2 = c as X509Certificate2;
                //    if (c2 == null)
                //        c2 = new X509Certificate2(c.GetRawCertData());
                //    client_cert = c2;
                //    client_cert_errors = new int[] { (int)e };
                //    return true;
                //});
                //stream = ssl_stream.AuthenticatedStream;

                ssl_stream = _streamFactory.CreateSslStream(_streamFactory.CreateNetworkStream(sock, false), false);
                await _streamFactory.AuthenticateSslStreamAsServer(ssl_stream, cert).ConfigureAwait(false);
                stream = ssl_stream;
            }
            Init();
        }

        public static async Task<HttpConnection> Create(ILogger logger, ISocket sock, EndPointListener epl, bool secure, ICertificate cert, ICryptoProvider cryptoProvider, IStreamFactory streamFactory, IMemoryStreamFactory memoryStreamFactory, ITextEncoding textEncoding)
        {
            var connection = new HttpConnection(logger, sock, epl, secure, cert, cryptoProvider, streamFactory, memoryStreamFactory, textEncoding);

            await connection.InitStream().ConfigureAwait(false);

            return connection;
        }

        public Stream Stream
        {
            get
            {
                return stream;
            }
        }

        internal int[] ClientCertificateErrors
        {
            get { return client_cert_errors; }
        }

        void Init()
        {
            if (ssl_stream != null)
            {
                //ssl_stream.AuthenticateAsServer(client_cert, true, (SslProtocols)ServicePointManager.SecurityProtocol, false);
                //_streamFactory.AuthenticateSslStreamAsServer(ssl_stream, cert);
            }

            context_bound = false;
            i_stream = null;
            o_stream = null;
            prefix = null;
            chunked = false;
            ms = _memoryStreamFactory.CreateNew();
            position = 0;
            input_state = InputState.RequestLine;
            line_state = LineState.None;
            context = new HttpListenerContext(this, _logger, _cryptoProvider, _memoryStreamFactory, _textEncoding);
        }

        public bool IsClosed
        {
            get { return (sock == null); }
        }

        public int Reuses
        {
            get { return reuses; }
        }

        public IpEndPointInfo LocalEndPoint
        {
            get
            {
                if (local_ep != null)
                    return local_ep;

                local_ep = (IpEndPointInfo)sock.LocalEndPoint;
                return local_ep;
            }
        }

        public IpEndPointInfo RemoteEndPoint
        {
            get { return (IpEndPointInfo)sock.RemoteEndPoint; }
        }

        public bool IsSecure
        {
            get { return secure; }
        }

        public ListenerPrefix Prefix
        {
            get { return prefix; }
            set { prefix = value; }
        }

        public async Task BeginReadRequest()
        {
            if (buffer == null)
                buffer = new byte[BufferSize];

            try
            {
                //if (reuses == 1)
                //    s_timeout = 15000;
                var nRead = await stream.ReadAsync(buffer, 0, BufferSize).ConfigureAwait(false);

                OnReadInternal(nRead);
            }
            catch (Exception ex)
            {
                OnReadInternalException(ms, ex);
            }
        }

        public RequestStream GetRequestStream(bool chunked, long contentlength)
        {
            if (i_stream == null)
            {
                byte[] buffer;
                _memoryStreamFactory.TryGetBuffer(ms, out buffer);

                int length = (int)ms.Length;
                ms = null;
                if (chunked)
                {
                    this.chunked = true;
                    //context.Response.SendChunked = true;
                    i_stream = new ChunkedInputStream(context, stream, buffer, position, length - position);
                }
                else
                {
                    i_stream = new RequestStream(stream, buffer, position, length - position, contentlength);
                }
            }
            return i_stream;
        }

        public Stream GetResponseStream(bool isExpect100Continue = false)
        {
            // TODO: can we get this stream before reading the input?
            if (o_stream == null)
            {
                //context.Response.DetermineIfChunked();

                if (context.Response.SendChunked || isExpect100Continue || context.Request.IsWebSocketRequest || true)
                {
                    o_stream = new ResponseStream(stream, context.Response, _memoryStreamFactory, _textEncoding);
                }
                else
                {
                    o_stream = stream;
                    using (var headerStream = ResponseStream.GetHeaders(context.Response, _memoryStreamFactory, false))
                    {
                        headerStream.CopyTo(o_stream);
                    }
                }
            }
            return o_stream;
        }

        void OnReadInternal(int nread)
        {
            ms.Write(buffer, 0, nread);
            if (ms.Length > 32768)
            {
                SendError("Bad request", 400);
                Close(true);
                return;
            }

            if (nread == 0)
            {
                //if (ms.Length > 0)
                //	SendError (); // Why bother?
                CloseSocket();
                Unbind();
                return;
            }

            if (ProcessInput(ms))
            {
                if (!context.HaveError)
                    context.Request.FinishInitialization();

                if (context.HaveError)
                {
                    SendError();
                    Close(true);
                    return;
                }

                if (!epl.BindContext(context))
                {
                    SendError("Invalid host", 400);
                    Close(true);
                    return;
                }
                HttpListener listener = epl.Listener;
                if (last_listener != listener)
                {
                    RemoveConnection();
                    listener.AddConnection(this);
                    last_listener = listener;
                }

                context_bound = true;
                listener.RegisterContext(context);
                return;
            }

            BeginReadRequest();
        }

        private void OnReadInternalException(MemoryStream ms, Exception ex)
        {
            //_logger.ErrorException("Error in HttpConnection.OnReadInternal", ex);

            if (ms != null && ms.Length > 0)
                SendError();
            if (sock != null)
            {
                CloseSocket();
                Unbind();
            }
        }

        void RemoveConnection()
        {
            if (last_listener == null)
                epl.RemoveConnection(this);
            else
                last_listener.RemoveConnection(this);
        }

        enum InputState
        {
            RequestLine,
            Headers
        }

        enum LineState
        {
            None,
            CR,
            LF
        }

        InputState input_state = InputState.RequestLine;
        LineState line_state = LineState.None;
        int position;

        // true -> done processing
        // false -> need more input
        bool ProcessInput(MemoryStream ms)
        {
            byte[] buffer;
            _memoryStreamFactory.TryGetBuffer(ms, out buffer);

            int len = (int)ms.Length;
            int used = 0;
            string line;

            while (true)
            {
                if (context.HaveError)
                    return true;

                if (position >= len)
                    break;

                try
                {
                    line = ReadLine(buffer, position, len - position, ref used);
                    position += used;
                }
                catch
                {
                    context.ErrorMessage = "Bad request";
                    context.ErrorStatus = 400;
                    return true;
                }

                if (line == null)
                    break;

                if (line == "")
                {
                    if (input_state == InputState.RequestLine)
                        continue;
                    current_line = null;
                    ms = null;
                    return true;
                }

                if (input_state == InputState.RequestLine)
                {
                    context.Request.SetRequestLine(line);
                    input_state = InputState.Headers;
                }
                else
                {
                    try
                    {
                        context.Request.AddHeader(line);
                    }
                    catch (Exception e)
                    {
                        context.ErrorMessage = e.Message;
                        context.ErrorStatus = 400;
                        return true;
                    }
                }
            }

            if (used == len)
            {
                ms.SetLength(0);
                position = 0;
            }
            return false;
        }

        string ReadLine(byte[] buffer, int offset, int len, ref int used)
        {
            if (current_line == null)
                current_line = new StringBuilder(128);
            int last = offset + len;
            used = 0;

            for (int i = offset; i < last && line_state != LineState.LF; i++)
            {
                used++;
                byte b = buffer[i];
                if (b == 13)
                {
                    line_state = LineState.CR;
                }
                else if (b == 10)
                {
                    line_state = LineState.LF;
                }
                else
                {
                    current_line.Append((char)b);
                }
            }

            string result = null;
            if (line_state == LineState.LF)
            {
                line_state = LineState.None;
                result = current_line.ToString();
                current_line.Length = 0;
            }

            return result;
        }

        public void SendError(string msg, int status)
        {
            try
            {
                HttpListenerResponse response = context.Response;
                response.StatusCode = status;
                response.ContentType = "text/html";
                string description = HttpListenerResponse.GetStatusDescription(status);
                string str;
                if (msg != null)
                    str = String.Format("<h1>{0} ({1})</h1>", description, msg);
                else
                    str = String.Format("<h1>{0}</h1>", description);

                byte[] error = context.Response.ContentEncoding.GetBytes(str);
                response.ContentLength64 = error.Length;
                response.OutputStream.Write(error, 0, (int)error.Length);
                response.Close();
            }
            catch
            {
                // response was already closed
            }
        }

        public void SendError()
        {
            SendError(context.ErrorMessage, context.ErrorStatus);
        }

        void Unbind()
        {
            if (context_bound)
            {
                epl.UnbindContext(context);
                context_bound = false;
            }
        }

        public void Close()
        {
            Close(false);
        }

        private void CloseSocket()
        {
            if (sock == null)
                return;

            try
            {
                sock.Close();
            }
            catch
            {
            }
            finally
            {
                sock = null;
            }
            RemoveConnection();
        }

        internal void Close(bool force_close)
        {
            if (sock != null)
            {
                if (!context.Request.IsWebSocketRequest || force_close)
                {
                    Stream st = GetResponseStream();
                    if (st != null)
                    {
                        st.Dispose();
                    }

                    o_stream = null;
                }
            }

            if (sock != null)
            {
                force_close |= !context.Request.KeepAlive;
                if (!force_close)
                    force_close = (string.Equals(context.Response.Headers["connection"], "close", StringComparison.OrdinalIgnoreCase));
                /*
				if (!force_close) {
//					bool conn_close = (status_code == 400 || status_code == 408 || status_code == 411 ||
//							status_code == 413 || status_code == 414 || status_code == 500 ||
//							status_code == 503);
					force_close |= (context.Request.ProtocolVersion <= HttpVersion.Version10);
				}
				*/

                if (!force_close && context.Request.FlushInput())
                {
                    reuses++;
                    Unbind();
                    Init();
                    BeginReadRequest();
                    return;
                }

                ISocket s = sock;
                sock = null;
                try
                {
                    if (s != null)
                        s.Shutdown(true);
                }
                catch
                {
                }
                finally
                {
                    if (s != null)
                        s.Close();
                }
                Unbind();
                RemoveConnection();
                return;
            }
        }
    }
}