///|
pub(all) enum SafetySeverity {
  SafetyWarning
  SafetyCritical
  SafetyEmergency
} derive(Eq, Debug)

///|
pub fn SafetySeverity::is_fatal(self : SafetySeverity) -> Bool {
  match self {
    SafetyWarning => false
    SafetyCritical | SafetyEmergency => true
  }
}

///|
pub fn SafetySeverity::to_string(self : SafetySeverity) -> String {
  match self {
    SafetyWarning => "WARNING"
    SafetyCritical => "CRITICAL"
    SafetyEmergency => "EMERGENCY"
  }
}

///|
pub(all) enum SafetyViolationKind {
  MissingTurnout
  TurnoutPositionMismatch
  TurnoutUnlocked
  FlankProtectionMismatch
  HostileRouteConflict
  MissingTrack
  FaultedRouteSection
  ProceedSignalUnsafe
  FaultSignalNotRed
  ReleaseBeforeTraversal
  OccupancyOrderViolation
  IllegalRouteTransition
} derive(Eq, Debug)

///|
pub fn SafetyViolationKind::to_string(self : SafetyViolationKind) -> String {
  match self {
    MissingTurnout => "MISSING_TURNOUT"
    TurnoutPositionMismatch => "TURNOUT_POSITION_MISMATCH"
    TurnoutUnlocked => "TURNOUT_UNLOCKED"
    FlankProtectionMismatch => "FLANK_PROTECTION_MISMATCH"
    HostileRouteConflict => "HOSTILE_ROUTE_CONFLICT"
    MissingTrack => "MISSING_TRACK"
    FaultedRouteSection => "FAULTED_ROUTE_SECTION"
    ProceedSignalUnsafe => "PROCEED_SIGNAL_UNSAFE"
    FaultSignalNotRed => "FAULT_SIGNAL_NOT_RED"
    ReleaseBeforeTraversal => "RELEASE_BEFORE_TRAVERSAL"
    OccupancyOrderViolation => "OCCUPANCY_ORDER_VIOLATION"
    IllegalRouteTransition => "ILLEGAL_ROUTE_TRANSITION"
  }
}

///|
pub(all) struct SafetyViolation {
  kind : SafetyViolationKind
  severity : SafetySeverity
  affected_route_id : RouteId?
  description : String
} derive(Debug)

///|
pub fn SafetyViolation::new(
  kind : SafetyViolationKind,
  severity : SafetySeverity,
  affected_route_id : RouteId?,
  description : String,
) -> SafetyViolation {
  { kind, severity, affected_route_id, description }
}

///|
pub fn SafetyViolation::to_string(self : SafetyViolation) -> String {
  self.severity.to_string() +
  " " +
  self.kind.to_string() +
  ": " +
  self.description
}

///|
pub(all) struct SafetyAuditReport {
  violations : Array[SafetyViolation]
  mut fatal_count : Int
  mut warning_count : Int
  mut can_clear_signal : Bool
} derive(Debug)

///|
pub fn SafetyAuditReport::new() -> SafetyAuditReport {
  { violations: [], fatal_count: 0, warning_count: 0, can_clear_signal: true }
}

///|
fn SafetyAuditReport::add_violation(
  self : SafetyAuditReport,
  violation : SafetyViolation,
) -> Unit {
  self.violations.push(violation)
  if violation.severity.is_fatal() {
    self.fatal_count = self.fatal_count + 1
    self.can_clear_signal = false
  } else {
    self.warning_count = self.warning_count + 1
  }
}

///|
pub fn SafetyAuditReport::contains_kind(
  self : SafetyAuditReport,
  kind : SafetyViolationKind,
) -> Bool {
  for violation in self.violations {
    if violation.kind == kind {
      return true
    }
  }
  false
}

///|
pub fn SafetyAuditReport::violations(
  self : SafetyAuditReport,
) -> Array[SafetyViolation] {
  self.violations
}

///|
pub(all) struct ScanSafetySummary {
  mut checked_routes : Int
  mut fatal_count : Int
  mut warning_count : Int
  mut can_clear_signal : Bool
} derive(Debug)

///|
fn ScanSafetySummary::new() -> ScanSafetySummary {
  {
    checked_routes: 0,
    fatal_count: 0,
    warning_count: 0,
    can_clear_signal: true,
  }
}

///|
fn ScanSafetySummary::record_fatal(self : ScanSafetySummary) -> Unit {
  self.fatal_count = self.fatal_count + 1
  self.can_clear_signal = false
}

