prns-core 0.3.4

Pure Reticulum engine and wire contract for Personal Reticulum
use crate::identity::IdentityHash;
use crate::lemire_index::buckets_for_two_thirds_load;
use crate::units::InstantMillis;

pub const fn blackhole_index_buckets(entries: usize) -> usize {
    buckets_for_two_thirds_load(entries)
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlackholeExpiry {
    Indefinite,
    At(InstantMillis),
}

impl BlackholeExpiry {
    pub const fn is_expired_at(self, now: InstantMillis) -> bool {
        match self {
            Self::Indefinite => false,
            Self::At(deadline) => deadline.0 < now.0,
        }
    }

    pub const fn first_expired_at(self) -> Option<InstantMillis> {
        match self {
            Self::Indefinite => None,
            Self::At(deadline) => match deadline.0.checked_add(1) {
                Some(at) => Some(InstantMillis(at)),
                None => None,
            },
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BlackholedIdentity<Reason> {
    pub identity: IdentityHash,
    pub source: IdentityHash,
    pub expiry: BlackholeExpiry,
    pub reason: Option<Reason>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlackholeIdentityOutcome {
    Added,
    AlreadyPresent,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlackholeInsertFailure {
    CapacityExhausted,
    ReasonTooLong,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnblackholeIdentityOutcome {
    Removed,
    NotFound,
}

pub trait BlackholeTable {
    type InsertError;

    fn classify_insert_error(error: Self::InsertError) -> BlackholeInsertFailure;

    fn len(&self) -> usize;
    fn index_of(&self, identity: &IdentityHash) -> Option<usize>;
    fn identities(&self) -> &[IdentityHash];
    fn sources(&self) -> &[IdentityHash];
    fn expiries(&self) -> &[BlackholeExpiry];
    fn reason_at(&self, index: usize) -> Option<&str>;
    fn push(&mut self, entry: BlackholedIdentity<&str>) -> Result<(), Self::InsertError>;
    fn swap_remove(&mut self, index: usize);

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

#[derive(Debug)]
pub struct IdentityBlackholes<Table> {
    table: Table,
}

impl<Table: BlackholeTable + Default> Default for IdentityBlackholes<Table> {
    fn default() -> Self {
        Self {
            table: Table::default(),
        }
    }
}

impl<Table: BlackholeTable> IdentityBlackholes<Table> {
    pub fn len(&self) -> usize {
        self.table.len()
    }

    pub fn is_empty(&self) -> bool {
        self.table.is_empty()
    }

    pub fn is_blackholed(&self, identity: &IdentityHash) -> bool {
        self.table.index_of(identity).is_some()
    }

    pub fn entries(&self) -> impl Iterator<Item = BlackholedIdentity<&str>> + '_ {
        (0..self.table.len()).map(|index| BlackholedIdentity {
            identity: self.table.identities()[index],
            source: self.table.sources()[index],
            expiry: self.table.expiries()[index],
            reason: self.table.reason_at(index),
        })
    }

    pub fn earliest_expiry_at(&self) -> Option<InstantMillis> {
        self.table
            .expiries()
            .iter()
            .filter_map(|expiry| expiry.first_expired_at())
            .min()
    }

    pub fn blackhole_identity(
        &mut self,
        entry: BlackholedIdentity<&str>,
    ) -> Result<BlackholeIdentityOutcome, Table::InsertError> {
        if self.is_blackholed(&entry.identity) {
            return Ok(BlackholeIdentityOutcome::AlreadyPresent);
        }
        self.table.push(entry)?;
        Ok(BlackholeIdentityOutcome::Added)
    }

    pub fn unblackhole_identity(&mut self, identity: &IdentityHash) -> UnblackholeIdentityOutcome {
        let Some(index) = self.table.index_of(identity) else {
            return UnblackholeIdentityOutcome::NotFound;
        };
        self.table.swap_remove(index);
        UnblackholeIdentityOutcome::Removed
    }

    pub fn cull_expired(&mut self, now: InstantMillis) -> usize {
        let mut removed = 0;
        let mut index = 0;
        while index < self.table.len() {
            if self.table.expiries()[index].is_expired_at(now) {
                self.table.swap_remove(index);
                removed += 1;
            } else {
                index += 1;
            }
        }
        removed
    }
}