///|
fn ignore_list(text : String) -> Array[String] raise ParseError {
  let text = text.split("#").collect()[0].trim().to_owned()
  let codes = text.split(",").map(c => c.trim().to_owned()).collect()
  for code in codes {
    if code.length() != 6 ||
      !["DL", "ML", "SC"].contains(code[:2].to_owned()) ||
      !code[2:].iter().all(c => c >= '0' && c <= '9') {
      raise Invalid("invalid suppression code: " + code)
    }
  }
  codes
}

///|
/// Caller suppression plus hadolint-compatible instruction/global comments.
pub fn check(
  source : String,
  ignore_codes? : Array[String] = [],
  honor_inline? : Bool = true,
  trusted_registries? : Array[String] = [],
) -> Array[Diagnostic] raise ParseError {
  let instructions = parse(source)
  let global_codes = ignore_codes.copy()
  let local_codes : Map[Int, Array[String]] = Map([])
  if honor_inline {
    let mut cursor = 0
    for i, raw in source.split("\n").collect() {
      let line = i + 1
      while cursor < instructions.length() &&
            instructions[cursor].end_line < line {
        cursor += 1
      }
      // Never interpret heredoc content or continuation comments as linter directives.
      if cursor < instructions.length() && instructions[cursor].line <= line {
        continue
      }
      let comment = raw.trim().to_owned()
      let global_prefix = "# hadolint global ignore="
      let local_prefix = "# hadolint ignore="
      if comment.has_prefix(global_prefix) {
        for code in ignore_list(comment[global_prefix.length():].to_owned()) {
          global_codes.push(code)
        }
      } else if comment.has_prefix(local_prefix) &&
        cursor < instructions.length() {
        let target = instructions[cursor].line
        let codes = local_codes.get(target).unwrap_or([])
        for code in ignore_list(comment[local_prefix.length():].to_owned()) {
          codes.push(code)
        }
        local_codes[target] = codes
      }
    }
  }
  let diagnostics = lint(instructions)
  if !trusted_registries.is_empty() && !trusted_registries.contains("*") {
    let aliases : Array[String] = []
    for ins in instructions {
      if ins.name != "FROM" {
        continue
      }
      let values = ins.split_arguments().values
      let base = values[0]
      if !aliases.contains(base.to_lower()) &&
        base != "scratch" &&
        !base.contains("$") {
        let parts = base.split("/").collect()
        let first = parts[0].to_owned()
        let registry = if parts.length() > 1 &&
          (first.contains(".") || first.contains(":") || first == "localhost") {
          first
        } else {
          "docker.io"
        }
        if !trusted_registries.contains(registry) {
          diagnostics.push({
            line: ins.line,
            code: "DL3026",
            message: "Image registry is not trusted: " + registry,
          })
        }
      }
      if values.length() == 3 && values[1].to_upper() == "AS" {
        aliases.push(values[2].to_lower())
      }
    }
  }
  diagnostics.filter(d => {
    !global_codes.contains(d.code) &&
    !local_codes.get(d.line).unwrap_or([]).contains(d.code)
  })
}

///|
/// JSON diagnostics with explicit severity and a source filename supplied by the caller.
pub fn report_json(
  source : String,
  filename? : String = "Dockerfile",
  honor_inline? : Bool = true,
) -> String raise ParseError {
  let entries = check(source, honor_inline~).map(d => {
    "file": filename.to_json(),
    "line": d.line.to_json(),
    "code": d.code.to_json(),
    "message": d.message.to_json(),
    "level": d.severity().to_json(),
  })
  entries.to_json().stringify()
}