// 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.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
#if !NETSTANDARD2_1_OR_GREATER
using System.Buffers;
#endif
using System.Threading;
using System.Threading.Tasks;
#if NETSTANDARD2_1_OR_GREATER
using System.ComponentModel;
#endif

namespace CommunityToolkit.HighPerformance;

/// <summary>
/// Helpers for working with the <see cref="Stream"/> type.
/// </summary>
public static class StreamExtensions
{
    /// <summary>
    /// Asynchronously reads a sequence of bytes from a given <see cref="Stream"/> instance.
    /// </summary>
    /// <param name="stream">The source <see cref="Stream"/> to read data from.</param>
    /// <param name="buffer">The destination <see cref="Memory{T}"/> to write data to.</param>
    /// <param name="cancellationToken">The optional <see cref="CancellationToken"/> for the operation.</param>
    /// <returns>A <see cref="ValueTask"/> representing the operation being performed.</returns>
#if NETSTANDARD2_1_OR_GREATER
    [Obsolete("This API is only available for binary compatibility, but Stream.ReadAsync should be used instead.")]
    [EditorBrowsable(EditorBrowsableState.Never)]
#endif
    public static ValueTask<int> ReadAsync(this Stream stream, Memory<byte> buffer, CancellationToken cancellationToken = default)
    {
#if NETSTANDARD2_1_OR_GREATER
        return stream.ReadAsync(buffer, cancellationToken);
#else
        if (cancellationToken.IsCancellationRequested)
        {
            return new(Task.FromCanceled<int>(cancellationToken));
        }

        // If the memory wraps an array, extract it and use it directly
        if (MemoryMarshal.TryGetArray(buffer, out ArraySegment<byte> segment))
        {
            return new(stream.ReadAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken));
        }

        // Local function used as the fallback path. This happens when the input memory
        // doesn't wrap an array instance we can use. We use a local function as we need
        // the body to be asynchronous, in order to execute the finally block after the
        // write operation has been completed. By separating the logic, we can keep the
        // main method as a synchronous, value-task returning function. This fallback
        // path should hopefully be pretty rare, as memory instances are typically just
        // created around arrays, often being rented from a memory pool in particular.
        static async Task<int> ReadAsyncFallback(Stream stream, Memory<byte> buffer, CancellationToken cancellationToken)
        {
            byte[] rent = ArrayPool<byte>.Shared.Rent(buffer.Length);

            try
            {
                int bytesRead = await stream.ReadAsync(rent, 0, buffer.Length, cancellationToken).ConfigureAwait(false);

                if (bytesRead > 0)
                {
                    rent.AsSpan(0, bytesRead).CopyTo(buffer.Span);
                }

                return bytesRead;
            }
            finally
            {
                ArrayPool<byte>.Shared.Return(rent);
            }
        }

        return new(ReadAsyncFallback(stream, buffer, cancellationToken));
#endif
    }

    /// <summary>
    /// Asynchronously writes a sequence of bytes to a given <see cref="Stream"/> instance.
    /// </summary>
    /// <param name="stream">The destination <see cref="Stream"/> to write data to.</param>
    /// <param name="buffer">The source <see cref="ReadOnlyMemory{T}"/> to read data from.</param>
    /// <param name="cancellationToken">The optional <see cref="CancellationToken"/> for the operation.</param>
    /// <returns>A <see cref="ValueTask"/> representing the operation being performed.</returns>
#if NETSTANDARD2_1_OR_GREATER
    [Obsolete("This API is only available for binary compatibility, but Stream.WriteAsync should be used instead.")]
    [EditorBrowsable(EditorBrowsableState.Never)]
#endif
    public static ValueTask WriteAsync(this Stream stream, ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken = default)
    {
#if NETSTANDARD2_1_OR_GREATER
        return stream.WriteAsync(buffer, cancellationToken);
#else
        if (cancellationToken.IsCancellationRequested)
        {
            return new(Task.FromCanceled(cancellationToken));
        }

        if (MemoryMarshal.TryGetArray(buffer, out ArraySegment<byte> segment))
        {
            return new(stream.WriteAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken));
        }

        // Local function, same idea as above
        static async Task WriteAsyncFallback(Stream stream, ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
        {
            byte[] rent = ArrayPool<byte>.Shared.Rent(buffer.Length);

            try
            {
                buffer.Span.CopyTo(rent);

                await stream.WriteAsync(rent, 0, buffer.Length, cancellationToken).ConfigureAwait(false);
            }
            finally
            {
                ArrayPool<byte>.Shared.Return(rent);
            }
        }

        return new(WriteAsyncFallback(stream, buffer, cancellationToken));
#endif
    }

    /// <summary>
    /// Reads a sequence of bytes from a given <see cref="Stream"/> instance.
    /// </summary>
    /// <param name="stream">The source <see cref="Stream"/> to read data from.</param>
    /// <param name="buffer">The target <see cref="Span{T}"/> to write data to.</param>
    /// <returns>The number of bytes that have been read.</returns>
