///|
/// A half-open generated interval owned by one mapping.
///
/// `end=None` means the segment remains active to the end of its generated
/// line. A next mapping on another line does not close the previous line.
pub(all) struct GeneratedSpan {
  mapping : Mapping
  end : Position?
} derive(Eq, Debug)

///|
/// Per-source mapping usage metrics.
pub(all) struct SourceUsage {
  source_index : Int
  url : String?
  mapped_segments : Int
  named_segments : Int
  original_lines : Int
  ignored : Bool
  has_content : Bool
} derive(Eq, Debug)

///|
/// Mapping distribution for one generated line.
pub(all) struct GeneratedLineUsage {
  line : Int
  segments : Int
  mapped_segments : Int
  generated_only_segments : Int
  named_segments : Int
  duplicate_positions : Int
  first_column : Int
  last_column : Int
} derive(Eq, Debug)

///|
/// Structural quality report for one decoded source map.
pub(all) struct SourceMapReport {
  file : String?
  sources : Int
  mappings : Int
  mapped_segments : Int
  generated_only_segments : Int
  named_segments : Int
  ignored_sources : Int
  embedded_sources : Int
  referenced_sources : Int
  unreferenced_sources : Int
  generated_lines : Int
  original_lines : Int
  duplicate_positions : Int
  open_spans : Int
  source_usage : Array[SourceUsage]
  generated_line_usage : Array[GeneratedLineUsage]
} derive(Eq, Debug)

///|
/// Return whether a generated position is covered by this span.
pub fn GeneratedSpan::contains(
  self : GeneratedSpan,
  position : Position,
) -> Bool {
  if position.line != self.mapping.generated.line ||
    position.compare(self.mapping.generated) < 0 {
    return false
  }
  match self.end {
    Some(end) => position.compare(end) < 0
    None => true
  }
}

///|
/// Return the finite column width, or `None` for an open span.
pub fn GeneratedSpan::column_width(self : GeneratedSpan) -> Int? {
  match self.end {
    Some(end) =>
      if end.line == self.mapping.generated.line {
        Some(end.column - self.mapping.generated.column)
      } else {
        None
      }
    None => None
  }
}

///|
/// Convert ordered mappings to generated spans.
///
/// Duplicate positions produce zero-width spans except for the last duplicate,
/// matching GLB lookup behavior.
pub fn mapping_spans(map : DecodedSourceMap) -> Array[GeneratedSpan] {
  let mappings = if mappings_are_sorted(map.mappings) {
    map.mappings.copy()
  } else {
    sort_mappings(map.mappings)
  }
  let spans : Array[GeneratedSpan] = []
  for index, mapping in mappings {
    let end = if index + 1 < mappings.length() &&
      mappings[index + 1].generated.line == mapping.generated.line {
      Some(mappings[index + 1].generated)
    } else {
      None
    }
    spans.push({ mapping, end })
  }
  spans
}

///|
/// Return spans beginning on one generated line.
pub fn mapping_spans_for_line(
  map : DecodedSourceMap,
  line : Int,
) -> Array[GeneratedSpan] {
  if line < 0 {
    return []
  }
  mapping_spans(map).filter(span => span.mapping.generated.line == line)
}

