// 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 System.Globalization;
using System.Text;
using Microsoft.Extensions.Primitives;

namespace Microsoft.Net.Http.Headers
{
    // According to the RFC, in places where a "parameter" is required, the value is mandatory
    // (e.g. Media-Type, Accept). However, we don't introduce a dedicated type for it. So NameValueHeaderValue supports
    // name-only values in addition to name/value pairs.
    /// <summary>
    /// Represents a name/value pair used in various headers as defined in RFC 2616.
    /// </summary>
    public class NameValueHeaderValue
    {
        private static readonly HttpHeaderParser<NameValueHeaderValue> SingleValueParser
            = new GenericHeaderParser<NameValueHeaderValue>(false, GetNameValueLength);
        internal static readonly HttpHeaderParser<NameValueHeaderValue> MultipleValueParser
            = new GenericHeaderParser<NameValueHeaderValue>(true, GetNameValueLength);

        private StringSegment _name;
        private StringSegment _value;
        private bool _isReadOnly;

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

        /// <summary>
        /// Initializes a new instance of <see cref="NameValueHeaderValue"/>.
        /// </summary>
        /// <param name="name">The header name.</param>
        public NameValueHeaderValue(StringSegment name)
            : this(name, null)
        {
        }

        /// <summary>
        /// Initializes a new instance of <see cref="NameValueHeaderValue"/>.
        /// </summary>
        /// <param name="name">The header name.</param>
        /// <param name="value">The header value.</param>
        public NameValueHeaderValue(StringSegment name, StringSegment value)
        {
            CheckNameValueFormat(name, value);

            _name = name;
            _value = value;
        }

        /// <summary>
        /// Gets the header name.
        /// </summary>
        public StringSegment Name
        {
            get { return _name; }
        }

        /// <summary>
        /// Gets or sets the header value.
        /// </summary>
        public StringSegment Value
        {
            get { return _value; }
            set
            {
                HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
                CheckValueFormat(value);
                _value = value;
            }
        }

        /// <summary>
        /// Gets a value that determines if this header is read only.
        /// </summary>
        public bool IsReadOnly { get { return _isReadOnly; } }

        /// <summary>
        /// Provides a copy of this object without the cost of re-validating the values.
        /// </summary>
        /// <returns>A copy.</returns>
        public NameValueHeaderValue Copy()
        {
            return new NameValueHeaderValue()
            {
                _name = _name,
                _value = _value
            };
        }

        /// <summary>
        /// Provides a copy of this instance while making it immutable.
        /// </summary>
        /// <returns>The readonly <see cref="NameValueHeaderValue"/>.</returns>
        public NameValueHeaderValue CopyAsReadOnly()
        {
            if (IsReadOnly)
            {
                return this;
            }

            return new NameValueHeaderValue()
            {
                _name = _name,
                _value = _value,
                _isReadOnly = true
            };
        }

        /// <inheritdoc/>
        public override int GetHashCode()
        {
            Contract.Assert(_name != null);

            var nameHashCode = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_name);

            if (!StringSegment.IsNullOrEmpty(_value))
            {
                // If we have a quoted-string, then just use the hash code. If we have a token, convert to lowercase
                // and retrieve the hash code.
                if (_value[0] == '"')
                {
                    return nameHashCode ^ _value.GetHashCode();
                }

                return nameHashCode ^ StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_value);
            }

