///|
/// A mark placed in the rendered text: its index in the report, its span in
/// scalar indices after clamping and snapping, and where it is drawn.
priv struct Placed {
  index : Int32
  start : Int32
  end : Int32
  line : Int32
  col : Int32
  width : Int32
}

///|
/// The rendered text of a sequence, and for an array sequence the scalar span
/// `[start, end)` of each element in it.
fn[T : Show] layout(seq : Sequence[T]) -> (String, Array[(Int32, Int32)]?) {
  match seq {
    Str(s) => (s, None)
    Arr(a, j) => {
      let parts = render_parts(a, j)
      let join_len = j.to_array().length()
      let spans = []
      let mut pos = 0
      for i, p in parts {
        if i > 0 {
          pos += join_len
        }
        let len = p.to_array().length()
        spans.push((pos, pos + len))
        pos += len
      }
      (parts.join(j), Some(spans))
    }
  }
}

///|
/// The scalar span of `span` in the text: element spans become the hull of
/// their elements; then clamping (kappa) and snapping (sigma) of the 0.2.0
/// contract.
fn scalar_span(
  span : Span,
  elements : Array[(Int32, Int32)]?,
  scalars : Int32,
  bounds : Array[Int32],
) -> (Int32, Int32) {
  let (a, b) = match elements {
    None => {
      let a = if span.start > scalars { scalars } else { span.start }
      let b = if span.end > scalars { scalars } else { span.end }
      (a, b)
    }
    Some(els) => {
      let n = els.length()
      let i = if span.start > n { n } else { span.start }
      let j = if span.end > n { n } else { span.end }
      if i == j {
        let p = if i < n { els[i].0 } else { scalars }
        (p, p)
      } else {
        (els[i].0, els[j - 1].1)
      }
    }
  }
  @ucd.snap(bounds, a, b)
}

