use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use super::primitives::bit_width;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ValueCtx(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ExiOptions {
pub value_max_length: Option<usize>,
pub value_partition_capacity: Option<usize>,
}
impl ExiOptions {
pub const ISO15118: Self = Self { value_max_length: None, value_partition_capacity: None };
#[must_use]
pub const fn table_enabled(&self) -> bool {
!matches!(self.value_partition_capacity, Some(0))
}
#[must_use]
pub const fn admits(&self, char_len: usize) -> bool {
match self.value_max_length {
Some(max) => char_len <= max,
None => true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Hit {
Local(u64),
Global(u64),
Miss,
}
#[derive(Debug, Default)]
struct Partition {
entries: Vec<usize>,
lookup: BTreeMap<String, u64>,
}
#[derive(Debug, Default)]
pub struct ValueTable {
globals: Vec<String>,
global_lookup: BTreeMap<String, u64>,
locals: BTreeMap<ValueCtx, Partition>,
}
impl ValueTable {
#[must_use]
pub const fn new() -> Self {
Self { globals: Vec::new(), global_lookup: BTreeMap::new(), locals: BTreeMap::new() }
}
#[must_use]
pub(crate) fn local_index_width(&self, ctx: ValueCtx) -> u32 {
bit_width(self.local_len(ctx))
}
#[must_use]
pub(crate) fn global_index_width(&self) -> u32 {
bit_width(self.globals.len() as u64)
}
fn local_len(&self, ctx: ValueCtx) -> u64 {
self.locals.get(&ctx).map_or(0, |p| p.entries.len() as u64)
}
pub(crate) fn find(&self, ctx: ValueCtx, value: &str) -> Hit {
if let Some(p) = self.locals.get(&ctx)
&& let Some(&idx) = p.lookup.get(value)
{
return Hit::Local(idx);
}
if let Some(&idx) = self.global_lookup.get(value) {
return Hit::Global(idx);
}
Hit::Miss
}
pub(crate) fn local(&self, ctx: ValueCtx, index: u64) -> Option<&str> {
let p = self.locals.get(&ctx)?;
let global_id = *p.entries.get(usize::try_from(index).ok()?)?;
self.globals.get(global_id).map(String::as_str)
}
pub(crate) fn global(&self, index: u64) -> Option<&str> {
self.globals.get(usize::try_from(index).ok()?).map(String::as_str)
}
pub(crate) fn insert(&mut self, ctx: ValueCtx, value: &str) {
let global_id = if let Some(&id) = self.global_lookup.get(value) {
id
} else {
let id = self.globals.len() as u64;
self.globals.push(String::from(value));
self.global_lookup.insert(String::from(value), id);
id
};
let Ok(global_id) = usize::try_from(global_id) else { return };
let p = self.locals.entry(ctx).or_default();
if !p.lookup.contains_key(value) {
let local_idx = p.entries.len() as u64;
p.entries.push(global_id);
p.lookup.insert(String::from(value), local_idx);
}
}
#[must_use]
pub fn len(&self) -> usize {
self.globals.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.globals.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
const A: ValueCtx = ValueCtx(1);
const B: ValueCtx = ValueCtx(2);
#[test]
fn first_sighting_misses() {
let t = ValueTable::new();
assert_eq!(t.find(A, "x"), Hit::Miss);
}
#[test]
fn repeat_in_same_context_hits_locally() {
let mut t = ValueTable::new();
t.insert(A, "x");
assert_eq!(t.find(A, "x"), Hit::Local(0));
}
#[test]
fn repeat_in_another_context_hits_globally() {
let mut t = ValueTable::new();
t.insert(A, "x");
assert_eq!(t.find(B, "x"), Hit::Global(0));
}
#[test]
fn a_global_hit_stored_locally_reuses_the_global_id() {
let mut t = ValueTable::new();
t.insert(A, "x");
t.insert(B, "x");
assert_eq!(t.len(), 1, "the value is global only once");
assert_eq!(t.find(B, "x"), Hit::Local(0));
assert_eq!(t.local(B, 0), Some("x"));
}
#[test]
fn index_widths_grow_with_the_partitions() {
let mut t = ValueTable::new();
assert_eq!(t.local_index_width(A), 0);
t.insert(A, "one");
assert_eq!(t.local_index_width(A), 0, "one entry needs no bits");
t.insert(A, "two");
assert_eq!(t.local_index_width(A), 1);
t.insert(A, "three");
assert_eq!(t.local_index_width(A), 2);
}
#[test]
fn out_of_range_indices_resolve_to_nothing() {
let mut t = ValueTable::new();
t.insert(A, "x");
assert_eq!(t.local(A, 1), None);
assert_eq!(t.global(9), None);
}
#[test]
fn iso15118_options_enable_an_unbounded_table() {
let o = ExiOptions::ISO15118;
assert!(o.table_enabled());
assert!(o.admits(usize::MAX));
}
#[test]
fn zero_capacity_disables_the_table() {
let o = ExiOptions { value_partition_capacity: Some(0), ..ExiOptions::default() };
assert!(!o.table_enabled());
}
}