// 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;

namespace Microsoft.NET.StringTools
{
    /// <summary>
    /// A StringBuilder replacement that keeps a list of <see cref="ReadOnlyMemory{T}"/> spans making up the intermediate string rather
    /// than a copy of its characters. This has positive impact on both memory (no need to allocate space for the intermediate string)
    /// and time (no need to copy characters to the intermediate string).
    /// </summary>
    /// <remarks>
    /// The <see cref="ToString"/> method tries to intern the resulting string without even allocating it if it's already interned.
    /// Use <see cref="Strings.GetSpanBasedStringBuilder"/> to take advantage of pooling to eliminate allocation overhead of this class.
    /// </remarks>
    public class SpanBasedStringBuilder : IDisposable
    {
        /// <summary>
        /// Enumerator for the top-level class. Enumerates characters of the string.
        /// </summary>
        public struct Enumerator
        {
            /// <summary>
            /// The spans being enumerated.
            /// </summary>
            private readonly List<ReadOnlyMemory<char>> _spans;

            /// <summary>
            /// Index of the current span.
            /// </summary>
            private int _spanIndex;

            /// <summary>
            /// Index of the current character in the current span, -1 if MoveNext has not been called yet.
            /// </summary>
            private int _charIndex;

            internal Enumerator(List<ReadOnlyMemory<char>> spans)
            {
                _spans = spans;
                _spanIndex = 0;
                _charIndex = -1;
            }

            /// <summary>
            /// Returns the current character.
            /// </summary>
            public readonly char Current
            {
                get
                {
                    ReadOnlyMemory<char> span = _spans[_spanIndex];
                    return span.Span[_charIndex];
                }
            }

            /// <summary>
            /// Moves to the next character.
            /// </summary>
            /// <returns>True if there is another character, false if the enumerator reached the end.</returns>
            public bool MoveNext()
            {
                int newCharIndex = _charIndex + 1;
                while (_spanIndex < _spans.Count)
                {
                    if (newCharIndex < _spans[_spanIndex].Length)
                    {
                        _charIndex = newCharIndex;
                        return true;
                    }
                    _spanIndex++;
                    newCharIndex = 0;
                }
                return false;
            }
        }

        /// <summary>
        /// Spans making up the rope.
        /// </summary>
        private readonly List<ReadOnlyMemory<char>> _spans;

        /// <summary>
        /// Internal getter to get the list of spans out of the SpanBasedStringBuilder.
        /// </summary>
        internal List<ReadOnlyMemory<char>> Spans => _spans;

        /// <summary>
        /// Constructs a new SpanBasedStringBuilder containing the given string.
        /// </summary>
        /// <param name="str">The string to wrap, must be non-null.</param>
        public SpanBasedStringBuilder(string str)
            : this()
        {
            if (str == null)
            {
                throw new ArgumentNullException(nameof(str));
            }
            Append(str);
        }

        /// <summary>
        /// Constructs a new empty SpanBasedStringBuilder with the given expected number of spans.
        /// </summary>
        public SpanBasedStringBuilder(int capacity = 4)
        {
            _spans = new List<ReadOnlyMemory<char>>(capacity);
            Length = 0;
        }

        /// <summary>
        /// Gets the length of the string.
        /// </summary>
        public int Length { get; private set; }

        /// <summary>
        /// Gets the capacity of the SpanBasedStringBuilder in terms of number of spans it can hold without allocating.
        /// </summary>
        public int Capacity => _spans.Capacity;

        public char this[int index]
        {
            get
            {
                if (index < 0 || index >= Length)
                {
                    throw new ArgumentOutOfRangeException(nameof(index));
                }

                int adjustedIndex = index;
                foreach (ReadOnlyMemory<char> span in _spans)
                {
                    if (adjustedIndex < span.Length)
                    {
                        return span.Span[adjustedIndex];
                    }
                    else
                    {
                        adjustedIndex -= span.Length;
                    }
                }

                // Should never reach here if Length is correct
                throw new IndexOutOfRangeException();
            }
        }

        /// <summary>
        /// Creates a new enumerator for enumerating characters in this string. Does not allocate.
        /// </summary>
        /// <returns>The enumerator.</returns>
        public Enumerator GetEnumerator()
        {
            return new Enumerator(_spans);
        }

        public bool Equals(string other) => Equals(other.AsSpan(), StringComparison.Ordinal);

        public bool Equals(string other, StringComparison comparison) => Equals(other.AsSpan(), comparison);

        public bool Equals(ReadOnlySpan<char> other) => Equals(other, StringComparison.Ordinal);

        public bool Equals(ReadOnlySpan<char> other, StringComparison comparison)
        {
            if (_spans.Count == 0 && other.IsEmpty)
            {
                return true;
            }

            if (_spans.Count == 0 || other.IsEmpty || Length != other.Length)
            {
                return false;
            }

            int otherIndex = 0;
            foreach (ReadOnlyMemory<char> internalSpan in _spans)
            {
                if (!MemoryExtensions.Equals(other.Slice(otherIndex, internalSpan.Length), internalSpan.Span, comparison))
                {
                    return false;
                }

                otherIndex += internalSpan.Length;
            }

            return true;
        }