            return nameHashCode;
        }

        /// <inheritdoc/>
        public override bool Equals(object? obj)
        {
            var other = obj as NameValueHeaderValue;

            if (other == null)
            {
                return false;
            }

            if (!_name.Equals(other._name, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }

            // RFC2616: 14.20: unquoted tokens should use case-INsensitive comparison; quoted-strings should use
            // case-sensitive comparison. The RFC doesn't mention how to compare quoted-strings outside the "Expect"
            // header. We treat all quoted-strings the same: case-sensitive comparison.

            if (StringSegment.IsNullOrEmpty(_value))
            {
                return StringSegment.IsNullOrEmpty(other._value);
            }

            if (_value[0] == '"')
            {
                // We have a quoted string, so we need to do case-sensitive comparison.
                return (_value.Equals(other._value, StringComparison.Ordinal));
            }
            else
            {
                return (_value.Equals(other._value, StringComparison.OrdinalIgnoreCase));
            }
        }

        /// <summary>
        /// If the value is a quoted-string as defined by <see href="https://tools.ietf.org/html/rfc7230#section-3.2.6">the RFC specification</see>,
        /// removes quotes and unescapes backslashes and quotes.
        /// </summary>
        /// <returns>An unescaped version of <see cref="Value"/>.</returns>
        public StringSegment GetUnescapedValue()
        {
            if (!HeaderUtilities.IsQuoted(_value))
            {
                return _value;
            }
            return HeaderUtilities.UnescapeAsQuotedString(_value);
        }

        /// <summary>
        /// Sets <see cref="Value"/> after it has been quoted as defined by <see href="https://tools.ietf.org/html/rfc7230#section-3.2.6">the RFC specification</see>.
        /// </summary>
        /// <param name="value"></param>
        public void SetAndEscapeValue(StringSegment value)
        {
            HeaderUtilities.ThrowIfReadOnly(IsReadOnly);
            if (StringSegment.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length))
            {
                _value = value;
            }
            else
            {
                Value = HeaderUtilities.EscapeAsQuotedString(value);
            }
        }

        /// <summary>
        /// Parses <paramref name="input"/> as a <see cref="NameValueHeaderValue"/> value.
        /// </summary>
        /// <param name="input">The values to parse.</param>
        /// <returns>The parsed values.</returns>
        public static NameValueHeaderValue 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="NameValueHeaderValue"/>.
        /// </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="NameValueHeaderValue"/>, otherwise <see langword="false"/>.</returns>
        public static bool TryParse(StringSegment input, [NotNullWhen(true)] out NameValueHeaderValue? parsedValue)
        {
            var index = 0;
            return SingleValueParser.TryParseValue(input, ref index, out parsedValue!);
        }

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

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

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

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

        /// <inheritdoc />
        public override string ToString()
        {
            if (!StringSegment.IsNullOrEmpty(_value))
            {
                return _name + "=" + _value;
            }
            return _name.ToString();
        }

        internal static void ToString(
            IList<NameValueHeaderValue>? values,
            char separator,
            bool leadingSeparator,
            StringBuilder destination)
        {
            Contract.Assert(destination != null);

            if ((values == null) || (values.Count == 0))
            {
                return;
            }

            for (var i = 0; i < values.Count; i++)
            {
                if (leadingSeparator || (destination.Length > 0))
                {
                    destination.Append(separator);
                    destination.Append(' ');
                }
                destination.Append(values[i].Name.AsSpan());
                if (!StringSegment.IsNullOrEmpty(values[i].Value))
                {
                    destination.Append('=');
                    destination.Append(values[i].Value.AsSpan());
                }
            }
        }

        internal static string? ToString(IList<NameValueHeaderValue>? values, char separator, bool leadingSeparator)
        {
            if ((values == null) || (values.Count == 0))
            {
                return null;
            }

            var sb = new StringBuilder();

            ToString(values, separator, leadingSeparator, sb);

            return sb.ToString();
        }

        internal static int GetHashCode(IList<NameValueHeaderValue>? values)
        {
            if ((values == null) || (values.Count == 0))
            {
                return 0;
            }

            var result = 0;
            for (var i = 0; i < values.Count; i++)
            {
                result = result ^ values[i].GetHashCode();
            }
            return result;
        }

        private static int GetNameValueLength(StringSegment input, int startIndex, out NameValueHeaderValue? parsedValue)
        {
            Contract.Requires(startIndex >= 0);

            parsedValue = null;

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

            // Parse the name, i.e. <name> in name/value string "<name>=<value>". Caller must remove
            // leading whitespaces.
            var nameLength = HttpRuleParser.GetTokenLength(input, startIndex);

            if (nameLength == 0)
            {
                return 0;
            }

            var name = input.Subsegment(startIndex, nameLength);
            var current = startIndex + nameLength;
            current = current + HttpRuleParser.GetWhitespaceLength(input, current);

            // Parse the separator between name and value
            if ((current == input.Length) || (input[current] != '='))
            {
                // We only have a name and that's OK. Return.
                parsedValue = new NameValueHeaderValue();
                parsedValue._name = name;
                current = current + HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces
                return current - startIndex;
            }

            current++; // skip delimiter.
            current = current + HttpRuleParser.GetWhitespaceLength(input, current);

            // Parse the value, i.e. <value> in name/value string "<name>=<value>"
            int valueLength = GetValueLength(input, current);

            // Value after the '=' may be empty
            // Use parameterless ctor to avoid double-parsing of name and value, i.e. skip public ctor validation.
            parsedValue = new NameValueHeaderValue();
            parsedValue._name = name;
            parsedValue._value = input.Subsegment(current, valueLength);
            current = current + valueLength;
            current = current + HttpRuleParser.GetWhitespaceLength(input, current); // skip whitespaces
            return current - startIndex;
        }

        // Returns the length of a name/value list, separated by 'delimiter'. E.g. "a=b, c=d, e=f" adds 3
        // name/value pairs to 'nameValueCollection' if 'delimiter' equals ','.
        internal static int GetNameValueListLength(
            StringSegment input,
            int startIndex,
            char delimiter,
            IList<NameValueHeaderValue> nameValueCollection)
        {
            Contract.Requires(startIndex >= 0);

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

            var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex);
            while (true)
            {
                var nameValueLength = GetNameValueLength(input, current, out var parameter);

                if (nameValueLength == 0)
                {
                    // There may be a trailing ';'
                    return current - startIndex;
                }

                nameValueCollection!.Add(parameter!);
                current = current + nameValueLength;
                current = current + HttpRuleParser.GetWhitespaceLength(input, current);

                if ((current == input.Length) || (input[current] != delimiter))
                {
                    // We're done and we have at least one valid name/value pair.
                    return current - startIndex;
                }

                // input[current] is 'delimiter'. Skip the delimiter and whitespaces and try to parse again.
                current++; // skip delimiter.
                current = current + HttpRuleParser.GetWhitespaceLength(input, current);
            }
        }

        /// <summary>
        /// Finds a <see cref="NameValueHeaderValue"/> with the specified <paramref name="name"/>.
        /// </summary>
        /// <param name="values">The collection to search.</param>
        /// <param name="name">The name to find.</param>
        /// <returns>The <see cref="NameValueHeaderValue" /> if found, otherwise <see langword="null" />.</returns>
        public static NameValueHeaderValue? Find(IList<NameValueHeaderValue>? values, StringSegment name)
        {
            Contract.Requires(name.Length > 0);

            if ((values == null) || (values.Count == 0))
            {
                return null;
            }

            for (var i = 0; i < values.Count; i++)
            {
                var value = values[i];
                if (value.Name.Equals(name, StringComparison.OrdinalIgnoreCase))
                {
                    return value;
                }
            }
            return null;
        }

        internal static int GetValueLength(StringSegment input, int startIndex)
        {
            if (startIndex >= input.Length)
            {
                return 0;
            }

            var valueLength = HttpRuleParser.GetTokenLength(input, startIndex);

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

        private static void CheckNameValueFormat(StringSegment name, StringSegment value)
        {
            HeaderUtilities.CheckValidToken(name, nameof(name));
            CheckValueFormat(value);
        }

        private static void CheckValueFormat(StringSegment value)
        {
            // Either value is null/empty or a valid token/quoted string
            if (!(StringSegment.IsNullOrEmpty(value) || (GetValueLength(value, 0) == value.Length)))
            {
                throw new FormatException(string.Format(CultureInfo.InvariantCulture, "The header value is invalid: '{0}'", value));
            }
        }

        private static NameValueHeaderValue CreateNameValue()
        {
            return new NameValueHeaderValue();
        }
    }
}
