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

namespace Microsoft.Net.Http.Headers
{
    /// <summary>
    /// Represents a <c>Range</c> header value.
    /// <para>
    /// The <see cref="RangeHeaderValue"/> class provides support for the Range header as defined in
    /// <see href="https://tools.ietf.org/html/rfc2616">RFC 2616</see>.
    /// </para>
    /// </summary>
    public class RangeHeaderValue
    {
        private static readonly HttpHeaderParser<RangeHeaderValue> Parser
            = new GenericHeaderParser<RangeHeaderValue>(false, GetRangeLength);

        private StringSegment _unit;
        private ICollection<RangeItemHeaderValue>? _ranges;

        /// <summary>
        /// Initializes a new instance of <see cref="RangeHeaderValue"/>.
        /// </summary>
        public RangeHeaderValue()
        {
            _unit = HeaderUtilities.BytesUnit;
        }

        /// <summary>
        /// Initializes a new instance of <see cref="RangeHeaderValue"/>.
        /// </summary>
        /// <param name="from">The position at which to start sending data.</param>
        /// <param name="to">The position at which to stop sending data.</param>
        public RangeHeaderValue(long? from, long? to)
        {
            // convenience ctor: "Range: bytes=from-to"
            _unit = HeaderUtilities.BytesUnit;
            Ranges.Add(new RangeItemHeaderValue(from, to));
        }

        /// <summary>
        /// Gets or sets the unit from the header.
        /// </summary>
        /// <value>Defaults to <c>bytes</c>.</value>
        public StringSegment Unit
        {
            get { return _unit; }
            set
            {
                HeaderUtilities.CheckValidToken(value, nameof(value));
                _unit = value;
            }
        }

        /// <summary>
        /// Gets the ranges specified in the header.
        /// </summary>
        public ICollection<RangeItemHeaderValue> Ranges
        {
            get
            {
                if (_ranges == null)
                {
                    _ranges = new ObjectCollection<RangeItemHeaderValue>();
                }
                return _ranges;
            }
        }

        /// <inheritdoc />
        public override string ToString()
        {
            var sb = new StringBuilder();
            sb.Append(_unit.AsSpan());
            sb.Append('=');

            var first = true;
            foreach (var range in Ranges)
            {
                if (first)
                {
                    first = false;
                }
                else
                {
                    sb.Append(", ");
                }

                sb.Append(range.From);
                sb.Append('-');
                sb.Append(range.To);
            }

            return sb.ToString();
        }

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

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

            return StringSegment.Equals(_unit, other._unit, StringComparison.OrdinalIgnoreCase) &&
                HeaderUtilities.AreEqualCollections(Ranges, other.Ranges);
        }

        /// <inheritdoc />
        public override int GetHashCode()
        {
            var result = StringSegmentComparer.OrdinalIgnoreCase.GetHashCode(_unit);

            foreach (var range in Ranges)
            {
                result = result ^ range.GetHashCode();
            }

            return result;
        }

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

        /// <summary>
        /// Attempts to parse the specified <paramref name="input"/> as a <see cref="RangeHeaderValue"/>.
        /// </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="RangeHeaderValue"/>, otherwise <see langword="false"/>.</returns>
        public static bool TryParse(StringSegment input, [NotNullWhen(true)] out RangeHeaderValue parsedValue)
        {
            var index = 0;
            return Parser.TryParseValue(input, ref index, out parsedValue!);
        }

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

            parsedValue = null;

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

            // Parse the unit string: <unit> in '<unit>=<from1>-<to1>, <from2>-<to2>'
            var unitLength = HttpRuleParser.GetTokenLength(input, startIndex);

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

            RangeHeaderValue result = new RangeHeaderValue();
            result._unit = input.Subsegment(startIndex, unitLength);
            var current = startIndex + unitLength;
            current = current + HttpRuleParser.GetWhitespaceLength(input, current);

            if ((current == input.Length) || (input[current] != '='))
            {
                return 0;
            }

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

            var rangesLength = RangeItemHeaderValue.GetRangeItemListLength(input, current, result.Ranges);

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

            current = current + rangesLength;
            Contract.Assert(current == input.Length, "GetRangeItemListLength() should consume the whole string or fail.");

            parsedValue = result;
            return current - startIndex;
        }
    }
}
