///|
/// Text carried by SARIF objects. SARIF allows both plain text and Markdown;
/// the plain text form is required in most places and is kept as the stable
/// fallback for tools that do not render Markdown.
pub struct SarifMessage {
  text : String
  markdown : String?
} derive(Eq, Debug, ToJson)

///|
pub fn SarifMessage::SarifMessage(
  text : StringView,
  markdown? : String,
) -> SarifMessage {
  { text: text.to_owned(), markdown }
}

///|
/// URI location for an artifact. `uri_base_id` mirrors SARIF's `uriBaseId`
/// field and is useful when a CI runner wants relative paths anchored to a
/// checkout root.
pub struct ArtifactLocation {
  uri : String
  uri_base_id : String?
  index : Int?
} derive(Eq, Debug, ToJson)

///|
pub fn ArtifactLocation::ArtifactLocation(
  uri : StringView,
  uri_base_id? : String,
  index? : Int,
) -> ArtifactLocation {
  { uri: uri.to_owned(), uri_base_id, index }
}

///|
/// Region in an artifact. SARIF positions are one-based. End positions are
/// optional because many diagnostics naturally refer to a single point.
pub struct Region {
  start_line : Int
  start_column : Int
  end_line : Int?
  end_column : Int?
  char_offset : Int?
  char_length : Int?
} derive(Eq, Debug, ToJson)

///|
pub fn Region::Region(
  start_line : Int,
  start_column? : Int = 1,
  end_line? : Int,
  end_column? : Int,
  char_offset? : Int,
  char_length? : Int,
) -> Region {
  { start_line, start_column, end_line, end_column, char_offset, char_length }
}

///|
pub fn Region::single_line(
  line : Int,
  start_column : Int,
  end_column : Int,
) -> Region {
  Region::Region(line, start_column~, end_line=line, end_column~)
}

///|
pub fn Region::contains_line(self : Region, line : Int) -> Bool {
  if line < self.start_line {
    return false
  }
  match self.end_line {
    Some(end_line) => line <= end_line
    None => line == self.start_line
  }
}

///|
pub fn Region::span_text(self : Region) -> String {
  let end_line = match self.end_line {
    Some(value) => value.to_string()
    None => self.start_line.to_string()
  }
  let end_column = match self.end_column {
    Some(value) => value.to_string()
    None => self.start_column.to_string()
  }
  "\{self.start_line}:\{self.start_column}-\{end_line}:\{end_column}"
}

///|
pub struct PhysicalLocation {
  artifact_location : ArtifactLocation
  region : Region?
} derive(Eq, Debug, ToJson)

///|
pub fn PhysicalLocation::PhysicalLocation(
  artifact_location : ArtifactLocation,
  region? : Region,
) -> PhysicalLocation {
  { artifact_location, region }
}

///|
pub fn PhysicalLocation::from_path(
  uri : StringView,
  line : Int,
  column? : Int = 1,
) -> PhysicalLocation {
  {
    artifact_location: ArtifactLocation::ArtifactLocation(uri),
    region: Some(Region::Region(line, start_column=column)),
  }
}

///|
pub struct SarifLocation {
  physical_location : PhysicalLocation
  message : SarifMessage?
  logical_name : String?
} derive(Eq, Debug, ToJson)

///|
pub fn SarifLocation::SarifLocation(
  physical_location : PhysicalLocation,
  message? : SarifMessage,
  logical_name? : String,
) -> SarifLocation {
  { physical_location, message, logical_name }
}

///|
pub fn SarifLocation::from_path(
  uri : StringView,
  line : Int,
  column? : Int = 1,
  message? : String,
) -> SarifLocation {
  {
    physical_location: PhysicalLocation::from_path(uri, line, column~),
    message: message.map(text => SarifMessage::SarifMessage(text)),
    logical_name: None,
  }
}

///|
pub struct Artifact {
  location : ArtifactLocation
  length : Int?
  mime_type : String?
  roles : Array[String]
} derive(Eq, Debug, ToJson)

