// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Globalization;
using System.Text;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.Extensions.Primitives;

namespace Microsoft.AspNetCore.Mvc.Formatters
{
    /// <summary>
    /// A media type value.
    /// </summary>
    public readonly struct MediaType
    {
        private static readonly StringSegment QualityParameter = new StringSegment("q");

        private readonly ReadOnlyMediaTypeHeaderValue _mediaTypeHeaderValue;

        /// <summary>
        /// Initializes a <see cref="MediaType"/> instance.
        /// </summary>
        /// <param name="mediaType">The <see cref="string"/> with the media type.</param>
        public MediaType(string mediaType)
            : this(mediaType, 0, mediaType.Length)
        {
        }

        /// <summary>
        /// Initializes a <see cref="MediaType"/> instance.
        /// </summary>
        /// <param name="mediaType">The <see cref="StringSegment"/> with the media type.</param>
        public MediaType(StringSegment mediaType)
            : this(mediaType.Buffer, mediaType.Offset, mediaType.Length)
        {
        }

        /// <summary>
        /// Initializes a <see cref="MediaTypeParameterParser"/> instance.
        /// </summary>
        /// <param name="mediaType">The <see cref="string"/> with the media type.</param>
        /// <param name="offset">The offset in the <paramref name="mediaType"/> where the parsing starts.</param>
        /// <param name="length">The length of the media type to parse if provided.</param>
        public MediaType(string mediaType, int offset, int? length)
        {
            if (mediaType == null)
            {
                throw new ArgumentNullException(nameof(mediaType));
            }

            if (offset < 0 || offset >= mediaType.Length)
            {
                throw new ArgumentOutOfRangeException(nameof(offset));
            }

            if (length != null)
            {
                if(length < 0 || length > mediaType.Length)
                {
                    throw new ArgumentOutOfRangeException(nameof(length));
                }

                if (offset > mediaType.Length - length)
                {
                    throw new ArgumentException(Resources.FormatArgument_InvalidOffsetLength(nameof(offset), nameof(length)));
                }
            }

            _mediaTypeHeaderValue = new ReadOnlyMediaTypeHeaderValue(mediaType, offset, length);
        }

        /// <summary>
        /// Gets the type of the <see cref="MediaType"/>.
        /// </summary>
        /// <example>
        /// For the media type <c>"application/json"</c>, this property gives the value <c>"application"</c>.
        /// </example>
        public StringSegment Type => _mediaTypeHeaderValue.Type;

        /// <summary>
        /// Gets whether this <see cref="MediaType"/> matches all types.
        /// </summary>
        public bool MatchesAllTypes => _mediaTypeHeaderValue.MatchesAllTypes;

        /// <summary>
        /// Gets the subtype of the <see cref="MediaType"/>.
        /// </summary>
        /// <example>
        /// For the media type <c>"application/vnd.example+json"</c>, this property gives the value
        /// <c>"vnd.example+json"</c>.
        /// </example>
        public StringSegment SubType => _mediaTypeHeaderValue.SubType;

        /// <summary>
        /// Gets the subtype of the <see cref="MediaType"/>, excluding any structured syntax suffix.
        /// </summary>
        /// <example>
        /// For the media type <c>"application/vnd.example+json"</c>, this property gives the value
        /// <c>"vnd.example"</c>.
        /// </example>
        public StringSegment SubTypeWithoutSuffix => _mediaTypeHeaderValue.SubTypeWithoutSuffix;

        /// <summary>
        /// Gets the structured syntax suffix of the <see cref="MediaType"/> if it has one.
        /// </summary>
        /// <example>
        /// For the media type <c>"application/vnd.example+json"</c>, this property gives the value
        /// <c>"json"</c>.
        /// </example>
        public StringSegment SubTypeSuffix => _mediaTypeHeaderValue.SubTypeSuffix;

        /// <summary>
        /// Gets whether this <see cref="MediaType"/> matches all subtypes.
        /// </summary>
        /// <example>
        /// For the media type <c>"application/*"</c>, this property is <c>true</c>.
        /// </example>
        /// <example>
        /// For the media type <c>"application/json"</c>, this property is <c>false</c>.
        /// </example>
        public bool MatchesAllSubTypes => _mediaTypeHeaderValue.MatchesAllSubTypes;

        /// <summary>
        /// Gets whether this <see cref="MediaType"/> matches all subtypes, ignoring any structured syntax suffix.
        /// </summary>
        /// <example>
        /// For the media type <c>"application/*+json"</c>, this property is <c>true</c>.
        /// </example>
        /// <example>
        /// For the media type <c>"application/vnd.example+json"</c>, this property is <c>false</c>.
        /// </example>
        public bool MatchesAllSubTypesWithoutSuffix => _mediaTypeHeaderValue.MatchesAllSubTypesWithoutSuffix;

        /// <summary>
        /// Gets the <see cref="System.Text.Encoding"/> of the <see cref="MediaType"/> if it has one.
        /// </summary>
        public Encoding? Encoding => _mediaTypeHeaderValue.Encoding;

        /// <summary>
        /// Gets the charset parameter of the <see cref="MediaType"/> if it has one.
        /// </summary>
        public StringSegment Charset => _mediaTypeHeaderValue.Charset;

        /// <summary>
        /// Determines whether the current <see cref="MediaType"/> contains a wildcard.
        /// </summary>
        /// <returns>
        /// <c>true</c> if this <see cref="MediaType"/> contains a wildcard; otherwise <c>false</c>.
        /// </returns>
        public bool HasWildcard => _mediaTypeHeaderValue.HasWildcard;

        /// <summary>
        /// Determines whether the current <see cref="MediaType"/> is a subset of the <paramref name="set"/>
        /// <see cref="MediaType"/>.
        /// </summary>
        /// <param name="set">The set <see cref="MediaType"/>.</param>
        /// <returns>
        /// <c>true</c> if this <see cref="MediaType"/> is a subset of <paramref name="set"/>; otherwise <c>false</c>.
        /// </returns>
        public bool IsSubsetOf(MediaType set)
            => _mediaTypeHeaderValue.IsSubsetOf(set._mediaTypeHeaderValue);

        /// <summary>
        /// Gets the parameter <paramref name="parameterName"/> of the media type.
        /// </summary>
        /// <param name="parameterName">The name of the parameter to retrieve.</param>
        /// <returns>
        /// The <see cref="StringSegment"/>for the given <paramref name="parameterName"/> if found; otherwise
        /// <c>null</c>.
        /// </returns>
        public StringSegment GetParameter(string parameterName)
            => _mediaTypeHeaderValue.GetParameter(parameterName);

        /// <summary>
        /// Gets the parameter <paramref name="parameterName"/> of the media type.
        /// </summary>
        /// <param name="parameterName">The name of the parameter to retrieve.</param>
        /// <returns>
        /// The <see cref="StringSegment"/>for the given <paramref name="parameterName"/> if found; otherwise
        /// <c>null</c>.
        /// </returns>
        public StringSegment GetParameter(StringSegment parameterName)
            => _mediaTypeHeaderValue.GetParameter(parameterName);

        /// <summary>
        /// Replaces the encoding of the given <paramref name="mediaType"/> with the provided
        /// <paramref name="encoding"/>.
        /// </summary>
        /// <param name="mediaType">The media type whose encoding will be replaced.</param>
        /// <param name="encoding">The encoding that will replace the encoding in the <paramref name="mediaType"/>.
        /// </param>
        /// <returns>A media type with the replaced encoding.</returns>
        public static string ReplaceEncoding(string mediaType, Encoding encoding)
        {
            return ReplaceEncoding(new StringSegment(mediaType), encoding);
        }

        /// <summary>
        /// Replaces the encoding of the given <paramref name="mediaType"/> with the provided
        /// <paramref name="encoding"/>.
        /// </summary>
        /// <param name="mediaType">The media type whose encoding will be replaced.</param>
        /// <param name="encoding">The encoding that will replace the encoding in the <paramref name="mediaType"/>.
        /// </param>
        /// <returns>A media type with the replaced encoding.</returns>
        public static string ReplaceEncoding(StringSegment mediaType, Encoding encoding)
        {
            var parsedMediaType = new MediaType(mediaType);
            var charset = parsedMediaType.GetParameter("charset");

            if (charset.HasValue && charset.Equals(encoding.WebName, StringComparison.OrdinalIgnoreCase))
            {
                return mediaType.Value;
            }

            if (!charset.HasValue)
            {
                return CreateMediaTypeWithEncoding(mediaType, encoding);
            }

            var charsetOffset = charset.Offset - mediaType.Offset;
            var restOffset = charsetOffset + charset.Length;
            var restLength = mediaType.Length - restOffset;
            var finalLength = charsetOffset + encoding.WebName.Length + restLength;

            var builder = new StringBuilder(mediaType.Buffer, mediaType.Offset, charsetOffset, finalLength);
            builder.Append(encoding.WebName);
            builder.Append(mediaType.Buffer, restOffset, restLength);

            return builder.ToString();
        }

        /// <summary>
        /// Get an encoding for a mediaType.
        /// </summary>
        /// <param name="mediaType">The mediaType.</param>
        /// <returns>The encoding.</returns>
        public static Encoding? GetEncoding(string mediaType)
        {
            return GetEncoding(new StringSegment(mediaType));
        }

        /// <summary>
        /// Get an encoding for a mediaType.
        /// </summary>
        /// <param name="mediaType">The mediaType.</param>
        /// <returns>The encoding.</returns>
        public static Encoding? GetEncoding(StringSegment mediaType)
        {
            var parsedMediaType = new MediaType(mediaType);
            return parsedMediaType.Encoding;
        }

        /// <summary>
        /// Creates an <see cref="MediaTypeSegmentWithQuality"/> containing the media type in <paramref name="mediaType"/>
        /// and its associated quality.
        /// </summary>
        /// <param name="mediaType">The media type to parse.</param>
        /// <param name="start">The position at which the parsing starts.</param>
        /// <returns>The parsed media type with its associated quality.</returns>
        public static MediaTypeSegmentWithQuality CreateMediaTypeSegmentWithQuality(string mediaType, int start)
        {
            var parsedMediaType = new ReadOnlyMediaTypeHeaderValue(mediaType, start, length: null);

            // Short-circuit use of the MediaTypeParameterParser if constructor detected an invalid type or subtype.
            // Parser would set ParsingFailed==true in this case. But, we handle invalid parameters as a separate case.
            if (parsedMediaType.Type.Equals(default(StringSegment)) ||
                parsedMediaType.SubType.Equals(default(StringSegment)))
            {
                return default(MediaTypeSegmentWithQuality);
            }

            var quality = 1.0d;

            var parser = parsedMediaType.ParameterParser;
            while (parser.ParseNextParameter(out var parameter))
            {
                if (parameter.HasName(QualityParameter))
                {
                    // If media type contains two `q` values i.e. it's invalid in an uncommon way, pick last value.
                    quality = double.Parse(
                        parameter.Value.AsSpan(), NumberStyles.AllowDecimalPoint,
                        NumberFormatInfo.InvariantInfo);
                }
            }

            // We check if the parsed media type has a value at this stage when we have iterated
            // over all the parameters and we know if the parsing was successful.
            if (parser.ParsingFailed)
            {
                return default(MediaTypeSegmentWithQuality);
            }

            return new MediaTypeSegmentWithQuality(
                new StringSegment(mediaType, start, parser.CurrentOffset - start),
                quality);
        }

        private static Encoding? GetEncodingFromCharset(StringSegment charset)
        {
            if (charset.Equals("utf-8", StringComparison.OrdinalIgnoreCase))
            {
                // This is an optimization for utf-8 that prevents the Substring caused by
                // charset.Value
                return Encoding.UTF8;
            }

            try
            {
                // charset.Value might be an invalid encoding name as in charset=invalid.
                // For that reason, we catch the exception thrown by Encoding.GetEncoding
                // and return null instead.
                return charset.HasValue ? Encoding.GetEncoding(charset.Value) : null;
            }
            catch (Exception)
            {
                return null;
            }
        }

        private static string CreateMediaTypeWithEncoding(StringSegment mediaType, Encoding encoding)
        {
            return $"{mediaType.Value}; charset={encoding.WebName}";
        }

        private bool MatchesType(MediaType set)
        {
            return set.MatchesAllTypes ||
                set.Type.Equals(Type, StringComparison.OrdinalIgnoreCase);
        }

        private bool MatchesSubtype(MediaType set)
        {
            if (set.MatchesAllSubTypes)
            {
                return true;
            }

            if (set.SubTypeSuffix.HasValue)
            {
                if (SubTypeSuffix.HasValue)
                {
                    // Both the set and the media type being checked have suffixes, so both parts must match.
                    return MatchesSubtypeWithoutSuffix(set) && MatchesSubtypeSuffix(set);
                }
                else
                {
                    // The set has a suffix, but the media type being checked doesn't. We never consider this to match.
                    return false;
                }
            }
            else
            {
                // If this subtype or suffix matches the subtype of the set,
                // it is considered a subtype.
                // Ex: application/json > application/val+json
                return MatchesEitherSubtypeOrSuffix(set);
            }
        }

        private bool MatchesSubtypeWithoutSuffix(MediaType set)
        {
            return set.MatchesAllSubTypesWithoutSuffix ||
                set.SubTypeWithoutSuffix.Equals(SubTypeWithoutSuffix, StringComparison.OrdinalIgnoreCase);
        }

        private bool MatchesSubtypeSuffix(MediaType set)
        {
            // We don't have support for wildcards on suffixes alone (e.g., "application/entity+*")
            // because there's no clear use case for it.
            return set.SubTypeSuffix.Equals(SubTypeSuffix, StringComparison.OrdinalIgnoreCase);
        }

        private bool MatchesEitherSubtypeOrSuffix(MediaType set)
        {
            return set.SubType.Equals(SubType, StringComparison.OrdinalIgnoreCase) ||
                set.SubType.Equals(SubTypeSuffix, StringComparison.OrdinalIgnoreCase);
        }

        private bool ContainsAllParameters(MediaTypeParameterParser setParameters)
        {
            var parameterFound = true;
            while (setParameters.ParseNextParameter(out var setParameter) && parameterFound)
            {
                if (setParameter.HasName("q"))
                {
                    // "q" and later parameters are not involved in media type matching. Quoting the RFC: The first
                    // "q" parameter (if any) separates the media-range parameter(s) from the accept-params.
                    break;
                }

                if (setParameter.HasName("*"))
                {
                    // A parameter named "*" has no effect on media type matching, as it is only used as an indication
                    // that the entire media type string should be treated as a wildcard.
                    continue;
                }

                // Copy the parser as we need to iterate multiple times over it.
                // We can do this because it's a struct
                var subSetParameters = _mediaTypeHeaderValue.ParameterParser;
                parameterFound = false;
                while (subSetParameters.ParseNextParameter(out var subSetParameter) && !parameterFound)
                {
                    parameterFound = subSetParameter.Equals(setParameter);
                }
            }

            return parameterFound;
        }

        private struct MediaTypeParameterParser
        {
            private readonly string _mediaTypeBuffer;
            private readonly int? _length;

            public MediaTypeParameterParser(string mediaTypeBuffer, int offset, int? length)
            {
                _mediaTypeBuffer = mediaTypeBuffer;
                _length = length;
                CurrentOffset = offset;
                ParsingFailed = false;
            }

            public int CurrentOffset { get; private set; }

            public bool ParsingFailed { get; private set; }

            public bool ParseNextParameter(out MediaTypeParameter result)
            {
                if (_mediaTypeBuffer == null)
                {
                    ParsingFailed = true;
                    result = default(MediaTypeParameter);
                    return false;
                }

                var parameterLength = GetParameterLength(_mediaTypeBuffer, CurrentOffset, out result);
                CurrentOffset +=  parameterLength;

                if (parameterLength == 0)
                {
                    ParsingFailed = _length != null && CurrentOffset < _length;
                    return false;
                }

                return true;
            }

            private static int GetParameterLength(string input, int startIndex, out MediaTypeParameter parsedValue)
            {
                if (OffsetIsOutOfRange(startIndex, input.Length) || input[startIndex] != ';')
                {
                    parsedValue = default(MediaTypeParameter);
                    return 0;
                }

                var nameLength = GetNameLength(input, startIndex, out var name);

                var current = startIndex + nameLength;

                if (nameLength == 0 || OffsetIsOutOfRange(current, input.Length) || input[current] != '=')
                {
                    if (current == input.Length && name.Equals("*", StringComparison.OrdinalIgnoreCase))
                    {
                        // As a special case, we allow a trailing ";*" to indicate a wildcard
                        // string allowing any other parameters. It's the same as ";*=*".
                        var asterisk = new StringSegment("*");
                        parsedValue = new MediaTypeParameter(asterisk, asterisk);
                        return current - startIndex;
                    }
                    else
                    {
                        parsedValue = default(MediaTypeParameter);
                        return 0;
                    }
                }

                var valueLength = GetValueLength(input, current, out var value);

                parsedValue = new MediaTypeParameter(name, value);
                current +=  valueLength;

                return current - startIndex;
            }

            private static int GetNameLength(string input, int startIndex, out StringSegment name)
            {
                var current = startIndex;

                current++; // skip ';'
                current +=  HttpTokenParsingRules.GetWhitespaceLength(input, current);

                var nameLength = HttpTokenParsingRules.GetTokenLength(input, current);
                if (nameLength == 0)
                {
                    name = default(StringSegment);
                    return 0;
                }

                name = new StringSegment(input, current, nameLength);

                current +=  nameLength;
                current +=  HttpTokenParsingRules.GetWhitespaceLength(input, current);

                return current - startIndex;
            }

            private static int GetValueLength(string input, int startIndex, out StringSegment value)
            {
                var current = startIndex;

                current++; // skip '='.
                current +=  HttpTokenParsingRules.GetWhitespaceLength(input, current);

                var valueLength = HttpTokenParsingRules.GetTokenLength(input, current);

                if (valueLength == 0)
                {
                    // A value can either be a token or a quoted string. Check if it is a quoted string.
                    var result = HttpTokenParsingRules.GetQuotedStringLength(input, current, out valueLength);
                    if (result != HttpParseResult.Parsed)
                    {
                        // We have an invalid value. Reset the name and return.
                        value = default(StringSegment);
                        return 0;
                    }

                    // Quotation marks are not part of a quoted parameter value.
                    value = new StringSegment(input, current + 1, valueLength - 2);
                }
                else
                {
                    value = new StringSegment(input, current, valueLength);
                }

                current +=  valueLength;
                current +=  HttpTokenParsingRules.GetWhitespaceLength(input, current);

                return current - startIndex;
            }

            private static bool OffsetIsOutOfRange(int offset, int length)
            {
                return offset < 0 || offset >= length;
            }
        }

        private readonly struct MediaTypeParameter : IEquatable<MediaTypeParameter>
        {
            public MediaTypeParameter(StringSegment name, StringSegment value)
            {
                Name = name;
                Value = value;
            }

            public StringSegment Name { get; }

            public StringSegment Value { get; }

            public bool HasName(string name)
            {
                return HasName(new StringSegment(name));
            }

            public bool HasName(StringSegment name)
            {
                return Name.Equals(name, StringComparison.OrdinalIgnoreCase);
            }

            public bool Equals(MediaTypeParameter other)
            {
                return HasName(other.Name) && Value.Equals(other.Value, StringComparison.OrdinalIgnoreCase);
            }

            public override string ToString() => $"{Name}={Value}";
        }
    }
}
