///|
pub struct SizeSpec {
  target : String
  length : Int
  endian : Endian
  offset : Int
  inclusive : Bool
  /// Upstream output_format="ascii" (size.py:41-48): render the computed
  /// length as ASCII decimal digits instead of a fixed-width binary
  /// integer — HTTP Content-Length being the canonical use.
  ascii : Bool
  mutations : Array[Bytes]
}

///|
pub fn Node::size(
  name : String,
  target : String,
  length? : Int = 4,
  endian? : Endian = Little,
  offset? : Int = 0,
  inclusive? : Bool = false,
  ascii? : Bool = false,
  mutations? : Array[UInt64] = [],
  fuzzable? : Bool = true,
) -> Node raise ModelError {
  guard length == 1 || length == 2 || length == 4 || length == 8 else {
    raise Invalid("size length must be 1, 2, 4 or 8 bytes")
  }
  // ASCII width varies with the value, so inclusive self-counting (which
  // assumes the field's fixed width) cannot apply.
  guard !(ascii && inclusive) else {
    raise Invalid("ascii size cannot be inclusive")
  }
  let maximum = integer_max(length * 8)
  // With no explicit values, upstream delegates mutations to an inner
  // BitField over the full width (boofuzz/blocks/size.py:91-104, 518c139);
  // the encoded values then bypass length computation (size.py:106-115).
  // In ascii mode the same boundary values render as decimal text.
  let selected : Array[UInt64] = if !fuzzable {
    []
  } else if mutations.is_empty() {
    integer_boundaries(maximum)
  } else {
    for value in mutations {
      guard value <= maximum else {
        raise Invalid("explicit size mutation overflow")
      }
    }
    mutations
  }
  let values = selected.map(value => {
    if ascii {
      @utf8.encode(value.to_string())
    } else {
      encode_integer(value, length * 8, endian)
    }
  })
  Sized(name, {
    target,
    length,
    endian,
    offset,
    inclusive,
    ascii,
    mutations: values,
  })
}

///|
fn CompiledRequest::measure(
  self : CompiledRequest,
  index : Int,
  replacements : Replacements,
  vars : Map[String, Bytes]?,
  active? : Array[Int] = [],
) -> Int raise ModelError {
  guard self.is_visible(index, replacements, vars) else { return 0 }
  let size = match self.entries[index].kind {
    Atom(field) =>
      match replacements.lookup(index) {
        Some(value) => value.length()
        None => field.value.length()
      }
    Dynamic(field, variable) =>
      match replacements.lookup(index) {
        Some(value) => value.length()
        None =>
          match vars {
            Some(map) =>
              match map.get(variable) {
                Some(bytes) => bytes.length()
                None => raise Invalid("missing session variable: " + variable)
              }
            None => field.value.length()
          }
      }
    ComputedSize(spec) =>
      // An ascii field's width varies with its value; measure the actual
      // default/current rendering. Safe from recursion because ascii
      // sizes may not be contained in their own target.
      if spec.ascii {
        self.size_value(spec, replacements, vars).length()
      } else {
        spec.length
      }
    ComputedChecksum(spec) => spec.algorithm.length()
    Mirror(target) => {
      guard !active.contains(index) else { return 0 }
      let nested = active.copy()
      nested.push(index)
      self.measure(self.resolve(target), replacements, vars, active=nested)
    }
    Repetition(path, _, _, _, variable) => {
      let n = match replacements.lookup(index) {
        Some(bytes) => decode_repeat(bytes)
        None =>
          match variable {
            Some(name) =>
              match vars {
                Some(map) =>
                  match map.get(name) {
                    Some(bytes) => {
                      guard bytes.length() >= 4 else {
                        raise Invalid("repeat variable needs 4 bytes")
                      }
                      decode_repeat(bytes)
                    }
                    None => raise Invalid("missing session variable: " + name)
                  }
                None => 0
              }
            None => 0
          }
      }
      let child = self.measure(self.resolve(path), replacements, vars)
      guard child == 0 || n <= self.max_bytes / child else {
        raise Limit("repeated length exceeds byte limit")
      }
      child * n
    }
    Container(children) => {
      let mut total = 0
      for child in children {
        let n = self.measure(child, replacements, vars)
        guard n <= self.max_bytes - total else {
          raise Limit("measured request exceeds byte limit")
        }
        total += n
      }
      if self.entries[index].alignment is Some((modulus, _)) {
        let padding = modulus - total % modulus
        guard padding <= self.max_bytes - total else {
          raise Limit("aligned length exceeds byte limit")
        }
        total += padding
      }
      total
    }
  }
  guard size <= self.max_bytes else {
    raise Limit("field length exceeds byte limit")
  }
  size
}

