prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use super::super::core::RouteWarmth;
use crate::interfaces::InterfaceId;
use crate::units::InstantMillis;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Departure {
    Forgotten,
    MayReturn,
}

pub const DEPARTED_INTERFACE_GRACE_MS: u64 = 5 * 60 * 1_000;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DepartedInterface {
    pub interface: InterfaceId,
    pub warm_until: InstantMillis,
}

pub trait DepartedInterfaceTable {
    fn capacity(&self) -> usize;
    fn len(&self) -> usize;

    fn is_empty(&self) -> bool {
        self.len() == 0
    }

    fn interfaces(&self) -> &[InterfaceId];
    fn warm_untils(&self) -> &[InstantMillis];
    fn push(&mut self, entry: DepartedInterface);
    fn swap_remove(&mut self, index: usize);
}

#[derive(Debug, Default)]
pub struct DepartedInterfaces<C: DepartedInterfaceTable> {
    table: C,
}

impl<C: DepartedInterfaceTable> DepartedInterfaces<C> {
    pub fn record(&mut self, interface: InterfaceId, departure: Departure, now: InstantMillis) {
        let mut index = 0;
        while index < self.table.len() {
            if self.table.interfaces()[index] == interface || self.table.warm_untils()[index] <= now
            {
                self.table.swap_remove(index);
            } else {
                index += 1;
            }
        }
        if departure == Departure::Forgotten {
            return;
        }
        if self.table.len() >= self.table.capacity() {
            self.evict_soonest_expiring();
        }
        self.table.push(DepartedInterface {
            interface,
            warm_until: InstantMillis(now.0.saturating_add(DEPARTED_INTERFACE_GRACE_MS)),
        });
    }

    pub fn evict_expired(&mut self, now: InstantMillis) -> usize {
        let mut evicted = 0;
        while let Some(index) = self
            .table
            .warm_untils()
            .iter()
            .position(|warm_until| *warm_until <= now)
        {
            self.table.swap_remove(index);
            evicted += 1;
        }
        evicted
    }

    fn evict_soonest_expiring(&mut self) {
        let Some(index) = self
            .table
            .warm_untils()
            .iter()
            .enumerate()
            .min_by_key(|(_, warm_until)| **warm_until)
            .map(|(index, _)| index)
        else {
            return;
        };
        self.table.swap_remove(index);
    }
}

impl<C: DepartedInterfaceTable> RouteWarmth for DepartedInterfaces<C> {
    fn warm_until(&self, interface: InterfaceId) -> Option<InstantMillis> {
        self.table
            .interfaces()
            .iter()
            .position(|candidate| *candidate == interface)
            .map(|index| self.table.warm_untils()[index])
    }
}