// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.

using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http.Formatting.Parsers;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web.Http;

namespace System.Net.Http
{
    /// <summary>
    /// Extension methods to read <see cref="HttpRequestMessage"/> and <see cref="HttpResponseMessage"/> entities from <see cref="HttpContent"/> instances.
    /// </summary>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public static class HttpContentMessageExtensions
    {
        private const int MinBufferSize = 256;
        private const int DefaultBufferSize = 32 * 1024;

        /// <summary>
        /// Determines whether the specified content is HTTP request message content.
        /// </summary>
        /// <param name="content">The content.</param>
        /// <returns>
        ///   <c>true</c> if the specified content is HTTP message content; otherwise, <c>false</c>.
        /// </returns>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to false.")]
        public static bool IsHttpRequestMessageContent(this HttpContent content)
        {
            if (content == null)
            {
                throw Error.ArgumentNull("content");
            }

            try
            {
                return HttpMessageContent.ValidateHttpMessageContent(content, true, false);
            }
            catch (Exception)
            {
                return false;
            }
        }

        /// <summary>
        /// Determines whether the specified content is HTTP response message content.
        /// </summary>
        /// <param name="content">The content.</param>
        /// <returns>
        ///   <c>true</c> if the specified content is HTTP message content; otherwise, <c>false</c>.
        /// </returns>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to false.")]
        public static bool IsHttpResponseMessageContent(this HttpContent content)
        {
            if (content == null)
            {
                throw Error.ArgumentNull("content");
            }

            try
            {
                return HttpMessageContent.ValidateHttpMessageContent(content, false, false);
            }
            catch (Exception)
            {
                return false;
            }
        }

        /// <summary>
        /// Read the <see cref="HttpContent"/> as an <see cref="HttpRequestMessage"/>.
        /// </summary>
        /// <param name="content">The content to read.</param>
        /// <returns>The parsed <see cref="HttpRequestMessage"/> instance.</returns>
        public static Task<HttpRequestMessage> ReadAsHttpRequestMessageAsync(this HttpContent content)
        {
            return ReadAsHttpRequestMessageAsync(content, Uri.UriSchemeHttp, DefaultBufferSize);
        }

        /// <summary>
        /// Read the <see cref="HttpContent"/> as an <see cref="HttpRequestMessage"/>.
        /// </summary>
        /// <param name="content">The content to read.</param>
        /// <param name="uriScheme">The URI scheme to use for the request URI.</param>
        /// <returns>The parsed <see cref="HttpRequestMessage"/> instance.</returns>
        [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a full URI but only the URI scheme")]
        public static Task<HttpRequestMessage> ReadAsHttpRequestMessageAsync(this HttpContent content, string uriScheme)
        {
            return ReadAsHttpRequestMessageAsync(content, uriScheme, DefaultBufferSize);
        }

        /// <summary>
        /// Read the <see cref="HttpContent"/> as an <see cref="HttpRequestMessage"/>.
        /// </summary>
        /// <param name="content">The content to read.</param>
        /// <param name="uriScheme">The URI scheme to use for the request URI (the 
        /// URI scheme is not actually part of the HTTP Request URI and so must be provided externally).</param>
        /// <param name="bufferSize">Size of the buffer.</param>
        /// <returns>The parsed <see cref="HttpRequestMessage"/> instance.</returns>
        [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a full URI but only the URI scheme")]
        public static Task<HttpRequestMessage> ReadAsHttpRequestMessageAsync(this HttpContent content, string uriScheme, int bufferSize)
        {
            return ReadAsHttpRequestMessageAsync(content, uriScheme, bufferSize, HttpRequestHeaderParser.DefaultMaxHeaderSize);
        }

        /// <summary>
        /// Read the <see cref="HttpContent"/> as an <see cref="HttpRequestMessage"/>.
        /// </summary>
        /// <param name="content">The content to read.</param>
        /// <param name="uriScheme">The URI scheme to use for the request URI (the 
        /// URI scheme is not actually part of the HTTP Request URI and so must be provided externally).</param>
        /// <param name="bufferSize">Size of the buffer.</param>
        /// <param name="maxHeaderSize">The max length of the HTTP header.</param>
        /// <returns>The parsed <see cref="HttpRequestMessage"/> instance.</returns>
        [SuppressMessage("Microsoft.Design", "CA1054:UriParametersShouldNotBeStrings", MessageId = "1#", Justification = "This is not a full URI but only the URI scheme")]
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to parser state.")]
        public static Task<HttpRequestMessage> ReadAsHttpRequestMessageAsync(this HttpContent content, string uriScheme, int bufferSize, int maxHeaderSize)
        {
            if (content == null)
            {
                throw Error.ArgumentNull("content");
            }

            if (uriScheme == null)
            {
                throw Error.ArgumentNull("uriScheme");
            }

            if (!Uri.CheckSchemeName(uriScheme))
            {
                throw Error.Argument("uriScheme", Properties.Resources.HttpMessageParserInvalidUriScheme, uriScheme, typeof(Uri).Name);
            }

            if (bufferSize < MinBufferSize)
            {
                throw Error.ArgumentMustBeGreaterThanOrEqualTo("bufferSize", bufferSize, MinBufferSize);
            }

            if (maxHeaderSize < InternetMessageFormatHeaderParser.MinHeaderSize)
            {
                throw Error.ArgumentMustBeGreaterThanOrEqualTo("maxHeaderSize", maxHeaderSize, InternetMessageFormatHeaderParser.MinHeaderSize);
            }

            HttpMessageContent.ValidateHttpMessageContent(content, true, true);

            return content.ReadAsStreamAsync().Then(stream =>
            {
                HttpUnsortedRequest httpRequest = new HttpUnsortedRequest();
                HttpRequestHeaderParser parser = new HttpRequestHeaderParser(httpRequest, HttpRequestHeaderParser.DefaultMaxRequestLineSize, maxHeaderSize);
                ParserState parseStatus;

                byte[] buffer = new byte[bufferSize];
                int bytesRead = 0;
                int headerConsumed = 0;

                while (true)
                {
                    try
                    {
                        bytesRead = stream.Read(buffer, 0, buffer.Length);
                    }
                    catch (Exception e)
                    {
                        throw new IOException(Properties.Resources.HttpMessageErrorReading, e);
                    }

                    try
                    {
                        parseStatus = parser.ParseBuffer(buffer, bytesRead, ref headerConsumed);
                    }
                    catch (Exception)
                    {
                        parseStatus = ParserState.Invalid;
                    }

                    if (parseStatus == ParserState.Done)
                    {
                        return CreateHttpRequestMessage(uriScheme, httpRequest, stream, bytesRead - headerConsumed);
                    }
                    else if (parseStatus != ParserState.NeedMoreData)
                    {
                        throw Error.InvalidOperation(Properties.Resources.HttpMessageParserError, headerConsumed, buffer);
                    }
                }
            });
        }

        /// <summary>
        /// Read the <see cref="HttpContent"/> as an <see cref="HttpResponseMessage"/>.
        /// </summary>
        /// <param name="content">The content to read.</param>
        /// <returns>The parsed <see cref="HttpResponseMessage"/> instance.</returns>
        public static Task<HttpResponseMessage> ReadAsHttpResponseMessageAsync(this HttpContent content)
        {
            return ReadAsHttpResponseMessageAsync(content, DefaultBufferSize);
        }

        /// <summary>
        /// Read the <see cref="HttpContent"/> as an <see cref="HttpResponseMessage"/>.
        /// </summary>
        /// <param name="content">The content to read.</param>
        /// <param name="bufferSize">Size of the buffer.</param>
        /// <returns>The parsed <see cref="HttpResponseMessage"/> instance.</returns>
        public static Task<HttpResponseMessage> ReadAsHttpResponseMessageAsync(this HttpContent content, int bufferSize)
        {
            return ReadAsHttpResponseMessageAsync(content, bufferSize, HttpResponseHeaderParser.DefaultMaxHeaderSize);
        }

        /// <summary>
        /// Read the <see cref="HttpContent"/> as an <see cref="HttpResponseMessage"/>.
        /// </summary>
        /// <param name="content">The content to read.</param>
        /// <param name="bufferSize">Size of the buffer.</param>
        /// <param name="maxHeaderSize">The max length of the HTTP header.</param>
        /// <returns>The parsed <see cref="HttpResponseMessage"/> instance.</returns>
        [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Exception translates to parser state.")]
        public static Task<HttpResponseMessage> ReadAsHttpResponseMessageAsync(this HttpContent content, int bufferSize, int maxHeaderSize)
        {
            if (content == null)
            {
                throw Error.ArgumentNull("content");
            }

            if (bufferSize < MinBufferSize)
            {
                throw Error.ArgumentMustBeGreaterThanOrEqualTo("bufferSize", bufferSize, MinBufferSize);
            }

            if (maxHeaderSize < InternetMessageFormatHeaderParser.MinHeaderSize)
            {
                throw Error.ArgumentMustBeGreaterThanOrEqualTo("maxHeaderSize", maxHeaderSize, InternetMessageFormatHeaderParser.MinHeaderSize);
            }

            HttpMessageContent.ValidateHttpMessageContent(content, false, true);

            return content.ReadAsStreamAsync().Then(stream =>
            {
                HttpUnsortedResponse httpResponse = new HttpUnsortedResponse();
                HttpResponseHeaderParser parser = new HttpResponseHeaderParser(httpResponse, HttpResponseHeaderParser.DefaultMaxStatusLineSize, maxHeaderSize);
                ParserState parseStatus;

                byte[] buffer = new byte[bufferSize];
                int bytesRead = 0;
                int headerConsumed = 0;

                while (true)
                {
                    try
                    {
                        bytesRead = stream.Read(buffer, 0, buffer.Length);
                    }
                    catch (Exception e)
                    {
                        throw new IOException(Properties.Resources.HttpMessageErrorReading, e);
                    }

                    try
                    {
                        parseStatus = parser.ParseBuffer(buffer, bytesRead, ref headerConsumed);
                    }
                    catch (Exception)
                    {
                        parseStatus = ParserState.Invalid;
                    }

                    if (parseStatus == ParserState.Done)
                    {
                        // Create and return parsed HttpResponseMessage
                        return CreateHttpResponseMessage(httpResponse, stream, bytesRead - headerConsumed);
                    }
                    else if (parseStatus != ParserState.NeedMoreData)
                    {
                        throw Error.InvalidOperation(Properties.Resources.HttpMessageParserError, headerConsumed, buffer);
                    }
                }
            });
        }

        /// <summary>
        /// Creates the request URI by combining scheme (provided) with parsed values of
        /// host and path.
        /// </summary>
        /// <param name="uriScheme">The URI scheme to use for the request URI.</param>
        /// <param name="httpRequest">The unsorted HTTP request.</param>
        /// <returns>A fully qualified request URI.</returns>
        private static Uri CreateRequestUri(string uriScheme, HttpUnsortedRequest httpRequest)
        {
            Contract.Assert(httpRequest != null, "httpRequest cannot be null.");
            Contract.Assert(uriScheme != null, "uriScheme cannot be null");

            IEnumerable<string> hostValues;
            if (httpRequest.HttpHeaders.TryGetValues(FormattingUtilities.HttpHostHeader, out hostValues))
            {
                int hostCount = hostValues.Count();
                if (hostCount != 1)
                {
                    throw Error.InvalidOperation(Properties.Resources.HttpMessageParserInvalidHostCount, FormattingUtilities.HttpHostHeader, hostCount);
                }
            }
            else
            {
                throw Error.InvalidOperation(Properties.Resources.HttpMessageParserInvalidHostCount, FormattingUtilities.HttpHostHeader, 0);
            }

            // We don't use UriBuilder as hostValues.ElementAt(0) contains 'host:port' and UriBuilder needs these split out into separate host and port.
            string requestUri = String.Format(CultureInfo.InvariantCulture, "{0}://{1}{2}", uriScheme, hostValues.ElementAt(0), httpRequest.RequestUri);
            return new Uri(requestUri);
        }

        /// <summary>
        /// Copies the unsorted header fields to a sorted collection.
        /// </summary>
        /// <param name="source">The unsorted source headers</param>
        /// <param name="destination">The destination <see cref="HttpRequestHeaders"/> or <see cref="HttpResponseHeaders"/>.</param>
        /// <param name="contentStream">The input <see cref="Stream"/> used to form any <see cref="HttpContent"/> being part of this HTTP request.</param>
        /// <param name="rewind">Start location of any request entity within the <paramref name="contentStream"/>.</param>
        /// <returns>An <see cref="HttpContent"/> instance if header fields contained and <see cref="HttpContentHeaders"/>.</returns>
        private static HttpContent CreateHeaderFields(HttpHeaders source, HttpHeaders destination, Stream contentStream, int rewind)
        {
            Contract.Assert(source != null, "source headers cannot be null");
            Contract.Assert(destination != null, "destination headers cannot be null");
            Contract.Assert(contentStream != null, "contentStream must be non null");
            HttpContentHeaders contentHeaders = null;
            HttpContent content = null;

            // Set the header fields
            foreach (KeyValuePair<string, IEnumerable<string>> header in source)
            {
                if (!destination.TryAddWithoutValidation(header.Key, header.Value))
                {
                    if (contentHeaders == null)
                    {
                        contentHeaders = FormattingUtilities.CreateEmptyContentHeaders();
                    }

                    contentHeaders.TryAddWithoutValidation(header.Key, header.Value);
                }
            }

            // If we have content headers then create an HttpContent for this Response
            if (contentHeaders != null)
            {
                // Need to rewind the input stream to be at the position right after the HTTP header
                // which we may already have parsed as we read the content stream.
                if (!contentStream.CanSeek)
                {
                    throw Error.InvalidOperation(Properties.Resources.HttpMessageContentStreamMustBeSeekable, "ContentReadStream", FormattingUtilities.HttpResponseMessageType.Name);
                }

                contentStream.Seek(0 - rewind, SeekOrigin.Current);
                content = new StreamContent(contentStream);
                contentHeaders.CopyTo(content.Headers);
            }

            return content;
        }

        /// <summary>
        /// Creates an <see cref="HttpRequestMessage"/> based on information provided in <see cref="HttpUnsortedRequest"/>.
        /// </summary>
        /// <param name="uriScheme">The URI scheme to use for the request URI.</param>
        /// <param name="httpRequest">The unsorted HTTP request.</param>
        /// <param name="contentStream">The input <see cref="Stream"/> used to form any <see cref="HttpContent"/> being part of this HTTP request.</param>
        /// <param name="rewind">Start location of any request entity within the <paramref name="contentStream"/>.</param>
        /// <returns>A newly created <see cref="HttpRequestMessage"/> instance.</returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "caller becomes owner.")]
        private static HttpRequestMessage CreateHttpRequestMessage(string uriScheme, HttpUnsortedRequest httpRequest, Stream contentStream, int rewind)
        {
            Contract.Assert(uriScheme != null, "URI scheme must be non null");
            Contract.Assert(httpRequest != null, "httpRequest must be non null");
            Contract.Assert(contentStream != null, "contentStream must be non null");

            HttpRequestMessage httpRequestMessage = new HttpRequestMessage();

            // Set method, requestURI, and version
            httpRequestMessage.Method = httpRequest.Method;
            httpRequestMessage.RequestUri = CreateRequestUri(uriScheme, httpRequest);
            httpRequestMessage.Version = httpRequest.Version;

            // Set the header fields and content if any
            httpRequestMessage.Content = CreateHeaderFields(httpRequest.HttpHeaders, httpRequestMessage.Headers, contentStream, rewind);

            return httpRequestMessage;
        }

        /// <summary>
        /// Creates an <see cref="HttpResponseMessage"/> based on information provided in <see cref="HttpUnsortedResponse"/>.
        /// </summary>
        /// <param name="httpResponse">The unsorted HTTP Response.</param>
        /// <param name="contentStream">The input <see cref="Stream"/> used to form any <see cref="HttpContent"/> being part of this HTTP Response.</param>
        /// <param name="rewind">Start location of any Response entity within the <paramref name="contentStream"/>.</param>
        /// <returns>A newly created <see cref="HttpResponseMessage"/> instance.</returns>
        [SuppressMessage("Microsoft.Reliability", "CA2000:Dispose objects before losing scope", Justification = "caller becomes owner.")]
        private static HttpResponseMessage CreateHttpResponseMessage(HttpUnsortedResponse httpResponse, Stream contentStream, int rewind)
        {
            Contract.Assert(httpResponse != null, "httpResponse must be non null");
            Contract.Assert(contentStream != null, "contentStream must be non null");

            HttpResponseMessage httpResponseMessage = new HttpResponseMessage();

            // Set version, status code and reason phrase
            httpResponseMessage.Version = httpResponse.Version;
            httpResponseMessage.StatusCode = httpResponse.StatusCode;
            httpResponseMessage.ReasonPhrase = httpResponse.ReasonPhrase;

            // Set the header fields and content if any
            httpResponseMessage.Content = CreateHeaderFields(httpResponse.HttpHeaders, httpResponseMessage.Headers, contentStream, rewind);

            return httpResponseMessage;
        }
    }
}
