// GZipStream.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-09 12:04:28>
//
// ------------------------------------------------------------------
//
// This module defines the GZipStream class, which can be used as a replacement for
// the System.IO.Compression.GZipStream class in the .NET BCL.  NB: The design is not
// completely OO clean: there is some intelligence in the ZlibBaseStream that reads the
// GZip header.
//
// ------------------------------------------------------------------

using System;
using System.Buffers.Binary;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using SharpCompress.Common;
using SharpCompress.Common.Options;

namespace SharpCompress.Compressors.Deflate;

public partial class GZipStream : Stream
#if LEGACY_DOTNET
        , IAsyncDisposable
#endif
{
    internal static readonly DateTime UNIX_EPOCH = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

    private string? _comment;
    private string? _fileName;
    private DateTime? _lastModified;

    internal ZlibBaseStream BaseStream;
    private bool _disposed;
    private bool _firstReadDone;
    private int _headerByteCount;

    private readonly Encoding _encoding;

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

    public GZipStream(Stream stream, CompressionMode mode, IReaderOptions readerOptions)
        : this(stream, mode, CompressionLevel.Default, readerOptions) { }

    public GZipStream(
        Stream stream,
        CompressionMode mode,
        CompressionLevel level,
        IReaderOptions readerOptions
    )
        : this(
            stream,
            mode,
            level,
            (
                readerOptions ?? throw new ArgumentNullException(nameof(readerOptions))
            ).ArchiveEncoding.GetEncoding(),
            readerOptions.LeaveStreamOpen
        ) { }

    public GZipStream(
        Stream stream,
        CompressionMode mode,
        CompressionLevel level,
        Encoding encoding,
        bool leaveOpen = false
    )
    {
        BaseStream = new ZlibBaseStream(
            stream,
            mode,
            level,
            ZlibStreamFlavor.GZIP,
            leaveOpen,
            encoding
        );
        _encoding = encoding;
    }

    #region Zlib properties

    public virtual FlushType FlushMode
    {
        get => (BaseStream._flushMode);
        set
        {
            if (_disposed)
            {
                throw new ObjectDisposedException("GZipStream");
            }
            BaseStream._flushMode = value;
        }
    }

    public int BufferSize
    {
        get => BaseStream._bufferSize;
        set
        {
            if (_disposed)
            {
                throw new ObjectDisposedException("GZipStream");
            }
            if (BaseStream._workingBuffer != null)
            {
                throw new ZlibException("The working buffer is already set.");
            }
            if (value < ZlibConstants.WorkingBufferSizeMin)
            {
                throw new ZlibException(
                    string.Format(
                        Constants.DefaultCultureInfo,
                        "Don't be silly. {0} bytes?? Use a bigger buffer, at least {1}.",
                        value,
                        ZlibConstants.WorkingBufferSizeMin
                    )
                );
            }
            BaseStream._bufferSize = value;
        }
    }

    internal virtual long TotalIn => BaseStream._z.TotalBytesIn;

    internal virtual long TotalOut => BaseStream._z.TotalBytesOut;

    #endregion

    #region 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("GZipStream");
            }
            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("GZipStream");
            }
            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 + _headerByteCount;
            }
            if (BaseStream._streamMode == ZlibBaseStream.StreamMode.Reader)
            {
                return BaseStream._z.TotalBytesIn + BaseStream._gzipHeaderByteCount;
            }
            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 != null))
                {
                    BaseStream.Dispose();
                    Crc32 = BaseStream.Crc32;
                }
                _disposed = true;
            }
        }
        finally
        {
            base.Dispose(disposing);
        }
    }

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

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

    #endregion Stream methods

    public string? Comment
    {
        get => _comment;
        set
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(nameof(GZipStream));
            }
            _comment = value;
        }
    }

    public DateTime? LastModified
    {
        get => _lastModified;
        set
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(nameof(GZipStream));
            }
            _lastModified = value;
        }
    }

    public string? FileName
    {
        get => _fileName;
        set
        {
            if (_disposed)
            {
                throw new ObjectDisposedException(nameof(GZipStream));
            }
            _fileName = value;
            if (_fileName is null)
            {
                return;
            }
#if LEGACY_DOTNET
            if (_fileName.Contains('/'))
#else
            if (_fileName.Contains('/', StringComparison.Ordinal))
#endif
            {
                _fileName = _fileName.Replace('/', '\\');
            }
            if (_fileName.EndsWith('\\'))
            {
                throw new ArchiveOperationException("Illegal filename");
            }

#if LEGACY_DOTNET
            if (_fileName.Contains('\\'))
#else
            if (_fileName.Contains('\\', StringComparison.Ordinal))
#endif
            {
                // trim any leading path
                _fileName = Path.GetFileName(_fileName);
            }
        }
    }

    public int Crc32 { get; private set; }

    private byte[] BuildHeader()
    {
        var commentBytes = (Comment is null) ? null : _encoding.GetBytes(Comment);
        var filenameBytes = (FileName is null) ? null : _encoding.GetBytes(FileName);

        var cbLength = commentBytes?.Length + 1 ?? 0;
        var fnLength = filenameBytes?.Length + 1 ?? 0;

        var bufferLength = 10 + cbLength + fnLength;
        var header = new byte[bufferLength];
        var i = 0;

        // ID
        header[i++] = 0x1F;
        header[i++] = 0x8B;

        // compression method
        header[i++] = 8;
        byte flag = 0;
        if (Comment != null)
        {
            flag ^= 0x10;
        }
        if (FileName != null)
        {
            flag ^= 0x8;
        }

        // flag
        header[i++] = flag;

        // mtime
        LastModified ??= DateTime.Now;
        var delta = LastModified.Value - UNIX_EPOCH;
        var timet = (int)delta.TotalSeconds;
        BinaryPrimitives.WriteInt32LittleEndian(header.AsSpan(i), timet);
        i += 4;

        // xflg
        header[i++] = 0; // this field is totally useless

        // OS
        header[i++] = 0xFF; // 0xFF == unspecified

        // extra field length - only if FEXTRA is set, which it is not.
        //header[i++]= 0;
        //header[i++]= 0;

        // filename
        if (fnLength != 0)
        {
            Array.Copy(filenameBytes!, 0, header, i, fnLength - 1);
            i += fnLength - 1;
            header[i++] = 0; // terminate
        }

        // comment
        if (cbLength != 0)
        {
            Array.Copy(commentBytes!, 0, header, i, cbLength - 1);
            i += cbLength - 1;
            header[i++] = 0; // terminate
        }

        return header;
    }

    [Zomp.SyncMethodGenerator.CreateSyncVersion]
    private async ValueTask<int> EmitHeaderAsync(CancellationToken cancellationToken)
    {
        var header = BuildHeader();
        await BaseStream
            ._stream.WriteAsync(header, 0, header.Length, cancellationToken)
            .ConfigureAwait(false);
        return header.Length; // bytes written
    }
}
