///|
pub fn validate_route_transition(
  from : RouteState,
  to : RouteState,
) -> Result[Unit, String] {
  let allowed = match (from, to) {
    (RouteInactive, RouteRequested) => true
    (RouteRequested, SwitchesThrowing) => true
    (RouteRequested, RouteInactive) => true
    (SwitchesThrowing, RouteLocked) => true
    (SwitchesThrowing, RouteFault) => true
    (SwitchesThrowing, RouteInactive) => true
    (RouteLocked, ApproachLocked) => true
    (RouteLocked, TrainInRoute) => true
    (RouteLocked, RouteCancelling) => true
    (RouteLocked, RouteFault) => true
    (ApproachLocked, TrainInRoute) => true
    (ApproachLocked, RouteCancelling) => true
    (ApproachLocked, RouteFault) => true
    (TrainInRoute, SectionalUnlocking) => true
    (TrainInRoute, RouteFault) => true
    (SectionalUnlocking, TrainInRoute) => true
    (SectionalUnlocking, RouteInactive) => true
    (SectionalUnlocking, RouteFault) => true
    (RouteCancelling, RouteInactive) => true
    (RouteCancelling, RouteFault) => true
    (RouteFault, RouteCancelling) => true
    (RouteFault, RouteInactive) => true
    _ => false
  }
  if allowed {
    Ok(())
  } else {
    Err(
      "illegal route transition: " + from.to_string() + " -> " + to.to_string(),
    )
  }
}

///|
pub(all) struct RouteLifecycleAudit {
  route_id : RouteId
  mut is_valid : Bool
  mut transition_count : Int
  errors : Array[String]
} derive(Debug)

///|
fn RouteLifecycleAudit::new(route_id : RouteId) -> RouteLifecycleAudit {
  { route_id, is_valid: true, transition_count: 0, errors: [] }
}

///|
fn RouteLifecycleAudit::add_error(
  self : RouteLifecycleAudit,
  message : String,
) -> Unit {
  self.errors.push(message)
  self.is_valid = false
}

///|
fn ActiveRouteState::has_occupied_section(self : ActiveRouteState) -> Bool {
  let mut index = 0
  while index < 256 {
    if self.occupied_sections[index] {
      return true
    }
    index = index + 1
  }
  false
}

///|
fn ActiveRouteState::has_invalid_release_history(
  self : ActiveRouteState,
) -> Bool {
  for track_id in self.rule.track_sections {
    if self.was_unlocked(track_id) && !self.was_occupied(track_id) {
      return true
    }
  }
  false
}

///|
fn ActiveRouteState::lifecycle_snapshot_is_safe(
  self : ActiveRouteState,
) -> Bool {
  if self.occupancy_order_violation || self.has_invalid_release_history() {
    return false
  }
  match self.state {
    RouteLocked => !self.has_occupied_section()
    TrainInRoute | SectionalUnlocking => self.has_occupied_section()
    _ => true
  }
}

///|
pub fn InterlockingTable::validate_active_route_lifecycle(
  self : InterlockingTable,
  station : @topology.Station,
  route_id : RouteId,
) -> RouteLifecycleAudit {
  let audit = RouteLifecycleAudit::new(route_id)
  match self.get_active_route(route_id) {
    None => audit.add_error("route is not present in active table")
    Some(active_route) => {
      audit.transition_count = 1
      if !active_route.lifecycle_snapshot_is_safe() {
        audit.add_error("active route state does not match occupancy history")
      }
      if active_route.state == RouteFault {
        match station.graph.get_signal(active_route.rule.entry_signal) {
          Some(signal) =>
            if signal.current_aspect != @topology.Red {
              audit.add_error("faulted route entry signal is not red")
            }
          None => audit.add_error("faulted route entry signal is missing")
        }
      }
    }
  }
  audit
}