///|
pub fn InterlockingTable::audit_safety(
  self : InterlockingTable,
  station : @topology.Station,
) -> SafetyAuditReport {
  let report = SafetyAuditReport::new()

  for _, active_route in self.active_routes {
    if !self.is_route_reserved(active_route.rule.id) {
      continue
    }
    let rule = active_route.rule
    let lifecycle = self.validate_active_route_lifecycle(station, rule.id)
    for lifecycle_error in lifecycle.errors {
      report.add_violation(
        SafetyViolation::new(
          IllegalRouteTransition,
          SafetyEmergency,
          Some(rule.id),
          lifecycle_error,
        ),
      )
    }
    if active_route.occupancy_order_violation {
      report.add_violation(
        SafetyViolation::new(
          OccupancyOrderViolation,
          SafetyEmergency,
          Some(rule.id),
          "track occupancy was observed out of route order",
        ),
      )
    }

    for sw in rule.switches {
      match station.graph.get_turnout(sw.turnout_id) {
        None =>
          report.add_violation(
            SafetyViolation::new(
              MissingTurnout,
              SafetyEmergency,
              Some(rule.id),
              "main route turnout is missing",
            ),
          )
        Some(turnout) => {
          if turnout.position != sw.required_position {
            report.add_violation(
              SafetyViolation::new(
                TurnoutPositionMismatch,
                SafetyEmergency,
                Some(rule.id),
                "main route turnout position does not match its rule",
              ),
            )
          }
          if !turnout.is_locked {
            report.add_violation(
              SafetyViolation::new(
                TurnoutUnlocked,
                SafetyEmergency,
                Some(rule.id),
                "main route turnout is not locked",
              ),
            )
          }
        }
      }
    }

    for flank in rule.flank_switches {
      match station.graph.get_turnout(flank.turnout_id) {
        None =>
          report.add_violation(
            SafetyViolation::new(
              MissingTurnout,
              SafetyEmergency,
              Some(rule.id),
              "flank protection turnout is missing",
            ),
          )
        Some(turnout) => {
          if turnout.position != flank.required_position {
            report.add_violation(
              SafetyViolation::new(
                FlankProtectionMismatch,
                SafetyEmergency,
                Some(rule.id),
                flank.description,
              ),
            )
          }
          if !turnout.is_locked {
            report.add_violation(
              SafetyViolation::new(
                TurnoutUnlocked,
                SafetyEmergency,
                Some(rule.id),
                "flank protection turnout is not locked",
              ),
            )
          }
        }
      }
    }

    for hostile_id in rule.hostile_route_ids {
      if self.is_route_reserved(hostile_id) {
        report.add_violation(
          SafetyViolation::new(
            HostileRouteConflict,
            SafetyEmergency,
            Some(rule.id),
            "hostile route is active concurrently",
          ),
        )
      }
    }

    let entry_signal = station.graph.get_signal(rule.entry_signal)
    match entry_signal {
      None =>
        report.add_violation(
          SafetyViolation::new(
            ProceedSignalUnsafe,
            SafetyEmergency,
            Some(rule.id),
            "entry signal is missing",
          ),
        )
      Some(signal) => {
        if active_route.state == RouteFault && signal.current_aspect != Red {
          report.add_violation(
            SafetyViolation::new(
              FaultSignalNotRed,
              SafetyEmergency,
              Some(rule.id),
              "faulted route entry signal is not red",
            ),
          )
        }
        for track_id in rule.track_sections {
          if active_route.was_unlocked(track_id) &&
            !active_route.was_occupied(track_id) {
            report.add_violation(
              SafetyViolation::new(
                ReleaseBeforeTraversal,
                SafetyEmergency,
                Some(rule.id),
                "route section was released without a recorded traversal",
              ),
            )
          }
          match station.graph.get_track(track_id) {
            None =>
              report.add_violation(
                SafetyViolation::new(
                  MissingTrack,
                  SafetyEmergency,
                  Some(rule.id),
                  "route track section is missing",
                ),
              )
            Some(track) => {
              if track.state == Fault {
                report.add_violation(
                  SafetyViolation::new(
                    FaultedRouteSection,
                    SafetyEmergency,
                    Some(rule.id),
                    "route track section is faulted",
                  ),
                )
              }
              if signal.current_aspect.is_proceed() &&
                (track.state == Occupied || track.state == Fault) {
                report.add_violation(
                  SafetyViolation::new(
                    ProceedSignalUnsafe,
                    SafetyEmergency,
                    Some(rule.id),
                    "entry signal proceeds over an occupied or faulted section",
                  ),
                )
              }
            }
          }
        }
      }
    }
  }
  report
}

///|
pub fn InterlockingTable::scan_safety_summary(
  self : InterlockingTable,
  station : @topology.Station,
) -> ScanSafetySummary {
  let summary = ScanSafetySummary::new()

  for _, active_route in self.active_routes {
    if !self.is_route_reserved(active_route.rule.id) {
      continue
    }
    summary.checked_routes = summary.checked_routes + 1
    let rule = active_route.rule

    if !active_route.lifecycle_snapshot_is_safe() {
      summary.record_fatal()
    }

    for sw in rule.switches {
      match station.graph.get_turnout(sw.turnout_id) {
        None => summary.record_fatal()
        Some(turnout) =>
          if turnout.position != sw.required_position || !turnout.is_locked {
            summary.record_fatal()
          }
      }
    }

    for flank in rule.flank_switches {
      match station.graph.get_turnout(flank.turnout_id) {
        None => summary.record_fatal()
        Some(turnout) =>
          if turnout.position != flank.required_position || !turnout.is_locked {
            summary.record_fatal()
          }
      }
    }

    for hostile_id in rule.hostile_route_ids {
      if self.is_route_reserved(hostile_id) {
        summary.record_fatal()
      }
    }

    match station.graph.get_signal(rule.entry_signal) {
      None => summary.record_fatal()
      Some(signal) => {
        if active_route.state == RouteFault && signal.current_aspect != Red {
          summary.record_fatal()
        }
        for track_id in rule.track_sections {
          if active_route.was_unlocked(track_id) &&
            !active_route.was_occupied(track_id) {
            summary.record_fatal()
          }
          match station.graph.get_track(track_id) {
            None => summary.record_fatal()
            Some(track) => {
              if track.state == Fault {
                summary.record_fatal()
              }
              if signal.current_aspect.is_proceed() &&
                (track.state == Occupied || track.state == Fault) {
                summary.record_fatal()
              }
            }
          }
        }
      }
    }
  }
  summary
}

///|
pub(all) struct ReleaseCounters {
  mut sections_unlocked : Int
  mut switches_unlocked : Int
  mut is_route_completed : Bool
  mut entered_route : Bool
} derive(Debug)

///|
fn ReleaseCounters::empty() -> ReleaseCounters {
  {
    sections_unlocked: 0,
    switches_unlocked: 0,
    is_route_completed: false,
    entered_route: false,
  }
}