//
//  PacketStringBuilder.cs
//
//  Copyright (c) František Boháček. All rights reserved.
//  Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Buffers;
using System.Text;
using Remora.Results;

namespace NosSmooth.PacketSerializer.Abstractions;

/// <summary>
/// String builder for packets.
/// </summary>
public ref struct PacketStringBuilder
{
    private Span<char> _buffer;
    private char[]? _bufferArray;
    private int _position;
    private StringBuilderLevel _currentLevel;
    private char? _insertSeparator;

    /// <summary>
    /// Initializes a new instance of the <see cref="PacketStringBuilder"/> struct.
    /// </summary>
    /// <param name="initialBuffer">The initial buffer to store the packet to. Will grow in size if needed.</param>
    /// <param name="separator">The top level separator.</param>
    public PacketStringBuilder(Span<char> initialBuffer, char separator = ' ')
    {
        _currentLevel = new StringBuilderLevel(null, separator);
        _insertSeparator = null;
        _buffer = initialBuffer;
        _position = 0;
        _bufferArray = null;
    }

    /// <summary>
    /// Sets the separator to search for only once.
    /// </summary>
    /// <remarks>
    /// This separator will have the most priority.
    /// </remarks>
    /// <param name="separator">The separator to look for.</param>
    public void SetAfterSeparatorOnce(char separator)
    {
        _currentLevel.SeparatorOnce = separator;
    }

    /// <summary>
    /// Prepare the given level that can be set when needed.
    /// </summary>
    /// <param name="separator">The separator of the prepared level.</param>
    public void PrepareLevel(char separator)
    {
        _currentLevel.PreparedLevelSeparator = separator;
    }

    /// <summary>
    /// Reset the prepared level, if there is any.
    /// </summary>
    public void RemovePreparedLevel()
    {
        _currentLevel.PreparedLevelSeparator = null;
    }

    /// <summary>
    /// Create next level with the separator given in the prepared level.
    /// </summary>
    /// <remarks>
    /// Level of the current builder will stay the same.
    /// Will return null, if there is not a level prepared.
    /// </remarks>
    /// <returns>An enumerator with the new level pushed.</returns>
    public bool PushPreparedLevel()
    {
        if (_currentLevel.PreparedLevelSeparator is null)
        {
            return false;
        }

        _currentLevel = new StringBuilderLevel(_currentLevel, _currentLevel.PreparedLevelSeparator.Value);
        return true;
    }

    /// <summary>
    /// Push new separator level to the stack.
    /// </summary>
    /// <remarks>
    /// This will change the current enumerator.
    /// It has to be <see cref="PopLevel"/> after parent level should be used.
    /// </remarks>
    /// <param name="separator">The separator of the new level.</param>
    public void PushLevel(char separator)
    {
        _currentLevel = new StringBuilderLevel(_currentLevel, separator);
    }

    /// <summary>
    /// Pop the current level.
    /// </summary>
    /// <param name="replaceSeparator">Whether to replace the last separator with the parent one.</param>
    /// <returns>A result that may or may not have succeeded. There will be an error if the current level is the top one.</returns>
    public Result PopLevel(bool replaceSeparator = true)
    {
        if (_currentLevel.Parent is null)
        {
            return new InvalidOperationError("The level cannot be popped, the stack is already at the top level.");
        }

        if (replaceSeparator)
        {
            ReplaceWithParentSeparator();
        }

        _currentLevel = _currentLevel.Parent;

        return Result.FromSuccess();
    }

    /// <summary>
    /// Appends a value that is span formattable.
    /// </summary>
    /// <param name="value">The value to append.</param>
    /// <typeparam name="T">The span formattable type.</typeparam>
    public void Append<T>(T value)
        where T : ISpanFormattable
    {
        AppendSpanFormattable(value);
    }

    /// <summary>
    /// Appends the value to the string.
    /// </summary>
    /// <param name="value">The value to append.</param>
    public void Append(ReadOnlySpan<char> value)
    {
        BeforeAppend();
        while (!value.TryCopyTo(_buffer.Slice(_position)))
        {
            GrowBuffer(value.Length);
        }
        _position += value.Length;
        AfterAppend();
    }

    /// <summary>
    /// Appends the value to the string.
    /// </summary>
    /// <param name="value">The value to append.</param>
    public void Append(int value)
        => AppendSpanFormattable(value);

    /// <summary>
    /// Appends the value to the string.
    /// </summary>
    /// <param name="value">The value to append.</param>
    public void Append(uint value)
        => AppendSpanFormattable(value);

    /// <summary>
    /// Appends the value to the string.
    /// </summary>
    /// <param name="value">The value to append.</param>
    public void Append(short value)
        => AppendSpanFormattable(value);

    /// <summary>
    /// Appends the value to the string.
    /// </summary>
    /// <param name="value">The value to append.</param>
    public void Append(char value)
        => AppendSpanFormattable(value);

    /// <summary>
    /// Appends the value to the string.
    /// </summary>
    /// <param name="value">The value to append.</param>
    public void Append(ushort value)
        => AppendSpanFormattable(value);

    /// <summary>
    /// Appends the value to the string.
    /// </summary>
    /// <param name="value">The value to append.</param>
    public void Append(long value)
        => AppendSpanFormattable(value);

    /// <summary>
    /// Appends the value to the string.
    /// </summary>
    /// <param name="value">The value to append.</param>
    public void Append(ulong value)
        => AppendSpanFormattable(value);

    /// <summary>
    /// Appends the value to the string.
    /// </summary>
    /// <param name="value">The value to append.</param>
    public void Append(byte value)
        => AppendSpanFormattable(value);

    /// <summary>
    /// Appends the value to the string.
    /// </summary>
    /// <param name="value">The value to append.</param>
    public void Append(sbyte value)
        => AppendSpanFormattable(value);

    /// <summary>
    /// Appends the value to the string.
    /// </summary>
    /// <param name="value">The value to append.</param>
    public void Append(float value)
        => AppendSpanFormattable(value);

    /// <summary>
    /// Appends the value to the string.
    /// </summary>
    /// <param name="value">The value to append.</param>
    public void Append(double value)
        => AppendSpanFormattable(value);

    /// <summary>
    /// Appends the value to the string.
    /// </summary>
    /// <param name="value">The value to append.</param>
    public void Append(decimal value)
        => AppendSpanFormattable(value);

    /// <summary>
    /// Returns buffer to ArrayPool if it has been used.
    /// </summary>
    public void Dispose()
    {
        if (_bufferArray is not null)
        {
            ArrayPool<char>.Shared.Return(_bufferArray);
        }
    }

    private void AppendSpanFormattable<T>(T value)
        where T : ISpanFormattable
    {
        BeforeAppend();
        int charsWritten;
        while (!value.TryFormat(_buffer.Slice(_position), out charsWritten, default, null))
        {
            GrowBuffer();
        }
        _position += charsWritten;
        AfterAppend();
    }

    private void GrowBuffer(int needed = 0)
    {
        var sizeNeeded = _buffer.Length + needed;
        var doubleSize = _buffer.Length * 2;
        var newSize = Math.Max(doubleSize, sizeNeeded);
        var newBuffer = ArrayPool<char>.Shared.Rent(newSize);

        _buffer.CopyTo(newBuffer);
        _buffer = newBuffer;

        if (_bufferArray is not null)
        {
            ArrayPool<char>.Shared.Return(_bufferArray);
        }
        _bufferArray = newBuffer;
    }

    private void BeforeAppend()
    {
        if (_insertSeparator is not null)
        {
            if (_buffer.Length <= _position + 1)
            {
                GrowBuffer();
            }

            _buffer[_position] = _insertSeparator.Value;
            _position += 1;
            _insertSeparator = null;
        }
    }

    private void AfterAppend()
    {
        _insertSeparator = _currentLevel.SeparatorOnce ?? _currentLevel.Separator;
        _currentLevel.SeparatorOnce = null;
    }

    /// <summary>
    /// Replace the last separator with the parent separator.
    /// </summary>
    public void ReplaceWithParentSeparator()
    {
        var parent = _currentLevel.Parent;
        if (_insertSeparator is null || parent is null)
        {
            return;
        }

        _insertSeparator = parent.SeparatorOnce ?? parent.Separator;
        parent.SeparatorOnce = null;
    }

    /// <inheritdoc />
    public override string ToString()
    {
        return _buffer.Slice(0, _position).ToString();
    }

    private class StringBuilderLevel
    {
        public StringBuilderLevel(StringBuilderLevel? parent, char separator)
        {
            Parent = parent;
            Separator = separator;
        }

        public StringBuilderLevel? Parent { get; }

        public char? PreparedLevelSeparator { get; set; }

        public char Separator { get; }

        public char? SeparatorOnce { get; set; }
    }
}