///|
priv struct LengthPieces {
  separator_length : Int32
  limit : Int32
  mut used : Int32
  mut count : Int32
}

///|
fn LengthPieces::new(separator_length : Int32, limit : Int32) -> LengthPieces {
  { separator_length, limit, used: 0, count: 0, }
}

///|
fn LengthPieces::push(
  self : LengthPieces,
  length : Int32,
) -> Result[Unit, ExpandError] {
  let separator = if self.count == 0 { 0 } else { self.separator_length }
  if length < 0 ||
    self.used > self.limit - separator ||
    length > self.limit - separator - self.used {
    return Err(OutputTooLong)
  }
  self.used += separator + length
  self.count += 1
  Ok(())
}

///|
fn measured_named_length(
  op : Operator,
  name : String,
  value_length : Int32,
  limit : Int32,
) -> Result[Int32, ExpandError] {
  if !operator_named(op) {
    return Ok(value_length)
  }
  let overhead = if value_length == 0 && !operator_form(op) {
    name.length()
  } else {
    name.length() + 1
  }
  if overhead > limit || value_length > limit - overhead {
    Err(OutputTooLong)
  } else {
    Ok(overhead + value_length)
  }
}

///|
fn measured_pair_length(
  op : Operator,
  key_length : Int32,
  value_length : Int32,
  limit : Int32,
) -> Result[Int32, ExpandError] {
  let equal_sign = if value_length == 0 && !operator_form(op) { 0 } else { 1 }
  if key_length > limit - equal_sign ||
    value_length > limit - equal_sign - key_length {
    Err(OutputTooLong)
  } else {
    Ok(key_length + equal_sign + value_length)
  }
}

///|
fn measure_variable(
  op : Operator,
  spec : VarSpec,
  value : Value,
  pieces : LengthPieces,
) -> Result[Unit, ExpandError] {
  let reserved = operator_allows_reserved(op)
  let limit = pieces.limit
  match value {
    Scalar(raw) => {
      let selected = match spec.prefix {
        None => raw
        Some(length) => scalar_prefix(raw, length)
      }
      let encoded = match measure_value_encoding(selected, reserved, limit) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      let length = match measured_named_length(op, spec.name, encoded, limit) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      pieces.push(length)
    }
    List(items) => {
      if spec.prefix is Some(_) {
        return Err(InvalidValue(spec.name))
      }
      if spec.explode {
        for item in items {
          let encoded = match measure_value_encoding(item, reserved, limit) {
            Ok(value) => value
            Err(error) => return Err(error)
          }
          let length = match
            measured_named_length(op, spec.name, encoded, limit) {
            Ok(value) => value
            Err(error) => return Err(error)
          }
          match pieces.push(length) {
            Ok(_) => ()
            Err(error) => return Err(error)
          }
        }
        Ok(())
      } else if !items.is_empty() {
        let values = LengthPieces::new(1, limit)
        for item in items {
          let encoded = match measure_value_encoding(item, reserved, limit) {
            Ok(value) => value
            Err(error) => return Err(error)
          }
          match values.push(encoded) {
            Ok(_) => ()
            Err(error) => return Err(error)
          }
        }
        let length = match
          measured_named_length(op, spec.name, values.used, limit) {
          Ok(value) => value
          Err(error) => return Err(error)
        }
        pieces.push(length)
      } else {
        Ok(())
      }
    }
    Assoc(entries) => {
      if spec.prefix is Some(_) {
        return Err(InvalidValue(spec.name))
      }
      if spec.explode {
        for entry in entries {
          let (name, item) = entry
          let key = match measure_value_encoding(name, reserved, limit) {
            Ok(value) => value
            Err(error) => return Err(error)
          }
          let encoded = match measure_value_encoding(item, reserved, limit) {
            Ok(value) => value
            Err(error) => return Err(error)
          }
          let length = match measured_pair_length(op, key, encoded, limit) {
            Ok(value) => value
            Err(error) => return Err(error)
          }
          match pieces.push(length) {
            Ok(_) => ()
            Err(error) => return Err(error)
          }
        }
        Ok(())
      } else if !entries.is_empty() {
        let values = LengthPieces::new(1, limit)
        for entry in entries {
          let (name, item) = entry
          let key = match measure_value_encoding(name, reserved, limit) {
            Ok(value) => value
            Err(error) => return Err(error)
          }
          let encoded = match measure_value_encoding(item, reserved, limit) {
            Ok(value) => value
            Err(error) => return Err(error)
          }
          match values.push(key) {
            Ok(_) => ()
            Err(error) => return Err(error)
          }
          match values.push(encoded) {
            Ok(_) => ()
            Err(error) => return Err(error)
          }
        }
        let length = match
          measured_named_length(op, spec.name, values.used, limit) {
          Ok(value) => value
          Err(error) => return Err(error)
        }
        pieces.push(length)
      } else {
        Ok(())
      }
    }
    SparseList(items) => {
      let defined : Array[String] = []
      for item in items {
        match item {
          Some(text) => defined.push(text)
          None => ()
        }
      }
      measure_variable(op, spec, List(defined), pieces)
    }
    SparseAssoc(entries) => {
      let defined : Array[(String, String)] = []
      for entry in entries {
        let (name, item) = entry
        match item {
          Some(text) => defined.push((name, text))
          None => ()
        }
      }
      measure_variable(op, spec, Assoc(defined), pieces)
    }
  }
}

///|
fn measure_expression(
  op : Operator,
  specs : Array[VarSpec],
  values : Map[String, Value],
  limit : Int32,
) -> Result[Int32, ExpandError] {
  let prefix_length = operator_prefix(op).length()
  let pieces = LengthPieces::new(
    operator_separator(op).length(),
    limit - prefix_length,
  )
  for spec in specs {
    match values.get(spec.name) {
      None => ()
      Some(value) =>
        match measure_variable(op, spec, value, pieces) {
          Ok(_) => ()
          Err(error) => return Err(error)
        }
    }
  }
  if pieces.count == 0 {
    Ok(0)
  } else {
    Ok(prefix_length + pieces.used)
  }
}

///|
/// Return the exact UTF-16 length of the expanded URI without constructing
/// its output string. This performs the same binding checks as `expand`.
pub fn Template::measure(
  self : Template,
  bindings : Array[(String, Value)],
  max_output~ : Int32 = 1048576,
  max_bindings~ : Int32 = 4096,
  max_value_length~ : Int32 = 1048576,
  max_items~ : Int32 = 4096,
) -> Result[Int32, ExpandError] {
  if max_output < 0 {
    return Err(OutputTooLong)
  }
  if max_value_length < 0 {
    return Err(InvalidLimit("max_value_length"))
  }
  if max_items < 0 {
    return Err(InvalidLimit("max_items"))
  }
  let values = match
    prepare_bindings(bindings, max_bindings, max_value_length, max_items) {
    Ok(value) => value
    Err(error) => return Err(error)
  }
  let mut used = 0
  for segment in self.segments {
    let length = match segment {
      Literal(raw) =>
        match measure_literal_encoding(raw, max_output - used) {
          Ok(value) => value
          Err(error) => return Err(error)
        }
      Expression(op, specs, _, _) =>
        match measure_expression(op, specs, values, max_output - used) {
          Ok(value) => value
          Err(error) => return Err(error)
        }
    }
    if length > max_output - used {
      return Err(OutputTooLong)
    }
    used += length
  }
  Ok(used)
}