// One-shot DEFLATE compression: the whole input in, a complete raw DEFLATE
// stream out. The counterpart to the streaming `Deflater` (deflate.mbt) for
// when all input is already in hand.

///|
/// A reusable, whole-buffer raw DEFLATE compressor. Unlike the suspendable
/// `Deflater`, it reads the input directly and writes one complete stream into
/// either a caller-provided buffer or reusable growable output storage.
pub struct Compressor {
  priv level : Int
  priv fast_store : Bool
  priv cfg : LevelConfig
  priv finder : MatchFinder
  priv tokens : Array[Int]
  priv mut output : FixedArray[Byte]
  priv ll_freq : FixedArray[Int]
  priv d_freq : FixedArray[Int]
}

///|
/// Create a reusable whole-buffer compressor. `level` 0-9 is clamped, with
/// level 0 selecting stored blocks only. `fast_store` defaults to false; when
/// enabled, sampled high-diversity inputs may bypass compression and use stored
/// blocks. It trades compression ratio for speed without changing the data.
pub fn Compressor::Compressor(
  level? : Int = 6,
  fast_store? : Bool = false,
) -> Compressor {
  let level = clamp_level(level)
  {
    level,
    fast_store,
    cfg: level_configs[level],
    finder: MatchFinder(input_size=0),
    tokens: [],
    output: [],
    ll_freq: FixedArray::make(MAX_NUM_LIT, 0),
    d_freq: FixedArray::make(MAX_NUM_DIST, 0),
  }
}

///|
/// A conservative output capacity for `deflate_into` or
/// `Compressor::compress_into`. The returned value covers the fixed 16 KiB
/// minimum block cadence and final bit padding for a non-negative input length.
pub fn deflate_bound(input_size : Int) -> Int {
  let input_size = input_size.max(0)
  let block_count = if input_size == 0 {
    1
  } else {
    (input_size - 1) / DEFLATE_BLOCK_SIZE + 1
  }
  input_size + 5 * block_count + 1
}

///|
/// Compress `input` directly into `output`, returning its byte length. `None`
/// means that `output` was too small; any prefix written before exhaustion is
/// unspecified. Reuse one `Compressor` and one caller-owned output buffer to
/// avoid per-stream workspace and result-buffer allocation.
pub fn Compressor::compress_into(
  self : Compressor,
  input : Bytes,
  output : FixedArray[Byte],
) -> Int? {
  self.encode(input, BitWriter::into(output))
}

///|
/// Compress a complete stream, reusing the parsing and output workspace.
/// The returned bytes are independent of subsequent calls to this compressor.
pub fn Compressor::compress(self : Compressor, input : Bytes) -> Bytes {
  let writer = BitWriter::growing(self.output)
  ignore(self.encode(input, writer))
  if writer.direct_output is Some(output) {
    self.output = output
  }
  Bytes::from_array(self.output[:writer.direct_pos])
}

///|
fn Compressor::encode(self : Compressor, input : Bytes, w : BitWriter) -> Int? {
  let n = input.length()
  // Level 0 or an explicit heuristic decision bypasses parsing entirely.
  guard self.level != 0 && !(self.fast_store && should_fast_store(input)) else {
    write_stored(w, input, 0, n, is_final=true)
    w.flush()
    if w.overflowed {
      None
    } else {
      Some(w.direct_pos)
    }
  }
  self.finder.reset_for_input(input)
  // The default level amortizes block construction over 65535 bytes; a
  // boundary match may overhang, so `processed` (not `target`) sets the next
  // start.
  // Whole-buffer callers can amortize one Huffman header over the largest
  // legal DEFLATE block; the streaming encoder keeps its smaller cadence.
  let block_size = 65535
  let mut pos = 0
  for ;; {
    let target = (pos + block_size).min(n)
    self.tokens.clear()
    self.ll_freq.fill(0)
    self.d_freq.fill(0)
    let processed = tokenize(
      input,
      self.finder,
      pos,
      target,
      self.cfg,
      self.tokens,
      self.ll_freq,
      self.d_freq,
    )
    emit_block(
      w,
      input,
      pos,
      processed,
      self.tokens[:],
      self.ll_freq,
      self.d_freq,
      is_final=processed >= n,
    )
    pos = processed
    guard pos < n else { break }
  }
  w.flush()
  if w.overflowed {
    None
  } else {
    Some(w.direct_pos)
  }
}

///|
/// Compress bytes into a complete raw DEFLATE stream held entirely in memory.
/// The whole input is tokenized directly, advancing a single offset and a
/// session-owned fixed matchfinder; unlike `Deflater`, no input staging ring
/// is needed. Opt-in `fast_store` may emit stored blocks after a heuristic
/// check; false (the default) preserves normal compression behavior.
pub fn deflate_all(
  input : Bytes,
  level? : Int = 6,
  fast_store? : Bool = false,
) -> Bytes {
  let lv = clamp_level(level)
  let w = BitWriter()
  let n = input.length()
  // Level 0 or an explicit heuristic decision bypasses parsing entirely.
  guard lv != 0 && !(fast_store && should_fast_store(input)) else {
    write_stored(w, input, 0, n, is_final=true)
    w.flush()
    return Bytes::from_array(w.out)
  }
  let cfg = level_configs[lv]
  let long_hash_bits = long_hash_bits_for_input(input)
  let finder = MatchFinder(input_size=n, long_hash_bits~)
  let block_size = 65535
  // Reuse token and frequency storage across blocks. A whole-buffer stream
  // can span several legal blocks; reallocating these fixed workspaces for
  // every block only adds GC pressure and does not improve the parse.
  let tokens : Array[Int] = []
  let ll_freq = FixedArray::make(MAX_NUM_LIT, 0)
  let d_freq = FixedArray::make(MAX_NUM_DIST, 0)
  finder.reset_for_input(input, long_hash_bits~)
  let mut pos = 0
  for ;; {
    let target = (pos + block_size).min(n)
    tokens.clear()
    ll_freq.fill(0)
    d_freq.fill(0)
    let processed = tokenize(
      input, finder, pos, target, cfg, tokens, ll_freq, d_freq,
    )
    emit_block(
      w,
      input,
      pos,
      processed,
      tokens[:],
      ll_freq,
      d_freq,
      is_final=processed >= n,
    )
    pos = processed
    guard pos < n else { break }
  }
  w.flush()
  Bytes::from_array(w.out)
}

///|
/// One-shot convenience form of `Compressor::compress_into`. It creates a
/// compressor for this call; use `Compressor` when encoding many streams.
/// Optional `fast_store` has the same opt-in speed/ratio trade-off as `deflate_all`.
pub fn deflate_into(
  input : Bytes,
  output : FixedArray[Byte],
  level? : Int = 6,
  fast_store? : Bool = false,
) -> Int? {
  Compressor(level~, fast_store~).compress_into(input, output)
}