use core::ops::{Index, IndexMut};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::coord::{Idx, Tag};
use crate::grid::Grid;
use alloc::vec;
use alloc::vec::Vec;
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct CellMap<T> {
of: Vec<T>,
#[cfg_attr(feature = "serde", serde(skip))]
tag: Option<Tag>,
}
impl<T> CellMap<T> {
#[must_use]
pub fn new<B: Grid + ?Sized>(g: &B, value: T) -> Self
where
T: Clone,
{
Self {
of: vec![value; g.len()],
tag: Some(g.tag()),
}
}
#[must_use]
pub fn from_fn<B: Grid + ?Sized>(g: &B, f: impl Fn(B::Cell) -> T) -> Self {
Self {
of: g.cells().map(f).collect(),
tag: Some(g.tag()),
}
}
pub fn iter(&self) -> impl Iterator<Item = (Idx, &T)> {
let tag = self.tag.unwrap_or(Tag::ANY);
#[allow(clippy::cast_possible_truncation)]
self.of
.iter()
.enumerate()
.map(move |(i, v)| (Idx::new(tag, i as u32), v))
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = (Idx, &mut T)> {
let tag = self.tag.unwrap_or(Tag::ANY);
#[allow(clippy::cast_possible_truncation)]
self.of
.iter_mut()
.enumerate()
.map(move |(i, v)| (Idx::new(tag, i as u32), v))
}
#[must_use]
pub fn as_slice(&self) -> &[T] {
&self.of
}
#[must_use]
pub fn as_mut_slice(&mut self) -> &mut [T] {
&mut self.of
}
#[must_use]
pub fn into_vec(self) -> Vec<T> {
self.of
}
#[must_use]
pub fn len(&self) -> usize {
self.of.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.of.is_empty()
}
pub fn fill(&mut self, value: T)
where
T: Clone,
{
self.of.fill(value);
}
#[must_use]
pub fn get(&self, i: Idx) -> Option<&T> {
self.check_tag(i);
self.of.get(i.raw() as usize)
}
#[must_use]
pub fn get_mut(&mut self, i: Idx) -> Option<&mut T> {
self.check_tag(i);
self.of.get_mut(i.raw() as usize)
}
fn check_tag(&self, i: Idx) {
debug_assert!(
self.tag.is_none_or(|t| t.agrees(i.tag())),
"cell {i} was issued by a different grid than the one this CellMap was built from \
(a map is positional, and this index is in range for both, so nothing else can \
catch it)",
);
}
#[track_caller]
fn slot(&self, i: Idx) -> usize {
self.check_tag(i);
assert!(
(i.raw() as usize) < self.of.len(),
"cell {i} is not in this CellMap, which covers {} cells (a map is positional, so \
one built before `filtered` renumbered will not do)",
self.of.len(),
);
i.raw() as usize
}
}
impl<T> Index<Idx> for CellMap<T> {
type Output = T;
#[track_caller]
fn index(&self, i: Idx) -> &T {
&self.of[self.slot(i)]
}
}
impl<T> IndexMut<Idx> for CellMap<T> {
#[track_caller]
fn index_mut(&mut self, i: Idx) -> &mut T {
let slot = self.slot(i);
&mut self.of[slot]
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::coord::Sq;
use crate::full::{Adjacency, FullGrid};
#[test]
fn a_map_covers_every_cell_and_answers_for_the_right_one() {
let g = FullGrid::square(4, 3, Adjacency::Four);
let m = CellMap::from_fn(&g, |c: Sq| c.x + c.y);
assert_eq!(m.len(), g.len());
for i in g.indices() {
assert_eq!(m[i], g.coord(i).x + g.coord(i).y);
}
assert_eq!(m.iter().count(), g.len());
}
#[test]
fn a_stale_index_is_refused_rather_than_answered_for_the_wrong_cell() {
let full = FullGrid::square(3, 3, Adjacency::Four);
let map = CellMap::new(&full.filtered(|c| c.x != 2), 0u8);
assert_eq!(map.len(), 6);
let stale = full.at(Sq::new(2, 2));
assert!(std::panic::catch_unwind(|| map[stale]).is_err());
}
#[test]
fn a_map_supports_checked_access_and_slice_interop() {
let g = FullGrid::square(3, 1, Adjacency::Four);
let mut map = CellMap::from_fn(&g, |c: Sq| c.x);
assert_eq!(map.get(g.at(Sq::new(1, 0))), Some(&1));
assert_eq!(map.get(Idx::new(Tag::ANY, 3)), None);
for (_, value) in map.iter_mut() {
*value += 10;
}
assert_eq!(map.as_slice(), &[10, 11, 12]);
map.as_mut_slice()[1] = 99;
assert_eq!(map.into_vec(), vec![10, 99, 12]);
}
#[test]
fn fill_replaces_every_value() {
let g = FullGrid::square(2, 2, Adjacency::Four);
let mut map = CellMap::new(&g, false);
map.fill(true);
assert!(map.iter().all(|(_, value)| *value));
}
}