using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Archives;
using SharpCompress.Archives.GZip;
using SharpCompress.Archives.Tar;
using SharpCompress.Common;
using SharpCompress.Common.Options;
using SharpCompress.IO;
using SharpCompress.Providers;
using SharpCompress.Readers;
using SharpCompress.Readers.GZip;
using SharpCompress.Readers.Tar;
using SharpCompress.Writers;
using SharpCompress.Writers.GZip;

namespace SharpCompress.Factories;

/// <summary>
/// Represents the foundation factory of GZip archive.
/// </summary>
public class GZipFactory
    : Factory,
        IArchiveFactory,
        IMultiArchiveFactory,
        IReaderFactory,
        IWriterFactory,
        IWritableArchiveFactory<GZipWriterOptions>
{
    #region IFactory

    /// <inheritdoc/>
    public override string Name => "GZip";

    /// <inheritdoc/>
    public override ArchiveType? KnownArchiveType => ArchiveType.GZip;

    /// <inheritdoc/>
    public override IEnumerable<string> GetSupportedExtensions()
    {
        yield return "gz";
    }

    /// <inheritdoc/>
    public override bool IsArchive(Stream stream, ReaderOptions readerOptions) =>
        GZipArchive.IsGZipFile(stream);

    /// <inheritdoc/>
    public override ValueTask<bool> IsArchiveAsync(
        Stream stream,
        ReaderOptions readerOptions,
        CancellationToken cancellationToken = default
    ) => GZipArchive.IsGZipFileAsync(stream, cancellationToken);

    #endregion

    #region IArchiveFactory

    /// <inheritdoc/>
    public IArchive OpenArchive(Stream stream, ReaderOptions? readerOptions = null) =>
        GZipArchive.OpenArchive(stream, readerOptions);

    /// <inheritdoc/>
    public ValueTask<IAsyncArchive> OpenAsyncArchive(
        Stream stream,
        ReaderOptions? readerOptions = null,
        CancellationToken cancellationToken = default
    )
    {
        cancellationToken.ThrowIfCancellationRequested();
        return new((IAsyncArchive)OpenArchive(stream, readerOptions));
    }

    /// <inheritdoc/>
    public ValueTask<IAsyncArchive> OpenAsyncArchive(
        FileInfo fileInfo,
        ReaderOptions? readerOptions = null,
        CancellationToken cancellationToken = default
    )
    {
        cancellationToken.ThrowIfCancellationRequested();
        return new((IAsyncArchive)OpenArchive(fileInfo, readerOptions));
    }

    #endregion

    #region IMultiArchiveFactory

    /// <inheritdoc/>
    public IArchive OpenArchive(
        IReadOnlyList<Stream> streams,
        ReaderOptions? readerOptions = null
    ) => GZipArchive.OpenArchive(streams, readerOptions);

    /// <inheritdoc/>
    public async ValueTask<IAsyncArchive> OpenAsyncArchive(
        IReadOnlyList<Stream> streams,
        ReaderOptions? readerOptions = null,
        CancellationToken cancellationToken = default
    )
    {
        cancellationToken.ThrowIfCancellationRequested();
        return await GZipArchive
            .OpenAsyncArchive(streams, readerOptions, cancellationToken)
            .ConfigureAwait(false);
    }

    /// <inheritdoc/>
    public IArchive OpenArchive(
        IReadOnlyList<FileInfo> fileInfos,
        ReaderOptions? readerOptions = null
    ) => GZipArchive.OpenArchive(fileInfos, readerOptions);

    /// <inheritdoc/>
    public async ValueTask<IAsyncArchive> OpenAsyncArchive(
        IReadOnlyList<FileInfo> fileInfos,
        ReaderOptions? readerOptions = null,
        CancellationToken cancellationToken = default
    )
    {
        cancellationToken.ThrowIfCancellationRequested();
        return await GZipArchive
            .OpenAsyncArchive(fileInfos, readerOptions, cancellationToken)
            .ConfigureAwait(false);
    }

    #endregion

    #region IReaderFactory

    /// <inheritdoc/>
    internal override bool TryOpenReader(
        SharpCompressStream sharpCompressStream,
        ReaderOptions options,
        out IReader? reader
    )
    {
        reader = null;

        if (GZipArchive.IsGZipFile(sharpCompressStream))
        {
            sharpCompressStream.Rewind();
            // Do not dispose the probe GZip stream directly. Disposing after a partial probe read
            // can throw in the decompressor finalize path, so probe through a non-disposing wrapper.
            using var testStream = SharpCompressStream.CreateNonDisposing(
                options.Providers.CreateDecompressStream(
                    CompressionType.GZip,
                    SharpCompressStream.CreateNonDisposing(sharpCompressStream),
                    CompressionContext.FromStream(sharpCompressStream).WithReaderOptions(options)
                )
            );
            var isTarArchive = TarArchive.IsTarFile(testStream);

            // The TAR probe can consume arbitrary compressed input before it rejects a stream.
            sharpCompressStream.Rewind();
            sharpCompressStream.StopRecording();
            if (isTarArchive)
            {
                reader = new TarReader(sharpCompressStream, options, CompressionType.GZip);
                return true;
            }
            reader = OpenReader(sharpCompressStream, options);
            return true;
        }
        sharpCompressStream.Rewind();
        return false;
    }

    internal override async ValueTask<IAsyncReader?> TryOpenReaderAsync(
        SharpCompressStream sharpCompressStream,
        ReaderOptions options,
        CancellationToken cancellationToken = default
    )
    {
        if (
            !await GZipArchive
                .IsGZipFileAsync(sharpCompressStream, cancellationToken)
                .ConfigureAwait(false)
        )
        {
            sharpCompressStream.Rewind();
            return null;
        }

        sharpCompressStream.Rewind();
        using var testStream = SharpCompressStream.CreateNonDisposing(
            await options
                .Providers.CreateDecompressStreamAsync(
                    CompressionType.GZip,
                    SharpCompressStream.CreateNonDisposing(sharpCompressStream),
                    CompressionContext.FromStream(sharpCompressStream).WithReaderOptions(options),
                    cancellationToken
                )
                .ConfigureAwait(false)
        );
        var isTarArchive = await TarArchive
            .IsTarFileAsync(testStream, cancellationToken)
            .ConfigureAwait(false);

        sharpCompressStream.Rewind();
        sharpCompressStream.StopRecording();
        if (isTarArchive)
        {
            return new TarReader(sharpCompressStream, options, CompressionType.GZip);
        }

        return await OpenAsyncReader(sharpCompressStream, options, cancellationToken)
            .ConfigureAwait(false);
    }

    /// <inheritdoc/>
    public IReader OpenReader(Stream stream, ReaderOptions? options) =>
        GZipReader.OpenReader(stream, options);

    /// <inheritdoc/>
    public ValueTask<IAsyncReader> OpenAsyncReader(
        Stream stream,
        ReaderOptions? options,
        CancellationToken cancellationToken = default
    )
    {
        cancellationToken.ThrowIfCancellationRequested();
        return new((IAsyncReader)GZipReader.OpenReader(stream, options));
    }

    /// <inheritdoc/>
    public IArchive OpenArchive(FileInfo fileInfo, ReaderOptions? readerOptions = null) =>
        GZipArchive.OpenArchive(fileInfo, readerOptions);

    #endregion

    #region IWriterFactory

    /// <inheritdoc/>
    public IWriter OpenWriter(Stream stream, IWriterOptions writerOptions)
    {
        if (writerOptions.CompressionType != CompressionType.GZip)
        {
            throw new InvalidFormatException("GZip archives only support GZip compression type.");
        }

        GZipWriterOptions gzipOptions = writerOptions switch
        {
            GZipWriterOptions gwo => gwo,
            WriterOptions wo => new GZipWriterOptions(wo),
            _ => throw new ArgumentException(
                $"Expected WriterOptions or GZipWriterOptions, got {writerOptions.GetType().Name}",
                nameof(writerOptions)
            ),
        };
        return new GZipWriter(stream, gzipOptions);
    }

    /// <inheritdoc/>
    public ValueTask<IAsyncWriter> OpenAsyncWriter(
        Stream stream,
        IWriterOptions writerOptions,
        CancellationToken cancellationToken = default
    )
    {
        cancellationToken.ThrowIfCancellationRequested();
        var writer = OpenWriter(stream, writerOptions);
        return new((IAsyncWriter)writer);
    }

    #endregion

    #region IWritableArchiveFactory

    /// <inheritdoc/>
    public IWritableArchive<GZipWriterOptions> CreateArchive() => GZipArchive.CreateArchive();

    #endregion
}
