// ZlibStream.cs
// ------------------------------------------------------------------
//
// Copyright (c) 2009 Dino Chiesa and Microsoft Corporation.
// All rights reserved.
//
// This code module is part of DotNetZip, a zipfile class library.
//
// ------------------------------------------------------------------
//
// This code is licensed under the Microsoft Public License.
// See the file License.txt for the license details.
// More info on: http://dotnetzip.codeplex.com
//
// ------------------------------------------------------------------
//
// last saved (in emacs):
// Time-stamp: <2010-January-29 16:35:23>
//
// ------------------------------------------------------------------
//
// This module defines the ZlibStream class, which is similar in idea to
// the System.IO.Compression.DeflateStream and
// System.IO.Compression.GZipStream classes in the .NET BCL.
//
// ------------------------------------------------------------------

using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.IO;

namespace SharpCompress.Compressors.Deflate;

public class ZlibStream : Stream, IStreamStack
{
#if DEBUG_STREAMS
    long IStreamStack.InstanceId { get; set; }
#endif
    int IStreamStack.DefaultBufferSize { get; set; }

    Stream IStreamStack.BaseStream() => _baseStream;

    int IStreamStack.BufferSize
    {
        get => 0;
        set { }
    }
    int IStreamStack.BufferPosition
    {
        get => 0;
        set { }
    }

    void IStreamStack.SetPosition(long position) { }

    private readonly ZlibBaseStream _baseStream;
    private bool _disposed;

    public ZlibStream(Stream stream, CompressionMode mode)
        : this(stream, mode, CompressionLevel.Default, Encoding.UTF8) { }

    public ZlibStream(Stream stream, CompressionMode mode, CompressionLevel level)
        : this(stream, mode, level, Encoding.UTF8) { }

    public ZlibStream(
        Stream stream,
        CompressionMode mode,
        CompressionLevel level,
        Encoding encoding
    )
    {
        _baseStream = new ZlibBaseStream(stream, mode, level, ZlibStreamFlavor.ZLIB, encoding);
#if DEBUG_STREAMS
        this.DebugConstruct(typeof(ZlibStream));
#endif
    }

    #region Zlib properties

    /// <summary>
    /// This property sets the flush behavior on the stream.
    /// Sorry, though, not sure exactly how to describe all the various settings.
    /// </summary>
    public virtual FlushType FlushMode
    {
        get => (_baseStream._flushMode);
        set
        {
            if (_disposed)
            {
                throw new ObjectDisposedException("ZlibStream");
            }
            _baseStream._flushMode = value;
        }
    }

    /// <summary>
    ///   The size of the working buffer for the compression codec.
    /// </summary>
    ///
    /// <remarks>
    /// <para>
    ///   The working buffer is used for all stream operations.  The default size is
    ///   1024 bytes. The minimum size is 128 bytes. You may get better performance
    ///   with a larger buffer.  Then again, you might not.  You would have to test
    ///   it.
    /// </para>
    ///
    /// <para>
    ///   Set this before the first call to <c>Read()</c> or <c>Write()</c> on the
    ///   stream. If you try to set it afterwards, it will throw.
    /// </para>
    /// </remarks>
    public int BufferSize
    {
        get => _baseStream._bufferSize;
        set
        {
            if (_disposed)
            {
                throw new ObjectDisposedException("ZlibStream");
            }
            if (_baseStream._workingBuffer != null)
            {
                throw new ZlibException("The working buffer is already set.");
            }
            if (value < ZlibConstants.WorkingBufferSizeMin)
            {
                throw new ZlibException(
                    string.Format(
                        "Don't be silly. {0} bytes?? Use a bigger buffer, at least {1}.",
                        value,
                        ZlibConstants.WorkingBufferSizeMin
                    )
                );
            }
            _baseStream._bufferSize = value;
        }
    }

    /// <summary> Returns the total number of bytes input so far.</summary>
    public virtual long TotalIn => _baseStream._z.TotalBytesIn;

    /// <summary> Returns the total number of bytes output so far.</summary>
    public virtual long TotalOut => _baseStream._z.TotalBytesOut;

    #endregion

    #region System.IO.Stream methods

