///|
priv struct EncodedModuleSizeState {
  funcs : Array[Func]
  sizes : Array[Int32]
  body_sizes : Array[Int32]
  local_sizes : Array[Int32]
  reuse : EncodedModuleSizeState?
  mut strings : Array[Bytes]
  mut code_size : UInt64
  mut data_size : UInt64
}

///|
fn EncodedModuleSizeState::new(
  reuse : EncodedModuleSizeState?,
) -> EncodedModuleSizeState {
  {
    funcs: [],
    sizes: [],
    body_sizes: [],
    local_sizes: [],
    reuse,
    strings: [],
    code_size: 0,
    data_size: 0,
  }
}

///|
// A u32 encoded as signed33 has width1..5. Five is an unsupported-shape
// sentinel, outside every possible width delta, and needs no optional box.
fn encoded_blocktype_remap_delta(
  before : BlockType,
  after : BlockType,
) -> Int32 {
  if before == after {
    return 0
  }
  match (before, after) {
    (TypeIdxBlockType(TypeIdx(a)), TypeIdxBlockType(TypeIdx(b))) =>
      size_signed(b.to_int64(), 33).unwrap() -
      size_signed(a.to_int64(), 33).unwrap()
    _ => 5
  }
}

///|
// Only local and supported type indices may change encoded width. All other
// immediates and control framing must match. The explicit worklist preserves
// the encoder's deep-control behavior without introducing recursive calls.
// String-pool agreement is checked before reusing a measured expression.
fn encoded_local_remap_size_delta(
  before : Array[Instruction],
  after : Array[Instruction],
) -> Int32? {
  let work = [(before, after)]
  let mut delta = 0
  while work.pop() is Some((left, right)) {
    if physical_equal(left, right) {
      continue
    }
    if left.length() != right.length() {
      return None
    }
    for index = 0; index < left.length(); index = index + 1 {
      let old = left[index]
      let next = right[index]
      if physical_equal(old, next) {
        continue
      }
      match (old, next) {
        (LocalGet(LocalIdx(a)), LocalGet(LocalIdx(b)))
        | (LocalSet(LocalIdx(a)), LocalSet(LocalIdx(b)))
        | (LocalTee(LocalIdx(a)), LocalTee(LocalIdx(b))) =>
          delta += size_unsigned(b.to_uint64(), 32).unwrap() -
            size_unsigned(a.to_uint64(), 32).unwrap()
        (CallIndirect(TypeIdx(a), ta), CallIndirect(TypeIdx(b), tb))
        | (
          ReturnCallIndirect(TypeIdx(a), ta),
          ReturnCallIndirect(TypeIdx(b), tb),
        ) if ta == tb =>
          delta += size_unsigned(b.to_uint64(), 32).unwrap() -
            size_unsigned(a.to_uint64(), 32).unwrap()
        (CallRef(TypeIdx(a)), CallRef(TypeIdx(b)))
        | (ReturnCallRef(TypeIdx(a)), ReturnCallRef(TypeIdx(b))) =>
          delta += size_unsigned(b.to_uint64(), 32).unwrap() -
            size_unsigned(a.to_uint64(), 32).unwrap()
        (Block(a, Expr(xs)), Block(b, Expr(ys)))
        | (Loop(a, Expr(xs)), Loop(b, Expr(ys))) => {
          let change = encoded_blocktype_remap_delta(a, b)
          if change == 5 {
            return None
          }
          delta += change
          work.push((xs, ys))
        }
        (TryTable(a, ac, Expr(xs)), TryTable(b, bc, Expr(ys))) if ac == bc => {
          let change = encoded_blocktype_remap_delta(a, b)
          if change == 5 {
            return None
          }
          delta += change
          work.push((xs, ys))
        }
        (If(a, ax, ae), If(b, bx, be)) => {
          let change = encoded_blocktype_remap_delta(a, b)
          if change == 5 {
            return None
          }
          delta += change
          work.push((ax, bx))
          match (ae, be) {
            (None, None) => ()
            (Some(xs), Some(ys)) => work.push((xs, ys))
            _ => return None
          }
        }
        (Block(_, _), _)
        | (Loop(_, _), _)
        | (If(_, _, _), _)
        | (TryTable(_, _, _), _)
        | (Try(_, _, _, _), _) => return None
        _ => if old != next { return None }
      }
    }
  }
  Some(delta)
}

