///|
/// A diagnostic application session defined by ISO 14229.
pub enum DiagnosticSessionKind {
  DefaultSession
  ProgrammingSession
  ExtendedSession
  SafetySystemSession
  SupplierSession(Byte)
}

///|
/// Construct all standard session variants for capability discovery.
pub fn diagnostic_session_kinds() -> Array[DiagnosticSessionKind] {
  [
    DefaultSession,
    ProgrammingSession,
    ExtendedSession,
    SafetySystemSession,
    SupplierSession(0),
  ]
}

///|
/// Addressing modes supported by a diagnostic transport endpoint.
pub enum DiagnosticAddressingMode {
  NormalAddressing
  ExtendedAddressing
  MixedAddressing(Byte)
}

///|
/// Construct the addressing modes accepted by the portable transport layer.
pub fn diagnostic_addressing_modes() -> Array[DiagnosticAddressingMode] {
  [NormalAddressing, ExtendedAddressing, MixedAddressing(0)]
}

///|
/// Timing and security policy for a diagnostic session.
pub struct DiagnosticSessionProfile {
  kind : DiagnosticSessionKind
  p2_server_us : UInt64
  p2_star_server_us : UInt64
  s3_server_us : UInt64
  addressing : DiagnosticAddressingMode
  security_level : Byte
}

///|
/// Create a standard diagnostic session profile.
pub fn diagnostic_session_profile(
  kind : DiagnosticSessionKind,
  p2_server_us : UInt64,
  p2_star_server_us : UInt64,
  s3_server_us : UInt64,
  addressing? : DiagnosticAddressingMode = NormalAddressing,
  security_level? : Byte = 0,
) -> DiagnosticSessionProfile {
  {
    kind,
    p2_server_us,
    p2_star_server_us,
    s3_server_us,
    addressing,
    security_level,
  }
}

///|
/// Return the default session profile used by a tester.
pub fn default_diagnostic_session() -> DiagnosticSessionProfile {
  diagnostic_session_profile(DefaultSession, 50_000, 5_000_000, 5_000_000)
}

///|
/// Return an extended diagnostic session profile.
pub fn extended_diagnostic_session() -> DiagnosticSessionProfile {
  diagnostic_session_profile(ExtendedSession, 50_000, 5_000_000, 5_000_000)
}

///|
/// Return a programming session profile.
pub fn programming_diagnostic_session() -> DiagnosticSessionProfile {
  diagnostic_session_profile(
    ProgrammingSession,
    50_000,
    10_000_000,
    10_000_000,
    addressing=NormalAddressing,
    security_level=1,
  )
}

///|
/// Return the session kind.
pub fn DiagnosticSessionProfile::kind(
  self : DiagnosticSessionProfile,
) -> DiagnosticSessionKind {
  self.kind
}

///|
/// Return the P2 timeout in microseconds.
pub fn DiagnosticSessionProfile::p2_server_us(
  self : DiagnosticSessionProfile,
) -> UInt64 {
  self.p2_server_us
}

///|
/// Return the P2-star timeout in microseconds.
pub fn DiagnosticSessionProfile::p2_star_server_us(
  self : DiagnosticSessionProfile,
) -> UInt64 {
  self.p2_star_server_us
}

///|
/// Return the S3 timeout in microseconds.
pub fn DiagnosticSessionProfile::s3_server_us(
  self : DiagnosticSessionProfile,
) -> UInt64 {
  self.s3_server_us
}

///|
/// Return the addressing mode.
pub fn DiagnosticSessionProfile::addressing(
  self : DiagnosticSessionProfile,
) -> DiagnosticAddressingMode {
  self.addressing
}

///|
/// Return the required security level.
pub fn DiagnosticSessionProfile::security_level(
  self : DiagnosticSessionProfile,
) -> Byte {
  self.security_level
}

///|
/// Return a stable session name for reports.
pub fn diagnostic_session_name(kind : DiagnosticSessionKind) -> String {
  match kind {
    DefaultSession => "default"
    ProgrammingSession => "programming"
    ExtendedSession => "extended"
    SafetySystemSession => "safety-system"
    SupplierSession(number) => "supplier-\{number}"
  }
}

