///|
/// One indexed line of embedded source content.
pub(all) struct SourceLine {
  number : Int
  start : Int
  end : Int
  text : String
} derive(Eq, Debug)

///|
/// A line index over one `sourcesContent` entry.
pub(all) struct SourceTextIndex {
  content : String
  starts : Array[Int]
  ends : Array[Int]
}

///|
/// Source lines surrounding an original position.
pub(all) struct SourceContext {
  source : String?
  focus_line : Int
  focus_column : Int
  start_line : Int
  end_line : Int
  lines : Array[String]
} derive(Eq, Debug)

///|
/// Build a CRLF-aware source text index.
pub fn SourceTextIndex::new(content : String) -> SourceTextIndex {
  let starts : Array[Int] = [0]
  let ends : Array[Int] = []
  let mut cursor = 0
  while cursor < content.length() {
    match content.get_char(cursor) {
      Some('\r') => {
        ends.push(cursor)
        if cursor + 1 < content.length() &&
          content.get_char(cursor + 1) == Some('\n') {
          cursor = cursor + 2
        } else {
          cursor = cursor + 1
        }
        starts.push(cursor)
      }
      Some('\n') => {
        ends.push(cursor)
        cursor = cursor + 1
        starts.push(cursor)
      }
      Some(_) => cursor = cursor + 1
      None => cursor = cursor + 1
    }
  }
  ends.push(content.length())
  { content, starts, ends }
}

///|
/// Return the number of logical source lines.
pub fn SourceTextIndex::line_count(self : SourceTextIndex) -> Int {
  self.starts.length()
}

///|
/// Return one indexed line.
pub fn SourceTextIndex::line(
  self : SourceTextIndex,
  number : Int,
) -> SourceLine? {
  if number < 0 || number >= self.starts.length() {
    None
  } else {
    let start = self.starts[number]
    let end = self.ends[number]
    Some({ number, start, end, text: self.content[start:end].to_owned() })
  }
}

///|
/// Check whether a zero-based original position exists in the source.
pub fn SourceTextIndex::contains(
  self : SourceTextIndex,
  line~ : Int,
  column~ : Int,
) -> Bool {
  match self.line(line) {
    Some(source_line) => column >= 0 && column <= source_line.text.length()
    None => false
  }
}

///|
fn context_bounds(line : Int, radius : Int, count : Int) -> (Int, Int) {
  let safe_radius = if radius < 0 { 0 } else { radius }
  let start = if line < safe_radius { 0 } else { line - safe_radius }
  let wanted_end = line + safe_radius + 1
  let end = if wanted_end > count { count } else { wanted_end }
  (start, end)
}

///|
/// Extract source context around a zero-based position.
pub fn SourceTextIndex::context(
  self : SourceTextIndex,
  source? : String,
  line~ : Int,
  column~ : Int,
  radius? : Int = 1,
) -> SourceContext? {
  if !self.contains(line~, column~) {
    return None
  }
  let (start_line, end_exclusive) = context_bounds(
    line,
    radius,
    self.line_count(),
  )
  let lines : Array[String] = []
  for number in start_line.. lines.push(source_line.text)
      None => ()
    }
  }
  Some({
    source,
    focus_line: line,
    focus_column: column,
    start_line,
    end_line: end_exclusive - 1,
    lines,
  })
}

///|
/// Render source context with line numbers and a caret.
pub fn SourceContext::render(self : SourceContext) -> String {
  let output = StringBuilder()
  for index, line in self.lines {
    let number = self.start_line + index
    let marker = if number == self.focus_line { ">" } else { " " }
    output.write_string("\{marker} \{number} | \{line}")
    output.write_char('\n')
    if number == self.focus_line {
      output.write_string("    | ")
      for _ in 0.. Json {
  Json::object({
    "source": match self.source {
      Some(source) => Json::string(source)
      None => Json::null()
    },
    "focus_line": Json::number(self.focus_line.to_double()),
    "focus_column": Json::number(self.focus_column.to_double()),
    "start_line": Json::number(self.start_line.to_double()),
    "end_line": Json::number(self.end_line.to_double()),
    "lines": Json::array(self.lines.map(Json::string)),
  })
}

///|
/// Resolve embedded source context for a mapped segment.
pub fn source_context_for_mapping(
  map : DecodedSourceMap,
  mapping : Mapping,
  radius? : Int = 1,
) -> SourceContext? {
  match mapping.original {
    None => None
    Some(original) =>
      if original.source_index < 0 ||
        original.source_index >= map.sources.length() {
        None
      } else {
        let source = map.sources[original.source_index]
        match source.content {
          None => None
          Some(content) =>
            SourceTextIndex::new(content).context(
              source?=source.url,
              line=original.line,
              column=original.column,
              radius~,
            )
        }
      }
  }
}

///|
/// Resolve embedded source context for a symbolized frame.
pub fn source_context_for_frame(
  map : DecodedSourceMap,
  frame : SymbolizedFrame,
  radius? : Int = 1,
) -> SourceContext? {
  if !frame.matched {
    return None
  }
  let mapping = original_position_for(
    map,
    generated=Position::new(
      line=frame.generated.line,
      column=frame.generated.column,
    ),
  )
  match mapping {
    Some(mapping) => source_context_for_mapping(map, mapping, radius~)
    None => None
  }
}