// Rendering a diagnostic against its source text.

///|
fn repeat_str(s : String, n : Int) -> String {
  if n <= 0 {
    return ""
  }
  let out = StringBuilder::new()
  for _ in 0.. String {
  repeat_str(" ", width - s.length()) + s
}

///|
/// Render `d` with a source snippet.
fn output_with_source(
  sink : Sink,
  theme : Theme,
  source : String,
  d : Diagnostic,
) -> Unit {
  let annotations = get_annotations(theme, d)
  let hunks = get_hunks(annotations)
  let line_starts = get_line_starts(source)
  let total_lines = line_starts.length()
  let mut max_hunk_line = 0
  for h in hunks {
    max_hunk_line = @cmp.maximum(max_hunk_line, h.1)
  }
  // The gutter is sized for the widest line number ACTUALLY printed, not for
  // the file's last line: a diagnostic near the top of a 10000-line file gets a
  // narrow gutter.
  let max_line = @cmp.minimum(max_hunk_line, total_lines)
  let gutter_width = @cmp.maximum(1, max_line.to_string().length())
  let gutter_padding = repeat_str(" ", gutter_width)
  let start = d.loc.start
  output_no_loc(sink, theme, d.severity, d.warning, None, d.message)
  // The location line. Its column is 1-BASED, unlike the JSON format's.
  line(
    sink,
    with_style(theme.line_numbers, gutter_padding + "──➤") +
    "  \{start.fname}:\{start.lnum}:\{start.column1()}",
  )
  fn print_line(
    header : String,
    gutter_char : String,
    contents : String,
  ) -> Unit {
    line(
      sink,
      with_style(theme.line_numbers, header + " " + gutter_char) +
      " " +
      contents,
    )
  }

  let mut curr_pos = 0
  let mut curr_line = 1
  let total_hunks = hunks.length()
  for i, hunk in hunks {
    if i > 0 {
      // A gap between hunks, marked rather than silently skipped.
      line(
        sink,
        with_style(theme.line_numbers, gutter_padding + " ·") + " ...",
      )
    }
    let (s_line, e_line) = hunk
    if s_line <= total_lines {
      curr_pos = line_starts[s_line - 1]
      curr_line = s_line
    } else {
      curr_pos = source.length()
      curr_line = total_lines + 1
    }
    let last_line_of_hunk = @cmp.minimum(e_line, total_lines)
    while curr_line <= last_line_of_hunk {
      let is_last_line = curr_line == last_line_of_hunk && i == total_hunks - 1
      let (raw_content, next_eol) = line_info(source, curr_pos)
      print_line(
        pad_left(curr_line.to_string(), gutter_width),
        "│",
        @unicode.expand_tabs(raw_content),
      )
      let on_this_line = annotations.filter(a => {
        curr_line >= a.start_line && curr_line <= a.end_line
      })
      let num_annots = on_this_line.length()
      // A column measured in DISPLAY columns, not bytes: a wide glyph or a tab
      // ahead of the span shifts the caret.
      fn vis_col(col : Int) -> Int {
        let col = @cmp.minimum(raw_content.length(), col)
        @unicode.terminal_width(raw_content.view(end_offset=col).to_owned())
      }

      for j, a in on_this_line {
        let is_last_annot = is_last_line && j == num_annots - 1
        let gutter_char = if is_last_annot { " " } else { "·" }
        let is_start = curr_line == a.start_line
        let is_end = curr_line == a.end_line
        // A single-line span is underlined with a run of carets. A span
        // crossing several lines is drawn as a SPINE: the start line marks the
        // construct's first column with a corner and a reach out to it, each
        // middle line carries a vertical connector, and the last line closes
        // with a corner reaching the final column, then the label.
        let carets = if is_start && is_end {
          let start_vis = vis_col(a.start_col)
          let end_vis = vis_col(a.end_col)
          repeat_str(" ", start_vis) +
          repeat_str("^", @cmp.maximum(1, end_vis - start_vis))
        } else {
          // One box-drawing glyph is one display column, so a reach of
          // `caret - 1` dashes lands the caret at column `caret`. A construct
          // flush to column 0 has the corner itself sit on it, so no reach.
          fn reach(caret : Int) -> String {
            if caret <= 0 {
              ""
            } else {
              repeat_str("─", caret - 1) + "^"
            }
          }

          if is_start {
            "╭" + reach(vis_col(a.start_col))
          } else if is_end {
            "╰" + reach(@cmp.maximum(1, vis_col(a.end_col)) - 1)
          } else {
            "│"
          }
        }
        // Labels are short, and rendered FLAT so they sit on the caret's line
        // in the caret's colour rather than nested inside body ANSI.
        let label = match a.label {
          Some(m) if is_end =>
            " " + with_style(a.color, @message.to_plain_string(m))
          _ => ""
        }
        print_line(
          gutter_padding,
          gutter_char,
          with_style(a.color, carets) + label,
        )
      }
      curr_pos = @cmp.minimum(source.length(), next_eol + 1)
      curr_line = curr_line + 1
    }
  }
  print_hint(sink, theme, d.hint)
  print_fix(sink, theme, d.severity, d.warning, d.edit)
}

///|
/// Render one diagnostic in whichever format is selected.
pub fn output_error(
  sink : Sink,
  theme : Theme,
  format : OutputFormat,
  source : String?,
  d : Diagnostic,
) -> Unit {
  match format {
    Json => output_json(sink, d)
    Short => output_short(sink, d)
    Human =>
      // With no span at all there is nothing to point at; with a span but no
      // source text, the location is stated in prose instead.
      if d.loc.start.is_dummy() {
        output_no_loc(sink, theme, d.severity, d.warning, d.hint, d.message)
      } else {
        match source {
          None => output_no_source(sink, theme, d)
          Some(src) => output_with_source(sink, theme, src, d)
        }
      }
  }
}