// rustc-style rendering of a ParseError (spec M3c section 5).

///|
let red : String = "\u{1b}[1;31m"

///|
let blue : String = "\u{1b}[1;34m"

///|
let bold : String = "\u{1b}[1m"

///|
let reset : String = "\u{1b}[0m"

///|
fn paint(text : String, style : String, color : Bool) -> String {
  if color && text != "" {
    style + text + reset
  } else {
    text
  }
}

///|
/// `a, b or c`
fn or_list(items : Array[String]) -> String {
  match items.length() {
    0 => ""
    1 => items[0]
    n => items[:n - 1].to_owned().join(", ") + " or " + items[n - 1]
  }
}

///|
/// Expands tabs to 4 spaces.
fn expand(text : String) -> String {
  let buf = StringBuilder()
  for c in text {
    match c {
      '\t' => buf.write_string("    ")
      // Control characters never reach the terminal.
      _ if c.to_int() < 0x20 || c.to_int() == 0x7F => buf.write_char('\u{FFFD}')
      _ => buf.write_char(c)
    }
  }
  buf.to_string()
}

///|
/// Spaces that cover the first `count` code points of `text` (a tab takes 4).
fn padding(text : String, count : Int) -> String {
  let buf = StringBuilder()
  let mut seen = 0
  for c in text {
    if seen >= count {
      break
    }
    buf.write_string(if c == '\t' { "    " } else { " " })
    seen += 1
  }
  buf.to_string()
}

///|
/// The number of code points of `text`.
fn code_points(text : String) -> Int {
  let mut n = 0
  for _ in text {
    n += 1
  }
  n
}

///|
/// The rendered diagnostic of `error` in `source` (named `name` in the
/// location line). Ends with a line break.
pub fn render(
  error : @ast.ParseError,
  source : String,
  name~ : String,
  color? : Bool = false,
) -> String {
  let (message, span, expected, found) = match error {
    Syntax(message~, span~, expected~, found~) =>
      (message, span, expected, found)
    Unsupported(message~, span~) => (message, span, [], "")
  }
  // An error at the end of a source that ends with a line break points at
  // the end of the last line.
  let start = if span.start >= source.length() && source.has_suffix("\r\n") {
    source.length() - 2
  } else if span.start >= source.length() && source.has_suffix("\n") {
    source.length() - 1
  } else {
    span.start
  }
  let at = position(source, start)
  // A moved end-of-input position, or an empty span, marks one character.
  let end_offset = if start != span.start || span.end <= start {
    start
  } else {
    span.end
  }
  let stop = position(source, end_offset)
  let text = line_text(source, at.line)
  let line_length = code_points(text)
  let marks = if stop.line > at.line {
    line_length - (at.column - 1)
  } else {
    stop.column - at.column
  }
  let marks = if marks < 1 { 1 } else { marks }
  let gutter = "\{at.line}".length()
  let pad = " ".repeat(gutter)
  let expected_kind = message.has_prefix("expected")
  let header = if expected_kind && found != "" {
    message + ", found " + found
  } else {
    message
  }
  let buf = StringBuilder()
  buf.write_string(paint("error", red, color))
  buf.write_string(paint(": " + header, bold, color))
  buf.write_string("\n")
  buf.write_string(
    pad + paint("-->", blue, color) + " \{name}:\{at.line}:\{at.column}\n",
  )
  buf.write_string(pad + " " + paint("|", blue, color) + "\n")
  buf.write_string(
    paint("\{at.line}", blue, color) +
    " " +
    paint("|", blue, color) +
    " " +
    expand(text) +
    "\n",
  )
  let label = if expected_kind { " " + message } else { "" }
  buf.write_string(
    pad +
    " " +
    paint("|", blue, color) +
    " " +
    padding(text, at.column - 1) +
    paint("^".repeat(marks) + label, red, color) +
    "\n",
  )
  let notes : Array[String] = []
  if stop.line > at.line {
    notes.push("note: the span ends at line \{stop.line}")
  }
  if expected.length() >= 2 {
    notes.push("expected: " + or_list(expected))
  }
  if !notes.is_empty() {
    buf.write_string(pad + " " + paint("|", blue, color) + "\n")
    for note in notes {
      buf.write_string(pad + " " + paint("=", blue, color) + " " + note + "\n")
    }
  }
  buf.to_string()
}