///|
pub(all) struct LineIndex {
  starts : Array[Int]
  ends : Array[Int]
  text_length : Int
} derive(Eq, Debug)

///|
pub fn SourceFile::line_index(self : SourceFile) -> LineIndex {
  let starts : Array[Int] = [0]
  let ends : Array[Int] = []
  let chars = self.text.to_array()
  for index, character in chars {
    if character == '\n' {
      ends.push(index)
      starts.push(index + 1)
    }
  }
  ends.push(chars.length())
  { starts, ends, text_length: chars.length() }
}

///|
pub fn LineIndex::line_count(self : LineIndex) -> Int {
  self.starts.length()
}

///|
pub fn LineIndex::contains_offset(self : LineIndex, offset : Int) -> Bool {
  offset >= 0 && offset <= self.text_length
}

///|
pub fn LineIndex::line_at(self : LineIndex, offset : Int) -> Int {
  if self.starts.length() == 0 {
    return 0
  }
  let safe = if offset < 0 {
    0
  } else if offset > self.text_length {
    self.text_length
  } else {
    offset
  }
  let mut result = 0
  for index, start in self.starts {
    if start <= safe {
      result = index
    }
  }
  result
}

///|
pub fn LineIndex::column_at(self : LineIndex, offset : Int) -> Int {
  let line = self.line_at(offset)
  let safe = if offset < 0 {
    0
  } else if offset > self.text_length {
    self.text_length
  } else {
    offset
  }
  safe - self.starts[line]
}

///|
pub fn LineIndex::span_for_line(self : LineIndex, line : Int) -> Span? {
  if line < 0 || line >= self.starts.length() {
    None
  } else {
    Some(Span::new(self.starts[line], self.ends[line]))
  }
}

///|
pub fn LineIndex::offset_at(self : LineIndex, line : Int, column : Int) -> Int? {
  match self.span_for_line(line) {
    None => None
    Some(span) => {
      let safe_column = if column < 0 {
        0
      } else if column > span.length() {
        span.length()
      } else {
        column
      }
      Some(span.start + safe_column)
    }
  }
}

///|
pub fn LineIndex::line_starts(self : LineIndex) -> Array[Int] {
  self.starts.copy()
}

///|
pub fn LineIndex::line_ends(self : LineIndex) -> Array[Int] {
  self.ends.copy()
}

///|
pub fn SourceFile::offset_line_column(
  self : SourceFile,
  offset : Int,
) -> (Int, Int) {
  let index = self.line_index()
  (index.line_at(offset), index.column_at(offset))
}

///|
pub fn SourceFile::span_text(self : SourceFile, line : Int) -> String? {
  match self.line_index().span_for_line(line) {
    Some(span) => Some(self.slice(span).trim_end().to_owned())
    None => None
  }
}

///|
pub fn SourceFile::offset_after_text(self : SourceFile, text : String) -> Int? {
  let chars = self.text.to_array()
  let wanted = text.to_array()
  if wanted.length() == 0 {
    return Some(0)
  }
  if wanted.length() > chars.length() {
    return None
  }
  for start in 0..<=(chars.length() - wanted.length()) {
    let mut matched = true
    for index in 0.. Span {
  Span::new(
    if left.start < right.start {
      left.start
    } else {
      right.start
    },
    if left.end > right.end {
      left.end
    } else {
      right.end
    },
  )
}

///|
pub fn source_spans_union(spans : Array[Span]) -> Span? {
  if spans.length() == 0 {
    return None
  }
  let mut result = spans[0]
  for span in spans[1:] {
    result = source_span_union(result, span)
  }
  Some(result)
}