// 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.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using Microsoft.Extensions.Primitives;

namespace Microsoft.Net.Http.Headers
{
    /// <summary>
    /// Represents an entity-tag (<c>etag</c>) header value.
    /// </summary>
    public class EntityTagHeaderValue
    {
        // Note that the ETag header does not allow a * but we're not that strict: We allow both '*' and ETag values in a single value.
        // We can't guarantee that a single parsed value will be used directly in an ETag header.
        private static readonly HttpHeaderParser<EntityTagHeaderValue> SingleValueParser
            = new GenericHeaderParser<EntityTagHeaderValue>(false, GetEntityTagLength);
        // Note that if multiple ETag values are allowed (e.g. 'If-Match', 'If-None-Match'), according to the RFC
        // the value must either be '*' or a list of ETag values. It's not allowed to have both '*' and a list of
        // ETag values. We're not that strict: We allow both '*' and ETag values in a list. If the server sends such
        // an invalid list, we want to be able to represent it using the corresponding header property.
        private static readonly HttpHeaderParser<EntityTagHeaderValue> MultipleValueParser
            = new GenericHeaderParser<EntityTagHeaderValue>(true, GetEntityTagLength);

        private StringSegment _tag;
        private bool _isWeak;

        private EntityTagHeaderValue()
        {
            // Used by the parser to create a new instance of this type.
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EntityTagHeaderValue"/>.
        /// </summary>
        /// <param name="tag">A <see cref="StringSegment"/> that contains an <see cref="EntityTagHeaderValue"/>.</param>
        public EntityTagHeaderValue(StringSegment tag)
            : this(tag, isWeak: false)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="EntityTagHeaderValue"/>.
        /// </summary>
        /// <param name="tag">A <see cref="StringSegment"/> that contains an <see cref="EntityTagHeaderValue"/>.</param>
        /// <param name="isWeak">A value that indicates if this entity-tag header is a weak validator.</param>
        public EntityTagHeaderValue(StringSegment tag, bool isWeak)
        {
            if (StringSegment.IsNullOrEmpty(tag))
            {
                throw new ArgumentException("An empty string is not allowed.", nameof(tag));
            }

            if (!isWeak && StringSegment.Equals(tag, "*", StringComparison.Ordinal))
            {
                // * is valid, but W/* isn't.
                _tag = tag;
            }
            else if ((HttpRuleParser.GetQuotedStringLength(tag, 0, out var length) != HttpParseResult.Parsed) ||
                (length != tag.Length))
            {
                // Note that we don't allow 'W/' prefixes for weak ETags in the 'tag' parameter. If the user wants to
                // add a weak ETag, they can set 'isWeak' to true.
                throw new FormatException("Invalid ETag name");
            }

            _tag = tag;
            _isWeak = isWeak;
        }

        /// <summary>
        /// Gets the "any" etag.
        /// </summary>
        public static EntityTagHeaderValue Any { get; } = new EntityTagHeaderValue("*", isWeak: false);

        /// <summary>
        /// Gets the quoted tag.
        /// </summary>
        public StringSegment Tag => _tag;

        /// <summary>
        /// Gets a value that determines if the entity-tag header is a weak validator.
        /// </summary>
        public bool IsWeak => _isWeak;

        /// <inheritdoc />
        public override string ToString()
        {
            if (_isWeak)
            {
                return "W/" + _tag.ToString();
            }
            return _tag.ToString();
        }

        /// <summary>
        /// Check against another <see cref="EntityTagHeaderValue"/> for equality.
        /// This equality check should not be used to determine if two values match under the RFC specifications (https://tools.ietf.org/html/rfc7232#section-2.3.2).
        /// </summary>
        /// <param name="obj">The other value to check against for equality.</param>
        /// <returns>
        /// <c>true</c> if the strength and tag of the two values match,
        /// <c>false</c> if the other value is null, is not an <see cref="EntityTagHeaderValue"/>, or if there is a mismatch of strength or tag between the two values.
        /// </returns>
        public override bool Equals(object? obj)
        {
            // Since the tag is a quoted-string we treat it case-sensitive.
            return obj is EntityTagHeaderValue other && _isWeak == other._isWeak && StringSegment.Equals(_tag, other._tag, StringComparison.Ordinal);
        }

        /// <inheritdoc />
        public override int GetHashCode()
        {
            // Since the tag is a quoted-string we treat it case-sensitive.
            return _tag.GetHashCode() ^ _isWeak.GetHashCode();
        }

        /// <summary>
        /// Compares against another <see cref="EntityTagHeaderValue"/> to see if they match under the RFC specifications (https://tools.ietf.org/html/rfc7232#section-2.3.2).
        /// </summary>
        /// <param name="other">The other <see cref="EntityTagHeaderValue"/> to compare against.</param>
        /// <param name="useStrongComparison"><c>true</c> to use a strong comparison, <c>false</c> to use a weak comparison</param>
        /// <returns>
        /// <c>true</c> if the <see cref="EntityTagHeaderValue"/> match for the given comparison type,
        /// <c>false</c> if the other value is null or the comparison failed.
        /// </returns>
        public bool Compare(EntityTagHeaderValue? other, bool useStrongComparison)
        {
            if (other == null)
            {
                return false;
            }

            if (useStrongComparison)
            {
                return !IsWeak && !other.IsWeak && StringSegment.Equals(Tag, other.Tag, StringComparison.Ordinal);
            }
            else
            {
                return StringSegment.Equals(Tag, other.Tag, StringComparison.Ordinal);
            }
        }

        /// <summary>
        /// Parses <paramref name="input"/> as a <see cref="EntityTagHeaderValue"/> value.
        /// </summary>
        /// <param name="input">The values to parse.</param>
        /// <returns>The parsed values.</returns>
        public static EntityTagHeaderValue Parse(StringSegment input)
        {
            var index = 0;
            return SingleValueParser.ParseValue(input, ref index)!;
        }

        /// <summary>
        /// Attempts to parse the specified <paramref name="input"/> as a <see cref="EntityTagHeaderValue"/>.
        /// </summary>
        /// <param name="input">The value to parse.</param>
        /// <param name="parsedValue">The parsed value.</param>
        /// <returns><see langword="true"/> if input is a valid <see cref="EntityTagHeaderValue"/>, otherwise <see langword="false"/>.</returns>
        public static bool TryParse(StringSegment input, [NotNullWhen(true)] out EntityTagHeaderValue parsedValue)
        {
            var index = 0;
            return SingleValueParser.TryParseValue(input, ref index, out parsedValue!);
        }

        /// <summary>
        /// Parses a sequence of inputs as a sequence of <see cref="EntityTagHeaderValue"/> values.
        /// </summary>
        /// <param name="inputs">The values to parse.</param>
        /// <returns>The parsed values.</returns>
        public static IList<EntityTagHeaderValue> ParseList(IList<string>? inputs)
        {
            return MultipleValueParser.ParseValues(inputs);
        }

        /// <summary>
        /// Parses a sequence of inputs as a sequence of <see cref="EntityTagHeaderValue"/> values using string parsing rules.
        /// </summary>
        /// <param name="inputs">The values to parse.</param>
        /// <returns>The parsed values.</returns>
        public static IList<EntityTagHeaderValue> ParseStrictList(IList<string>? inputs)
        {
            return MultipleValueParser.ParseStrictValues(inputs);
        }

        /// <summary>
        /// Attempts to parse the sequence of values as a sequence of <see cref="EntityTagHeaderValue"/>.
        /// </summary>
        /// <param name="inputs">The values to parse.</param>
        /// <param name="parsedValues">The parsed values.</param>
        /// <returns><see langword="true"/> if all inputs are valid <see cref="EntityTagHeaderValue"/>, otherwise <see langword="false"/>.</returns>
        public static bool TryParseList(IList<string>? inputs, [NotNullWhen(true)] out IList<EntityTagHeaderValue>? parsedValues)
        {
            return MultipleValueParser.TryParseValues(inputs, out parsedValues);
        }

        /// <summary>
        /// Attempts to parse the sequence of values as a sequence of <see cref="EntityTagHeaderValue"/> using string parsing rules.
        /// </summary>
        /// <param name="inputs">The values to parse.</param>
        /// <param name="parsedValues">The parsed values.</param>
        /// <returns><see langword="true"/> if all inputs are valid <see cref="EntityTagHeaderValue"/>, otherwise <see langword="false"/>.</returns>
        public static bool TryParseStrictList(IList<string>? inputs, [NotNullWhen(true)] out IList<EntityTagHeaderValue>? parsedValues)
        {
            return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
        }

        internal static int GetEntityTagLength(StringSegment input, int startIndex, out EntityTagHeaderValue? parsedValue)
        {
            Contract.Requires(startIndex >= 0);

            parsedValue = null;

            if (StringSegment.IsNullOrEmpty(input) || (startIndex >= input.Length))
            {
                return 0;
            }

            // Caller must remove leading whitespaces. If not, we'll return 0.
            var isWeak = false;
            var current = startIndex;

            var firstChar = input[startIndex];
            if (firstChar == '*')
            {
                // We have '*' value, indicating "any" ETag.
                parsedValue = Any;
                current++;
            }
            else
            {
                // The RFC defines 'W/' as prefix, but we'll be flexible and also accept lower-case 'w'.
                if ((firstChar == 'W') || (firstChar == 'w'))
                {
                    current++;
                    // We need at least 3 more chars: the '/' character followed by two quotes.
                    if ((current + 2 >= input.Length) || (input[current] != '/'))
                    {
                        return 0;
                    }
                    isWeak = true;
                    current++; // we have a weak-entity tag.
                    current = current + HttpRuleParser.GetWhitespaceLength(input, current);
                }

                var tagStartIndex = current;
                var tagLength = 0;
                if (HttpRuleParser.GetQuotedStringLength(input, current, out tagLength) != HttpParseResult.Parsed)
                {
                    return 0;
                }

                parsedValue = new EntityTagHeaderValue();
                if (tagLength == input.Length)
                {
                    // Most of the time we'll have strong ETags without leading/trailing whitespaces.
                    Contract.Assert(startIndex == 0);
                    Contract.Assert(!isWeak);
                    parsedValue._tag = input;
                    parsedValue._isWeak = false;
                }
                else
                {
                    parsedValue._tag = input.Subsegment(tagStartIndex, tagLength);
                    parsedValue._isWeak = isWeak;
                }

                current = current + tagLength;
            }
            current = current + HttpRuleParser.GetWhitespaceLength(input, current);

            return current - startIndex;
        }
    }
}
