///|
fn json_escape(value : String) -> String {
  let builder = StringBuilder()
  for character in value {
    match character {
      '"' => builder.write_string("\\\"")
      '\\' => builder.write_string("\\\\")
      '\n' => builder.write_string("\\n")
      '\r' => builder.write_string("\\r")
      '\t' => builder.write_string("\\t")
      _ => builder.write_char(character)
    }
  }
  builder.to_string()
}

///|
fn diagnostic_file(diagnostic : Diagnostic, fallback : String) -> String {
  if diagnostic.file.is_empty() {
    fallback
  } else {
    diagnostic.file
  }
}

///|
/// Render diagnostics in a compiler-friendly `file:line:column` format.
pub fn render_text(
  diagnostics : Array[Diagnostic],
  source : @source.SourceFile,
) -> String {
  let builder = StringBuilder()
  for diagnostic in diagnostics {
    let (line, column) = diagnostic.at_line_col(source)
    builder.write_string(diagnostic_file(diagnostic, source.name()))
    builder.write_char(':')
    builder.write_object(line + 1)
    builder.write_char(':')
    builder.write_object(column + 1)
    builder.write_string(": ")
    builder.write_string(diagnostic.severity.label())
    builder.write_char('[')
    builder.write_string(diagnostic.code)
    builder.write_string("]: ")
    builder.write_string(diagnostic.message)
    builder.write_char('\n')
    for note in diagnostic.notes {
      builder.write_string("  note: ")
      builder.write_string(note)
      builder.write_char('\n')
    }
  }
  builder.to_string()
}

///|
/// Render one JSON object per diagnostic for CI and editor integrations.
pub fn render_json(diagnostics : Array[Diagnostic]) -> String {
  let builder = StringBuilder()
  builder.write_char('[')
  for index, diagnostic in diagnostics {
    if index > 0 {
      builder.write_char(',')
    }
    builder.write_string("{\"code\":\"")
    builder.write_string(json_escape(diagnostic.code))
    builder.write_string("\",\"severity\":\"")
    builder.write_string(diagnostic.severity.label())
    builder.write_string("\",\"message\":\"")
    builder.write_string(json_escape(diagnostic.message))
    builder.write_string("\",\"file\":\"")
    builder.write_string(json_escape(diagnostic.file))
    builder.write_string("\",\"start\":")
    builder.write_object(diagnostic.span.start)
    builder.write_string(",\"end\":")
    builder.write_object(diagnostic.span.end)
    builder.write_string("}")
  }
  builder.write_string("]\n")
  builder.to_string()
}

///|
/// Render a semantic dependency index as a Graphviz DOT document.
pub fn render_graph(index : @semantic.SemanticIndex) -> String {
  let builder = StringBuilder()
  builder.write_string("digraph rtl {\n")
  builder.write_string("  rankdir=LR;\n")
  if index.modules.length() > 0 {
    for mod in index.modules {
      builder.write_string("  \"")
      builder.write_string(mod.name)
      builder.write_string("\";\n")
    }
  }
  for edge in index.graph.edges() {
    let (parent, child) = edge
    builder.write_string("  \"")
    builder.write_string(parent)
    builder.write_string("\" -> \"")
    builder.write_string(child)
    builder.write_string("\";\n")
  }
  builder.write_string("}\n")
  builder.to_string()
}