use alloc::borrow::{Cow, ToOwned};
use alloc::string::String;
use core::cell::{Cell, RefCell};
use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::path::Path;
use std::sync::{OnceLock, PoisonError, RwLock};
use mf2_catalog::{Catalog, Entry, MsgId};
use mf2_runtime::{BidiStrategy, FormatContext, Formatter, Sink, TimeZone};
use super::{Catalogs, Error, LocaleSource};
use crate::{Corpus, Message};
pub(crate) trait TextOf {
fn msg_id(&self) -> MsgId;
fn write_text(&self, f: &Formatter<'_>, out: &mut dyn Sink);
}
static STORE: OnceLock<Catalogs> = OnceLock::new();
const NONE: usize = usize::MAX;
static ACTIVE: AtomicUsize = AtomicUsize::new(NONE);
std::thread_local! {
static OVERRIDE: Cell<usize> = const { Cell::new(NONE) };
}
fn pack(index: usize, source: LocaleSource) -> usize {
let code = match source {
LocaleSource::Explicit => 0,
LocaleSource::System => 1,
LocaleSource::Source => 2,
};
(index << 2) | code
}
fn unpack(active: usize) -> (usize, LocaleSource) {
let source = match active & 3 {
0 => LocaleSource::Explicit,
1 => LocaleSource::System,
_ => LocaleSource::Source,
};
(active >> 2, source)
}
fn store_of(
corpus: &'static Corpus,
load: impl FnOnce() -> Result<Catalogs, Error>,
) -> Result<&'static Catalogs, Error> {
let store = if let Some(store) = STORE.get() {
store
} else {
let catalogs = load()?;
STORE.get_or_init(move || catalogs)
};
if core::ptr::eq(store.corpus(), corpus) {
Ok(store)
} else {
Err(Error::AnotherCorpus)
}
}
pub fn install(corpus: &'static Corpus) {
match store_of(corpus, || Catalogs::embedded(corpus)) {
Ok(store) => choose(store),
Err(error) => refused("install()", &error),
}
}
pub fn install_from_directory(
corpus: &'static Corpus,
directory: impl AsRef<Path>,
) -> Result<(), Error> {
let store = store_of(corpus, || Catalogs::from_directory(corpus, directory))?;
choose(store);
Ok(())
}
fn choose(store: &Catalogs) {
if ACTIVE.load(Ordering::Acquire) != NONE {
return;
}
let (index, source) = store.system_choice();
let _ = ACTIVE.compare_exchange(
NONE,
pack(index, source),
Ordering::AcqRel,
Ordering::Acquire,
);
}
pub fn set_locale(locale: &str) -> Result<(), Error> {
let (Some(store), false) = (STORE.get(), ACTIVE.load(Ordering::Acquire) == NONE) else {
not_installed()
};
let index = store
.index(locale)
.ok_or_else(|| Error::UnknownLocale(locale.to_owned()))?;
ACTIVE.store(pack(index, LocaleSource::Explicit), Ordering::Release);
Ok(())
}
#[must_use]
pub fn locale() -> &'static str {
match current() {
Some((store, index)) => store.tag(index),
None => not_installed(),
}
}
#[must_use]
pub fn locale_source() -> LocaleSource {
match ACTIVE.load(Ordering::Acquire) {
NONE => not_installed(),
active => unpack(active).1,
}
}
pub fn with_locale<R>(
corpus: &'static Corpus,
locale: &str,
body: impl FnOnce() -> R,
) -> Result<R, Error> {
let store = store_of(corpus, || Catalogs::embedded(corpus))?;
let index = store
.index(locale)
.ok_or_else(|| Error::UnknownLocale(locale.to_owned()))?;
let _restore = Restore(OVERRIDE.with(|o| o.replace(index)));
Ok(body())
}
struct Restore(usize);
impl Drop for Restore {
fn drop(&mut self) {
OVERRIDE.with(|o| o.set(self.0));
}
}
fn typed(corpus: &'static Corpus, call: &str) -> &'static Catalogs {
match store_of(corpus, || Catalogs::embedded(corpus)) {
Ok(store) => store,
Err(error) => refused(call, &error),
}
}
pub(crate) fn format_in(corpus: &'static Corpus, locale: &str, message: &impl Message) -> String {
let store = typed(corpus, "Locale::format");
store.format_at(store.typed_index(locale), message)
}
pub(crate) fn with_locale_in<R>(
corpus: &'static Corpus,
locale: &str,
body: impl FnOnce() -> R,
) -> R {
let store = typed(corpus, "with_locale");
let _restore = Restore(OVERRIDE.with(|o| o.replace(store.typed_index(locale))));
body()
}
#[cfg(all(
any(feature = "ssr", feature = "hydrate", feature = "csr"),
any(feature = "leptos", feature = "leptos-0-8")
))]
pub(crate) fn try_locale() -> Option<&'static str> {
current().map(|(store, index)| store.tag(index))
}
#[inline]
fn current() -> Option<(&'static Catalogs, usize)> {
let store = STORE.get()?;
let own = OVERRIDE.with(Cell::get);
let index = if own == NONE {
match ACTIVE.load(Ordering::Relaxed) {
NONE => return None,
active => unpack(active).0,
}
} else {
own
};
Some((store, index))
}
#[cold]
#[inline(never)]
#[allow(
clippy::panic,
reason = "native only: the ambient forms of a build whose only mode is `native` panic before install() (plans/19 §5)"
)]
pub(crate) fn not_installed() -> ! {
panic!("mf2: no catalogs are installed: call install() at start-up")
}
#[cold]
#[inline(never)]
#[allow(
clippy::panic,
reason = "native only: install() returns nothing, and a catalog that does not load means a corrupt executable (plans/19 §5)"
)]
fn refused(call: &str, error: &Error) -> ! {
panic!("mf2: {call}: {error}")
}
#[derive(Clone, Copy)]
struct Settings {
bidi: BidiStrategy,
time_zone: Option<TimeZone>,
}
static SETTINGS: RwLock<Settings> = RwLock::new(Settings {
bidi: BidiStrategy::None,
time_zone: None,
});
static GENERATION: AtomicU64 = AtomicU64::new(1);
std::thread_local! {
static LOCAL: Cell<(u64, BidiStrategy, TimeZone)> =
const { Cell::new((0, BidiStrategy::None, TimeZone::UTC)) };
}
fn update(change: impl FnOnce(&mut Settings)) {
let mut settings = SETTINGS.write().unwrap_or_else(PoisonError::into_inner);
change(&mut settings);
GENERATION.fetch_add(1, Ordering::Release);
}
#[inline]
fn settings() -> (BidiStrategy, TimeZone) {
let generation = GENERATION.load(Ordering::Acquire);
LOCAL.with(|local| {
let (taken, bidi, zone) = local.get();
if taken == generation {
(bidi, zone)
} else {
refresh(local, generation)
}
})
}
#[cold]
#[inline(never)]
fn refresh(
local: &Cell<(u64, BidiStrategy, TimeZone)>,
generation: u64,
) -> (BidiStrategy, TimeZone) {
let settings = *SETTINGS.read().unwrap_or_else(PoisonError::into_inner);
let zone = settings.time_zone.unwrap_or_else(super::zone::system);
local.set((generation, settings.bidi, zone));
(settings.bidi, zone)
}
pub fn set_bidi(bidi: BidiStrategy) {
update(|s| s.bidi = bidi);
}
#[must_use]
pub fn bidi() -> BidiStrategy {
settings().0
}
pub fn set_time_zone(zone: TimeZone) {
update(|s| s.time_zone = Some(zone));
}
#[must_use]
pub fn time_zone() -> TimeZone {
settings().1
}
pub(crate) fn text(m: &dyn TextOf, plain: bool) -> Option<Cow<'static, str>> {
let (store, index) = current()?;
let catalog: &'static Catalog = store.catalog(index)?;
if let Entry::Simple(r) = catalog.get(m.msg_id())
&& let Some(text) = catalog.text(r)
{
return Some(Cow::Borrowed(text));
}
let (bidi, zone) = settings();
let mut cx = FormatContext::new(store.context().host);
cx.bidi = if plain { BidiStrategy::None } else { bidi };
cx.time_zone = zone;
let f = Formatter::new(catalog, store.corpus().registry(), &cx);
Some(Cow::Owned(with_scratch(|buf| {
m.write_text(&f, buf);
String::from(buf.as_str())
})))
}
#[cfg(feature = "ratatui")]
pub(crate) fn with_formatter<R>(
body: impl FnOnce(&'static Catalog, &Formatter<'_>) -> R,
) -> Option<R> {
let (store, index) = current()?;
let catalog: &'static Catalog = store.catalog(index)?;
let mut cx = FormatContext::new(store.context().host);
cx.bidi = BidiStrategy::None;
cx.time_zone = settings().1;
let f = Formatter::new(catalog, store.corpus().registry(), &cx);
Some(body(catalog, &f))
}
std::thread_local! {
static SCRATCH: RefCell<String> = const { RefCell::new(String::new()) };
}
pub(crate) fn with_scratch<R>(body: impl FnOnce(&mut String) -> R) -> R {
let mut buf = SCRATCH.with(|s| {
s.try_borrow_mut()
.map(|mut held| core::mem::take(&mut *held))
.unwrap_or_default()
});
buf.clear();
let out = body(&mut buf);
SCRATCH.with(|s| {
if let Ok(mut slot) = s.try_borrow_mut()
&& slot.capacity() < buf.capacity()
{
*slot = buf;
}
});
out
}