///|
/// Limits count Unicode code points; output is ASCII URI bytes. Zero is allowed.
pub(all) struct ParseLimits {
  max_template_codepoints : Int32
  max_expressions : Int32
  max_variables_per_expression : Int32
  max_total_variables : Int32
} derive(Debug)

///|
pub fn ParseLimits::new(
  max_template_codepoints~ : Int32 = 1048576,
  max_expressions~ : Int32 = 4096,
  max_variables_per_expression~ : Int32 = 256,
  max_total_variables~ : Int32 = 65536,
) -> ParseLimits {
  {
    max_template_codepoints,
    max_expressions,
    max_variables_per_expression,
    max_total_variables,
  }
}

///|
pub(all) struct ExpansionLimits {
  max_bindings : Int32
  max_members : Int32
  max_value_codepoints : Int32
  max_total_value_codepoints : Int32
  max_output_bytes : Int32
} derive(Debug)

///|
pub fn ExpansionLimits::new(
  max_bindings~ : Int32 = 4096,
  max_members~ : Int32 = 65536,
  max_value_codepoints~ : Int32 = 1048576,
  max_total_value_codepoints~ : Int32 = 4194304,
  max_output_bytes~ : Int32 = 1048576,
) -> ExpansionLimits {
  {
    max_bindings,
    max_members,
    max_value_codepoints,
    max_total_value_codepoints,
    max_output_bytes,
  }
}

///|
fn exceeds_codepoints(text : String, limit : Int32) -> Bool {
  let mut remaining = limit
  for _ in text.iter() {
    if remaining == 0 {
      return true
    }
    remaining = remaining - 1
  }
  false
}

///|
// Return remaining total, or -1/-2 for the first individual/total overflow.
// Walk UTF-16 directly to avoid a capturing iterator and per-character options.
// Like String::iter, only a valid surrogate pair counts as one code point.
fn consume_input_text(
  text : String,
  individual : Int32,
  total : Int32,
) -> Int32 {
  let mut index = 0
  let mut left = individual
  let mut remaining = total
  while index < text.length() {
    if left == 0 {
      return -1
    }
    if remaining == 0 {
      return -2
    }
    let unit = text[index].to_int()
    if unit >= 0xD800 && unit <= 0xDBFF && index + 1 < text.length() {
      let next = text[index + 1].to_int()
      if next >= 0xDC00 && next <= 0xDFFF {
        index = index + 1
      }
    }
    index = index + 1
    left = left - 1
    remaining = remaining - 1
  }
  remaining
}

///|
fn input_text_error(remaining : Int32) -> TemplateError {
  if remaining == -1 {
    problem(
      "ValueLimit", "a name, key or value exceeds code point budget", 0, 0,
    )
  } else {
    problem("TotalValueLimit", "total input code points exceed budget", 0, 0)
  }
}

///|
fn check_input_budget(
  vars : Variables,
  limits : ExpansionLimits,
) -> Result[Int32, TemplateError] {
  check_input_entries(vars.values.length(), vars.values.iter2(), limits)
}

///|
fn check_input_entries(
  binding_count : Int32,
  entries : Iter2[String, Value],
  limits : ExpansionLimits,
) -> Result[Int32, TemplateError] {
  if limits.max_bindings < 0 ||
    limits.max_members < 0 ||
    limits.max_value_codepoints < 0 ||
    limits.max_total_value_codepoints < 0 ||
    limits.max_output_bytes < 0 {
    return Err(problem("InvalidLimit", "limits must be nonnegative", 0, 0))
  }
  if binding_count > limits.max_bindings {
    return Err(problem("BindingLimit", "binding count exceeds budget", 0, 0))
  }
  let mut members = limits.max_members
  let mut total = limits.max_total_value_codepoints
  // Includes unused bindings, binding names, associative keys and values.
  for name, value in entries {
    // Collection count still takes precedence over its name/value lengths.
    let count = match value {
      Scalar(_) => 0
      List(items) => items.length()
      Assoc(entries) => entries.entries.length()
    }
    if count > members {
      return Err(
        problem("MemberLimit", "total collection members exceed budget", 0, 0),
      )
    }
    members = members - count
    total = consume_input_text(name, limits.max_value_codepoints, total)
    if total < 0 {
      return Err(input_text_error(total))
    }
    match value {
      Scalar(text) => {
        total = consume_input_text(text, limits.max_value_codepoints, total)
        if total < 0 {
          return Err(input_text_error(total))
        }
      }
      List(items) =>
        for text in items {
          total = consume_input_text(text, limits.max_value_codepoints, total)
          if total < 0 {
            return Err(input_text_error(total))
          }
        }
      Assoc(entries) =>
        for (key, text) in entries.entries {
          total = consume_input_text(key, limits.max_value_codepoints, total)
          if total < 0 {
            return Err(input_text_error(total))
          }
          total = consume_input_text(text, limits.max_value_codepoints, total)
          if total < 0 {
            return Err(input_text_error(total))
          }
        }
    }
  }
  Ok(total)
}

///|
pub extend ParseLimits with Debug::{to_repr}

///|
pub extend ExpansionLimits with Debug::{to_repr}