///|
/// A generated-code span derived from adjacent mapping segments on the same
/// generated line. `end_column=None` means the span extends to the end of line.
pub struct GeneratedSpan {
  line : Int
  start_column : Int
  end_column : Int?
  source_index : Int?
  original_line : Int?
  original_column : Int?
  name_index : Int?
} derive(Eq, Debug, ToJson)

///|
pub struct GeneratedGap {
  line : Int
  start_column : Int
  end_column : Int?
} derive(Eq, Debug, ToJson)

///|
pub struct LineSummary {
  line : Int
  segment_count : Int
  mapped_segment_count : Int
  unmapped_segment_count : Int
  first_column : Int?
  last_column : Int?
} derive(Eq, Debug, ToJson)

///|
fn span_from_segment(
  segment : MappingSegment,
  end_column : Int?,
) -> GeneratedSpan {
  {
    line: segment.generated_line,
    start_column: segment.generated_column,
    end_column,
    source_index: segment.source_index,
    original_line: segment.original_line,
    original_column: segment.original_column,
    name_index: segment.name_index,
  }
}

///|
pub fn SourceMap::spans(self : SourceMap) -> Array[GeneratedSpan] {
  let ordered = sorted_mapping_copy(self.mappings)
  let spans : Array[GeneratedSpan] = []
  for i, segment in ordered {
    let end_column = if i + 1 < ordered.length() &&
      ordered[i + 1].generated_line == segment.generated_line {
      Some(ordered[i + 1].generated_column)
    } else {
      None
    }
    spans.push(span_from_segment(segment, end_column))
  }
  spans
}

///|
pub fn SourceMap::spans_for_line(
  self : SourceMap,
  line : Int,
) -> Array[GeneratedSpan] {
  self.spans().filter(span => span.line == line)
}

///|
pub fn SourceMap::gaps_for_line(
  self : SourceMap,
  line : Int,
  line_length : Int,
) -> Array[GeneratedGap] {
  let gaps : Array[GeneratedGap] = []
  let spans = self.spans_for_line(line)
  let mut cursor = 0
  for span in spans {
    if span.start_column > cursor {
      gaps.push({
        line,
        start_column: cursor,
        end_column: Some(span.start_column),
      })
    }
    cursor = match span.end_column {
      Some(end_column) => Int::max(cursor, end_column)
      None => line_length
    }
  }
  if cursor < line_length {
    gaps.push({ line, start_column: cursor, end_column: Some(line_length) })
  }
  gaps
}

///|
pub fn SourceMap::line_summaries(self : SourceMap) -> Array[LineSummary] {
  let summaries : Array[LineSummary] = []
  let line_count = self.generated_lines()
  for line in 0.. {
      segment.generated_line == line
    })
    let mapped = segments.count_if(segment => segment.is_mapped())
    let unmapped = segments.length() - mapped
    let mut first : Int? = None
    let mut last : Int? = None
    for segment in segments {
      match first {
        None => first = Some(segment.generated_column)
        Some(value) =>
          if segment.generated_column < value {
            first = Some(segment.generated_column)
          }
      }
      match last {
        None => last = Some(segment.generated_column)
        Some(value) =>
          if segment.generated_column > value {
            last = Some(segment.generated_column)
          }
      }
    }
    summaries.push({
      line,
      segment_count: segments.length(),
      mapped_segment_count: mapped,
      unmapped_segment_count: unmapped,
      first_column: first,
      last_column: last,
    })
  }
  summaries
}

///|
pub fn SourceMap::unmapped_spans(self : SourceMap) -> Array[GeneratedSpan] {
  self.spans().filter(span => span.source_index is None)
}

///|
pub fn SourceMap::mapped_spans(self : SourceMap) -> Array[GeneratedSpan] {
  self.spans().filter(span => span.source_index is Some(_))
}

///|
pub fn GeneratedSpan::contains(
  self : GeneratedSpan,
  line : Int,
  column : Int,
) -> Bool {
  if self.line != line || column < self.start_column {
    return false
  }
  match self.end_column {
    Some(end_column) => column < end_column
    None => true
  }
}

///|
pub fn SourceMap::span_at(
  self : SourceMap,
  line : Int,
  column : Int,
) -> GeneratedSpan? {
  for span in self.spans() {
    if span.contains(line, column) {
      return Some(span)
    }
  }
  None
}

///|
pub fn GeneratedSpan::width_hint(
  self : GeneratedSpan,
  fallback_end : Int,
) -> Int {
  let end_column = match self.end_column {
    Some(value) => value
    None => fallback_end
  }
  Int::max(0, end_column - self.start_column)
}