#### Bzip2 Implementation
#### Copyright (C) 2023-2024 Remilia Scarlet
#### Copyright (c) 2022 drone1400
#### Copyright (C) 2015 Jaime Olivares
#### Copyright (c) 2011 Matthew Francis
#### MIT License
####
#### Ported from the Java implementation by Matthew Francis:
#### https://github.com/MateuszBartosiewicz/bzip2.
####
#### Modified by drone1400
####
#### Ported by Remilia Scarlet from the C# implementation by Jamie Olivares:
#### http://github.com/jaime-olivares/bzip2
require "./blockcompressor"

module RemiLib::Compression::BZip2
  # A write-only `IO` object to compress data in the BZip2 format.
  #
  # Instances of this class wrap another IO object. When you write to this
  # instance, it compresses the data and writes it to the underlying IO.
  #
  # NOTE: unless created with a block, `close` must be invoked after all data
  # has been written to an instance.
  #
  # Example of a simple compressor:
  #
  # ```crystal
  # # Compress `file` and save to `output`.
  # File.open(bzip2file, "rb") do |file|
  #   File.open(outputfile, "wb") do |output|
  #     RemiLib::Compression::BZip2::Writer.open(output) do |bzio|
  #       IO.copy(bzio, output)
  #     end
  #   end
  # end
  #```
  class Writer < IO
    # If `#sync_close?` is `true`, closing this IO will close the underlying IO.
    property? sync_close : Bool

    @output : IO
    @bitWriter : BitWriter
    @streamBlockSize : Int32
    @streamCRC : UInt32 = 0
    @int32Pool : ArrayPool(Int32) = ArrayPool(Int32).new(0)
    private getter! blockComp : BlockCompressor?

    # Creates an instance of `Writer`. `#close` must be invoked after all data has
    # written.  `level` will be clamped to `MIN_COMPRESSION_LEVEL` and
    # `MAX_COMPRESSION_LEVEL` if it is outside those values.
    def initialize(@output : IO, level : Int32 = DEFAULT_COMPRESSION_LEVEL, @sync_close : Bool = false)
      level = level.clamp(MIN_COMPRESSION_LEVEL, MAX_COMPRESSION_LEVEL)

      @closed = false
      @streamBlockSize = level * 100_000
      @bitWriter = BitWriter.new(@output)

      @bitWriter.writeBits(16, STREAM_START_MARKER_1)
      @bitWriter.writeBits(8, STREAM_START_MARKER_2)
      @bitWriter.writeBits(8, ('0'.ord + level).to_u32)

      initNextBlock
    end

    # Creates a new writer for the given *io*, yields it to the given block,
    # then closes it at its end.
    def self.open(io : IO, level : Int32 = DEFAULT_COMPRESSION_LEVEL, sync_close : Bool = false, &)
      writer = new(io, level: level, sync_close: sync_close)
      begin
        yield writer
      ensure
        writer.close
      end
    end

    # Always raises `IO::Error` because this is a write-only `IO`.
    def read(slice : Bytes) : NoReturn
      raise "Can't read from BZip2::Writer"
    end

    # See `IO#write`.
    def write(slice : Bytes) : Nil
      check_open

      pos = 0
      offset = 0
      while offset < slice.size
        pos = blockComp.write(slice[offset..])
        if pos < slice.size
          compressBlock
          initNextBlock
        end
        offset += pos
      end
    end

    # Not implemented for a BZip2 `Writer`, always raises an exception.
    def flush : Nil
      raise "Cannot #flush a BZip2::Writer"
    end

    # Closes this writer. Must be invoked after all data has been written.
    def close : Nil
      return if @closed || @output.closed?
      finish
      @output.close if @sync_close
    end

    # Returns `true` if this IO is closed.
    def closed? : Bool
      @closed
    end

    def inspect(io : IO) : Nil
      to_s(io)
    end

    @[AlwaysInline]
    private def initNextBlock : Nil
      @blockComp = BlockCompressor.new(@bitWriter, @streamBlockSize, @int32Pool)
    end

    @[AlwaysInline]
    private def compressBlock : Nil
      return if blockComp.empty?
      blockComp.closeBlock
      @streamCRC = ((@streamCRC << 1) | (@streamCRC >> 31)) ^ blockComp.crc
    end

    private def finish : Nil
      unless @closed
        begin
          compressBlock
          @bitWriter.writeBits(24, STREAM_END_MARKER_1)
          @bitWriter.writeBits(24, STREAM_END_MARKER_2)
          @bitWriter.writeInt32(@streamCRC)
          @bitWriter.flush
          @output.flush
        ensure
          @blockComp = nil
          @closed = true
        end
      end
    end
  end
end
