use std::borrow::Cow;
use std::collections::{BTreeMap, HashMap};
use std::sync::{Arc, Mutex, PoisonError};
use crate::error::Error;
use crate::number::table::{Fields, unescape};
#[derive(Debug, Default)]
pub struct Blocks<'t> {
pub globals: Vec<&'t str>,
pub by_locale: BTreeMap<&'t str, &'t str>,
}
pub type Block<'t> = BTreeMap<String, Fields<'t>>;
impl<'t> Blocks<'t> {
pub fn index(text: &'t str, locale_kinds: &[&str]) -> Result<Blocks<'t>, Error> {
let mut b = Blocks::default();
let mut current: Option<(&'t str, usize, usize)> = None;
let mut at = 0usize;
for (n, line) in text.split_inclusive('\n').enumerate() {
let start = at;
at += line.len();
let body = line.trim_end_matches('\n');
if body.is_empty() || body.starts_with('#') {
continue;
}
let mut words = body.splitn(3, '\t');
let kind = words.next().unwrap_or("");
if !locale_kinds.contains(&kind) {
b.globals.push(body);
continue;
}
let locale = words.next().ok_or_else(|| Error::Table {
line: n + 1,
message: "no locale".to_owned(),
})?;
match current {
Some((l, s, _)) if l == locale => current = Some((l, s, at)),
_ => {
if let Some((l, s, e)) = current.take()
&& b.by_locale.insert(l, &text[s..e]).is_some()
{
return Err(Error::Table {
line: n + 1,
message: format!("the lines of {l} are not contiguous"),
});
}
if b.by_locale.contains_key(locale) {
return Err(Error::Table {
line: n + 1,
message: format!("the lines of {locale} are not contiguous"),
});
}
current = Some((locale, start, at));
}
}
}
if let Some((l, s, e)) = current {
b.by_locale.insert(l, &text[s..e]);
}
Ok(b)
}
pub fn block(
&self,
locale: &str,
key_words: impl Fn(&str) -> usize,
known: impl Fn(&str, &str) -> bool,
) -> Result<Block<'t>, Error> {
let mut out = Block::new();
let Some(text) = self.by_locale.get(locale) else {
return Ok(out);
};
for line in text.lines() {
let bad = |message: String| Error::Table { line: 0, message };
let mut words = line.split('\t');
let kind = words.next().unwrap_or("");
let _locale = words.next();
let mut key = kind.to_owned();
for _ in 0..key_words(kind) {
let w = words
.next()
.ok_or_else(|| bad(format!("{locale}: short line {line:?}")))?;
key.push(' ');
key.push_str(w);
}
let mut fields = Fields::new();
for w in words {
let (k, v) = w
.split_once('=')
.ok_or_else(|| bad(format!("{locale}: expected key=value, found {w:?}")))?;
if !known(kind, k) {
return Err(bad(format!("{locale}: unknown field {k:?} of {kind}")));
}
let v: Cow<'t, str> =
unescape(v).ok_or_else(|| bad(format!("{locale}: bad escape in {w:?}")))?;
fields.insert(k, v);
}
if out.insert(key, fields).is_some() {
return Err(bad(format!("{locale}: a key twice in {line:?}")));
}
}
Ok(out)
}
}
#[derive(Debug, Default)]
pub struct Cache {
blocks: Mutex<HashMap<String, Arc<Block<'static>>>>,
}
impl Cache {
pub fn get(
&self,
blocks: &Blocks<'static>,
locale: &str,
key_words: impl Fn(&str) -> usize,
known: impl Fn(&str, &str) -> bool,
) -> Result<Arc<Block<'static>>, Error> {
if let Some(b) = self
.blocks
.lock()
.unwrap_or_else(PoisonError::into_inner)
.get(locale)
{
return Ok(Arc::clone(b));
}
let b = Arc::new(blocks.block(locale, key_words, known)?);
self.blocks
.lock()
.unwrap_or_else(PoisonError::into_inner)
.insert(locale.to_owned(), Arc::clone(&b));
Ok(b)
}
}
pub fn get<'b>(
chain: &[&'b Block<'_>],
key: &str,
field: &str,
fallback: &impl Fn(&str) -> Option<&'static str>,
) -> Option<&'b str> {
for b in chain {
if let Some(v) = b.get(key).and_then(|f| f.get(field)) {
return Some(v.as_ref());
}
}
get(chain, key, fallback(field)?, fallback)
}
pub fn resolve<'b>(
chain: &[&'b Block<'_>],
key: &str,
fields: &[&'static str],
fallback: &impl Fn(&str) -> Option<&'static str>,
) -> BTreeMap<&'static str, &'b str> {
fields
.iter()
.filter_map(|f| Some((*f, get(chain, key, f, fallback)?)))
.collect()
}
pub fn keys<'b>(chain: &[&'b Block<'_>], kind: &str) -> std::collections::BTreeSet<&'b str> {
let prefix = format!("{kind} ");
chain
.iter()
.flat_map(|b| b.keys())
.filter(|k| k.starts_with(&prefix))
.map(String::as_str)
.collect()
}