// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Buffers;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using CommunityToolkit.HighPerformance.Buffers;
using CommunityToolkit.HighPerformance.Streams;

namespace CommunityToolkit.HighPerformance;

/// <summary>
/// Helpers for working with the <see cref="IBufferWriter{T}"/> type.
/// </summary>
public static class IBufferWriterExtensions
{
    /// <summary>
    /// Returns a <see cref="Stream"/> that can be used to write to a target an <see cref="IBufferWriter{T}"/> of <see cref="byte"/> instance.
    /// </summary>
    /// <param name="writer">The target <see cref="IBufferWriter{T}"/> instance.</param>
    /// <returns>A <see cref="Stream"/> wrapping <paramref name="writer"/> and writing data to its underlying buffer.</returns>
    /// <remarks>The returned <see cref="Stream"/> can only be written to and does not support seeking.</remarks>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static Stream AsStream(this IBufferWriter<byte> writer)
    {
        if (writer.GetType() == typeof(ArrayPoolBufferWriter<byte>))
        {
            // If the input writer is of type ArrayPoolBufferWriter<byte>, we can use the type
            // specific buffer writer owner to let the JIT elide callvirts when accessing it.
            ArrayPoolBufferWriter<byte>? internalWriter = Unsafe.As<ArrayPoolBufferWriter<byte>>(writer)!;

            return new IBufferWriterStream<ArrayBufferWriterOwner>(new ArrayBufferWriterOwner(internalWriter));
        }

        return new IBufferWriterStream<IBufferWriterOwner>(new IBufferWriterOwner(writer));
    }

    /// <summary>
    /// Writes a value of a specified type into a target <see cref="IBufferWriter{T}"/> instance.
    /// </summary>
    /// <typeparam name="T">The type of value to write.</typeparam>
    /// <param name="writer">The target <see cref="IBufferWriter{T}"/> instance to write to.</param>
    /// <param name="value">The input value to write to <paramref name="writer"/>.</param>
    /// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="writer"/> reaches the end.</exception>
    public static void Write<T>(this IBufferWriter<byte> writer, T value)
        where T : unmanaged
    {
        int length = Unsafe.SizeOf<T>();
        Span<byte> span = writer.GetSpan(1);

        if (span.Length < length)
        {
            ThrowArgumentExceptionForEndOfBuffer();
        }

        ref byte r0 = ref MemoryMarshal.GetReference(span);

        Unsafe.WriteUnaligned(ref r0, value);

        writer.Advance(length);
    }

    /// <summary>
    /// Writes a value of a specified type into a target <see cref="IBufferWriter{T}"/> instance.
    /// </summary>
    /// <typeparam name="T">The type of value to write.</typeparam>
    /// <param name="writer">The target <see cref="IBufferWriter{T}"/> instance to write to.</param>
    /// <param name="value">The input value to write to <paramref name="writer"/>.</param>
    /// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="writer"/> reaches the end.</exception>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Write<T>(this IBufferWriter<T> writer, T value)
    {
        Span<T> span = writer.GetSpan(1);

        if (span.Length < 1)
        {
            ThrowArgumentExceptionForEndOfBuffer();
        }

        MemoryMarshal.GetReference(span) = value;

        writer.Advance(1);
    }

    /// <summary>
    /// Writes a series of items of a specified type into a target <see cref="IBufferWriter{T}"/> instance.
    /// </summary>
    /// <typeparam name="T">The type of value to write.</typeparam>
    /// <param name="writer">The target <see cref="IBufferWriter{T}"/> instance to write to.</param>
    /// <param name="span">The input <see cref="ReadOnlySpan{T}"/> to write to <paramref name="writer"/>.</param>
    /// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="writer"/> reaches the end.</exception>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Write<T>(this IBufferWriter<byte> writer, ReadOnlySpan<T> span)
        where T : unmanaged
    {
        ReadOnlySpan<byte> source = MemoryMarshal.AsBytes(span);
        Span<byte> destination = writer.GetSpan(source.Length);

        source.CopyTo(destination);

        writer.Advance(source.Length);
    }

#if !NETSTANDARD2_1_OR_GREATER
    /// <summary>
    /// Writes a series of items of a specified type into a target <see cref="IBufferWriter{T}"/> instance.
    /// </summary>
    /// <typeparam name="T">The type of value to write.</typeparam>
    /// <param name="writer">The target <see cref="IBufferWriter{T}"/> instance to write to.</param>
    /// <param name="span">The input <see cref="ReadOnlySpan{T}"/> to write to <paramref name="writer"/>.</param>
    /// <exception cref="ArgumentOutOfRangeException">Thrown if <paramref name="writer"/> reaches the end.</exception>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static void Write<T>(this IBufferWriter<T> writer, ReadOnlySpan<T> span)
    {
        Span<T> destination = writer.GetSpan(span.Length);

        span.CopyTo(destination);

        writer.Advance(span.Length);
    }
#endif

    /// <summary>
    /// Throws an <see cref="ArgumentException"/> when trying to write too many bytes to the target writer.
    /// </summary>
    private static void ThrowArgumentExceptionForEndOfBuffer()
    {
        throw new ArgumentException("The current buffer writer can't contain the requested input data.");
    }
}