///|
/// Return whether the session should be kept alive by TesterPresent.
pub fn diagnostic_session_requires_keepalive(
  profile : DiagnosticSessionProfile,
) -> Bool {
  profile.s3_server_us > 0
}

///|
/// Return the next TesterPresent deadline.
pub fn diagnostic_keepalive_deadline(
  profile : DiagnosticSessionProfile,
  now_us : UInt64,
) -> UInt64 {
  now_us + profile.s3_server_us / 2
}

///|
/// A status bit set used by a stored diagnostic trouble code.
pub enum DtcStatusBit {
  TestFailed
  TestFailedThisOperationCycle
  PendingDtc
  ConfirmedDtc
  TestNotCompletedSinceClear
  TestFailedSinceClear
  TestNotCompletedThisOperationCycle
  WarningIndicatorRequested
}

///|
/// A normalized diagnostic trouble code record.
pub struct DiagnosticTroubleCode {
  code : UInt
  mut status : Byte
  severity : Byte
  mut occurrence_count : Int
  mut snapshot : Array[Byte]
  mut extended_data : Array[Byte]
  mut last_seen_us : UInt64
}

///|
/// Create a diagnostic trouble code record.
pub fn diagnostic_trouble_code(
  code : UInt,
  status : Byte,
  severity? : Byte = 0,
  snapshot? : Array[Byte] = [],
  extended_data? : Array[Byte] = [],
  timestamp_us? : UInt64 = 0,
) -> DiagnosticTroubleCode {
  {
    code,
    status,
    severity,
    occurrence_count: 1,
    snapshot: snapshot.copy(),
    extended_data: extended_data.copy(),
    last_seen_us: timestamp_us,
  }
}

///|
/// Return the numeric DTC code.
pub fn DiagnosticTroubleCode::code(self : DiagnosticTroubleCode) -> UInt {
  self.code
}

///|
/// Return the raw UDS status byte.
pub fn DiagnosticTroubleCode::status(self : DiagnosticTroubleCode) -> Byte {
  self.status
}

///|
/// Return the configured severity byte.
pub fn DiagnosticTroubleCode::severity(self : DiagnosticTroubleCode) -> Byte {
  self.severity
}

///|
/// Return the number of observed occurrences.
pub fn DiagnosticTroubleCode::occurrence_count(
  self : DiagnosticTroubleCode,
) -> Int {
  self.occurrence_count
}

///|
/// Return a copy of the freeze-frame snapshot.
pub fn DiagnosticTroubleCode::snapshot(
  self : DiagnosticTroubleCode,
) -> Array[Byte] {
  self.snapshot.copy()
}

///|
/// Return a copy of the extended DTC data.
pub fn DiagnosticTroubleCode::extended_data(
  self : DiagnosticTroubleCode,
) -> Array[Byte] {
  self.extended_data.copy()
}

///|
/// Return the most recent observation timestamp.
pub fn DiagnosticTroubleCode::last_seen_us(
  self : DiagnosticTroubleCode,
) -> UInt64 {
  self.last_seen_us
}

///|
/// Return whether a status bit is set.
pub fn DiagnosticTroubleCode::has_status(
  self : DiagnosticTroubleCode,
  bit : DtcStatusBit,
) -> Bool {
  let mask = match bit {
    TestFailed => 0x01
    TestFailedThisOperationCycle => 0x02
    PendingDtc => 0x04
    ConfirmedDtc => 0x08
    TestNotCompletedSinceClear => 0x10
    TestFailedSinceClear => 0x20
    TestNotCompletedThisOperationCycle => 0x40
    WarningIndicatorRequested => 0x80
  }
  (self.status.to_int() & mask) != 0
}

