///|
fn severity_text(severity : DiagnosticSeverity) -> String {
  match severity {
    Error => "error"
    Warning => "warning"
    Note => "note"
  }
}

///|
fn diagnostic(
  severity : DiagnosticSeverity,
  code : String,
  message : String,
  entity_id : Int?,
) -> Diagnostic {
  { severity, code, message, entity_id }
}

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

///|
pub fn ValidationReport::diagnostic_codes(
  self : ValidationReport,
) -> Array[String] {
  self.diagnostics.map(item => item.code)
}

///|
pub fn ValidationReport::format(self : ValidationReport) -> String {
  self.diagnostics
  .map(item => {
    let location = item.entity_id.map(id => " #" + id.to_string()).unwrap_or("")
    severity_text(item.severity) +
    "[" +
    item.code +
    "]" +
    location +
    ": " +
    item.message
  })
  .join("\n")
}

///|
fn add_diagnostic(
  diagnostics : Array[Diagnostic],
  severity : DiagnosticSeverity,
  code : String,
  message : String,
  entity_id : Int?,
) -> Unit {
  diagnostics.push(diagnostic(severity, code, message, entity_id))
}

///|
/// Check references and structural invariants without rejecting unknown IFC extensions.
pub fn validate(model : Model) -> ValidationReport {
  let index = model.index()
  let diagnostics : Array[Diagnostic] = []
  let mut error_count = 0
  let mut warning_count = 0
  let seen : Map[Int, Bool] = Map([])
  if model.header.schema_identifiers.length() == 0 {
    add_diagnostic(diagnostics, Error, "IFC001", "FILE_SCHEMA is empty", None)
    error_count += 1
  }
  for schema in model.header.schema_identifiers {
    let upper = schema.to_upper()
    if upper != "IFC2X3" &&
      upper != "IFC4" &&
      upper != "IFC4X1" &&
      upper != "IFC4X3" {
      add_diagnostic(
        diagnostics,
        Warning,
        "IFC002",
        "schema is not one of the built-in semantic profiles: " + schema,
        None,
      )
      warning_count += 1
    }
  }
  for entity in model.entities {
    if seen.contains(entity.id) {
      add_diagnostic(
        diagnostics,
        Error,
        "IFC003",
        "duplicate entity id",
        Some(entity.id),
      )
      error_count += 1
    } else {
      seen[entity.id] = true
    }
    if entity.name == "" {
      add_diagnostic(
        diagnostics,
        Error,
        "IFC004",
        "entity type is empty",
        Some(entity.id),
      )
      error_count += 1
    }
    for target in entity_references(entity) {
      if !index.has(target) {
        add_diagnostic(
          diagnostics,
          Error,
          "IFC005",
          "reference target does not exist",
          Some(entity.id),
        )
        error_count += 1
      }
    }
    let upper = entity.name.to_upper()
    if upper.has_prefix("IFCREL") && entity.arguments.length() < 6 {
      add_diagnostic(
        diagnostics,
        Warning,
        "IFC006",
        "relationship has fewer than six common attributes",
        Some(entity.id),
      )
      warning_count += 1
    }
  }
  { diagnostics, error_count, warning_count }
}

///|
pub fn Model::validate(self : Model) -> ValidationReport {
  validate(self)
}

///|
pub fn validation_summary(report : ValidationReport) -> String {
  if report.is_valid() {
    "valid: \{report.warning_count} warning(s)"
  } else {
    "invalid: \{report.error_count} error(s), \{report.warning_count} warning(s)"
  }
}