#if NETSTANDARD2_1_OR_GREATER
    [Obsolete("This API is only available for binary compatibility, but Stream.Read should be used instead.")]
    [EditorBrowsable(EditorBrowsableState.Never)]
#endif
    public static int Read(this Stream stream, Span<byte> buffer)
    {
#if NETSTANDARD2_1_OR_GREATER
        return stream.Read(buffer);
#else
        byte[] rent = ArrayPool<byte>.Shared.Rent(buffer.Length);

        try
        {
            int bytesRead = stream.Read(rent, 0, buffer.Length);

            if (bytesRead > 0)
            {
                rent.AsSpan(0, bytesRead).CopyTo(buffer);
            }

            return bytesRead;
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(rent);
        }
#endif
    }

    /// <summary>
    /// Writes a sequence of bytes to a given <see cref="Stream"/> instance.
    /// </summary>
    /// <param name="stream">The destination <see cref="Stream"/> to write data to.</param>
    /// <param name="buffer">The source <see cref="Span{T}"/> to read data from.</param>
#if NETSTANDARD2_1_OR_GREATER
    [Obsolete("This API is only available for binary compatibility, but Stream.Read should be used instead.")]
    [EditorBrowsable(EditorBrowsableState.Never)]
#endif
    public static void Write(this Stream stream, ReadOnlySpan<byte> buffer)
    {
#if NETSTANDARD2_1_OR_GREATER
        stream.Write(buffer);
#else
        byte[] rent = ArrayPool<byte>.Shared.Rent(buffer.Length);

        try
        {
            buffer.CopyTo(rent);

            stream.Write(rent, 0, buffer.Length);
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(rent);
        }
#endif
    }

    /// <summary>
    /// Reads a value of a specified type from a source <see cref="Stream"/> instance.
    /// </summary>
    /// <typeparam name="T">The type of value to read.</typeparam>
    /// <param name="stream">The source <see cref="Stream"/> instance to read from.</param>
    /// <returns>The <typeparamref name="T"/> value read from <paramref name="stream"/>.</returns>
    /// <exception cref="InvalidOperationException">Thrown if <paramref name="stream"/> reaches the end.</exception>
#if NETSTANDARD2_1_OR_GREATER
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
    public static T Read<T>(this Stream stream)
        where T : unmanaged
    {
#if NETSTANDARD2_1_OR_GREATER
        T result = default;
        int length = Unsafe.SizeOf<T>();

        unsafe
        {
            if (stream.Read(new Span<byte>(&result, length)) != length)
            {
                ThrowInvalidOperationExceptionForEndOfStream();
            }
        }

        return result;
#else
        int length = Unsafe.SizeOf<T>();
        byte[] buffer = ArrayPool<byte>.Shared.Rent(length);

        try
        {
            if (stream.Read(buffer, 0, length) != length)
            {
                ThrowInvalidOperationExceptionForEndOfStream();
            }

            return Unsafe.ReadUnaligned<T>(ref buffer[0]);
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(buffer);
        }
#endif
    }

    /// <summary>
    /// Writes a value of a specified type into a target <see cref="Stream"/> instance.
    /// </summary>
    /// <typeparam name="T">The type of value to write.</typeparam>
    /// <param name="stream">The target <see cref="Stream"/> instance to write to.</param>
    /// <param name="value">The input value to write to <paramref name="stream"/>.</param>
#if NETSTANDARD2_1_OR_GREATER
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
#endif
    public static void Write<T>(this Stream stream, in T value)
        where T : unmanaged
    {
#if NETSTANDARD2_1_OR_GREATER
        ref T r0 = ref Unsafe.AsRef(value);
        ref byte r1 = ref Unsafe.As<T, byte>(ref r0);
        int length = Unsafe.SizeOf<T>();

        ReadOnlySpan<byte> span = MemoryMarshal.CreateReadOnlySpan(ref r1, length);

        stream.Write(span);
#else
        int length = Unsafe.SizeOf<T>();
        byte[] buffer = ArrayPool<byte>.Shared.Rent(length);

        try
        {
            Unsafe.WriteUnaligned(ref buffer[0], value);

            stream.Write(buffer, 0, length);
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(buffer);
        }
#endif
    }

    /// <summary>
    /// Throws an <see cref="InvalidOperationException"/> when <see cref="Read{T}"/> fails.
    /// </summary>
    private static void ThrowInvalidOperationExceptionForEndOfStream()
    {
        throw new InvalidOperationException("The stream didn't contain enough data to read the requested item.");
    }
}