///|
/// Render a parsed value as deterministic human-readable JSON. Unlike
/// `canonicalize`, this adds indentation and line breaks for review output.
pub fn pretty_json(
  value : JsonValue,
  indent_width : Int,
) -> Result[String, CanonicalError] {
  let width = if indent_width < 0 { 0 } else { indent_width }
  pretty_value(value, 0, width)
}

///|
pub fn pretty_document(
  input : String,
  indent_width : Int,
) -> Result[String, String] {
  let value = match parse(input) {
    Ok(value) => value
    Err(error) => return Err(error.message())
  }
  match pretty_json(value, indent_width) {
    Ok(output) => Ok(output)
    Err(error) => Err(error.message())
  }
}

///|
fn pretty_value(
  value : JsonValue,
  depth : Int,
  width : Int,
) -> Result[String, CanonicalError] {
  match value {
    Null => Ok("null")
    Bool(value) => Ok(if value { "true" } else { "false" })
    Number(value) => canonical_number(value)
    String(value) => Ok(canonical_string(value))
    Array(values) => pretty_array(values, depth, width)
    Object(entries) => pretty_object(entries, depth, width)
  }
}

///|
fn pretty_array(
  values : Array[JsonValue],
  depth : Int,
  width : Int,
) -> Result[String, CanonicalError] {
  if values.length() == 0 {
    return Ok("[]")
  }
  if width == 0 {
    let mut compact = "["
    let mut first = true
    for value in values {
      if !first {
        compact = compact + ","
      }
      first = false
      let rendered = match pretty_value(value, depth + 1, width) {
        Ok(rendered) => rendered
        Err(error) => return Err(error)
      }
      compact = compact + rendered
    }
    return Ok(compact + "]")
  }
  let mut output = "[\n"
  let mut first = true
  for value in values {
    if !first {
      output = output + ",\n"
    }
    first = false
    output = output + indentation(depth + 1, width)
    let rendered = match pretty_value(value, depth + 1, width) {
      Ok(rendered) => rendered
      Err(error) => return Err(error)
    }
    output = output + rendered
  }
  Ok(output + "\n" + indentation(depth, width) + "]")
}

///|
fn pretty_object(
  entries : Array[(String, JsonValue)],
  depth : Int,
  width : Int,
) -> Result[String, CanonicalError] {
  if entries.length() == 0 {
    return Ok("{}")
  }
  let ordered : Array[(String, JsonValue)] = []
  for entry in entries {
    ordered.push(entry)
  }
  sort_entries(ordered)
  if width == 0 {
    let mut compact = "{"
    let mut first = true
    for entry in ordered {
      if !first {
        compact = compact + ","
      }
      first = false
      let value = match pretty_value(entry.1, depth + 1, width) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      compact = compact + canonical_string(entry.0) + ":" + value
    }
    return Ok(compact + "}")
  }
  let mut output = "{\n"
  let mut first = true
  for entry in ordered {
    if !first {
      output = output + ",\n"
    }
    first = false
    let value = match pretty_value(entry.1, depth + 1, width) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    output = output +
      indentation(depth + 1, width) +
      canonical_string(entry.0) +
      ": " +
      value
  }
  Ok(output + "\n" + indentation(depth, width) + "}")
}

///|
fn indentation(depth : Int, width : Int) -> String {
  let mut output = ""
  let mut count = 0
  while count < depth * width {
    output = output + " "
    count = count + 1
  }
  output
}