///|
/// Inclusive source-line window selected for a diagnostic label.
pub struct LineWindow {
  first : Int
  last : Int
  focus_first : Int
  focus_last : Int
} derive(Eq, Debug)

///|
pub fn LineWindow::first(self : LineWindow) -> Int {
  self.first
}

///|
pub fn LineWindow::last(self : LineWindow) -> Int {
  self.last
}

///|
pub fn LineWindow::focus_first(self : LineWindow) -> Int {
  self.focus_first
}

///|
pub fn LineWindow::focus_last(self : LineWindow) -> Int {
  self.focus_last
}

///|
/// Selects a bounded context window around a span.
pub fn Source::window(
  self : Source,
  span : Span,
  context_lines? : Int = 2,
) -> LineWindow {
  let bounded = span.clamp(self.byte_length())
  let start = self.location(bounded.start()).line()
  let end_offset = if bounded.is_empty() {
    bounded.end()
  } else {
    bounded.end() - 1
  }
  let finish = self.location(end_offset).line()
  let context = if context_lines < 0 { 0 } else { context_lines }
  {
    first: if start > context {
      start - context
    } else {
      0
    },
    last: if finish + context < self.line_count() {
      finish + context
    } else {
      self.line_count() - 1
    },
    focus_first: start,
    focus_last: finish,
  }
}

///|
/// Merges windows that overlap or are separated by at most `gap` lines.
pub fn merge_windows(
  windows : Array[LineWindow],
  gap? : Int = 1,
) -> Array[LineWindow] {
  if windows.length() == 0 {
    return []
  }
  let sorted = windows.copy()
  sorted.sort_by(fn(a, b) { a.first.compare(b.first) })
  let output : Array[LineWindow] = []
  let allowed_gap = if gap < 0 { 0 } else { gap }
  for current in sorted {
    if output.length() == 0 {
      output.push(current)
      continue
    }
    let previous = output[output.length() - 1]
    if current.first <= previous.last + allowed_gap + 1 {
      output[output.length() - 1] = {
        first: previous.first,
        last: if current.last > previous.last {
          current.last
        } else {
          previous.last
        },
        focus_first: if current.focus_first < previous.focus_first {
          current.focus_first
        } else {
          previous.focus_first
        },
        focus_last: if current.focus_last > previous.focus_last {
          current.focus_last
        } else {
          previous.focus_last
        },
      }
    } else {
      output.push(current)
    }
  }
  output
}