///|
/// Half-open generated-code range. `end_line=None` means the range has no upper
/// bound. When `end_line` is present and `end_column=None`, the range ends at
/// the start of `end_line`.
pub struct GeneratedRange {
  start_line : Int
  start_column : Int
  end_line : Int?
  end_column : Int?
} derive(Eq, Debug, ToJson)

///|
pub struct OriginalRange {
  source : String
  start_line : Int
  start_column : Int
  end_line : Int?
  end_column : Int?
} derive(Eq, Debug, ToJson)

///|
pub fn GeneratedRange::GeneratedRange(
  start_line : Int,
  start_column? : Int = 0,
  end_line? : Int,
  end_column? : Int,
) -> GeneratedRange {
  { start_line, start_column, end_line, end_column }
}

///|
pub fn OriginalRange::OriginalRange(
  source : StringView,
  start_line : Int,
  start_column? : Int = 0,
  end_line? : Int,
  end_column? : Int,
) -> OriginalRange {
  { source: source.to_owned(), start_line, start_column, end_line, end_column }
}

///|
fn before_range_start(
  line : Int,
  column : Int,
  start_line : Int,
  start_column : Int,
) -> Bool {
  line < start_line || (line == start_line && column < start_column)
}

///|
fn at_or_after_range_end(
  line : Int,
  column : Int,
  end_line : Int?,
  end_column : Int?,
) -> Bool {
  match end_line {
    None => false
    Some(limit_line) =>
      if line > limit_line {
        true
      } else if line < limit_line {
        false
      } else {
        match end_column {
          Some(limit_column) => column >= limit_column
          None => true
        }
      }
  }
}

///|
pub fn GeneratedRange::contains(
  self : GeneratedRange,
  line : Int,
  column : Int,
) -> Bool {
  !before_range_start(line, column, self.start_line, self.start_column) &&
  !at_or_after_range_end(line, column, self.end_line, self.end_column)
}

///|
pub fn OriginalRange::contains(
  self : OriginalRange,
  source : StringView,
  line : Int,
  column : Int,
) -> Bool {
  source.to_owned() == self.source &&
  !before_range_start(line, column, self.start_line, self.start_column) &&
  !at_or_after_range_end(line, column, self.end_line, self.end_column)
}

///|
pub fn GeneratedRange::is_empty(self : GeneratedRange) -> Bool {
  match self.end_line {
    Some(end_line) =>
      if end_line < self.start_line {
        true
      } else if end_line == self.start_line {
        match self.end_column {
          Some(end_column) => end_column <= self.start_column
          None => true
        }
      } else {
        false
      }
    None => false
  }
}

///|
pub fn OriginalRange::is_empty(self : OriginalRange) -> Bool {
  match self.end_line {
    Some(end_line) =>
      if end_line < self.start_line {
        true
      } else if end_line == self.start_line {
        match self.end_column {
          Some(end_column) => end_column <= self.start_column
          None => true
        }
      } else {
        false
      }
    None => false
  }
}

///|
fn rebase_generated_segment(
  segment : MappingSegment,
  range : GeneratedRange,
) -> MappingSegment {
  let line = segment.generated_line - range.start_line
  let column = if segment.generated_line == range.start_line {
    segment.generated_column - range.start_column
  } else {
    segment.generated_column
  }
  { ..segment, generated_line: line, generated_column: column }
}

///|
pub fn SourceMap::slice_generated_range(
  self : SourceMap,
  range : GeneratedRange,
  rebase? : Bool = true,
) -> SourceMap {
  if range.is_empty() {
    return { ..self, mappings: [] }.prune_unused()
  }
  let mappings : Array[MappingSegment] = []
  for segment in self.mappings {
    if range.contains(segment.generated_line, segment.generated_column) {
      if rebase {
        mappings.push(rebase_generated_segment(segment, range))
      } else {
        mappings.push(segment)
      }
    }
  }
  { ..self, mappings, }.prune_unused().sorted()
}

///|
pub fn SourceMap::drop_generated_before(
  self : SourceMap,
  line : Int,
  column? : Int = 0,
) -> SourceMap {
  self.slice_generated_range(
    GeneratedRange::GeneratedRange(line, start_column=column),
    rebase=false,
  )
}

///|
pub fn SourceMap::take_generated_before(
  self : SourceMap,
  line : Int,
  column? : Int,
) -> SourceMap {
  let range = match column {
    Some(value) =>
      GeneratedRange::GeneratedRange(0, end_line=line, end_column=value)
    None => GeneratedRange::GeneratedRange(0, end_line=line)
  }
  self.slice_generated_range(range, rebase=false)
}

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

///|
fn segment_in_original_range(
  map : SourceMap,
  segment : MappingSegment,
  range : OriginalRange,
) -> Bool {
  match
    (
      segment_original_source(map, segment),
      segment.original_line,
      segment.original_column,
    ) {
    (Some(source), Some(line), Some(column)) =>
      range.contains(source, line, column)
    _ => false
  }
}

///|
pub fn SourceMap::slice_original_range(
  self : SourceMap,
  range : OriginalRange,
  keep_unmapped? : Bool = false,
) -> SourceMap {
  if range.is_empty() {
    let mappings = if keep_unmapped {
      self.mappings.filter(segment => !segment.is_mapped())
    } else {
      []
    }
    return { ..self, mappings, }.prune_unused().sorted()
  }
  let mappings : Array[MappingSegment] = []
  for segment in self.mappings {
    if segment_in_original_range(self, segment, range) ||
      (keep_unmapped && !segment.is_mapped()) {
      mappings.push(segment)
    }
  }
  { ..self, mappings, }.prune_unused().sorted()
}

///|
pub fn SourceMap::sources_in_generated_range(
  self : SourceMap,
  range : GeneratedRange,
) -> Array[String] {
  let out : Array[String] = []
  for segment in self.mappings {
    if range.contains(segment.generated_line, segment.generated_column) {
      match segment_original_source(self, segment) {
        Some(source) => if !out.contains(source) { out.push(source) }
        None => ()
      }
    }
  }
  out
}

///|
pub fn SourceMap::generated_range_summary(
  self : SourceMap,
  range : GeneratedRange,
) -> String {
  let sliced = self.slice_generated_range(range, rebase=false)
  let stats = sliced.stats()
  let sources = sliced.sources.join(", ")
  "range segments=\{stats.total_segments} mapped=\{stats.mapped_segments} sources=[\{sources}]"
}