# A write-only `IO` object to compress data in the DEFLATE 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 a Flate::Writer instance.
class Compress::Deflate::Writer < IO
  # If `#sync_close?` is `true`, closing this IO will close the underlying IO.
  property? sync_close : Bool

  # Creates an instance of Flate::Writer. `close` must be invoked after all data
  # has written.
  def initialize(@output : IO, level : Int32 = Compress::Deflate::DEFAULT_COMPRESSION,
                 strategy : Compress::Deflate::Strategy = Compress::Deflate::Strategy::DEFAULT,
                 @sync_close : Bool = false, @dict : Bytes? = nil)
    unless -1 <= level <= 9
      raise ArgumentError.new("Invalid Flate level: #{level} (must be in -1..9)")
    end

    @buf = uninitialized UInt8[8192] # output buffer used by zlib
    @stream = LibZ::ZStream.new
    @stream.zalloc = LibZ::AllocFunc.new { |opaque, items, size| GC.malloc(items * size) }
    @stream.zfree = LibZ::FreeFunc.new { |opaque, address| GC.free(address) }
    @closed = false
    ret = LibZ.deflateInit2(pointerof(@stream), level, LibZ::Z_DEFLATED, -LibZ::MAX_BITS, LibZ::DEF_MEM_LEVEL,
      strategy.value, LibZ.zlibVersion, sizeof(LibZ::ZStream))
    raise Compress::Deflate::Error.new(ret, @stream) unless ret.ok?
  end

  # Creates a new writer for the given *io*, yields it to the given block,
  # and closes it at its end.
  def self.open(io : IO, level : Int32 = Compress::Deflate::DEFAULT_COMPRESSION,
                strategy : Compress::Deflate::Strategy = Compress::Deflate::Strategy::DEFAULT,
                sync_close : Bool = false, dict : Bytes? = nil, &)
    writer = new(io, level: level, strategy: strategy, sync_close: sync_close, dict: dict)
    yield writer ensure writer.close
  end

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

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

    return if slice.empty?

    @stream.avail_in = slice.size
    @stream.next_in = slice
    consume_output LibZ::Flush::NO_FLUSH
  end

  # See `IO#flush`.
  def flush : Nil
    return if @closed

    consume_output LibZ::Flush::SYNC_FLUSH
    @output.flush
  end

  # Closes this writer. Must be invoked after all data has been written.
  def close : Nil
    return if @closed
    @closed = true

    @stream.avail_in = 0
    @stream.next_in = Pointer(UInt8).null
    consume_output LibZ::Flush::FINISH

    ret = LibZ.deflateEnd(pointerof(@stream))
    raise Compress::Deflate::Error.new(ret, @stream) unless ret.ok?

    @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

  private def consume_output(flush)
    loop do
      @stream.next_out = @buf.to_unsafe
      @stream.avail_out = @buf.size.to_u32
      LibZ.deflate(pointerof(@stream), flush) # no bad return value
      @output.write(@buf.to_slice[0, @buf.size - @stream.avail_out])
      break if @stream.avail_out != 0
    end
  end
end
