///|
fn default_colors() -> Array[String] {
  ["0;90", "0;39", "0;39", "0;39", "0;32", "1;39", "1;39", "1;34"].map(code => {
    "\u{1b}[" + code + "m"
  })
}

///|
// jq 1.8.2 accepts partial palettes and resets the remaining slots to defaults.
// Empty intermediate slots mean SGR reset; an empty final slot is ignored.
fn parse_colors(value : String?) -> (Array[String], Bool) {
  let colors = default_colors()
  guard value is Some(text) else { return (colors, true) }
  let mut start = 0
  for slot in 0..<8 {
    let mut end = start
    while end < text.length() && text[end] is ('0'..='9' | ';') {
      end += 1
    }
    if end < text.length() && slot < 7 && text[end] != ':' {
      return (default_colors(), false)
    }
    if end == text.length() || slot == 7 {
      if end > start {
        colors[slot] = "\u{1b}[" + text[start:end].to_owned() + "m"
      }
      break
    }
    colors[slot] = "\u{1b}[" + text[start:end].to_owned() + "m"
    start = end + 1
  }
  (colors, true)
}

///|
fn json_color(value : Json) -> Int {
  match value {
    Null => 0
    False => 1
    True => 2
    Number(_, ..) => 3
    String(_) => 4
    Array(_) => 5
    Object(_) => 6
  }
}

///|
fn render_json(
  value : Json,
  pretty : Bool,
  indent : Int,
  tab : Bool,
  colors : Array[String]?,
) -> String {
  let output = StringBuilder()
  fn color(index : Int) -> Unit {
    if colors is Some(palette) {
      output.write_string(palette[index])
    }
  }
  fn reset() -> Unit {
    if colors is Some(_) {
      output.write_string("\u{1b}[0m")
    }
  }
  fn newline(depth : Int) -> Unit {
    if pretty {
      output.write_char('\n')
      for _ in 0..<(if tab { depth } else { depth * indent }) {
        output.write_char(if tab { '\t' } else { ' ' })
      }
    }
  }
  fn emit(value : Json, depth : Int) -> Unit {
    let tone = json_color(value)
    color(tone)
    match value {
      Array(items) if !items.is_empty() => {
        output.write_char('[')
        for index, item in items {
          if index > 0 {
            color(tone)
            output.write_char(',')
          }
          reset()
          newline(depth + 1)
          emit(item, depth + 1)
        }
        newline(depth)
        color(tone)
        output.write_char(']')
      }
      Object(fields) if !fields.is_empty() => {
        output.write_char('{')
        let mut first = true
        for name, item in fields {
          if !first {
            color(tone)
            output.write_char(',')
          }
          first = false
          reset()
          newline(depth + 1)
          color(7)
          output.write_string(Json::string(name).stringify())
          reset()
          color(tone)
          output.write_char(':')
          reset()
          if pretty {
            output.write_char(' ')
          }
          emit(item, depth + 1)
        }
        newline(depth)
        color(tone)
        output.write_char('}')
      }
      _ => output.write_string(value.stringify())
    }
    reset()
  }
  emit(value, 0)
  output.to_string()
}