///|
fn CompiledRequest::size_value(
  self : CompiledRequest,
  spec : SizeSpec,
  replacements : Replacements,
  vars : Map[String, Bytes]?,
) -> Bytes raise ModelError {
  let length = self
    .measure(self.resolve(spec.target), replacements, vars)
    .to_int64() +
    spec.offset.to_int64() +
    (if spec.inclusive { spec.length.to_int64() } else { 0L })
  guard length >= 0L else { raise Invalid("negative computed size") }
  let value = length.reinterpret_as_uint64()
  guard value <= integer_max(spec.length * 8) else {
    raise Invalid("computed size encoding overflow")
  }
  // Upstream output_format="ascii": the length becomes decimal text.
  if spec.ascii {
    @utf8.encode(length.to_string())
  } else {
    encode_integer(value, spec.length * 8, spec.endian)
  }
}

///|
fn CompiledRequest::size_dependencies(
  self : CompiledRequest,
  index : Int,
  output : Array[Int],
  visited : Array[Bool],
) -> Unit raise ModelError {
  if visited[index] {
    return
  }
  visited[index] = true
  match self.entries[index].kind {
    ComputedSize(_) => output.push(index)
    Container(children) =>
      for child in children {
        self.size_dependencies(child, output, visited)
      }
    Repetition(path, _, _, _, _) =>
      self.size_dependencies(self.resolve(path), output, visited)
    Dynamic(_, _) | Atom(_) | ComputedChecksum(_) | Mirror(_) => ()
  }
}

///|
fn visit_dependencies(
  index : Int,
  edges : Array[Array[Int]],
  states : Array[Int],
  depth : Int,
) -> Unit raise ModelError {
  guard depth <= 256 else {
    raise Invalid("derived dependency depth exceeds 256")
  }
  if states[index] == 2 {
    return
  }
  guard states[index] != 1 else {
    raise Invalid("derived field dependency cycle")
  }
  states[index] = 1
  for target in edges[index] {
    visit_dependencies(target, edges, states, depth + 1)
  }
  states[index] = 2
}

///|
fn CompiledRequest::validate_sizes(
  self : CompiledRequest,
) -> Unit raise ModelError {
  let edges : Array[Array[Int]] = Array::makei(self.entries.length(), _ => [])
  for i, entry in self.entries {
    if entry.kind is ComputedSize(spec) {
      let target = self.resolve(spec.target)
      guard self.entries[target].kind is Container(_) else {
        raise Invalid("size target must be a block")
      }
      let dependencies : Array[Int] = []
      self.size_dependencies(
        target,
        dependencies,
        Array::make(self.entries.length(), false),
      )
      for dependency in dependencies {
        if dependency != i {
          edges[i].push(dependency)
        } else {
          // Self-containing binary sizes measure their fixed width; an
          // ascii size's width varies with its value, so containing it in
          // its own target is ambiguous and rejected.
          guard !spec.ascii else {
            raise Invalid("ascii size must not be contained in its target")
          }
          guard entry.path.has_prefix(spec.target + ".") else {
            raise Invalid("indirect self size dependency")
          }
        }
      }
    }
  }
  let states = Array::make(self.entries.length(), 0)
  for i in 0..