// The source snippet: line gutter, carets, and the multi-line span spine.

///|
/// One span to be drawn under the source.
priv struct Annotation {
  start_line : Int
  end_line : Int
  start_col : Int
  end_col : Int
  color : String
  /// A note printed after the caret, on the span's last line.
  label : @message.Message?
}

///|
/// The main span, then one per related label.
fn get_annotations(theme : Theme, d : Diagnostic) -> Array[Annotation] {
  let color = match d.severity {
    Error => theme.error_label
    Warning => theme.warning_label
    Suggestion => theme.secondary_label
  }
  let out = [
    {
      start_line: d.loc.start.lnum,
      end_line: d.loc.end.lnum,
      start_col: d.loc.start.column0(),
      end_col: d.loc.end.column0(),
      color,
      label: None,
    },
  ]
  for l in d.related {
    out.push({
      start_line: l.loc.start.lnum,
      end_line: l.loc.end.lnum,
      start_col: l.loc.start.column0(),
      end_col: l.loc.end.column0(),
      color: theme.secondary_label,
      label: Some(l.message),
    })
  }
  out
}

///|
/// How many lines of context surround an annotated line.
const CONTEXT : Int = 2

///|
/// The line ranges to print: every annotation's endpoints plus context,
/// merged where they touch or overlap.
///
/// Ranges only ADJACENT (`s2 <= e1 + 1`) are merged too, so two hunks a single
/// line apart become one rather than being separated by a `...` marker that
/// hides nothing.
fn get_hunks(annotations : Array[Annotation]) -> Array[(Int, Int)] {
  let ranges = []
  for a in annotations {
    ranges.push((a.start_line - CONTEXT, a.start_line + CONTEXT))
    ranges.push((a.end_line - CONTEXT, a.end_line + CONTEXT))
  }
  // A STABLE sort on the start line: equal starts keep annotation order, so
  // the merge below is deterministic.
  let sorted = ranges.copy()
  sorted.sort_by((x, y) => x.0.compare(y.0))
  let merged : Array[(Int, Int)] = []
  for r in sorted {
    match merged.last() {
      Some(prev) if r.0 <= prev.1 + 1 =>
        merged[merged.length() - 1] = (
          @cmp.minimum(prev.0, r.0),
          @cmp.maximum(prev.1, r.1),
        )
      _ => merged.push(r)
    }
  }
  merged.map(r => (@cmp.maximum(1, r.0), r.1))
}

///|
/// The byte offset at which each line starts.
fn get_line_starts(source : String) -> Array[Int] {
  let starts = [0]
  let mut i = 0
  let n = source.length()
  while i < n {
    if source.unsafe_get(i).to_int() == 0x0A {
      starts.push(i + 1)
    }
    i = i + 1
  }
  starts
}

///|
/// The text of the line starting at `bol`, and the offset of its newline.
fn line_info(source : String, bol : Int) -> (String, Int) {
  if bol >= source.length() {
    return ("", bol)
  }
  let mut e = bol
  while e < source.length() && source.unsafe_get(e).to_int() != 0x0A {
    e = e + 1
  }
  (source.view(start_offset=bol, end_offset=e).to_owned(), e)
}