///|
fn duplicate_generated_positions(mappings : ArrayView[Mapping]) -> Int {
  if mappings.length() < 2 {
    return 0
  }
  let sorted = sort_mappings(mappings)
  let mut duplicates = 0
  for index in 1.. SourceUsage {
  let source = map.sources[source_index]
  let lines : Map[Int, Unit] = Map([])
  let mut mapped_segments = 0
  let mut named_segments = 0
  for mapping in map.mappings {
    match mapping.original {
      Some(original) =>
        if original.source_index == source_index {
          mapped_segments = mapped_segments + 1
          lines[original.line] = ()
          if mapping.name is Some(_) {
            named_segments = named_segments + 1
          }
        }
      None => ()
    }
  }
  {
    source_index,
    url: source.url,
    mapped_segments,
    named_segments,
    original_lines: lines.length(),
    ignored: source.ignored,
    has_content: source.content is Some(_),
  }
}

///|
fn all_source_usage(map : DecodedSourceMap) -> Array[SourceUsage] {
  let usage : Array[SourceUsage] = []
  for source_index in 0.. Array[GeneratedLineUsage] {
  let sorted = sort_mappings(map.mappings)
  let result : Array[GeneratedLineUsage] = []
  let mut cursor = 0
  while cursor < sorted.length() {
    let line = sorted[cursor].generated.line
    let first_column = sorted[cursor].generated.column
    let mut last_column = first_column
    let mut segments = 0
    let mut mapped_segments = 0
    let mut generated_only_segments = 0
    let mut named_segments = 0
    let mut duplicate_positions = 0
    let mut previous_column : Int? = None
    while cursor < sorted.length() && sorted[cursor].generated.line == line {
      let mapping = sorted[cursor]
      segments = segments + 1
      last_column = mapping.generated.column
      if previous_column == Some(mapping.generated.column) {
        duplicate_positions = duplicate_positions + 1
      }
      previous_column = Some(mapping.generated.column)
      if mapping.original is Some(_) {
        mapped_segments = mapped_segments + 1
      } else {
        generated_only_segments = generated_only_segments + 1
      }
      if mapping.name is Some(_) {
        named_segments = named_segments + 1
      }
      cursor = cursor + 1
    }
    result.push({
      line,
      segments,
      mapped_segments,
      generated_only_segments,
      named_segments,
      duplicate_positions,
      first_column,
      last_column,
    })
  }
  result
}

///|
/// Analyze decoded map structure without requiring generated source text.
///
/// Counts refer to mapping segments rather than bytes or characters, because a
/// source map does not carry the length of the generated artifact.
pub fn analyze_source_map(map : DecodedSourceMap) -> SourceMapReport {
  let stats = SourceMapConsumer::new(map).stats()
  let usage = all_source_usage(map)
  let mut referenced_sources = 0
  let mut ignored_sources = 0
  let mut embedded_sources = 0
  for source in usage {
    if source.mapped_segments > 0 {
      referenced_sources = referenced_sources + 1
    }
    if source.ignored {
      ignored_sources = ignored_sources + 1
    }
    if source.has_content {
      embedded_sources = embedded_sources + 1
    }
  }
  let spans = mapping_spans(map)
  let mut open_spans = 0
  for span in spans {
    if span.end is None {
      open_spans = open_spans + 1
    }
  }
  {
    file: map.file,
    sources: stats.sources,
    mappings: stats.mappings,
    mapped_segments: stats.mapped,
    generated_only_segments: stats.generated_only,
    named_segments: stats.named,
    ignored_sources,
    embedded_sources,
    referenced_sources,
    unreferenced_sources: stats.sources - referenced_sources,
    generated_lines: stats.generated_lines,
    original_lines: stats.original_lines,
    duplicate_positions: duplicate_generated_positions(map.mappings),
    open_spans,
    source_usage: usage,
    generated_line_usage: generated_line_usage(map),
  }
}

///|
fn report_optional_string(value : String?) -> Json {
  match value {
    Some(value) => Json::string(value)
    None => Json::null()
  }
}

///|
/// Convert source usage metrics to JSON.
pub fn SourceUsage::to_json(self : SourceUsage) -> Json {
  Json::object({
    "source_index": Json::number(self.source_index.to_double()),
    "url": report_optional_string(self.url),
    "mapped_segments": Json::number(self.mapped_segments.to_double()),
    "named_segments": Json::number(self.named_segments.to_double()),
    "original_lines": Json::number(self.original_lines.to_double()),
    "ignored": Json::boolean(self.ignored),
    "has_content": Json::boolean(self.has_content),
  })
}

///|
/// Convert generated-line usage to JSON.
pub fn GeneratedLineUsage::to_json(self : GeneratedLineUsage) -> Json {
  Json::object({
    "line": Json::number(self.line.to_double()),
    "segments": Json::number(self.segments.to_double()),
    "mapped_segments": Json::number(self.mapped_segments.to_double()),
    "generated_only_segments": Json::number(
      self.generated_only_segments.to_double(),
    ),
    "named_segments": Json::number(self.named_segments.to_double()),
    "duplicate_positions": Json::number(self.duplicate_positions.to_double()),
    "first_column": Json::number(self.first_column.to_double()),
    "last_column": Json::number(self.last_column.to_double()),
  })
}

///|
/// Convert the complete report to stable JSON.
pub fn SourceMapReport::to_json(self : SourceMapReport) -> Json {
  Json::object({
    "file": report_optional_string(self.file),
    "sources": Json::number(self.sources.to_double()),
    "mappings": Json::number(self.mappings.to_double()),
    "mapped_segments": Json::number(self.mapped_segments.to_double()),
    "generated_only_segments": Json::number(
      self.generated_only_segments.to_double(),
    ),
    "named_segments": Json::number(self.named_segments.to_double()),
    "ignored_sources": Json::number(self.ignored_sources.to_double()),
    "embedded_sources": Json::number(self.embedded_sources.to_double()),
    "referenced_sources": Json::number(self.referenced_sources.to_double()),
    "unreferenced_sources": Json::number(self.unreferenced_sources.to_double()),
    "generated_lines": Json::number(self.generated_lines.to_double()),
    "original_lines": Json::number(self.original_lines.to_double()),
    "duplicate_positions": Json::number(self.duplicate_positions.to_double()),
    "open_spans": Json::number(self.open_spans.to_double()),
    "source_usage": Json::array(
      self.source_usage.map(source => source.to_json()),
    ),
    "generated_line_usage": Json::array(
      self.generated_line_usage.map(line => line.to_json()),
    ),
  })
}

///|
fn report_source_label(source : SourceUsage) -> String {
  match source.url {
    Some(url) => url
    None => ""
  }
}

///|
/// Render a compact report for terminals and CI logs.
pub fn SourceMapReport::render(self : SourceMapReport) -> String {
  let output = StringBuilder()
  let file_label = self.file.unwrap_or("")
  output.write_string(
    "file: \{file_label}\n" +
    "sources: \{self.sources} (\{self.referenced_sources} referenced, " +
    "\{self.unreferenced_sources} unreferenced, \{self.embedded_sources} embedded)\n" +
    "mappings: \{self.mappings} (\{self.mapped_segments} mapped, " +
    "\{self.generated_only_segments} generated-only, \{self.named_segments} named)\n" +
    "lines: \{self.generated_lines} generated, \{self.original_lines} original\n" +
    "duplicates: \{self.duplicate_positions}; open spans: \{self.open_spans}\n",
  )
  for source in self.source_usage {
    let flags = StringBuilder()
    if source.ignored {
      flags.write_string(" ignored")
    }
    if source.has_content {
      flags.write_string(" embedded")
    }
    output.write_string(
      "[\{source.source_index}] \{report_source_label(source)}: " +
      "\{source.mapped_segments} mappings, \{source.original_lines} lines" +
      flags.to_string() +
      "\n",
    )
  }
  output.to_string()
}

///|
/// Produce reviewer-facing quality diagnostics from report metrics.
pub fn SourceMapReport::diagnostics(
  self : SourceMapReport,
) -> Array[Diagnostic] {
  let diagnostics : Array[Diagnostic] = []
  if self.mappings == 0 {
    diagnostics.push(
      Diagnostic::warning(
        code="empty_map",
        message="source map contains no mapping segments",
        path="$.mappings",
      ),
    )
  }
  if self.duplicate_positions > 0 {
    diagnostics.push(
      Diagnostic::warning(
        code="duplicate_generated_positions",
        message="source map contains \{self.duplicate_positions} duplicate generated positions",
        path="$.mappings",
      ),
    )
  }
  if self.unreferenced_sources > 0 {
    diagnostics.push(
      Diagnostic::warning(
        code="unreferenced_sources",
        message="\{self.unreferenced_sources} source entries are never referenced",
        path="$.sources",
      ),
    )
  }
  if self.sources > 0 && self.embedded_sources == 0 {
    diagnostics.push(
      Diagnostic::warning(
        code="no_embedded_sources",
        message="sourcesContent is unavailable for every source",
        path="$.sourcesContent",
      ),
    )
  }
  if self.mappings > 0 && self.generated_only_segments * 2 > self.mappings {
    diagnostics.push(
      Diagnostic::warning(
        code="mostly_generated_only",
        message="more than half of mapping segments lack original positions",
        path="$.mappings",
      ),
    )
  }
  diagnostics
}

///|
/// Parse, flatten and analyze a source-map JSON document.
pub fn analyze_source_map_json(
  input : StringView,
  map_url? : String,
) -> SourceMapReport raise SourceMapError {
  analyze_source_map(decode_document(parse_document(input), map_url?))
}

///|
/// Percentage of sources referenced by at least one mapping.
pub fn SourceMapReport::source_coverage_percent(self : SourceMapReport) -> Int {
  if self.sources == 0 {
    100
  } else {
    self.referenced_sources * 100 / self.sources
  }
}

///|
/// Percentage of segments carrying original positions.
pub fn SourceMapReport::mapping_coverage_percent(self : SourceMapReport) -> Int {
  if self.mappings == 0 {
    100
  } else {
    self.mapped_segments * 100 / self.mappings
  }
}

///|
/// Percentage of sources with embedded content.
pub fn SourceMapReport::content_coverage_percent(self : SourceMapReport) -> Int {
  if self.sources == 0 {
    100
  } else {
    self.embedded_sources * 100 / self.sources
  }
}

///|
/// Return whether every segment maps to an original position.
pub fn SourceMapReport::has_complete_mappings(self : SourceMapReport) -> Bool {
  self.mappings > 0 && self.generated_only_segments == 0
}

///|
/// Return whether every declared source is referenced.
pub fn SourceMapReport::has_complete_source_usage(
  self : SourceMapReport,
) -> Bool {
  self.unreferenced_sources == 0
}

///|
/// Return whether every source carries embedded content.
pub fn SourceMapReport::has_complete_sources_content(
  self : SourceMapReport,
) -> Bool {
  self.sources == self.embedded_sources
}

///|
/// Return a concise CI summary line.
pub fn SourceMapReport::summary(self : SourceMapReport) -> String {
  "mappings=\{self.mappings} mapped=\{self.mapping_coverage_percent()}% " +
  "sources=\{self.sources} referenced=\{self.source_coverage_percent()}% " +
  "embedded=\{self.content_coverage_percent()}% duplicates=\{self.duplicate_positions}"
}

///|
/// Return generated-line metrics for an exact line number.
pub fn SourceMapReport::generated_line(
  self : SourceMapReport,
  line : Int,
) -> GeneratedLineUsage? {
  for usage in self.generated_line_usage {
    if usage.line == line {
      return Some(usage)
    }
  }
  None
}

///|
/// Return the generated line with the most mapping segments.
pub fn SourceMapReport::busiest_generated_line(
  self : SourceMapReport,
) -> GeneratedLineUsage? {
  let mut busiest : GeneratedLineUsage? = None
  for usage in self.generated_line_usage {
    match busiest {
      Some(previous) =>
        if usage.segments > previous.segments {
          busiest = Some(usage)
        }
      None => busiest = Some(usage)
    }
  }
  busiest
}

///|
/// Count generated lines that contain no mapping segments.
pub fn SourceMapReport::empty_generated_lines(self : SourceMapReport) -> Int {
  if self.generated_lines == 0 {
    return 0
  }
  self.generated_lines - self.generated_line_usage.length()
}

///|
/// Return source indexes that are never referenced by mappings.
pub fn SourceMapReport::unreferenced_source_indices(
  self : SourceMapReport,
) -> Array[Int] {
  let result : Array[Int] = []
  for usage in self.source_usage {
    if usage.mapped_segments == 0 {
      result.push(usage.source_index)
    }
  }
  result
}

///|
/// Return source indexes marked as ignored.
pub fn SourceMapReport::ignored_source_indices(
  self : SourceMapReport,
) -> Array[Int] {
  let result : Array[Int] = []
  for usage in self.source_usage {
    if usage.ignored {
      result.push(usage.source_index)
    }
  }
  result
}

///|
/// Return source indexes without embedded source content.
pub fn SourceMapReport::sources_without_content(
  self : SourceMapReport,
) -> Array[Int] {
  let result : Array[Int] = []
  for usage in self.source_usage {
    if !usage.has_content {
      result.push(usage.source_index)
    }
  }
  result
}

///|
/// Return generated line numbers containing duplicate positions.
pub fn SourceMapReport::lines_with_duplicates(
  self : SourceMapReport,
) -> Array[Int] {
  let result : Array[Int] = []
  for usage in self.generated_line_usage {
    if usage.duplicate_positions > 0 {
      result.push(usage.line)
    }
  }
  result
}

///|
/// Return generated lines containing at least one generated-only segment.
pub fn SourceMapReport::generated_only_lines(
  self : SourceMapReport,
) -> Array[Int] {
  let result : Array[Int] = []
  for usage in self.generated_line_usage {
    if usage.generated_only_segments > 0 {
      result.push(usage.line)
    }
  }
  result
}

///|
/// Return whether a source is referenced by at least one segment.
pub fn SourceUsage::is_referenced(self : SourceUsage) -> Bool {
  self.mapped_segments > 0
}

///|
/// Percentage of segments on this line that carry original positions.
pub fn GeneratedLineUsage::mapping_coverage_percent(
  self : GeneratedLineUsage,
) -> Int {
  if self.segments == 0 {
    100
  } else {
    self.mapped_segments * 100 / self.segments
  }
}

///|
/// Percentage of mapped segments that preserve a symbol name.
pub fn SourceMapReport::named_mapping_percent(self : SourceMapReport) -> Int {
  if self.mapped_segments == 0 {
    100
  } else {
    self.named_segments * 100 / self.mapped_segments
  }
}

///|
/// Percentage of source entries marked as ignored.
pub fn SourceMapReport::ignored_source_percent(self : SourceMapReport) -> Int {
  if self.sources == 0 {
    0
  } else {
    self.ignored_sources * 100 / self.sources
  }
}

///|
/// Find the usage summary for a source index.
pub fn SourceMapReport::source(
  self : SourceMapReport,
  source_index : Int,
) -> SourceUsage? {
  for usage in self.source_usage {
    if usage.source_index == source_index {
      return Some(usage)
    }
  }
  None
}

///|
/// Return whether the report contains a mapping or source-quality warning.
pub fn SourceMapReport::has_quality_warnings(self : SourceMapReport) -> Bool {
  self.generated_only_segments > 0 ||
  self.duplicate_positions > 0 ||
  self.unreferenced_sources > 0
}

///|
/// Count source entries that are both referenced and backed by embedded text.
pub fn SourceMapReport::usable_embedded_sources(self : SourceMapReport) -> Int {
  let mut count = 0
  for usage in self.source_usage {
    if usage.is_referenced() && usage.has_content {
      count += 1
    }
  }
  count
}