        /// <summary>
        /// Converts this instance to a System.String while first searching for a match in the intern table.
        /// </summary>
        /// <remarks>
        /// May allocate depending on whether the string has already been interned.
        /// </remarks>
        public override string ToString()
        {
            return new InternableString(this).ToString();
        }

        /// <summary>
        /// Releases this instance.
        /// </summary>
        public void Dispose()
        {
            Strings.ReturnSpanBasedStringBuilder(this);
        }

        #region Public mutating methods

        /// <summary>
        /// Appends a string.
        /// </summary>
        /// <param name="value">The string to append.</param>
        public void Append(string? value)
        {
            if (!string.IsNullOrEmpty(value))
            {
                _spans.Add(value!.AsMemory());
                Length += value!.Length;
            }
        }

        /// <summary>
        /// Appends a substring.
        /// </summary>
        /// <param name="value">The string to append.</param>
        /// <param name="startIndex">The start index of the substring within <paramref name="value"/> to append.</param>
        /// <param name="count">The length of the substring to append.</param>
        public void Append(string value, int startIndex, int count)
        {
            if (value != null)
            {
                if (count > 0)
                {
                    _spans.Add(value.AsMemory(startIndex, count));
                    Length += count;
                }
            }
            else
            {
                if (startIndex != 0 || count != 0)
                {
                    throw new ArgumentNullException(nameof(value));
                }
            }
        }

        /// <summary>
        /// Appends a character span represented by <see cref="ReadOnlyMemory{T}" />.
        /// </summary>
        /// <param name="span">The character span to append.</param>
        public void Append(ReadOnlyMemory<char> span)
        {
            if (!span.IsEmpty)
            {
                _spans.Add(span);
                Length += span.Length;
            }
        }

        /// <summary>
        /// Removes leading white-space characters from the string.
        /// </summary>
        public void TrimStart()
        {
            for (int spanIdx = 0; spanIdx < _spans.Count; spanIdx++)
            {
                ReadOnlySpan<char> span = _spans[spanIdx].Span;
                int i = 0;
                while (i < span.Length && char.IsWhiteSpace(span[i]))
                {
                    i++;
                }
                if (i > 0)
                {
                    _spans[spanIdx] = _spans[spanIdx].Slice(i);
                    Length -= i;
                }
                if (!_spans[spanIdx].IsEmpty)
                {
                    return;
                }
            }
        }

        public void TrimStart(char c)
        {
            for (int spanIdx = 0; spanIdx < _spans.Count; spanIdx++)
            {
                ReadOnlySpan<char> span = _spans[spanIdx].Span;
                int i = 0;
                while (i < span.Length && span[i] == c)
                {
                    i++;
                }
                if (i > 0)
                {
                    _spans[spanIdx] = _spans[spanIdx].Slice(i);
                    Length -= i;
                }
                if (!_spans[spanIdx].IsEmpty)
                {
                    return;
                }
            }
        }

        /// <summary>
        /// Removes trailing white-space characters from the string.
        /// </summary>
        public void TrimEnd()
        {
            for (int spanIdx = _spans.Count - 1; spanIdx >= 0; spanIdx--)
            {
                ReadOnlySpan<char> span = _spans[spanIdx].Span;
                int i = span.Length - 1;
                while (i >= 0 && char.IsWhiteSpace(span[i]))
                {
                    i--;
                }
                if (i + 1 < span.Length)
                {
                    _spans[spanIdx] = _spans[spanIdx].Slice(0, i + 1);
                    Length -= span.Length - (i + 1);
                }
                if (!_spans[spanIdx].IsEmpty)
                {
                    return;
                }
            }
        }

        public void TrimEnd(char c)
        {
            for (int spanIdx = _spans.Count - 1; spanIdx >= 0; spanIdx--)
            {
                ReadOnlySpan<char> span = _spans[spanIdx].Span;
                int i = span.Length - 1;
                while (i >= 0 && span[i] == c)
                {
                    i--;
                }
                if (i + 1 < span.Length)
                {
                    _spans[spanIdx] = _spans[spanIdx].Slice(0, i + 1);
                    Length -= span.Length - (i + 1);
                }
                if (!_spans[spanIdx].IsEmpty)
                {
                    return;
                }
            }
        }

        /// <summary>
        /// Removes leading and trailing white-space characters from the string.
        /// </summary>
        public void Trim()
        {
            TrimStart();
            TrimEnd();
        }

        public void Trim(char c)
        {
            TrimStart(c);
            TrimEnd(c);
        }

        /// <summary>
        /// Clears this instance making it represent an empty string.
        /// </summary>
        public void Clear()
        {
            _spans.Clear();
            Length = 0;
        }

        #endregion
    }
}