///|
pub fn Artifact::Artifact(
  uri : StringView,
  length? : Int,
  mime_type? : String,
  roles? : ArrayView[String] = [],
) -> Artifact {
  {
    location: ArtifactLocation::ArtifactLocation(uri),
    length,
    mime_type,
    roles: roles.to_owned(),
  }
}

///|
pub struct SarifRule {
  id : String
  name : String?
  short_description : SarifMessage?
  full_description : SarifMessage?
  default_level : String
  help_uri : String?
  tags : Array[String]
} derive(Eq, Debug, ToJson)

///|
pub fn SarifRule::SarifRule(
  id : StringView,
  name? : String,
  short_description? : String,
  full_description? : String,
  default_level? : String = "warning",
  help_uri? : String,
  tags? : ArrayView[String] = [],
) -> SarifRule {
  {
    id: id.to_owned(),
    name,
    short_description: short_description.map(text => {
      SarifMessage::SarifMessage(text)
    }),
    full_description: full_description.map(text => {
      SarifMessage::SarifMessage(text)
    }),
    default_level,
    help_uri,
    tags: tags.to_owned(),
  }
}

///|
pub struct ToolDriver {
  name : String
  version : String?
  information_uri : String?
  rules : Array[SarifRule]
} derive(Eq, Debug, ToJson)

///|
pub fn ToolDriver::ToolDriver(
  name : StringView,
  version? : String,
  information_uri? : String,
  rules? : ArrayView[SarifRule] = [],
) -> ToolDriver {
  { name: name.to_owned(), version, information_uri, rules: rules.to_owned() }
}

///|
pub struct Fingerprint {
  key : String
  value : String
} derive(Eq, Debug, ToJson)

///|
pub fn Fingerprint::Fingerprint(
  key : StringView,
  value : StringView,
) -> Fingerprint {
  { key: key.to_owned(), value: value.to_owned() }
}

///|
pub struct SarifProperty {
  key : String
  value : String
} derive(Eq, Debug, ToJson)

///|
pub fn SarifProperty::SarifProperty(
  key : StringView,
  value : StringView,
) -> SarifProperty {
  { key: key.to_owned(), value: value.to_owned() }
}

///|
pub struct SarifResult {
  rule_id : String
  level : String
  kind : String
  message : SarifMessage
  locations : Array[SarifLocation]
  fingerprints : Array[Fingerprint]
  properties : Array[SarifProperty]
  baseline_state : String?
} derive(Eq, Debug, ToJson)

///|
pub fn SarifResult::SarifResult(
  rule_id : StringView,
  message : StringView,
  level? : String = "warning",
  kind? : String = "fail",
  locations? : ArrayView[SarifLocation] = [],
  fingerprints? : ArrayView[Fingerprint] = [],
  properties? : ArrayView[SarifProperty] = [],
  baseline_state? : String,
) -> SarifResult {
  {
    rule_id: rule_id.to_owned(),
    level,
    kind,
    message: SarifMessage::SarifMessage(message),
    locations: locations.to_owned(),
    fingerprints: fingerprints.to_owned(),
    properties: properties.to_owned(),
    baseline_state,
  }
}

///|
pub fn SarifResult::primary_uri(self : SarifResult) -> String? {
  if self.locations.length() == 0 {
    None
  } else {
    Some(self.locations[0].physical_location.artifact_location.uri)
  }
}

///|
pub fn SarifResult::primary_region(self : SarifResult) -> Region? {
  if self.locations.length() == 0 {
    None
  } else {
    self.locations[0].physical_location.region
  }
}

///|
pub fn SarifResult::fingerprint_value(
  self : SarifResult,
  key : StringView,
) -> String? {
  let needle = key.to_owned()
  for item in self.fingerprints {
    if item.key == needle {
      return Some(item.value)
    }
  }
  None
}

///|
pub fn SarifResult::property_value(
  self : SarifResult,
  key : StringView,
) -> String? {
  let needle = key.to_owned()
  for item in self.properties {
    if item.key == needle {
      return Some(item.value)
    }
  }
  None
}

///|
pub struct SarifInvocation {
  command_line : String?
  working_directory : String?
  execution_successful : Bool
} derive(Eq, Debug, ToJson)

