// The human renderer: a headed message, then a source snippet with carets.

///|
/// Lay out a headed diagnostic.
///
/// The styled header is emitted at its PLAIN text's width -- the colour is
/// zero-width -- then a soft break, then the body. A body that fits joins the
/// header on one line; anything wider, or containing a hard break, puts the
/// header on its own line with the body under a 2-column hanging indent.
fn headed(
  sink : Sink,
  header_plain : String,
  header_styled : String,
  body : (@printer.Printer) -> Unit,
) -> Unit {
  let rendered = @printer.run_string(p => {
    p.hvbox(indent=2, () => {
      p.string_as(header_plain.length(), header_styled)
      p.space()
      body(p)
    })
  })
  for l in rendered.split("\n") {
    line(sink, l.to_owned())
  }
}

///|
fn render_body(
  p : @printer.Printer,
  theme : Theme,
  m : @message.Message,
) -> Unit {
  @message.render(p, theme.body, m)
}

///|
fn print_hint(sink : Sink, theme : Theme, hint : @message.Message?) -> Unit {
  match hint {
    None => ()
    Some(m) =>
      headed(sink, "Hint:", with_style(theme.hint_header, "Hint") + ":", p => {
        render_body(p, theme, m)
      })
  }
}

///|
/// Render a machine-applicable edit as a "Help:" line, in prose.
///
/// Shown only for an UNNAMED error -- that is, a syntax error, which says what
/// is wrong rather than how to repair it, so spelling out the derived fix is
/// worth a line. A warning or suggestion carrying an edit already states its
/// fix in its own message, so repeating the raw text there would be redundant;
/// that is why the guard checks for the absence of a warning name and not just
/// the severity, since `-W name=error` promotes a warning to Error while it
/// keeps its name. The machine-readable form travels in JSON regardless.
fn print_fix(
  sink : Sink,
  theme : Theme,
  severity : Severity,
  warning : @warning.Warning?,
  edit : Edit?,
) -> Unit {
  match (severity, warning, edit) {
    (Error, None, Some(e)) => {
      let action = if e.loc.start.cnum == e.loc.end.cnum {
        "insert '\{e.new_text}'"
      } else if e.new_text == "" {
        "remove this"
      } else {
        "replace with '\{e.new_text}'"
      }
      headed(sink, "Help:", with_style(theme.hint_header, "Help") + ":", p => {
        p.string(action)
      })
    }
    _ => ()
  }
}

///|
/// The header line and body, with no source snippet.
fn output_no_loc(
  sink : Sink,
  theme : Theme,
  severity : Severity,
  warning : @warning.Warning?,
  hint : @message.Message?,
  msg : @message.Message,
) -> Unit {
  // A named warning's name is appended as ` [name]`, including one promoted to
  // Error severity by `-W name=error` -- which still carries its name.
  let name_suffix = match warning {
    Some(w) => " [" + w.name() + "]"
    None => ""
  }
  let (word, color) = match severity {
    Error => ("Error", theme.error_header)
    Warning => ("Warning", theme.warning_header)
    Suggestion => ("Suggestion", theme.hint_header)
  }
  headed(
    sink,
    word + name_suffix + ":",
    with_style(color, word) + name_suffix + ":",
    p => render_body(p, theme, msg),
  )
  print_hint(sink, theme, hint)
}

///|
/// A located diagnostic when the source text is not available.
fn output_no_source(sink : Sink, theme : Theme, d : Diagnostic) -> Unit {
  let s = d.loc.start
  let e = d.loc.end
  if s.lnum == e.lnum {
    line(
      sink,
      "File \"\{s.fname}\", line \{s.lnum}, characters \{s.column0()}-\{e.column0()}:",
    )
  } else {
    line(
      sink,
      "File \"\{s.fname}\", line \{s.lnum}, character \{s.column0()} to line \{e.lnum}, character \{e.column0()}:",
    )
  }
  output_no_loc(sink, theme, d.severity, d.warning, d.hint, d.message)
}