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

///|
fn EncodedModuleSizeState::new(
  reuse : EncodedModuleSizeState?,
) -> EncodedModuleSizeState {
  {
    funcs: [],
    sizes: [],
    body_sizes: [],
    local_sizes: [],
    reuse,
    strings: [],
    code_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) -> Int {
  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],
) -> Int? {
  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[Int]? = None,
  expression_encodes? : Array[Int]? = 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
    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
  }
  let prefix = match size_unsigned(payload.to_uint64(), 32) {
    Ok(size) => size
    Err(error) => return Err(error)
  }
  state.code_size = 1 + prefix + payload
  Ok(())
}

///|
/// Return exact canonical binary sizes without assembling either code section.
/// 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[(Int, Int), 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 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(_) =>
      Ok(
        (
          first_buffer.length() + first.code_size,
          second_buffer.length() + second.code_size,
        ),
      )
  }
}