///|
pub fn SarifInvocation::SarifInvocation(
  command_line? : String,
  working_directory? : String,
  execution_successful? : Bool = true,
) -> SarifInvocation {
  { command_line, working_directory, execution_successful }
}

///|
pub struct SarifRun {
  tool : ToolDriver
  results : Array[SarifResult]
  artifacts : Array[Artifact]
  invocations : Array[SarifInvocation]
  automation_id : String?
} derive(Eq, Debug, ToJson)

///|
pub fn SarifRun::SarifRun(
  tool : ToolDriver,
  results? : ArrayView[SarifResult] = [],
  artifacts? : ArrayView[Artifact] = [],
  invocations? : ArrayView[SarifInvocation] = [],
  automation_id? : String,
) -> SarifRun {
  {
    tool,
    results: results.to_owned(),
    artifacts: artifacts.to_owned(),
    invocations: invocations.to_owned(),
    automation_id,
  }
}

///|
pub struct SarifLog {
  version : String
  schema : String?
  runs : Array[SarifRun]
} derive(Eq, Debug, ToJson)

///|
pub fn SarifLog::SarifLog(
  runs : ArrayView[SarifRun],
  schema? : String = "https://json.schemastore.org/sarif-2.1.0.json",
) -> SarifLog {
  { version: "2.1.0", schema: Some(schema), runs: runs.to_owned() }
}

///|
pub struct SarifDiagnostic {
  severity : String
  code : String
  path : String
  message : String
} derive(Eq, Debug, ToJson)

///|
pub fn SarifDiagnostic::SarifDiagnostic(
  severity : StringView,
  code : StringView,
  path : StringView,
  message : StringView,
) -> SarifDiagnostic {
  {
    severity: severity.to_owned(),
    code: code.to_owned(),
    path: path.to_owned(),
    message: message.to_owned(),
  }
}

///|
pub(all) enum SarifError {
  JsonParse(message~ : String)
  JsonDecode(path~ : String, expected~ : String)
  MissingField(field~ : String)
  InvalidField(field~ : String, message~ : String)
} derive(Eq, Debug, ToJson)

///|
pub fn SarifError::message(self : SarifError) -> String {
  match self {
    JsonParse(message~) => "JSON parse error: \{message}"
    JsonDecode(path~, expected~) =>
      "JSON decode error at \{path}: expected \{expected}"
    MissingField(field~) => "missing required field: \{field}"
    InvalidField(field~, message~) => "invalid field \{field}: \{message}"
  }
}

///|
pub fn known_levels() -> Array[String] {
  ["none", "note", "warning", "error"]
}

///|
pub fn known_kinds() -> Array[String] {
  ["fail", "open", "review", "pass", "informational", "notApplicable"]
}

///|
pub fn known_baseline_states() -> Array[String] {
  ["new", "unchanged", "updated", "absent"]
}

///|
pub fn normalize_level(level : StringView) -> String {
  let text = level.trim().to_owned()
  match text {
    "fatal" | "critical" | "high" => "error"
    "medium" | "warn" => "warning"
    "low" | "info" | "notice" => "note"
    "none" | "note" | "warning" | "error" => text
    _ => text
  }
}

///|
pub fn is_known_level(level : StringView) -> Bool {
  known_levels().contains(normalize_level(level))
}

///|
pub fn level_rank(level : StringView) -> Int {
  match normalize_level(level) {
    "error" => 3
    "warning" => 2
    "note" => 1
    _ => 0
  }
}

///|
pub fn level_at_least(level : StringView, minimum : StringView) -> Bool {
  level_rank(level) >= level_rank(minimum)
}

///|
pub fn is_known_kind(kind : StringView) -> Bool {
  known_kinds().contains(kind.to_owned())
}

///|
pub fn is_known_baseline_state(state : StringView) -> Bool {
  known_baseline_states().contains(state.to_owned())
}

///|
pub fn is_blank(text : StringView) -> Bool {
  text.trim().length() == 0
}

///|
pub fn first_non_empty(primary : StringView, fallback : StringView) -> String {
  if is_blank(primary) {
    fallback.to_owned()
  } else {
    primary.to_owned()
  }
}