///|
pub struct ComposeSummary {
  input_segments : Int
  composed_segments : Int
  preserved_segments : Int
  unmapped_segments : Int
  missing_intermediate_segments : Int
} derive(Eq, Debug, ToJson)

///|
fn segment_source_name(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_name_value(map : SourceMap, segment : MappingSegment) -> String? {
  match segment.name_index {
    Some(index) =>
      if index >= 0 && index < map.names.length() {
        Some(map.names[index])
      } else {
        None
      }
    None => None
  }
}

///|
fn should_compose_source(source : String, selected : String?) -> Bool {
  match selected {
    Some(value) => source == value
    None => true
  }
}

///|
fn add_builder_mapping(
  builder : SourceMapBuilder,
  generated_line : Int,
  generated_column : Int,
  source : String,
  original_line : Int,
  original_column : Int,
  name : String?,
) -> Unit {
  match name {
    Some(value) =>
      builder.add_mapping(
        generated_line,
        generated_column,
        source,
        original_line,
        original_column,
        name=value,
      )
    None =>
      builder.add_mapping(
        generated_line, generated_column, source, original_line, original_column,
      )
  }
}

///|
fn best_composed_name(
  outer : SourceMap,
  segment : MappingSegment,
  original : OriginalPosition,
) -> String? {
  match original.name {
    Some(value) => Some(value)
    None => segment_name_value(outer, segment)
  }
}

///|
fn preserve_outer_segment(
  builder : SourceMapBuilder,
  outer : SourceMap,
  segment : MappingSegment,
) -> Unit {
  match
    (
      segment_source_name(outer, segment),
      segment.original_line,
      segment.original_column,
    ) {
    (Some(source), Some(line), Some(column)) =>
      add_builder_mapping(
        builder,
        segment.generated_line,
        segment.generated_column,
        source,
        line,
        column,
        segment_name_value(outer, segment),
      )
    _ => builder.add_unmapped(segment.generated_line, segment.generated_column)
  }
}

///|
pub fn SourceMap::compose_with(
  self : SourceMap,
  previous : SourceMap,
  intermediate_source? : String,
  preserve_unresolved? : Bool = true,
) -> SourceMap {
  let builder = SourceMapBuilder()
  for segment in sorted_mapping_copy(self.mappings) {
    match
      (
        segment_source_name(self, segment),
        segment.original_line,
        segment.original_column,
      ) {
      (Some(source), Some(line), Some(column)) =>
        if should_compose_source(source, intermediate_source) {
          match previous.find_original(line, column) {
            Some(original) =>
              add_builder_mapping(
                builder,
                segment.generated_line,
                segment.generated_column,
                original.source,
                original.line,
                original.column,
                best_composed_name(self, segment, original),
              )
            None =>
              if preserve_unresolved {
                preserve_outer_segment(builder, self, segment)
              } else {
                builder.add_unmapped(
                  segment.generated_line,
                  segment.generated_column,
                )
              }
          }
        } else {
          preserve_outer_segment(builder, self, segment)
        }
      _ =>
        builder.add_unmapped(segment.generated_line, segment.generated_column)
    }
  }
  let result = builder.build()
  { ..result, file: self.file, source_root: previous.source_root }.canonical()
}

///|
pub fn SourceMap::compose_summary(
  self : SourceMap,
  previous : SourceMap,
  intermediate_source? : String,
) -> ComposeSummary {
  let mut composed = 0
  let mut preserved = 0
  let mut unmapped = 0
  let mut missing = 0
  for segment in self.mappings {
    match
      (
        segment_source_name(self, segment),
        segment.original_line,
        segment.original_column,
      ) {
      (Some(source), Some(line), Some(column)) =>
        if should_compose_source(source, intermediate_source) {
          match previous.find_original(line, column) {
            Some(_) => composed += 1
            None => {
              preserved += 1
              missing += 1
            }
          }
        } else {
          preserved += 1
        }
      _ => unmapped += 1
    }
  }
  {
    input_segments: self.mappings.length(),
    composed_segments: composed,
    preserved_segments: preserved,
    unmapped_segments: unmapped,
    missing_intermediate_segments: missing,
  }
}

///|
pub fn ComposeSummary::summary(self : ComposeSummary) -> String {
  "compose input=\{self.input_segments} composed=\{self.composed_segments} preserved=\{self.preserved_segments} unmapped=\{self.unmapped_segments} missing=\{self.missing_intermediate_segments}"
}

///|
pub fn SourceMap::compose_report(
  self : SourceMap,
  previous : SourceMap,
  intermediate_source? : String,
) -> String {
  let summary = match intermediate_source {
    Some(source) => self.compose_summary(previous, intermediate_source=source)
    None => self.compose_summary(previous)
  }
  let result = match intermediate_source {
    Some(source) => self.compose_with(previous, intermediate_source=source)
    None => self.compose_with(previous)
  }
  let stats = result.stats()
  let buf = StringBuilder()
  buf.write_string("SourceMap compose\n")
  buf.write_string(summary.summary())
  buf.write_char('\n')
  buf.write_string(stats.summary())
  buf.write_char('\n')
  for source in result.sources {
    buf.write_string("source: \{source}\n")
  }
  buf.to_string()
}

///|
pub fn compose_chain(maps : ArrayView[SourceMap]) -> SourceMap {
  if maps.length() == 0 {
    return SourceMap::empty()
  }
  let mut current = maps[0]
  for i in 1..