///|
pub(all) struct TextMapping {
  paragraph : Int
  run : Int
  output_start : Int
  output_end : Int
  source : Span
} derive(Eq, Debug, ToJson)

///|
pub(all) struct MappedText {
  text : String
  mappings : Array[TextMapping]
} derive(Eq, Debug, ToJson)

///|
/// UTF-16 output ranges map to encompassing byte ranges, not one-to-one bytes.
pub fn Document::mapped_text(
  self : Document,
  include_hidden? : Bool = false,
  region? : Region = Body,
) -> MappedText {
  let out = StringBuilder()
  let mappings : Array[TextMapping] = []
  let mut offset = 0
  let mut first = true
  for p in 0.. Span? {
  if output_offset < 0 || output_offset >= self.text.length() {
    return None
  }
  let mut low = 0
  let mut high = self.mappings.length()
  while low < high {
    let mid = low + (high - low) / 2
    let mapping = self.mappings[mid]
    if output_offset < mapping.output_start {
      high = mid
    } else if output_offset >= mapping.output_end {
      low = mid + 1
    } else {
      return Some(mapping.source)
    }
  }
  None
}

///|
/// Map a nonempty, half-open UTF-16 selection to its contributing source runs.
/// Returns None for out-of-bounds, empty or reversed ranges. A selection of
/// generated separators is valid but has no sources. Source spans encompass
/// whole runs, even when only part of a run is selected. As with source_at,
/// offsets may address either UTF-16 unit of a supplementary character.
pub fn MappedText::source_range(
  self : MappedText,
  start : Int,
  end : Int,
) -> TextMatch? {
  if start < 0 || end <= start || end > self.text.length() {
    return None
  }
  Some(self.match_range(start, end))
}

///|
pub fn Document::runs_at_source(
  self : Document,
  byte_offset : Int,
) -> Array[Run] {
  let result : Array[Run] = []
  if byte_offset < 0 || byte_offset >= self.input_bytes {
    return result
  }
  for paragraph in self.paragraphs {
    for run in paragraph.runs {
      if run.span.contains(byte_offset) {
        result.push(run)
      }
    }
  }
  result
}

///|
pub fn MappedText::to_json_text(self : MappedText) -> String {
  Json::object({
    "schema": Json::string("moonrtf.source-map.v1"),
    "offset_unit": Json::string("utf16"),
    "text": Json::string(self.text),
    "mappings": Json::array(
      self.mappings.map(m => {
        Json::object({
          "paragraph": m.paragraph.to_json(),
          "run": m.run.to_json(),
          "output_start": m.output_start.to_json(),
          "output_end": m.output_end.to_json(),
          "source": span_json(m.source),
        })
      }),
    ),
  }).stringify()
}