    /// <summary>
    /// Indicates whether the stream can be read.
    /// </summary>
    /// <remarks>
    /// The return value depends on whether the captive stream supports reading.
    /// </remarks>
    public override bool CanRead
    {
        get
        {
            if (_disposed)
            {
                throw new ObjectDisposedException("ZlibStream");
            }
            return _baseStream._stream.CanRead;
        }
    }

    /// <summary>
    /// Indicates whether the stream supports Seek operations.
    /// </summary>
    /// <remarks>
    /// Always returns false.
    /// </remarks>
    public override bool CanSeek => false;

    /// <summary>
    /// Indicates whether the stream can be written.
    /// </summary>
    /// <remarks>
    /// The return value depends on whether the captive stream supports writing.
    /// </remarks>
    public override bool CanWrite
    {
        get
        {
            if (_disposed)
            {
                throw new ObjectDisposedException("ZlibStream");
            }
            return _baseStream._stream.CanWrite;
        }
    }

    /// <summary>
    /// Reading this property always throws a <see cref="NotImplementedException"/>.
    /// </summary>
    public override long Length => throw new NotSupportedException();

    /// <summary>
    ///   The position of the stream pointer.
    /// </summary>
    ///
    /// <remarks>
    ///   Setting this property always throws a <see
    ///   cref="NotImplementedException"/>. Reading will return the total bytes
    ///   written out, if used in writing, or the total bytes read in, if used in
    ///   reading.  The count may refer to compressed bytes or uncompressed bytes,
    ///   depending on how you've used the stream.
    /// </remarks>
    public override long Position
    {
        get
        {
            if (_baseStream._streamMode == ZlibBaseStream.StreamMode.Writer)
            {
                return _baseStream._z.TotalBytesOut;
            }
            if (_baseStream._streamMode == ZlibBaseStream.StreamMode.Reader)
            {
                return _baseStream._z.TotalBytesIn;
            }
            return 0;
        }
        set => throw new NotSupportedException();
    }

    /// <summary>
    /// Dispose the stream.
    /// </summary>
    /// <remarks>
    /// This may or may not result in a <c>Close()</c> call on the captive stream.
    /// </remarks>
    protected override void Dispose(bool disposing)
    {
        try
        {
            if (!_disposed)
            {
                if (disposing)
                {
                    _baseStream?.Dispose();
                }
                _disposed = true;
#if DEBUG_STREAMS
                this.DebugDispose(typeof(ZlibStream));
#endif
            }
        }
        finally
        {
            base.Dispose(disposing);
        }
    }

    /// <summary>
    /// Flush the stream.
    /// </summary>
    public override void Flush()
    {
        if (_disposed)
        {
            throw new ObjectDisposedException("ZlibStream");
        }
        _baseStream.Flush();
    }

    public override async Task FlushAsync(CancellationToken cancellationToken)
    {
        if (_disposed)
        {
            throw new ObjectDisposedException("ZlibStream");
        }
        await _baseStream.FlushAsync(cancellationToken).ConfigureAwait(false);
    }

#if !NETFRAMEWORK && !NETSTANDARD2_0
    public override async ValueTask DisposeAsync()
    {
        if (_disposed)
        {
            return;
        }
        _disposed = true;
        if (_baseStream != null)
        {
            await _baseStream.DisposeAsync().ConfigureAwait(false);
        }
#if DEBUG_STREAMS
        this.DebugDispose(typeof(ZlibStream));
#endif
        await base.DisposeAsync().ConfigureAwait(false);
    }
#endif

    /// <summary>
    /// Read data from the stream.
    /// </summary>
    ///
    /// <remarks>
    ///
    /// <para>
    ///   If you wish to use the <c>ZlibStream</c> to compress data while reading,
    ///   you can create a <c>ZlibStream</c> with <c>CompressionMode.Compress</c>,
    ///   providing an uncompressed data stream.  Then call <c>Read()</c> on that
    ///   <c>ZlibStream</c>, and the data read will be compressed.  If you wish to
    ///   use the <c>ZlibStream</c> to decompress data while reading, you can create
    ///   a <c>ZlibStream</c> with <c>CompressionMode.Decompress</c>, providing a
    ///   readable compressed data stream.  Then call <c>Read()</c> on that
    ///   <c>ZlibStream</c>, and the data will be decompressed as it is read.
    /// </para>
    ///
    /// <para>
    ///   A <c>ZlibStream</c> can be used for <c>Read()</c> or <c>Write()</c>, but
    ///   not both.
    /// </para>
    ///
    /// </remarks>
    /// <param name="buffer">The buffer into which the read data should be placed.</param>
    /// <param name="offset">the offset within that data array to put the first byte read.</param>
    /// <param name="count">the number of bytes to read.</param>
    public override int Read(byte[] buffer, int offset, int count)
    {
        if (_disposed)
        {
            throw new ObjectDisposedException("ZlibStream");
        }
        return _baseStream.Read(buffer, offset, count);
    }