///|
/// Compares two strings by code point (not by UTF-16 code unit).
fn compare_code_points(x : String, y : String) -> Int32 {
  let xs = x.to_array()
  let ys = y.to_array()
  let n = if xs.length() < ys.length() { xs.length() } else { ys.length() }
  for i in 0.. Bool {
  if x.priority != y.priority {
    return x.priority > y.priority
  }
  if xp.start != yp.start {
    return xp.start > yp.start
  }
  if xp.end != yp.end {
    return xp.end < yp.end
  }
  if x.style != y.style {
    return x.style == Primary
  }
  let mx = x.message.unwrap_or("")
  let my = y.message.unwrap_or("")
  compare_code_points(mx, my) < 0
}

///|
/// One rendered line of the text: its displayed form (tabs expanded) and its
/// width in columns.
priv struct Line {
  text : StringBuilder
  mut width : Int32
}

///|
fn sgr(code : String, s : String, color : Bool) -> String {
  if color {
    "\u{1B}[" + code + "m" + s + "\u{1B}[0m"
  } else {
    s
  }
}

///|
fn severity_sgr(s : Severity) -> String {
  match s {
    Error => "1;31"
    Warning => "1;33"
    Note => "1;36"
    Help => "1;32"
  }
}

///|
/// Renders the report on `seq` in the compact layout:
///
/// 1. the line of text that holds the primary mark (the whole text if there
///    are no marks), with tabs expanded by `model`;
/// 2. a marker row: `^` under primary marks and `-` under secondary marks;
///    where marks overlap, the winner is decided by priority, then the later
///    start, then the earlier end, then primary over secondary, then the
///    message (so the result does not depend on the order of the marks); the
///    message of the mark that ends rightmost follows the markers on the same
///    row;
/// 3. the other messages, each under the start of its mark, on as few rows as
///    possible (greedy by left end point);
/// 4. the header `severity[code]: message`, with ` at line:column` (1-based)
///    of the primary mark when the text has several lines;
/// 5. one `note: ...` line per note.
///
/// Marks whose start lies on another line than the primary mark are not
/// drawn. Positions are element indices for array sequences and Unicode
/// scalar indices for string sequences; spans are clamped to the sequence and
/// snapped outward to grapheme clusters. Widths follow Unicode 18.0.0 through
/// `model`. With `color`, ANSI SGR colours are used (ECMA-48).
pub fn[T : Show] Report::render(
  self : Report,
  seq : Sequence[T],
  model~ : @cursor.Model = @cursor.Model::new(),
  color~ : Bool = false,
) -> String {
  self.render_impl(seq, model, color, '^', None)
}

///|
/// `Report::render` with the primary marker character `glyph`, and, if
/// `header` is given, that line in place of the header (used by the
/// deprecated `ErrorData::mark`).
fn[T : Show] Report::render_impl(
  self : Report,
  seq : Sequence[T],
  model : @cursor.Model,
  color : Bool,
  glyph : Char,
  header : String?,
) -> String {
  // Marks equal in every field are one mark, so overlaying is idempotent.
  let marks : Array[Mark] = []
  for m in self.marks {
    if !marks.contains(m) {
      marks.push(m)
    }
  }
  let (text, elements) = layout(seq)
  let scalars = text.to_array().length()
  let bounds = @ucd.cluster_boundaries(text)
  // Lay out the text cluster by cluster: displayed lines, and the (line,
  // column) of every cluster boundary.
  let lines : Array[Line] = [{ text: StringBuilder(), width: 0, }]
  let at : Map[Int32, (Int32, Int32)] = Map([])
  let mut s = 0
  for g in @cursor.clusters(text) {
    let line = lines.length() - 1
    at[s] = (line, lines[line].width)
    match g {
      "\n" | "\r" | "\r\n" => lines.push({ text: StringBuilder(), width: 0, })
      "\t" => {
        let to = model.apply(model.tab(), { line: 0, col: lines[line].width, }).col
        for _ in lines[line].width.. {
        lines[line].text.write_string(g)
        lines[line].width += model.cluster_width(g)
      }
    }
    s += g.to_array().length()
  }
  at[scalars] = (lines.length() - 1, lines[lines.length() - 1].width)
  // Place the marks.
  let placed = marks.mapi((i, m) => {
    let (a, b) = scalar_span(m.span, elements, scalars, bounds)
    let (la, ca) = at.get(a).unwrap_or((0, 0))
    let (lb, cb) = at.get(b).unwrap_or((0, 0))
    let end_col = if lb != la { lines[la].width } else { cb }
    let width = if end_col > ca { end_col - ca } else { 1 }
    { index: i, start: a, end: b, line: la, col: ca, width, }
  })
  // The primary mark: highest priority, then earliest start, among primary
  // marks if there are any.
  let mut primary : Placed? = None
  for p in placed {
    let m = marks[p.index]
    match primary {
      None => primary = Some(p)
      Some(q) => {
        let n = marks[q.index]
        let better = if m.style != n.style {
          m.style == Primary
        } else if m.priority != n.priority {
          m.priority > n.priority
        } else if p.start != q.start {
          p.start < q.start
        } else {
          p.end < q.end
        }
        if better {
          primary = Some(p)
        }
      }
    }
  }
  let out = []
  let header_sev = sgr(severity_sgr(self.severity), self.severity.name(), color)
  let code = match self.code {
    Some(c) => "[" + c + "]"
    None => ""
  }
  match primary {
    None => {
      for l in lines {
        out.push(l.text.to_string())
      }
      out.push(header.unwrap_or(header_sev + code + ": " + self.message))
    }
    Some(pm) => {
      let shown = pm.line
      out.push(lines[shown].text.to_string())
      let on_line = placed.filter(p => p.line == shown)
      // Marker row: overlay as a semilattice.
      let mut cols = 0
      for p in on_line {
        if p.col + p.width > cols {
          cols = p.col + p.width
        }
      }
      let winner : Array[Int32] = Array::make(cols, -1)
      for p in on_line {
        for c in p.col..<(p.col + p.width) {
          let w = winner[c]
          if w < 0 ||
            beats(marks[p.index], p, marks[placed[w].index], placed[w]) {
            winner[c] = p.index
          }
        }
      }
      let row = StringBuilder()
      let mut c = 0
      while c < cols {
        let w = winner[c]
        if w < 0 {
          row.write_char(' ')
          c += 1
          continue
        }
        // A run of cells won by the same mark.
        let mut e = c
        while e < cols && winner[e] == w {
          e += 1
        }
        let m = marks[w]
        let g = if m.style == Primary { glyph } else { '-' }
        let code = if m.style == Primary {
          severity_sgr(self.severity)
        } else {
          "34"
        }
        row.write_string(sgr(code, String::make(e - c, g), color))
        c = e
      }
      // Messages: the mark that ends rightmost keeps its message on the
      // marker row; the others go below, greedy by left end point.
      let labelled = on_line.filter(p => marks[p.index].message is Some(_))
      let mut tail : Placed? = None
      for p in labelled {
        match tail {
          None => tail = Some(p)
          Some(q) => {
            let pe = p.col + p.width
            let qe = q.col + q.width
            if pe > qe ||
              (pe == qe && beats(marks[p.index], p, marks[q.index], q)) {
              tail = Some(p)
            }
          }
        }
      }
      let mut marker_row = row.to_string()
      if tail is Some(t) {
        marker_row = marker_row + " " + marks[t.index].message.unwrap()
      }
      out.push(marker_row)
      let rest = labelled.filter(p => {
        match tail {
          Some(t) => p.index != t.index
          None => true
        }
      })
      rest.sort_by((p, q) => {
        if p.col != q.col {
          return p.col - q.col
        }
        if beats(marks[p.index], p, marks[q.index], q) {
          -1
        } else if beats(marks[q.index], q, marks[p.index], p) {
          1
        } else {
          0
        }
      })
      let rows : Array[StringBuilder] = []
      // `free[r]`: first column where row `r` can take a label (one column
      // of gap after the previous label); `written[r]`: columns written.
      let free : Array[Int32] = []
      let written : Array[Int32] = []
      for p in rest {
        let msg = marks[p.index].message.unwrap()
        let w = model.apply(model.text(msg), { line: 0, col: 0, }).col
        let mut r = 0
        while r < rows.length() && free[r] > p.col {
          r += 1
        }
        if r == rows.length() {
          rows.push(StringBuilder())
          free.push(0)
          written.push(0)
        }
        for _ in written[r].. 1 {
        " at \{shown + 1}:\{pm.col + 1}"
      } else {
        ""
      }
      out.push(header.unwrap_or(header_sev + code + loc + ": " + self.message))
    }
  }
  for n in self.notes {
    out.push(sgr("1", "note", color) + ": " + n)
  }
  out.join("\n")
}

///|
/// The text that `Report::render` draws for this sequence: a string as it
/// is, or the elements of an array (quoted when needed) joined by the
/// separator.
pub fn[T : Show] Sequence::rendered(self : Sequence[T]) -> String {
  layout(self).0
}

///|
/// Where `span` lands in `rendered()`, as Unicode scalar indices
/// `(start, end)`: element spans become the hull of their elements, then the
/// span is clamped to the text and snapped outward to grapheme clusters, as
/// `Report::render` does.
pub fn[T : Show] Sequence::locate(
  self : Sequence[T],
  span : Span,
) -> (Int32, Int32) {
  let (text, elements) = layout(self)
  scalar_span(
    span,
    elements,
    text.to_array().length(),
    @ucd.cluster_boundaries(text),
  )
}