using System;
using System.Buffers;
using System.Buffers.Binary;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Crypto;

namespace SharpCompress.Compressors.LZMA;

public sealed partial class LZipStream
{
    public static LZipStream Create(Stream stream, CompressionMode mode, bool leaveOpen = false)
    {
        if (mode == CompressionMode.Compress)
        {
            WriteHeaderSize(stream);
        }
        return new LZipStream(stream, mode, leaveOpen);
    }

    public static async ValueTask<LZipStream> CreateAsync(
        Stream stream,
        CompressionMode mode,
        bool leaveOpen = false,
        CancellationToken cancellationToken = default
    )
    {
        cancellationToken.ThrowIfCancellationRequested();

        if (mode == CompressionMode.Compress)
        {
            await WriteHeaderSizeAsync(stream).ConfigureAwait(false);
        }
        return new LZipStream(stream, mode, leaveOpen);
    }

    public async ValueTask FinishAsync(CancellationToken cancellationToken)
    {
        if (_finished)
        {
            return;
        }

        if (Mode == CompressionMode.Compress)
        {
            cancellationToken.ThrowIfCancellationRequested();
            var crc32Stream = (Crc32Stream)_stream;
            await FinishWrappedStreamAsync(crc32Stream).ConfigureAwait(false);
            var compressedCount = _countingWritableSubStream.NotNull().BytesWritten;

            var intBuf = ArrayPool<byte>.Shared.Rent(8);
            try
            {
                BinaryPrimitives.WriteUInt32LittleEndian(intBuf, crc32Stream.Crc);
                await _countingWritableSubStream
                    .NotNull()
                    .WriteAsync(intBuf, 0, 4, cancellationToken)
                    .ConfigureAwait(false);

                BinaryPrimitives.WriteInt64LittleEndian(intBuf, _writeCount);
                await _countingWritableSubStream
                    .NotNull()
                    .WriteAsync(intBuf, 0, 8, cancellationToken)
                    .ConfigureAwait(false);

                // Total member size includes the 6-byte header and 20-byte trailer.
                BinaryPrimitives.WriteUInt64LittleEndian(
                    intBuf,
                    (ulong)compressedCount + (ulong)(6 + 20)
                );
                await _countingWritableSubStream
                    .NotNull()
                    .WriteAsync(intBuf, 0, 8, cancellationToken)
                    .ConfigureAwait(false);
            }
            finally
            {
                ArrayPool<byte>.Shared.Return(intBuf);
            }
        }

        _finished = true;
    }

    private static async ValueTask WriteHeaderSizeAsync(Stream stream) =>
        // hard coding the dictionary size encoding
        await stream.WriteAsync(headerBytes, 0, 6).ConfigureAwait(false);

    private static async ValueTask FinishWrappedStreamAsync(Crc32Stream crc32Stream)
    {
        if (crc32Stream.WrappedStream is IAsyncDisposable asyncDisposableWrappedStream)
        {
            await asyncDisposableWrappedStream.DisposeAsync().ConfigureAwait(false);
        }
        else
        {
#pragma warning disable VSTHRD103 // Fallback for streams that do not support async disposal.
            crc32Stream.WrappedStream.Dispose();
            crc32Stream.Dispose();
#pragma warning restore VSTHRD103
        }
    }

    /// <summary>
    /// Asynchronously determines if the given stream is positioned at the start of a v1 LZip
    /// file, as indicated by the ASCII characters "LZIP" and a version byte
    /// of 1, followed by at least one byte.
    /// </summary>
    /// <param name="stream">The stream to read from. Must not be null.</param>
    /// <param name="cancellationToken">Cancellation token.</param>
    /// <returns><c>true</c> if the given stream is an LZip file, <c>false</c> otherwise.</returns>
    public static async ValueTask<bool> IsLZipFileAsync(
        Stream stream,
        CancellationToken cancellationToken = default
    ) => await ValidateAndReadSizeAsync(stream, cancellationToken).ConfigureAwait(false) != 0;

    /// <summary>
    /// Asynchronously reads the 6-byte header of the stream, and returns 0 if either the header
    /// couldn't be read or it isn't a validate LZIP header, or the dictionary
    /// size if it *is* a valid LZIP file.
    /// </summary>
    public static async ValueTask<int> ValidateAndReadSizeAsync(
        Stream stream,
        CancellationToken cancellationToken
    )
    {
        // Read the header
        var header = ArrayPool<byte>.Shared.Rent(6);
        try
        {
            var n = await stream.ReadAsync(header, 0, 6, cancellationToken).ConfigureAwait(false);

            // TODO: Handle reading only part of the header?

            if (n != 6)
            {
                return 0;
            }

            if (
                header[0] != 'L'
                || header[1] != 'Z'
                || header[2] != 'I'
                || header[3] != 'P'
                || header[4] != 1 /* version 1 */
            )
            {
                return 0;
            }
            var basePower = header[5] & 0x1F;
            var subtractionNumerator = (header[5] & 0xE0) >> 5;
            if (basePower < 4 || basePower > 30)
            {
                return 0;
            }
            return (1 << basePower) - (subtractionNumerator * (1 << (basePower - 4)));
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(header);
        }
    }

#if !LEGACY_DOTNET
    /// <summary>
    /// Asynchronously reads bytes from the current stream into a buffer.
    /// </summary>
    public override ValueTask<int> ReadAsync(
        Memory<byte> buffer,
        CancellationToken cancellationToken = default
    ) => ReadAndValidateAsync(buffer, cancellationToken);
#endif

    /// <summary>
    /// Asynchronously reads bytes from the current stream into a buffer.
    /// </summary>
    public override Task<int> ReadAsync(
        byte[] buffer,
        int offset,
        int count,
        CancellationToken cancellationToken = default
    ) => ReadAndValidateAsync(buffer, offset, count, cancellationToken);

    private async Task<int> ReadAndValidateAsync(
        byte[] buffer,
        int offset,
        int count,
        CancellationToken cancellationToken
    )
    {
        var read = await _stream
            .ReadAsync(buffer, offset, count, cancellationToken)
            .ConfigureAwait(false);
        UpdateAndValidateAtEof(buffer.AsSpan(offset, read), read);
        return read;
    }

#if !LEGACY_DOTNET
    private async ValueTask<int> ReadAndValidateAsync(
        Memory<byte> buffer,
        CancellationToken cancellationToken
    )
    {
        var read = await _stream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
        UpdateAndValidateAtEof(buffer.Span[..read], read);
        return read;
    }
#endif

    /// <summary>
    /// Asynchronously writes bytes from a buffer to the current stream.
    /// </summary>
    public override async Task WriteAsync(
        byte[] buffer,
        int offset,
        int count,
        CancellationToken cancellationToken
    )
    {
        cancellationToken.ThrowIfCancellationRequested();
        await _stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false);
        _writeCount += count;
    }
}
