///|
pub struct SourceMapLintOptions {
  require_file : Bool
  require_sorted_mappings : Bool
  require_no_duplicate_sources : Bool
  require_no_duplicate_names : Bool
  require_all_sources_used : Bool
  require_all_names_used : Bool
  min_mapped_ratio : Double?
  max_unmapped_segments : Int?
} derive(Eq, Debug, ToJson)

///|
pub struct LintSummary {
  errors : Int
  warnings : Int
  mapped_ratio : Double
  unused_sources : Int
  unused_names : Int
  duplicate_sources : Int
  duplicate_names : Int
} derive(Eq, Debug, ToJson)

///|
pub fn SourceMapLintOptions::SourceMapLintOptions(
  require_file? : Bool = false,
  require_sorted_mappings? : Bool = true,
  require_no_duplicate_sources? : Bool = true,
  require_no_duplicate_names? : Bool = true,
  require_all_sources_used? : Bool = false,
  require_all_names_used? : Bool = false,
  min_mapped_ratio? : Double,
  max_unmapped_segments? : Int,
) -> SourceMapLintOptions {
  {
    require_file,
    require_sorted_mappings,
    require_no_duplicate_sources,
    require_no_duplicate_names,
    require_all_sources_used,
    require_all_names_used,
    min_mapped_ratio,
    max_unmapped_segments,
  }
}

///|
pub fn SourceMapLintOptions::ci() -> SourceMapLintOptions {
  {
    require_file: true,
    require_sorted_mappings: true,
    require_no_duplicate_sources: true,
    require_no_duplicate_names: true,
    require_all_sources_used: true,
    require_all_names_used: false,
    min_mapped_ratio: Some(0.50),
    max_unmapped_segments: None,
  }
}

///|
fn mapping_order_is_sorted(segments : ArrayView[MappingSegment]) -> Bool {
  let mut previous_line = -1
  let mut previous_column = -1
  for segment in segments {
    if segment.generated_line < previous_line {
      return false
    }
    if segment.generated_line == previous_line &&
      segment.generated_column < previous_column {
      return false
    }
    previous_line = segment.generated_line
    previous_column = segment.generated_column
  }
  true
}

///|
fn duplicate_count(items : ArrayView[String]) -> Int {
  let seen : Array[String] = []
  let duplicates : Array[String] = []
  for item in items {
    if seen.contains(item) {
      if !duplicates.contains(item) {
        duplicates.push(item)
      }
    } else {
      seen.push(item)
    }
  }
  duplicates.length()
}

///|
fn push_duplicate_diagnostics(
  diagnostics : Array[Diagnostic],
  items : ArrayView[String],
  path : String,
  code : String,
) -> Unit {
  let reported : Array[String] = []
  for i, item in items {
    let start = i + 1
    for j in start.. Unit {
  for value in values {
    diagnostics.push(
      diag_warning(code, path, "unused \{label} '\{value}' can be pruned"),
    )
  }
}

///|
pub fn SourceMap::lint(
  self : SourceMap,
  line_lengths? : ArrayView[Int] = [],
  options? : SourceMapLintOptions = SourceMapLintOptions::ci(),
) -> Array[Diagnostic] {
  let diagnostics = self.validate()
  if options.require_file && self.file is None {
    diagnostics.push(
      diag_error("lint.file", "file", "lint requires file field"),
    )
  }
  if options.require_sorted_mappings && !mapping_order_is_sorted(self.mappings) {
    diagnostics.push(
      diag_error(
        "lint.mapping.order", "mappings", "mappings must be sorted before publishing",
      ),
    )
  }
  if options.require_no_duplicate_sources {
    push_duplicate_diagnostics(
      diagnostics,
      self.sources,
      "sources",
      "lint.source.duplicate",
    )
  }
  if options.require_no_duplicate_names {
    push_duplicate_diagnostics(
      diagnostics,
      self.names,
      "names",
      "lint.name.duplicate",
    )
  }
  if options.require_all_sources_used {
    push_unused_string_diagnostics(
      diagnostics,
      self.unused_sources(),
      "sources",
      "lint.source.unused",
      "source",
    )
  }
  if options.require_all_names_used {
    push_unused_string_diagnostics(
      diagnostics,
      self.unused_names(),
      "names",
      "lint.name.unused",
      "name",
    )
  }
  if options.max_unmapped_segments is Some(limit) {
    let unmapped = self.stats().unmapped_segments
    if unmapped > limit {
      diagnostics.push(
        diag_error(
          "lint.unmapped.max",
          "mappings",
          "unmapped segment count \{unmapped} exceeds limit \{limit}",
        ),
      )
    }
  }
  if options.min_mapped_ratio is Some(limit) {
    let coverage = self.coverage_for_lengths(line_lengths)
    if coverage.total_columns > 0 && coverage.mapped_ratio < limit {
      diagnostics.push(
        diag_error(
          "lint.coverage.min",
          "mappings",
          "mapped ratio \{coverage.mapped_ratio} is below required \{limit}",
        ),
      )
    }
  }
  diagnostics
}

///|
pub fn SourceMap::lint_summary(
  self : SourceMap,
  line_lengths? : ArrayView[Int] = [],
  options? : SourceMapLintOptions = SourceMapLintOptions::ci(),
) -> LintSummary {
  let diagnostics = self.lint(line_lengths~, options~)
  let coverage = self.coverage_for_lengths(line_lengths)
  {
    errors: diagnostics.count_if(d => d.severity == "error"),
    warnings: diagnostics.count_if(d => d.severity == "warning"),
    mapped_ratio: coverage.mapped_ratio,
    unused_sources: self.unused_sources().length(),
    unused_names: self.unused_names().length(),
    duplicate_sources: duplicate_count(self.sources),
    duplicate_names: duplicate_count(self.names),
  }
}

///|
pub fn LintSummary::passes(self : LintSummary) -> Bool {
  self.errors == 0
}

///|
pub fn LintSummary::summary(self : LintSummary) -> String {
  "lint errors=\{self.errors} warnings=\{self.warnings} mapped_ratio=\{self.mapped_ratio} unused_sources=\{self.unused_sources} unused_names=\{self.unused_names} duplicate_sources=\{self.duplicate_sources} duplicate_names=\{self.duplicate_names}"
}

///|
pub fn SourceMap::lint_report(
  self : SourceMap,
  line_lengths? : ArrayView[Int] = [],
  options? : SourceMapLintOptions = SourceMapLintOptions::ci(),
) -> String {
  let summary = self.lint_summary(line_lengths~, options~)
  let diagnostics = self.lint(line_lengths~, options~)
  let buf = StringBuilder()
  buf.write_string("SourceMap lint\n")
  buf.write_string(summary.summary())
  buf.write_char('\n')
  for diagnostic in diagnostics {
    buf.write_string(
      "- [\{diagnostic.severity}] \{diagnostic.code} \{diagnostic.path}: \{diagnostic.message}\n",
    )
  }
  buf.to_string()
}

///|
pub fn SourceMap::passes_lint(
  self : SourceMap,
  line_lengths? : ArrayView[Int] = [],
  options? : SourceMapLintOptions = SourceMapLintOptions::ci(),
) -> Bool {
  self.lint_summary(line_lengths~, options~).passes()
}