///|
fn feature_line_number(target_index : Int) -> Int {
  target_index + 1
}

///|
fn add_issue(
  issues : Array[ValidationIssue],
  severity : Severity,
  code : String,
  line : Int,
  feature_type : String,
  message : String,
) -> Unit {
  issues.push({ severity, code, line, feature_type, message })
}

///|
fn has_feature_id(features : Array[Feature], id : String) -> Bool {
  for feature in features {
    if feature.attribute("ID") == Some(id) {
      return true
    }
  }
  false
}

///|
fn has_duplicate_id_before(
  features : Array[Feature],
  index : Int,
  id : String,
) -> Bool {
  for i in 0.. Unit {
  let line = feature_line_number(index)
  match feature.attribute("ID") {
    Some(id) =>
      if has_duplicate_id_before(annotation.features, index, id) {
        add_issue(
          issues,
          Error,
          "duplicate_id",
          line,
          feature.feature_type,
          "ID \{id} appears more than once",
        )
      }
    None =>
      if feature.feature_type == "gene" ||
        feature.feature_type == "mRNA" ||
        feature.feature_type == "transcript" {
        add_issue(
          issues,
          Warning,
          "missing_id",
          line,
          feature.feature_type,
          "\{feature.feature_type} has no ID attribute",
        )
      }
  }
  match feature.attribute("Parent") {
    Some(parent) =>
      if !has_feature_id(annotation.features, parent) {
        add_issue(
          issues,
          Error,
          "orphan_parent",
          line,
          feature.feature_type,
          "parent \{parent} is not present",
        )
      }
    None => ()
  }
  match feature.phase {
    Some(phase) =>
      if feature.feature_type == "CDS" &&
        !(phase == "0" || phase == "1" || phase == "2") {
        add_issue(
          issues,
          Warning,
          "invalid_phase",
          line,
          feature.feature_type,
          "CDS phase must be 0, 1, or 2",
        )
      }
    None => ()
  }
}

///|
fn validate_gtf_feature(
  issues : Array[ValidationIssue],
  feature : Feature,
  index : Int,
) -> Unit {
  let line = index + 1
  if feature.attribute("gene_id") is None {
    add_issue(
      issues,
      Error,
      "missing_gene_id",
      line,
      feature.feature_type,
      "GTF feature has no gene_id",
    )
  }
  if feature.feature_type != "gene" &&
    feature.attribute("transcript_id") is None {
    add_issue(
      issues,
      Error,
      "missing_transcript_id",
      line,
      feature.feature_type,
      "GTF non-gene feature has no transcript_id",
    )
  }
  if feature.feature_type == "CDS" {
    match feature.phase {
      Some(phase) =>
        if !(phase == "0" || phase == "1" || phase == "2") {
          add_issue(
            issues,
            Warning,
            "invalid_phase",
            line,
            feature.feature_type,
            "CDS phase must be 0, 1, or 2",
          )
        }
      None => ()
    }
  }
}

///|
pub fn Annotation::validate(self : Annotation) -> ValidationReport {
  let issues : Array[ValidationIssue] = []
  for index, feature in self.features {
    match self.format {
      GFF3 => validate_gff3_feature(self, issues, feature, index)
      GTF => validate_gtf_feature(issues, feature, index)
    }
  }
  { issues, }
}

///|
pub fn ValidationReport::issue_count(self : ValidationReport) -> Int {
  self.issues.length()
}

///|
pub fn ValidationReport::count_by_severity(
  self : ValidationReport,
  severity : Severity,
) -> Int {
  let mut count = 0
  for issue in self.issues {
    if issue.severity == severity {
      count += 1
    }
  }
  count
}

///|
pub fn ValidationReport::error_count(self : ValidationReport) -> Int {
  self.count_by_severity(Error)
}

///|
pub fn ValidationReport::warning_count(self : ValidationReport) -> Int {
  self.count_by_severity(Warning)
}

///|
pub fn ValidationReport::info_count(self : ValidationReport) -> Int {
  self.count_by_severity(Info)
}

///|
pub fn ValidationReport::has_errors(self : ValidationReport) -> Bool {
  self.error_count() > 0
}

///|
pub fn ValidationReport::to_text(self : ValidationReport) -> String {
  let lines : Array[String] = []
  for issue in self.issues {
    lines.push(
      "\{issue.severity.to_string()}\t\{issue.code}\tline \{issue.line}\t\{issue.message}",
    )
  }
  if lines.length() == 0 {
    ""
  } else {
    lines.join("\n") + "\n"
  }
}

///|
pub fn ValidationIssue::is_error(self : ValidationIssue) -> Bool {
  self.severity == Error
}

///|
pub fn ValidationIssue::is_warning(self : ValidationIssue) -> Bool {
  self.severity == Warning
}

///|
pub fn ValidationIssue::is_info(self : ValidationIssue) -> Bool {
  self.severity == Info
}

///|
pub fn ValidationIssue::summary(self : ValidationIssue) -> String {
  "\{self.severity.to_string()} \{self.code} at line \{self.line}"
}

///|
pub fn ValidationReport::filter_by_code(
  self : ValidationReport,
  code : String,
) -> Array[ValidationIssue] {
  let result : Array[ValidationIssue] = []
  for issue in self.issues {
    if issue.code == code {
      result.push(issue)
    }
  }
  result
}

///|
pub fn ValidationReport::filter_by_feature_type(
  self : ValidationReport,
  feature_type : String,
) -> Array[ValidationIssue] {
  let result : Array[ValidationIssue] = []
  for issue in self.issues {
    if issue.feature_type == feature_type {
      result.push(issue)
    }
  }
  result
}

///|
pub fn ValidationReport::codes(self : ValidationReport) -> Array[String] {
  let codes : Array[String] = []
  for issue in self.issues {
    if !codes.contains(issue.code) {
      codes.push(issue.code)
    }
  }
  codes
}