    public override async Task<int> ReadAsync(
        byte[] buffer,
        int offset,
        int count,
        CancellationToken cancellationToken
    )
    {
        if (_disposed)
        {
            throw new ObjectDisposedException("ZlibStream");
        }
        return await _baseStream
            .ReadAsync(buffer, offset, count, cancellationToken)
            .ConfigureAwait(false);
    }

#if !NETFRAMEWORK && !NETSTANDARD2_0
    public override async ValueTask<int> ReadAsync(
        Memory<byte> buffer,
        CancellationToken cancellationToken = default
    )
    {
        if (_disposed)
        {
            throw new ObjectDisposedException("ZlibStream");
        }
        return await _baseStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false);
    }
#endif

    public override int ReadByte()
    {
        if (_disposed)
        {
            throw new ObjectDisposedException("ZlibStream");
        }
        return _baseStream.ReadByte();
    }

    /// <summary>
    /// Calling this method always throws a <see cref="NotImplementedException"/>.
    /// </summary>
    public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException();

    /// <summary>
    /// Calling this method always throws a <see cref="NotImplementedException"/>.
    /// </summary>
    public override void SetLength(long value) => throw new NotSupportedException();

    /// <summary>
    /// Write data to the stream.
    /// </summary>
    ///
    /// <remarks>
    ///
    /// <para>
    ///   If you wish to use the <c>ZlibStream</c> to compress data while writing,
    ///   you can create a <c>ZlibStream</c> with <c>CompressionMode.Compress</c>,
    ///   and a writable output stream.  Then call <c>Write()</c> on that
    ///   <c>ZlibStream</c>, providing uncompressed data as input.  The data sent to
    ///   the output stream will be the compressed form of the data written.  If you
    ///   wish to use the <c>ZlibStream</c> to decompress data while writing, you
    ///   can create a <c>ZlibStream</c> with <c>CompressionMode.Decompress</c>, and a
    ///   writable output stream.  Then call <c>Write()</c> on that stream,
    ///   providing previously compressed data. The data sent to the output stream
    ///   will be the decompressed form of the data written.
    /// </para>
    ///
    /// <para>
    ///   A <c>ZlibStream</c> can be used for <c>Read()</c> or <c>Write()</c>, but not both.
    /// </para>
    /// </remarks>
    /// <param name="buffer">The buffer holding data to write to the stream.</param>
    /// <param name="offset">the offset within that data array to find the first byte to write.</param>
    /// <param name="count">the number of bytes to write.</param>
    public override void Write(byte[] buffer, int offset, int count)
    {
        if (_disposed)
        {
            throw new ObjectDisposedException("ZlibStream");
        }
        _baseStream.Write(buffer, offset, count);
    }

    public override async Task WriteAsync(
        byte[] buffer,
        int offset,
        int count,
        CancellationToken cancellationToken
    )
    {
        if (_disposed)
        {
            throw new ObjectDisposedException("ZlibStream");
        }
        await _baseStream
            .WriteAsync(buffer, offset, count, cancellationToken)
            .ConfigureAwait(false);
    }

#if !NETFRAMEWORK && !NETSTANDARD2_0
    public override async ValueTask WriteAsync(
        ReadOnlyMemory<byte> buffer,
        CancellationToken cancellationToken = default
    )
    {
        if (_disposed)
        {
            throw new ObjectDisposedException("ZlibStream");
        }
        await _baseStream.WriteAsync(buffer, cancellationToken).ConfigureAwait(false);
    }
#endif

    public override void WriteByte(byte value)
    {
        if (_disposed)
        {
            throw new ObjectDisposedException("ZlibStream");
        }
        _baseStream.WriteByte(value);
    }

    #endregion System.IO.Stream methods
}
