foch 0.0.1

EU4 mod analysis and merge toolkit
Documentation
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::iter::FromIterator;
use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign};
use std::sync::{OnceLock, RwLock};

#[derive(
	Clone,
	Copy,
	Debug,
	Eq,
	PartialEq,
	Hash,
	Serialize,
	Deserialize,
	rkyv::Archive,
	rkyv::Serialize,
	rkyv::Deserialize,
)]
pub struct ScopeType(u16);

impl ScopeType {
	pub fn name(self) -> &'static str {
		scope_name(self)
	}

	pub const fn as_index(self) -> u16 {
		self.0
	}
}

#[derive(
	Clone,
	Copy,
	Debug,
	Default,
	Eq,
	PartialEq,
	Hash,
	Serialize,
	Deserialize,
	rkyv::Archive,
	rkyv::Serialize,
	rkyv::Deserialize,
)]
pub enum MaybeScope {
	Known(ScopeType),
	#[default]
	Unknown,
}

impl MaybeScope {
	pub const fn known(scope_type: ScopeType) -> Self {
		Self::Known(scope_type)
	}

	pub const fn as_known(self) -> Option<ScopeType> {
		match self {
			Self::Known(scope_type) => Some(scope_type),
			Self::Unknown => None,
		}
	}

	pub const fn is_unknown(self) -> bool {
		matches!(self, Self::Unknown)
	}
}

impl From<ScopeType> for MaybeScope {
	fn from(value: ScopeType) -> Self {
		Self::Known(value)
	}
}

impl From<Option<ScopeType>> for MaybeScope {
	fn from(value: Option<ScopeType>) -> Self {
		value.map_or(Self::Unknown, Self::Known)
	}
}

#[derive(
	Clone,
	Copy,
	Debug,
	Default,
	Eq,
	PartialEq,
	Hash,
	Serialize,
	Deserialize,
	rkyv::Archive,
	rkyv::Serialize,
	rkyv::Deserialize,
)]
pub struct ScopeSet(u128);

impl ScopeSet {
	pub const EMPTY: Self = Self(0);
	pub const ALL: Self = Self(u128::MAX);

	pub fn from_scopes<I>(iter: I) -> Self
	where
		I: IntoIterator<Item = ScopeType>,
	{
		iter.into_iter().collect()
	}

	pub fn contains(self, scope_type: ScopeType) -> bool {
		(self.0 & bit_for(scope_type)) != 0
	}

	pub fn insert(&mut self, scope_type: ScopeType) {
		self.0 |= bit_for(scope_type);
	}

	pub const fn is_empty(self) -> bool {
		self.0 == 0
	}

	pub const fn union(self, other: Self) -> Self {
		Self(self.0 | other.0)
	}

	pub const fn intersect(self, other: Self) -> Self {
		Self(self.0 & other.0)
	}

	pub fn as_single_scope(self) -> Option<ScopeType> {
		(self.0.count_ones() == 1).then_some(ScopeType(self.0.trailing_zeros() as u16))
	}
}

impl FromIterator<ScopeType> for ScopeSet {
	fn from_iter<T: IntoIterator<Item = ScopeType>>(iter: T) -> Self {
		let mut set = Self::EMPTY;
		for scope_type in iter {
			set.insert(scope_type);
		}
		set
	}
}

impl From<ScopeType> for ScopeSet {
	fn from(value: ScopeType) -> Self {
		let mut set = Self::EMPTY;
		set.insert(value);
		set
	}
}

impl From<MaybeScope> for ScopeSet {
	fn from(value: MaybeScope) -> Self {
		value.as_known().map_or(Self::EMPTY, Self::from)
	}
}

impl BitOr for ScopeSet {
	type Output = Self;

	fn bitor(self, rhs: Self) -> Self::Output {
		self.union(rhs)
	}
}

impl BitOrAssign for ScopeSet {
	fn bitor_assign(&mut self, rhs: Self) {
		self.0 |= rhs.0;
	}
}

impl BitAnd for ScopeSet {
	type Output = Self;

	fn bitand(self, rhs: Self) -> Self::Output {
		self.intersect(rhs)
	}
}

impl BitAndAssign for ScopeSet {
	fn bitand_assign(&mut self, rhs: Self) {
		self.0 &= rhs.0;
	}
}

#[derive(Debug, Default)]
pub struct ScopeRegistry {
	names: Vec<&'static str>,
	by_name: HashMap<String, ScopeType>,
}

pub static GLOBAL_REGISTRY: OnceLock<RwLock<ScopeRegistry>> = OnceLock::new();

#[derive(Clone, Copy, Debug)]
struct BaseScopes {
	country: ScopeType,
	province: ScopeType,
}

static BASE_SCOPES: OnceLock<RwLock<Option<BaseScopes>>> = OnceLock::new();

impl ScopeRegistry {
	#[cfg(any(test, feature = "test-utils"))]
	pub fn install_test_defaults() {
		base_scope::init_base_scopes("country", "province");
	}
}

pub fn intern_scope(name: &str) -> ScopeType {
	let registry = registry();
	let mut registry = registry
		.write()
		.unwrap_or_else(|poisoned| poisoned.into_inner());
	if let Some(scope_type) = registry.by_name.get(name).copied() {
		return scope_type;
	}
	let leaked = Box::leak(name.to_string().into_boxed_str());
	let scope_type = ScopeType(registry.names.len() as u16);
	registry.names.push(leaked);
	registry.by_name.insert(leaked.to_string(), scope_type);
	scope_type
}