///|
fn measure_module_code(
  funcs : Array[Func],
  strings : Array[Bytes],
  state : EncodedModuleSizeState,
  body_buffers~ : Array[Int32]? = None,
  expression_encodes~ : Array[Int32]? = None,
) -> Result[Unit, BinaryEncodeError] {
  state.strings = strings
  let can_reuse = match state.reuse {
    Some(previous) => previous.strings == strings
    None => false
  }
  let mut payload = match size_unsigned(funcs.length().to_uint64(), 32) {
    Ok(size) => size.to_uint64()
    Err(error) => return Err(error)
  }
  for index = 0; index < funcs.length(); index = index + 1 {
    let func = funcs[index]
    let mut body_size = 0
    let mut local_size = 0
    let cached = match state.reuse {
      Some(previous) if can_reuse &&
        index < previous.funcs.length() &&
        physical_equal(func, previous.funcs[index]) => {
        body_size = previous.body_sizes[index]
        local_size = previous.local_sizes[index]
        Some(previous.sizes[index])
      }
      _ => None
    }
    let size = match cached {
      Some(size) => size
      None => {
        match body_buffers {
          Some(count) => count[0] += 1
          None => ()
        }
        let buffer = @buffer.new()
        let Func(locals, expr) = func
        let Expr(instrs) = expr
        if Encode::encode(locals, buffer) is Err(error) {
          return Err(error)
        }
        local_size = buffer.length()
        let reused_body = match state.reuse {
          Some(previous) if can_reuse && index < previous.funcs.length() => {
            let Func(_, Expr(old_instrs)) = previous.funcs[index]
            match encoded_local_remap_size_delta(old_instrs, instrs) {
              Some(delta) =>
                Some(
                  previous.body_sizes[index] -
                  previous.local_sizes[index] +
                  local_size +
                  delta,
                )
              None => None
            }
          }
          _ => None
        }
        body_size = match reused_body {
          Some(size) => size
          None => {
            if expression_encodes is Some(count) {
              count[0] += 1
            }
            if Encode::encode(expr, buffer) is Err(error) {
              return Err(error)
            }
            buffer.length()
          }
        }
        let prefix = match size_unsigned(body_size.to_uint64(), 32) {
          Ok(size) => size
          Err(error) => return Err(error)
        }
        prefix + body_size
      }
    }
    state.funcs.push(func)
    state.sizes.push(size)
    state.body_sizes.push(body_size)
    state.local_sizes.push(local_size)
    payload += size.to_uint64()
  }
  let prefix = match size_unsigned(payload, 32) {
    Ok(size) => size
    Err(error) => return Err(error)
  }
  state.code_size = 1UL + prefix.to_uint64() + payload
  Ok(())
}

///|
// Use the same mode/offset and ULEB encoders as Data::encode, but retain only
// metadata bytes. Immutable payload bytes contribute their length directly.
// This runs inside the module's stringref context, in ordinary section order.
fn measure_module_data(
  section : DataSec?,
  state : EncodedModuleSizeState,
) -> Result[Unit, BinaryEncodeError] {
  let segments = match section {
    Some(DataSec(segments)) => segments
    None => return Ok(())
  }
  let metadata = @buffer.new()
  if Encode::encode(U32(segments.length().reinterpret_as_uint()), metadata)
    is Err(err) {
    return Err(err)
  }
  let mut payload_bytes = 0UL
  for Data(mode, bytes) in segments {
    if encode_data_mode(mode, metadata) is Err(err) {
      return Err(err)
    }
    if Encode::encode(U32(bytes.length().reinterpret_as_uint()), metadata)
      is Err(err) {
      return Err(err)
    }
    payload_bytes += bytes.length().to_uint64()
  }
  let payload_size = metadata.length().to_uint64() + payload_bytes
  let prefix = match size_unsigned(payload_size, 32) {
    Ok(prefix) => prefix
    Err(err) => return Err(err)
  }
  state.data_size = 1UL + prefix.to_uint64() + payload_size
  Ok(())
}

///|
fn encoded_module_measured_size(
  buffer : @buffer.Buffer,
  state : EncodedModuleSizeState,
) -> Result[Int32, BinaryEncodeError] {
  let size = buffer.length().to_uint64() + state.code_size + state.data_size
  // Public sizes use Int. Reject values outside that representation instead
  // of wrapping a valid u32 section length into a negative module size.
  if size > 0x7FFF_FFFFUL {
    return Err(BinaryEncodeError::UnsignedLebValueOutOfRange)
  }
  Ok(size.to_int())
}

///|
/// Return exact canonical binary sizes without assembling either code section.
/// Data payloads contribute lengths without being copied into size buffers.
/// Shared function bodies are measured once when their stringref pools agree.
/// Local and supported type-index rewrites reuse exact expression LEB deltas.
/// Other rewrites retain complete body encoding.
/// Reuse is scoped to this call: mutable module data never enters a durable cache.
/// The result includes names, compiler facts, section framing, and body framing.
pub fn encoded_module_sizes(
  before : Module,
  after : Module,
) -> Result[(Int32, Int32), EncodeError] {
  let first = EncodedModuleSizeState::new(None)
  let first_buffer = @buffer.new()
  match
    encode_module_sections(before, first_buffer, false, size_state=Some(first)) {
    Err(error) => return Err(EncodeError::Encode(error))
    Ok(_) => ()
  }
  let first_size = match encoded_module_measured_size(first_buffer, first) {
    Ok(size) => size
    Err(error) => return Err(EncodeError::Encode(error))
  }
  let second = EncodedModuleSizeState::new(Some(first))
  let second_buffer = @buffer.new()
  match
    encode_module_sections(after, second_buffer, false, size_state=Some(second)) {
    Err(error) => Err(EncodeError::Encode(error))
    Ok(_) =>
      match encoded_module_measured_size(second_buffer, second) {
        Ok(second_size) => Ok((first_size, second_size))
        Err(error) => Err(EncodeError::Encode(error))
      }
  }
}