///|
/// Return a human-readable status summary.
pub fn DiagnosticTroubleCode::status_text(
  self : DiagnosticTroubleCode,
) -> String {
  let names : Array[String] = []
  let bits : Array[DtcStatusBit] = [
    TestFailed,
    TestFailedThisOperationCycle,
    PendingDtc,
    ConfirmedDtc,
    TestNotCompletedSinceClear,
    TestFailedSinceClear,
    TestNotCompletedThisOperationCycle,
    WarningIndicatorRequested,
  ]
  let labels : Array[String] = [
    "failed", "failed-this-cycle", "pending", "confirmed", "not-completed-since-clear",
    "failed-since-clear", "not-completed-this-cycle", "warning-requested",
  ]
  for index, item in bits {
    if self.has_status(item) {
      names.push(labels[index])
    }
  }
  if names.is_empty() {
    "clear"
  } else {
    names.join("|")
  }
}

///|
/// Apply a new observation to a DTC record.
pub fn DiagnosticTroubleCode::observe(
  self : DiagnosticTroubleCode,
  status : Byte,
  timestamp_us : UInt64,
) -> Unit {
  self.status = status
  self.last_seen_us = timestamp_us
  self.occurrence_count += 1
}

///|
/// Add or update a freeze-frame snapshot.
pub fn DiagnosticTroubleCode::set_snapshot(
  self : DiagnosticTroubleCode,
  data : Array[Byte],
) -> Unit {
  self.snapshot = data.copy()
}

///|
/// Add or update extended DTC data.
pub fn DiagnosticTroubleCode::set_extended_data(
  self : DiagnosticTroubleCode,
  data : Array[Byte],
) -> Unit {
  self.extended_data = data.copy()
}

///|
/// Serialize a DTC into the three-byte code plus status byte form.
pub fn DiagnosticTroubleCode::to_bytes(
  self : DiagnosticTroubleCode,
) -> Array[Byte] {
  [
    (self.code >> 16).to_byte(),
    (self.code >> 8).to_byte(),
    self.code.to_byte(),
    self.status,
  ]
}

///|
/// A DTC query selector.
pub enum DtcQuery {
  AllDtc
  ByCode(UInt)
  ByStatusMask(Byte)
  BySeverityAtLeast(Byte)
  ConfirmedOnly
  WarningOnly
  SeenAfter(UInt64)
}

///|
/// Construct representative selectors for a diagnostic UI.
pub fn diagnostic_dtc_queries() -> Array[DtcQuery] {
  [
    AllDtc,
    ByCode(0),
    ByStatusMask(0x08),
    BySeverityAtLeast(1),
    ConfirmedOnly,
    WarningOnly,
    SeenAfter(0),
  ]
}

///|
/// Return whether a DTC matches a query selector.
pub fn dtc_matches(dtc : DiagnosticTroubleCode, query : DtcQuery) -> Bool {
  match query {
    AllDtc => true
    ByCode(code) => dtc.code() == code
    ByStatusMask(mask) => (dtc.status().to_int() & mask.to_int()) != 0
    BySeverityAtLeast(level) => dtc.severity().to_int() >= level.to_int()
    ConfirmedOnly => dtc.has_status(ConfirmedDtc)
    WarningOnly => dtc.has_status(WarningIndicatorRequested)
    SeenAfter(timestamp) => dtc.last_seen_us() > timestamp
  }
}

///|
/// A bounded DTC store suitable for an ECU simulator or gateway.
pub struct DiagnosticTroubleCodeStore {
  capacity : Int
  records : Array[DiagnosticTroubleCode]
  mut generation : UInt
  mut cleared_at_us : UInt64
}

///|
/// Create an empty bounded DTC store.
pub fn new_diagnostic_trouble_code_store(
  capacity : Int,
) -> DiagnosticTroubleCodeStore {
  {
    capacity: if capacity < 1 {
      1
    } else {
      capacity
    },
    records: [],
    generation: 0,
    cleared_at_us: 0,
  }
}

///|
/// Return the configured store capacity.
pub fn DiagnosticTroubleCodeStore::capacity(
  self : DiagnosticTroubleCodeStore,
) -> Int {
  self.capacity
}

///|
/// Return the number of stored DTCs.
pub fn DiagnosticTroubleCodeStore::length(
  self : DiagnosticTroubleCodeStore,
) -> Int {
  self.records.length()
}

///|
/// Return the mutation generation.
pub fn DiagnosticTroubleCodeStore::generation(
  self : DiagnosticTroubleCodeStore,
) -> UInt {
  self.generation
}

