///|
fn predefined_sequence_entry(
  symbol_type : Int,
  state : Int,
) -> (Int, Int, Int, Int) raise ZstdError {
  if symbol_type == sequence_symbol_literal_length {
    ll_predefined_entry(state)
  } else if symbol_type == sequence_symbol_offset {
    of_predefined_entry(state)
  } else if symbol_type == sequence_symbol_match_length {
    ml_predefined_entry(state)
  } else {
    raise CorruptionDetected
  }
}

///|
fn predefined_state_limit(symbol_type : Int) -> Int raise ZstdError {
  if symbol_type == sequence_symbol_literal_length ||
    symbol_type == sequence_symbol_match_length {
    63
  } else if symbol_type == sequence_symbol_offset {
    31
  } else {
    raise CorruptionDetected
  }
}

///|
fn try_choose_predefined_state(
  symbol_type : Int,
  value : Int,
) -> (Bool, Int, UInt, Int) raise ZstdError {
  if value < 0 {
    return (false, 0, 0, 0)
  }
  let mut found = false
  let mut best_state = 0
  let mut best_extra : UInt = 0
  let mut best_nb_add_bits = 0

  let state_limit = predefined_state_limit(symbol_type)
  let mut state = 0
  while state <= state_limit {
    let (_, nb_add_bits, _, base) = predefined_sequence_entry(
      symbol_type, state,
    )
    let can_use = if symbol_type == sequence_symbol_offset {
      // Prefer direct-offset states and avoid repcode-special states in encode.
      nb_add_bits > 1
    } else {
      true
    }
    if can_use && value >= base {
      let span = if nb_add_bits == 0 {
        0
      } else {
        (((1 : UInt64) << nb_add_bits) - (1 : UInt64)).to_int()
      }
      let limit = base + span
      if value <= limit &&
        (
          !found ||
          nb_add_bits < best_nb_add_bits ||
          (nb_add_bits == best_nb_add_bits && state < best_state)
        ) {
        found = true
        best_state = state
        best_extra = (value - base).reinterpret_as_uint()
        best_nb_add_bits = nb_add_bits
      }
    }
    state = state + 1
  }
  if found {
    (true, best_state, best_extra, best_nb_add_bits)
  } else {
    (false, 0, 0, 0)
  }
}

///|
fn try_choose_predefined_offset_state_with_repcodes(
  value : Int,
  ll : Int,
  rep1 : Int,
  rep2 : Int,
  rep3 : Int,
) -> (Bool, Int, UInt, Int) raise ZstdError {
  if value <= 0 {
    return (false, 0, 0, 0)
  }
  let states : Array[Int] = Array::new()
  let extras : Array[UInt] = Array::new()
  let nb_add_bits : Array[Int] = Array::new()
  let next_states : Array[Int] = Array::new()
  let nb_state_bits : Array[Int] = Array::new()
  let out_r1 : Array[Int] = Array::new()
  let out_r2 : Array[Int] = Array::new()
  let out_r3 : Array[Int] = Array::new()
  collect_predefined_offset_candidates_for_context(
    value,
    ll == 0,
    rep1,
    rep2,
    rep3,
    states,
    extras,
    nb_add_bits,
    next_states,
    nb_state_bits,
    out_r1,
    out_r2,
    out_r3,
  )
  if states.length() == 0 {
    return (false, 0, 0, 0)
  }
  let mut best_idx = 0
  let mut i = 1
  while i < states.length() {
    if nb_add_bits[i] < nb_add_bits[best_idx] ||
      (nb_add_bits[i] == nb_add_bits[best_idx] && states[i] < states[best_idx]) {
      best_idx = i
    }
    i = i + 1
  }
  (true, states[best_idx], extras[best_idx], nb_add_bits[best_idx])
}

///|
fn build_single_match_payload_predefined(
  src : Bytes,
  start : Int,
  block_len : Int,
  ll : Int,
  offset : Int,
  ml : Int,
  max_offset : Int,
  rep1 : Int,
  rep2 : Int,
  rep3 : Int,
) -> Bytes raise ZstdError {
  if ll < 0 ||
    offset <= 0 ||
    ml < 3 ||
    ll + ml > block_len ||
    offset > max_offset {
    return b""
  }
  let (ll_ok, ll_state, ll_extra, ll_bits) = try_choose_predefined_state(
    sequence_symbol_literal_length, ll,
  )
  if !ll_ok {
    return b""
  }
  let (off_ok, off_state, off_extra, off_bits) = try_choose_predefined_offset_state_with_repcodes(
    offset, ll, rep1, rep2, rep3,
  )
  if !off_ok {
    return b""
  }
  let (ml_ok, ml_state, ml_extra, ml_bits) = try_choose_predefined_state(
    sequence_symbol_match_length, ml,
  )
  if !ml_ok {
    return b""
  }
  let literals = build_single_match_literals(src, start, block_len, ll, ml)

  let extra_bits : Array[Int] = Array::new()
  append_bits_be(
    extra_bits,
    ll_state.reinterpret_as_uint(),
    ll_predefined_table_log,
  )
  append_bits_be(
    extra_bits,
    off_state.reinterpret_as_uint(),
    of_predefined_table_log,
  )
  append_bits_be(
    extra_bits,
    ml_state.reinterpret_as_uint(),
    ml_predefined_table_log,
  )
  append_bits_be(extra_bits, off_extra, off_bits)
  append_bits_be(extra_bits, ml_extra, ml_bits)
  append_bits_be(extra_bits, ll_extra, ll_bits)
  let bitstream = build_reverse_bitstream(extra_bits)

  let payload : Array[Byte] = Array::new()
  append_best_literals_section(payload, literals)
  append_sequence_count(payload, 1)
  payload.push((0 : UInt).to_byte()) // all predefined sequence modes
  append_bytes(payload, bitstream, 0, bitstream.length())
  Bytes::from_array(payload)
}