///|
/// Incremental Brotli decompressor.
///
/// Input chunks are decoded as soon as complete Brotli meta-blocks are
/// available. Produced chunks are emitted through `ondata`.
pub(all) struct UnbrotliStream {
  /// Called with decompressed data as complete meta-blocks are decoded.
  mut ondata : @common.FbrStreamHandler?
  priv opts : UnbrotliOptions
  priv mut input : FixedArray[Byte]
  priv mut input_seen : Int
  priv mut reader : BrotliBitReader?
  priv mut output : BrotliOutputBuilder
  priv mut state : BrotliDecoderState?
  priv mut emitted_len : Int
  priv mut header_read : Bool
  priv mut finished : Bool
}

///|
/// Create a Brotli decompression stream.
pub fn UnbrotliStream::new(
  opts? : UnbrotliOptions = UnbrotliOptions::default(),
) -> UnbrotliStream {
  let stream_opts = {
    out: None,
    max_output_size: opts.max_output_size,
    max_input_size: opts.max_input_size,
  }
  {
    ondata: None,
    opts: stream_opts,
    input: FixedArray::make(0, b'\x00'),
    input_seen: 0,
    reader: None,
    output: BrotliOutputBuilder::new(stream_opts),
    state: None,
    emitted_len: 0,
    header_read: false,
    finished: false,
  }
}

///|
/// Set the output callback for a Brotli decompression stream.
pub fn UnbrotliStream::set_ondata(
  self : UnbrotliStream,
  handler : (FixedArray[Byte], Bool) -> Unit,
) -> Unit {
  self.ondata = Some(FbrStreamHandler(handler))
}

///|
/// Push one input chunk into the Brotli decompression stream.
pub fn UnbrotliStream::push(
  self : UnbrotliStream,
  chunk : FixedArray[Byte],
  final_? : Bool = false,
) -> Unit raise @common.FbrError {
  if self.finished {
    if chunk.length() > 0 {
      raise @common.fbr_err(
        InvalidZipData,
        msg="input after Brotli stream finished",
      )
    }
    return
  }
  if chunk.length() > self.opts.max_input_size - self.input_seen {
    raise @common.fbr_err(InvalidZipData, msg="input exceeds max_input_size")
  }
  self.input_seen += chunk.length()
  self.append_input(chunk)
  self.process_available(final_)
}

///|
fn UnbrotliStream::append_input(
  self : UnbrotliStream,
  chunk : FixedArray[Byte],
) -> Unit {
  if chunk.length() == 0 {
    return
  }
  let combined = FixedArray::make(self.input.length() + chunk.length(), b'\x00')
  if self.input.length() > 0 {
    self.input.blit_to(
      combined,
      len=self.input.length(),
      src_offset=0,
      dst_offset=0,
    )
  }
  chunk.blit_to(
    combined,
    len=chunk.length(),
    src_offset=0,
    dst_offset=self.input.length(),
  )
  self.input = combined
  match self.reader {
    Some(reader) => self.reader = Some(reader.with_buffer(self.input))
    None => ()
  }
}

///|
fn UnbrotliStream::process_available(
  self : UnbrotliStream,
  final_ : Bool,
) -> Unit raise @common.FbrError {
  if !self.header_read {
    let reader = BrotliBitReader::new(self.input, 0, self.input.length())
    try brotli_decode_window_bits(reader) catch {
      FbrError(code=UnexpectedEOF, ..) =>
        if final_ {
          raise @common.fbr_err(UnexpectedEOF)
        } else {
          return
        }
      e => raise e
    } noraise {
      window_bits => {
        brotli_validate_window_bits(window_bits)
        self.header_read = true
        self.reader = Some(reader)
        self.state = Some(BrotliDecoderState::new_with_window_bits(window_bits))
        self.compact_input()
      }
    }
  }
  while !self.finished {
    guard self.reader is Some(reader) else { return }
    guard self.state is Some(state) else { return }
    let trial_reader = reader.clone()
    let trial_output = self.output.clone()
    let trial_state = state.clone()
    try
      brotli_decode_next_metablock(trial_reader, trial_output, trial_state)
    catch {
      FbrError(code=UnexpectedEOF, ..) =>
        if final_ {
          raise @common.fbr_err(UnexpectedEOF)
        } else {
          return
        }
      e => raise e
    } noraise {
      done => {
        let produced = trial_output.len > self.emitted_len
        self.reader = Some(trial_reader)
        self.output = trial_output
        self.state = Some(trial_state)
        self.emit_new_output(done)
        self.compact_input()
        if done {
          self.finished = true
          if !produced {
            self.emit_empty_final()
          }
          return
        }
      }
    }
  }
}

///|
fn UnbrotliStream::emit_new_output(
  self : UnbrotliStream,
  final_ : Bool,
) -> Unit {
  let length = self.output.len - self.emitted_len
  if length <= 0 {
    return
  }
  let chunk = FixedArray::make(length, b'\x00')
  self.output.buf.blit_to(
    chunk,
    len=length,
    src_offset=self.emitted_len,
    dst_offset=0,
  )
  self.emitted_len = self.output.len
  match self.ondata {
    Some(h) => @common.call_handler(h, chunk, final_)
    None => ()
  }
}

///|
fn UnbrotliStream::emit_empty_final(self : UnbrotliStream) -> Unit {
  match self.ondata {
    Some(h) => @common.call_handler(h, FixedArray::make(0, b'\x00'), true)
    None => ()
  }
}

///|
fn UnbrotliStream::compact_input(self : UnbrotliStream) -> Unit {
  guard self.reader is Some(reader) else { return }
  if reader.byte_pos == 0 ||
    (reader.byte_pos < self.input.length() / 2 && reader.byte_pos < 65536) {
    return
  }
  let remaining = self.input.length() - reader.byte_pos
  let compacted = FixedArray::make(remaining, b'\x00')
  if remaining > 0 {
    self.input.blit_to(
      compacted,
      len=remaining,
      src_offset=reader.byte_pos,
      dst_offset=0,
    )
  }
  self.input = compacted
  // Rebase the reader onto the compacted buffer (consumed bytes dropped, so
  // byte_pos restarts at 0) while preserving the partially-filled bit
  // accumulator, whose bits came from the bytes we just discarded.
  let rebased = BrotliBitReader::new(self.input, 0, self.input.length())
  rebased.bit_buf = reader.bit_buf
  rebased.bits_avail = reader.bits_avail
  self.reader = Some(rebased)
}