///|
/// Return the timestamp of the last clear operation.
pub fn DiagnosticTroubleCodeStore::cleared_at_us(
  self : DiagnosticTroubleCodeStore,
) -> UInt64 {
  self.cleared_at_us
}

///|
/// Add a new DTC or update an existing code.
pub fn DiagnosticTroubleCodeStore::record(
  self : DiagnosticTroubleCodeStore,
  item : DiagnosticTroubleCode,
) -> Unit {
  match self.find_index(item.code()) {
    Some(index) => {
      self.records[index].observe(item.status(), item.last_seen_us())
      self.records[index].set_snapshot(item.snapshot())
      self.records[index].set_extended_data(item.extended_data())
    }
    None => {
      if self.records.length() >= self.capacity {
        ignore(self.records.remove(0))
      }
      self.records.push(item)
    }
  }
  self.generation += 1
}

///|
/// Find a DTC by numeric code.
pub fn DiagnosticTroubleCodeStore::find(
  self : DiagnosticTroubleCodeStore,
  code : UInt,
) -> DiagnosticTroubleCode? {
  match self.find_index(code) {
    Some(index) => Some(self.records[index])
    None => None
  }
}

///|
/// Query all matching DTCs in insertion order.
pub fn DiagnosticTroubleCodeStore::query(
  self : DiagnosticTroubleCodeStore,
  selector : DtcQuery,
) -> Array[DiagnosticTroubleCode] {
  let result : Array[DiagnosticTroubleCode] = []
  for item in self.records {
    if dtc_matches(item, selector) {
      result.push(item)
    }
  }
  result
}

///|
/// Return a defensive copy of all records.
pub fn DiagnosticTroubleCodeStore::records(
  self : DiagnosticTroubleCodeStore,
) -> Array[DiagnosticTroubleCode] {
  self.records.copy()
}

///|
/// Clear all records and update the clear timestamp.
pub fn DiagnosticTroubleCodeStore::clear(
  self : DiagnosticTroubleCodeStore,
  timestamp_us : UInt64,
) -> Unit {
  self.records.clear()
  self.cleared_at_us = timestamp_us
  self.generation += 1
}

///|
/// Remove one DTC by code and return whether it existed.
pub fn DiagnosticTroubleCodeStore::remove(
  self : DiagnosticTroubleCodeStore,
  code : UInt,
) -> Bool {
  match self.find_index(code) {
    Some(index) => {
      ignore(self.records.remove(index))
      self.generation += 1
      true
    }
    None => false
  }
}

