///|
fn source_name_at(map : SourceMap, index : Int) -> String? {
  if index >= 0 && index < map.sources.length() {
    Some(map.sources[index])
  } else {
    None
  }
}

///|
fn name_at(map : SourceMap, index : Int?) -> String? {
  match index {
    Some(i) =>
      if i >= 0 && i < map.names.length() {
        Some(map.names[i])
      } else {
        None
      }
    None => None
  }
}

///|
fn segment_to_original(
  map : SourceMap,
  segment : MappingSegment,
) -> OriginalPosition? {
  match (segment.source_index, segment.original_line, segment.original_column) {
    (Some(source_index), Some(line), Some(column)) =>
      match source_name_at(map, source_index) {
        Some(source) =>
          Some({ source, line, column, name: name_at(map, segment.name_index) })
        None => None
      }
    _ => None
  }
}

///|
pub fn SourceMap::find_original(
  self : SourceMap,
  generated_line : Int,
  generated_column : Int,
) -> OriginalPosition? {
  let mut best : MappingSegment? = None
  for segment in self.mappings {
    if segment.generated_line == generated_line &&
      segment.generated_column <= generated_column &&
      segment.is_mapped() {
      match best {
        None => best = Some(segment)
        Some(prev) =>
          if segment.generated_column >= prev.generated_column {
            best = Some(segment)
          }
      }
    }
  }
  match best {
    Some(segment) => segment_to_original(self, segment)
    None => None
  }
}

///|
pub fn SourceMap::find_generated(
  self : SourceMap,
  source : StringView,
  original_line : Int,
  original_column : Int,
) -> GeneratedPosition? {
  let mut best : MappingSegment? = None
  let source = source.to_owned()
  for segment in self.mappings {
    match
      (segment.source_index, segment.original_line, segment.original_column) {
      (Some(source_index), Some(line), Some(column)) =>
        if source_index >= 0 &&
          source_index < self.sources.length() &&
          self.sources[source_index] == source &&
          line == original_line &&
          column <= original_column {
          match best {
            None => best = Some(segment)
            Some(prev) =>
              if column >= prev.original_column.unwrap_or(-1) {
                best = Some(segment)
              }
          }
        }
      _ => ()
    }
  }
  match best {
    Some(segment) => Some(segment.generated_position())
    None => None
  }
}

///|
pub fn SourceMap::segments_for_source(
  self : SourceMap,
  source : StringView,
) -> Array[MappingSegment] {
  let out : Array[MappingSegment] = []
  let source = source.to_owned()
  for segment in self.mappings {
    if segment.source_index is Some(index) {
      if index >= 0 &&
        index < self.sources.length() &&
        self.sources[index] == source {
        out.push(segment)
      }
    }
  }
  out
}

///|
pub fn SourceMap::generated_lines(self : SourceMap) -> Int {
  let mut max_line = -1
  for segment in self.mappings {
    if segment.generated_line > max_line {
      max_line = segment.generated_line
    }
  }
  max_line + 1
}

///|
pub fn SourceMap::explain_generated(
  self : SourceMap,
  generated_line : Int,
  generated_column : Int,
) -> String {
  match self.find_original(generated_line, generated_column) {
    Some(original) => {
      let name_part = match original.name {
        Some(name) => " name=\{name}"
        None => ""
      }
      "generated \{generated_line}:\{generated_column} -> \{original.source}:\{original.line}:\{original.column}\{name_part}"
    }
    None => "generated \{generated_line}:\{generated_column} is unmapped"
  }
}

///|
pub fn SourceMap::shift_generated(
  self : SourceMap,
  line_delta : Int,
  column_delta_for_first_line : Int,
) -> SourceMap {
  let shifted = self.mappings.map(segment => {
    let column_delta = if segment.generated_line == 0 {
      column_delta_for_first_line
    } else {
      0
    }
    {
      ..segment,
      generated_line: segment.generated_line + line_delta,
      generated_column: segment.generated_column + column_delta,
    }
  })
  { ..self, mappings: shifted }
}