///|
/// Resource limits for untrusted JSON documents. Limits are checked after
/// strict parsing, before a caller stores or hashes a potentially huge tree.
pub(all) struct DocumentLimits {
  mut max_depth : Int
  mut max_values : Int
  mut max_array_length : Int
  mut max_object_members : Int
  mut max_string_length : Int
  mut max_number_length : Int
}

///|
pub enum LimitViolation {
  DepthExceeded(String, Int)
  ValuesExceeded(String, Int)
  ArrayTooLong(String, Int)
  ObjectTooLarge(String, Int)
  StringTooLong(String, Int)
  NumberTooLong(String, Int)
}

///|
pub fn DocumentLimits::default() -> DocumentLimits {
  {
    max_depth: 256,
    max_values: 100000,
    max_array_length: 10000,
    max_object_members: 10000,
    max_string_length: 1000000,
    max_number_length: 128,
  }
}

///|
pub fn LimitViolation::message(self : LimitViolation) -> String {
  match self {
    DepthExceeded(path, limit) =>
      "maximum depth " + limit.to_string() + " exceeded at " + path
    ValuesExceeded(path, limit) =>
      "maximum value count " + limit.to_string() + " exceeded at " + path
    ArrayTooLong(path, limit) =>
      "maximum array length " + limit.to_string() + " exceeded at " + path
    ObjectTooLarge(path, limit) =>
      "maximum object member count " +
      limit.to_string() +
      " exceeded at " +
      path
    StringTooLong(path, limit) =>
      "maximum string length " + limit.to_string() + " exceeded at " + path
    NumberTooLong(path, limit) =>
      "maximum number token length " +
      limit.to_string() +
      " exceeded at " +
      path
  }
}

///|
/// Return the first limit violation in deterministic document order.
pub fn check_limits(
  value : JsonValue,
  limits : DocumentLimits,
) -> LimitViolation? {
  let state = JsonStats::empty()
  check_value_limits(value, "$", 0, limits, state)
}

///|
/// Parse a document and enforce resource limits before returning its tree.
pub fn parse_with_limits(
  input : String,
  limits : DocumentLimits,
) -> Result[JsonValue, String] {
  let value = match parse(input) {
    Ok(value) => value
    Err(error) => return Err(error.message())
  }
  match check_limits(value, limits) {
    Some(violation) => Err(violation.message())
    None => Ok(value)
  }
}

///|
fn check_value_limits(
  value : JsonValue,
  path : String,
  depth : Int,
  limits : DocumentLimits,
  state : JsonStats,
) -> LimitViolation? {
  state.value_count = state.value_count + 1
  if depth > limits.max_depth {
    return Some(DepthExceeded(path, limits.max_depth))
  }
  if state.value_count > limits.max_values {
    return Some(ValuesExceeded(path, limits.max_values))
  }
  match value {
    Null | Bool(_) => None
    Number(number) =>
      if number.length() > limits.max_number_length {
        Some(NumberTooLong(path, limits.max_number_length))
      } else {
        None
      }
    String(string) =>
      if string.length() > limits.max_string_length {
        Some(StringTooLong(path, limits.max_string_length))
      } else {
        None
      }
    Array(values) => {
      if values.length() > limits.max_array_length {
        return Some(ArrayTooLong(path, limits.max_array_length))
      }
      let mut index = 0
      while index < values.length() {
        match
          check_value_limits(
            values[index],
            path + "/" + index.to_string(),
            depth + 1,
            limits,
            state,
          ) {
          Some(violation) => return Some(violation)
          None => ()
        }
        index = index + 1
      }
      None
    }
    Object(entries) => {
      if entries.length() > limits.max_object_members {
        return Some(ObjectTooLarge(path, limits.max_object_members))
      }
      for entry in entries {
        match
          check_value_limits(
            entry.1,
            path + "/" + entry.0,
            depth + 1,
            limits,
            state,
          ) {
          Some(violation) => return Some(violation)
          None => ()
        }
      }
      None
    }
  }
}