///|
/// Mark every stored DTC as not completed since clear.
pub fn DiagnosticTroubleCodeStore::mark_cycle_start(
  self : DiagnosticTroubleCodeStore,
) -> Unit {
  for index in 0.. Array[UInt] {
  let result : Array[UInt] = []
  for item in self.records {
    result.push(item.code())
  }
  result.sort()
  result
}

///|
/// Encode a DTC response payload for ReadDTCInformation.
pub fn DiagnosticTroubleCodeStore::encode_report(
  self : DiagnosticTroubleCodeStore,
  selector : DtcQuery,
  limit? : Int = 0,
) -> Array[Byte] {
  let items = self.query(selector)
  let result : Array[Byte] = [items.length().to_byte()]
  let maximum = if limit <= 0 || limit > items.length() {
    items.length()
  } else {
    limit
  }
  for index in 0.. Int? {
  for index, item in self.records {
    if item.code() == code {
      return Some(index)
    }
  }
  None
}

///|
/// A data identifier entry exposed by an ECU.
pub struct DiagnosticDataIdentifier {
  identifier : UInt
  name : String
  length : Int
  readable : Bool
  writable : Bool
  mut value : Array[Byte]
}

///|
/// Create a data identifier entry.
pub fn diagnostic_data_identifier(
  identifier : UInt,
  name : String,
  length : Int,
  readable? : Bool = true,
  writable? : Bool = false,
  initial? : Array[Byte] = [],
) -> DiagnosticDataIdentifier {
  {
    identifier,
    name,
    length: if length < 0 {
      0
    } else {
      length
    },
    readable,
    writable,
    value: initial.copy(),
  }
}

///|
pub fn DiagnosticDataIdentifier::identifier(
  self : DiagnosticDataIdentifier,
) -> UInt {
  self.identifier
}

///|
pub fn DiagnosticDataIdentifier::name(
  self : DiagnosticDataIdentifier,
) -> String {
  self.name
}

///|
pub fn DiagnosticDataIdentifier::length(self : DiagnosticDataIdentifier) -> Int {
  self.length
}

///|
pub fn DiagnosticDataIdentifier::readable(
  self : DiagnosticDataIdentifier,
) -> Bool {
  self.readable
}

///|
pub fn DiagnosticDataIdentifier::writable(
  self : DiagnosticDataIdentifier,
) -> Bool {
  self.writable
}

///|
pub fn DiagnosticDataIdentifier::value(
  self : DiagnosticDataIdentifier,
) -> Array[Byte] {
  self.value.copy()
}

///|
/// Update a DID value, applying the configured maximum length.
pub fn DiagnosticDataIdentifier::set_value(
  self : DiagnosticDataIdentifier,
  value : Array[Byte],
) -> Bool {
  if !self.writable || value.length() > self.length {
    false
  } else {
    self.value = value.copy()
    true
  }
}

///|
/// Return the readable value padded to the configured length.
pub fn DiagnosticDataIdentifier::read_value(
  self : DiagnosticDataIdentifier,
) -> Array[Byte] {
  let result = self.value.copy()
  while result.length() < self.length {
    result.push(0)
  }
  result
}

///|
/// A table of ECU data identifiers.
pub struct DiagnosticDataTable {
  entries : Array[DiagnosticDataIdentifier]
  mut revision : UInt
}

///|
pub fn new_diagnostic_data_table() -> DiagnosticDataTable {
  { entries: [], revision: 0 }
}

///|
pub fn DiagnosticDataTable::add(
  self : DiagnosticDataTable,
  entry : DiagnosticDataIdentifier,
) -> Bool {
  if self.find(entry.identifier()) is Some(_) {
    false
  } else {
    self.entries.push(entry)
    self.revision += 1
    true
  }
}

///|
pub fn DiagnosticDataTable::replace(
  self : DiagnosticDataTable,
  entry : DiagnosticDataIdentifier,
) -> Bool {
  match self.find_index(entry.identifier()) {
    Some(index) => {
      self.entries[index] = entry
      self.revision += 1
      true
    }
    None => false
  }
}

///|
pub fn DiagnosticDataTable::find(
  self : DiagnosticDataTable,
  identifier : UInt,
) -> DiagnosticDataIdentifier? {
  match self.find_index(identifier) {
    Some(index) => Some(self.entries[index])
    None => None
  }
}

///|
pub fn DiagnosticDataTable::read(
  self : DiagnosticDataTable,
  identifier : UInt,
) -> Array[Byte]? {
  match self.find(identifier) {
    Some(entry) =>
      if entry.readable() {
        Some(entry.read_value())
      } else {
        None
      }
    None => None
  }
}

///|
pub fn DiagnosticDataTable::write(
  self : DiagnosticDataTable,
  identifier : UInt,
  value : Array[Byte],
) -> Bool {
  match self.find_index(identifier) {
    Some(index) => {
      let changed = self.entries[index].set_value(value)
      if changed {
        self.revision += 1
      }
      changed
    }
    None => false
  }
}

///|
pub fn DiagnosticDataTable::length(self : DiagnosticDataTable) -> Int {
  self.entries.length()
}

///|
pub fn DiagnosticDataTable::revision(self : DiagnosticDataTable) -> UInt {
  self.revision
}

///|
pub fn DiagnosticDataTable::entries(
  self : DiagnosticDataTable,
) -> Array[DiagnosticDataIdentifier] {
  self.entries.copy()
}

///|
pub fn DiagnosticDataTable::identifiers(
  self : DiagnosticDataTable,
) -> Array[UInt] {
  let result : Array[UInt] = []
  for entry in self.entries {
    result.push(entry.identifier())
  }
  result.sort()
  result
}

///|
pub fn DiagnosticDataTable::to_text(self : DiagnosticDataTable) -> String {
  let lines : Array[String] = []
  for entry in self.entries {
    lines.push(
      "0x\{entry.identifier().to_string()} \{entry.name()} len=\{entry.length()} read=\{entry.readable()} write=\{entry.writable()}",
    )
  }
  lines.join("\n")
}

///|
fn DiagnosticDataTable::find_index(
  self : DiagnosticDataTable,
  identifier : UInt,
) -> Int? {
  for index, entry in self.entries {
    if entry.identifier() == identifier {
      return Some(index)
    }
  }
  None
}

///|
/// A diagnostic event suitable for a tester trace.
pub struct DiagnosticEvent {
  timestamp_us : UInt64
  request : DiagnosticRequest
  response : Array[Byte]
  positive : Bool
  duration_us : UInt64
}

///|
pub fn diagnostic_event(
  timestamp_us : UInt64,
  request : DiagnosticRequest,
  response : Array[Byte],
  duration_us : UInt64,
) -> DiagnosticEvent {
  {
    timestamp_us,
    request,
    response: response.copy(),
    positive: !response.is_empty() && response[0].to_int() != 0x7F,
    duration_us,
  }
}

///|
pub fn DiagnosticEvent::timestamp_us(self : DiagnosticEvent) -> UInt64 {
  self.timestamp_us
}

///|
pub fn DiagnosticEvent::request(self : DiagnosticEvent) -> DiagnosticRequest {
  self.request
}

///|
pub fn DiagnosticEvent::response(self : DiagnosticEvent) -> Array[Byte] {
  self.response.copy()
}

///|
pub fn DiagnosticEvent::positive(self : DiagnosticEvent) -> Bool {
  self.positive
}

///|
pub fn DiagnosticEvent::duration_us(self : DiagnosticEvent) -> UInt64 {
  self.duration_us
}

///|
/// An ordered diagnostic exchange log.
pub struct DiagnosticExchangeLog {
  events : Array[DiagnosticEvent]
  mut positive_count : Int
  mut negative_count : Int
}

///|
pub fn new_diagnostic_exchange_log() -> DiagnosticExchangeLog {
  { events: [], positive_count: 0, negative_count: 0 }
}

///|
pub fn DiagnosticExchangeLog::record(
  self : DiagnosticExchangeLog,
  event : DiagnosticEvent,
) -> Unit {
  self.events.push(event)
  if event.positive() {
    self.positive_count += 1
  } else {
    self.negative_count += 1
  }
}

///|
pub fn DiagnosticExchangeLog::events(
  self : DiagnosticExchangeLog,
) -> Array[DiagnosticEvent] {
  self.events.copy()
}

///|
pub fn DiagnosticExchangeLog::length(self : DiagnosticExchangeLog) -> Int {
  self.events.length()
}

///|
pub fn DiagnosticExchangeLog::positive_count(
  self : DiagnosticExchangeLog,
) -> Int {
  self.positive_count
}

///|
pub fn DiagnosticExchangeLog::negative_count(
  self : DiagnosticExchangeLog,
) -> Int {
  self.negative_count
}

///|
pub fn DiagnosticExchangeLog::average_duration_us(
  self : DiagnosticExchangeLog,
) -> UInt64 {
  if self.events.is_empty() {
    0
  } else {
    let mut total : UInt64 = 0
    for event in self.events {
      total += event.duration_us()
    }
    total / self.events.length().to_uint64()
  }
}

///|
pub fn DiagnosticExchangeLog::failed_services(
  self : DiagnosticExchangeLog,
) -> Array[String] {
  let result : Array[String] = []
  for event in self.events {
    if !event.positive() {
      result.push(event.request().service_name())
    }
  }
  result
}

///|
pub fn DiagnosticExchangeLog::to_text(self : DiagnosticExchangeLog) -> String {
  let lines : Array[String] = []
  for event in self.events {
    lines.push(
      "t=\{event.timestamp_us()} service=\{event.request().service_name()} positive=\{event.positive()} duration_us=\{event.duration_us()}",
    )
  }
  lines.join("\n")
}