///|
/// A zero-based line and byte column.
pub struct Location {
  line : Int
  column : Int
  offset : Int
} derive(Eq, Debug)

///|
pub fn Location::line(self : Location) -> Int {
  self.line
}

///|
pub fn Location::column(self : Location) -> Int {
  self.column
}

///|
pub fn Location::offset(self : Location) -> Int {
  self.offset
}

///|
/// Immutable source text with a precomputed line-start index.
pub struct Source {
  name : String
  text : String
  bytes : Bytes
  line_starts : Array[Int]
}

///|
pub fn Source::new(name : String, text : String) -> Source {
  let bytes = @utf8.encode(text)
  let starts = [0]
  let mut index = 0
  while index < bytes.length() {
    if bytes[index] == 10 {
      starts.push(index + 1)
    }
    index += 1
  }
  { name, text, bytes, line_starts: starts }
}

///|
pub fn Source::name(self : Source) -> String {
  self.name
}

///|
pub fn Source::text(self : Source) -> String {
  self.text
}

///|
pub fn Source::byte_length(self : Source) -> Int {
  self.bytes.length()
}

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

///|
fn Source::line_index_at(self : Source, offset : Int) -> Int {
  let target = if offset < 0 {
    0
  } else if offset > self.bytes.length() {
    self.bytes.length()
  } else {
    offset
  }
  let mut low = 0
  let mut high = self.line_starts.length()
  while low + 1 < high {
    let middle = low + (high - low) / 2
    if self.line_starts[middle] <= target {
      low = middle
    } else {
      high = middle
    }
  }
  low
}

///|
pub fn Source::location(self : Source, offset : Int) -> Location {
  let bounded = if offset < 0 {
    0
  } else if offset > self.bytes.length() {
    self.bytes.length()
  } else {
    offset
  }
  let line = self.line_index_at(bounded)
  { line, column: bounded - self.line_starts[line], offset: bounded }
}

///|
pub fn Source::line_span(self : Source, line : Int) -> Span? {
  if line < 0 || line >= self.line_starts.length() {
    return None
  }
  let start = self.line_starts[line]
  let raw_end = if line + 1 < self.line_starts.length() {
    self.line_starts[line + 1]
  } else {
    self.bytes.length()
  }
  let mut end = raw_end
  if end > start && self.bytes[end - 1] == 10 {
    end -= 1
  }
  if end > start && self.bytes[end - 1] == 13 {
    end -= 1
  }
  Some({ start, end })
}

///|
pub fn Source::slice(self : Source, span : Span) -> String {
  let bounded = span.clamp(self.bytes.length())
  @utf8.decode_lossy(self.bytes[bounded.start():bounded.end()])
}

///|
/// Returns the source text between two byte offsets after clamping both ends.
///
/// This is primarily useful to consumers that assemble transformed source
/// without exposing the source's internal UTF-8 byte buffer.
pub fn Source::slice_offsets(self : Source, start : Int, end : Int) -> String {
  let bounded_start = start.clamp(min=0, max=self.bytes.length())
  let bounded_end = end.clamp(min=bounded_start, max=self.bytes.length())
  @utf8.decode_lossy(self.bytes[bounded_start:bounded_end])
}

///|
pub fn Source::line_text(self : Source, line : Int) -> String? {
  match self.line_span(line) {
    Some(span) => Some(self.slice(span))
    None => None
  }
}