pub fn lookup_scope(name: &str) -> Option<ScopeType> {
	registry()
		.read()
		.unwrap_or_else(|poisoned| poisoned.into_inner())
		.by_name
		.get(name)
		.copied()
}

pub fn scope_name(scope_type: ScopeType) -> &'static str {
	let registry = registry()
		.read()
		.unwrap_or_else(|poisoned| poisoned.into_inner());
	registry
		.names
		.get(scope_type.as_index() as usize)
		.unwrap_or_else(|| panic!("scope index {} is not registered", scope_type.as_index()))
}

pub mod base_scope {
	use super::{BASE_SCOPES, BaseScopes, ScopeType, intern_scope};
	use std::sync::RwLock;

	pub fn country() -> ScopeType {
		base_scopes().country
	}

	pub fn province() -> ScopeType {
		base_scopes().province
	}

	pub fn is_initialized() -> bool {
		state()
			.read()
			.unwrap_or_else(|poisoned| poisoned.into_inner())
			.is_some()
	}

	pub fn init_base_scopes(country_name: &str, province_name: &str) {
		let mut state = state()
			.write()
			.unwrap_or_else(|poisoned| poisoned.into_inner());
		*state = Some(BaseScopes {
			country: intern_scope(country_name),
			province: intern_scope(province_name),
		});
	}

	fn state() -> &'static RwLock<Option<BaseScopes>> {
		BASE_SCOPES.get_or_init(|| RwLock::new(None))
	}

	fn base_scopes() -> BaseScopes {
		state()
			.read()
			.unwrap_or_else(|poisoned| poisoned.into_inner())
			.as_ref()
			.copied()
			.expect("base scopes are not initialized")
	}
}

fn registry() -> &'static RwLock<ScopeRegistry> {
	GLOBAL_REGISTRY.get_or_init(|| RwLock::new(ScopeRegistry::default()))
}

fn bit_for(scope_type: ScopeType) -> u128 {
	let index = scope_type.as_index() as u32;
	assert!(index < 128, "scope index {index} exceeds ScopeSet capacity");
	1_u128 << index
}

#[cfg(any(test, feature = "test-utils"))]
pub mod test_support {
	use super::ScopeRegistry;

	pub fn install_defaults() {
		ScopeRegistry::install_test_defaults();
	}
}

#[cfg(test)]
mod tests {
	use super::{MaybeScope, ScopeRegistry, ScopeSet, base_scope, intern_scope, scope_name};

	#[test]
	fn intern_scope_returns_same_scope_type_for_same_name() {
		let first = intern_scope("foo");
		let second = intern_scope("foo");
		assert_eq!(first, second);
	}

	#[test]
	fn scope_name_returns_registered_name() {
		let bar = intern_scope("bar");
		assert_eq!(scope_name(bar), "bar");
	}

	#[test]
	fn empty_scope_set_never_contains_scopes() {
		let any = intern_scope("baz");
		assert!(!ScopeSet::EMPTY.contains(any));
	}

	#[test]
	fn scope_set_from_iter_contains_inserted_scopes() {
		ScopeRegistry::install_test_defaults();
		let set = ScopeSet::from_scopes([base_scope::country(), base_scope::province()]);
		assert!(set.contains(base_scope::country()));
	}

	#[test]
	fn maybe_scope_known_returns_inner_scope() {
		let scope_type = intern_scope("qux");
		assert_eq!(MaybeScope::Known(scope_type).as_known(), Some(scope_type));
	}

	#[test]
	fn maybe_scope_unknown_reports_unknown() {
		assert!(MaybeScope::Unknown.is_unknown());
	}

	#[test]
	fn rkyv_round_trip_preserves_scope_types() {
		ScopeRegistry::install_test_defaults();
		let scope_type = base_scope::country();
		let scope_set = ScopeSet::from_scopes([base_scope::country(), base_scope::province()]);
		let maybe_scope = MaybeScope::Known(base_scope::province());

		let scope_type_bytes =
			rkyv::to_bytes::<rkyv::rancor::Error>(&scope_type).expect("encode scope type");
		let scope_set_bytes =
			rkyv::to_bytes::<rkyv::rancor::Error>(&scope_set).expect("encode scope set");
		let maybe_scope_bytes =
			rkyv::to_bytes::<rkyv::rancor::Error>(&maybe_scope).expect("encode maybe scope");

		let decoded_scope_type =
			rkyv::from_bytes::<super::ScopeType, rkyv::rancor::Error>(&scope_type_bytes)
				.expect("decode scope type");
		let decoded_scope_set = rkyv::from_bytes::<ScopeSet, rkyv::rancor::Error>(&scope_set_bytes)
			.expect("decode scope set");
		let decoded_maybe_scope =
			rkyv::from_bytes::<MaybeScope, rkyv::rancor::Error>(&maybe_scope_bytes)
				.expect("decode maybe scope");

		assert_eq!(decoded_scope_type, scope_type);
		assert_eq!(decoded_scope_set, scope_set);
		assert_eq!(decoded_maybe_scope, maybe_scope);
	}

	#[test]
	fn base_scope_initialization_exposes_registered_scopes() {
		ScopeRegistry::install_test_defaults();
		assert!(base_scope::is_initialized());
		assert_eq!(base_scope::country().name(), "country");
		assert_eq!(base_scope::province().name(), "province");
	}
}