///|
pub(all) struct InterlockingTable {
  rules : Map[Int, InterlockingRule]
  active_routes : Map[Int, ActiveRouteState]
  active_route_flags : FixedArray[Bool]
  signal_pair_index : Map[Int, RouteId]
  track_to_routes : Map[Int, Array[RouteId]]
  turnout_to_routes : Map[Int, Array[RouteId]]
  acquired_turnout_scratch : FixedArray[@topology.TurnoutId]
  acquired_track_scratch : FixedArray[@topology.TrackId]
  mut acquired_turnout_count : Int
  mut acquired_track_count : Int
  mut next_route_id : Int
} derive(Debug)

///|
pub fn InterlockingTable::new() -> InterlockingTable {
  {
    rules: Map([]),
    active_routes: Map([]),
    active_route_flags: FixedArray::make(256, false),
    signal_pair_index: Map([]),
    track_to_routes: Map([]),
    turnout_to_routes: Map([]),
    acquired_turnout_scratch: FixedArray::make(256, @topology.TurnoutId::new(0)),
    acquired_track_scratch: FixedArray::make(256, @topology.TrackId::new(0)),
    acquired_turnout_count: 0,
    acquired_track_count: 0,
    next_route_id: 1,
  }
}

///|
pub fn InterlockingTable::generate_pair_key(
  entry : @topology.SignalId,
  exit : @topology.SignalId,
) -> Int {
  entry.id * 10000 + exit.id
}

///|
pub fn InterlockingTable::register_rule(
  self : InterlockingTable,
  name : String,
  route_type : RouteType,
  entry_signal : @topology.SignalId,
  exit_signal : @topology.SignalId,
  switches : Array[SwitchRequirement],
  flank_switches : Array[FlankProtection],
  track_sections : Array[@topology.TrackId],
  hostile_route_ids : Array[RouteId],
  target_aspect : @topology.SignalAspect,
  approach_section : @topology.TrackId?,
) -> RouteId {
  match
    self.register_rule_checked(
      name, route_type, entry_signal, exit_signal, switches, flank_switches, track_sections,
      hostile_route_ids, target_aspect, approach_section,
    ) {
    Ok(route_id) => route_id
    Err(_) => RouteId::new(0)
  }
}

///|
pub fn InterlockingTable::register_rule_checked(
  self : InterlockingTable,
  name : String,
  route_type : RouteType,
  entry_signal : @topology.SignalId,
  exit_signal : @topology.SignalId,
  switches : Array[SwitchRequirement],
  flank_switches : Array[FlankProtection],
  track_sections : Array[@topology.TrackId],
  hostile_route_ids : Array[RouteId],
  target_aspect : @topology.SignalAspect,
  approach_section : @topology.TrackId?,
) -> Result[RouteId, String] {
  if self.next_route_id <= 0 || self.next_route_id >= 256 {
    return Err("Route capacity exhausted: maximum route id is 255")
  }

  let r_id = RouteId::new(self.next_route_id)
  self.next_route_id = self.next_route_id + 1

  let rule = InterlockingRule::new(
    r_id, name, route_type, entry_signal, exit_signal, switches, flank_switches,
    track_sections, hostile_route_ids, target_aspect, approach_section,
  )

  self.rules.set(r_id.id, rule)
  // Route state is allocated during timetable/configuration setup. The hot
  // scan path only replaces this existing map entry and never grows it.
  self.active_routes.set(r_id.id, ActiveRouteState::new(rule))

  let pair_key = InterlockingTable::generate_pair_key(entry_signal, exit_signal)
  self.signal_pair_index.set(pair_key, r_id)

  for tr in track_sections {
    match self.track_to_routes.get(tr.id) {
      Some(arr) => arr.push(r_id)
      None => self.track_to_routes.set(tr.id, [r_id])
    }
  }

  for sw in switches {
    match self.turnout_to_routes.get(sw.turnout_id.id) {
      Some(arr) => arr.push(r_id)
      None => self.turnout_to_routes.set(sw.turnout_id.id, [r_id])
    }
  }

  Ok(r_id)
}

///|
pub fn InterlockingTable::is_active_slot(
  self : InterlockingTable,
  slot : Int,
) -> Bool {
  if slot < 0 || slot >= 256 {
    false
  } else {
    self.active_route_flags[slot]
  }
}

///|
pub fn InterlockingTable::active_slot_count(self : InterlockingTable) -> Int {
  let mut count = 0
  for i = 0; i < 256; i = i + 1 {
    if self.active_route_flags[i] {
      count = count + 1
    }
  }
  count
}

///|
fn InterlockingTable::set_active_slot(
  self : InterlockingTable,
  route_id : RouteId,
  active : Bool,
) -> Unit {
  if route_id.id > 0 && route_id.id < 256 {
    self.active_route_flags[route_id.id] = active
  }
}

///|
fn InterlockingTable::deactivate_route(
  self : InterlockingTable,
  route_id : RouteId,
) -> Unit {
  match self.active_routes.get(route_id.id) {
    Some(active_route) => active_route.state = RouteInactive
    None => ()
  }
  self.set_active_slot(route_id, false)
}

///|
fn InterlockingTable::activate_route(
  self : InterlockingTable,
  route_id : RouteId,
  rule : InterlockingRule,
) -> Unit {
  match self.active_routes.get(route_id.id) {
    Some(active_route) => active_route.reset_for_rule(rule)
    None => self.active_routes.set(route_id.id, ActiveRouteState::new(rule))
  }
  match self.active_routes.get(route_id.id) {
    Some(active_route) => active_route.state = RouteLocked
    None => ()
  }
  self.set_active_slot(route_id, true)
}

///|
pub fn InterlockingTable::get_rule(
  self : InterlockingTable,
  route_id : RouteId,
) -> InterlockingRule? {
  self.rules.get(route_id.id)
}

///|
pub fn InterlockingTable::lookup_route_by_signals(
  self : InterlockingTable,
  entry : @topology.SignalId,
  exit : @topology.SignalId,
) -> RouteId? {
  let key = InterlockingTable::generate_pair_key(entry, exit)
  self.signal_pair_index.get(key)
}

///|
pub fn InterlockingTable::get_active_route(
  self : InterlockingTable,
  route_id : RouteId,
) -> ActiveRouteState? {
  match self.active_routes.get(route_id.id) {
    Some(active_route) =>
      if self.is_route_reserved(route_id) {
        Some(active_route)
      } else {
        None
      }
    None => None
  }
}

///|
pub fn InterlockingTable::is_route_active(
  self : InterlockingTable,
  route_id : RouteId,
) -> Bool {
  match self.active_routes.get(route_id.id) {
    None => false
    Some(ar) =>
      match ar.state {
        RouteInactive | RouteFault => false
        _ => true
      }
  }
}

///|
pub fn InterlockingTable::is_route_reserved(
  self : InterlockingTable,
  route_id : RouteId,
) -> Bool {
  match self.active_routes.get(route_id.id) {
    None => false
    Some(ar) =>
      match ar.state {
        RouteInactive => false
        _ => true
      }
  }
}

///|
pub fn InterlockingTable::get_routes_by_track(
  self : InterlockingTable,
  track_id : @topology.TrackId,
) -> Array[RouteId] {
  match self.track_to_routes.get(track_id.id) {
    None => []
    Some(arr) => arr
  }
}