use std::collections::BTreeMap;
use std::sync::{Arc, OnceLock};
use mf2_catalog::number::TemplatePart;
use mf2_catalog::unit::Width;
use mf2_catalog::writer::unit::{UnitSpec, UnitWidthSpec, UnitsSpec, units};
use crate::blocks::{Block, Blocks, Cache, get};
use crate::error::Error;
use crate::number::{Selection, UnitNeeds};
use crate::template::parse as tpl;
pub const CATEGORIES: [&str; 6] = ["zero", "one", "two", "few", "many", "other"];
pub const WIDTHS: [&str; 3] = ["long", "short", "narrow"];
pub const FIELDS: &[&str] = &["name", "per", "other", "zero", "one", "two", "few", "many"];
pub fn fallback(field: &str) -> Option<&'static str> {
match field {
"zero" | "one" | "two" | "few" | "many" => Some("other"),
_ => None,
}
}
const TABLE: &str = include_str!("../data/units.txt");
struct Loaded {
blocks: Blocks<'static>,
cache: Cache,
ids: BTreeMap<&'static str, &'static str>,
}
fn loaded() -> Result<&'static Loaded, Error> {
static LOADED: OnceLock<Result<Loaded, String>> = OnceLock::new();
let r = LOADED.get_or_init(|| {
let blocks = Blocks::index(TABLE, &["per", "unit"]).map_err(|e| e.to_string())?;
let mut ids = BTreeMap::new();
for line in &blocks.globals {
let mut w = line.split('\t');
if w.next() != Some("unit-id") {
continue;
}
let (Some(id), Some(category)) = (w.next(), w.next()) else {
return Err(format!("bad unit-id line {line:?}"));
};
ids.insert(id, category);
}
Ok(Loaded {
blocks,
cache: Cache::default(),
ids,
})
});
r.as_ref().map_err(|message| Error::Table {
line: 0,
message: message.clone(),
})
}
fn block(l: &Loaded, locale: &str) -> Result<Arc<Block<'static>>, Error> {
l.cache.get(
&l.blocks,
locale,
|kind| if kind == "unit" { 2 } else { 1 },
|kind, f| {
if kind == "per" {
f == "pattern"
} else {
FIELDS.contains(&f)
}
},
)
}
pub fn unit_ids() -> Result<Vec<&'static str>, Error> {
Ok(loaded()?.ids.keys().copied().collect())
}
pub fn unit_category(id: &str) -> Result<Option<&'static str>, Error> {
Ok(loaded()?.ids.get(id).copied())
}
pub fn composition(id: &str) -> Result<Option<(&'static str, &'static str)>, Error> {
let l = loaded()?;
if l.ids.contains_key(id) {
return Ok(None);
}
let mut from = 0;
while let Some(at) = id[from..].find("-per-") {
let at = from + at;
let (x, y) = (&id[..at], &id[at + 5..]);
if let (Some((x, _)), Some((y, _))) = (l.ids.get_key_value(x), l.ids.get_key_value(y)) {
return Ok(Some((x, y)));
}
from = at + 1;
}
Ok(None)
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct UnitWidthData {
pub name: Option<String>,
pub per: Option<String>,
pub patterns: Vec<String>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct UnitData {
pub id: String,
pub widths: [UnitWidthData; 3],
}
fn selected(l: &Loaded, ids: &Selection) -> Result<Vec<&'static str>, Error> {
Ok(match ids {
Selection::All => l.ids.keys().copied().collect(),
Selection::Listed(set) => {
let mut out = std::collections::BTreeSet::new();
for id in set {
if let Some((k, _)) = l.ids.get_key_value(id.as_str()) {
out.insert(*k);
} else if let Some((x, y)) = composition(id)? {
out.insert(x);
out.insert(y);
}
}
out.into_iter().collect()
}
})
}
pub(crate) fn unit_data(
chain_names: &[&str],
ids: &Selection,
) -> Result<([String; 3], Vec<UnitData>), Error> {
let l = loaded()?;
let blocks: Vec<Arc<Block<'static>>> = chain_names
.iter()
.map(|n| block(l, n))
.collect::<Result<_, _>>()?;
let chain: Vec<&Block<'_>> = blocks.iter().map(AsRef::as_ref).collect();
let per = WIDTHS.map(|w| {
get(&chain, &format!("per {w}"), "pattern", &fallback)
.unwrap_or("{0}/{1}")
.to_owned()
});
let mut out = Vec::new();
for id in selected(l, ids)? {
let widths = WIDTHS.map(|w| {
let key = format!("unit {w} {id}");
let f = |field: &str| get(&chain, &key, field, &fallback).map(str::to_owned);
let patterns = if f("other").is_some() {
CATEGORIES.iter().filter_map(|c| f(c)).collect()
} else {
Vec::new()
};
UnitWidthData {
name: f("name"),
per: f("per"),
patterns,
}
});
if widths
.iter()
.any(|w| !w.patterns.is_empty() || w.name.is_some())
{
out.push(UnitData {
id: id.to_owned(),
widths,
});
}
}
Ok((per, out))
}
pub(crate) fn unit_entry(
per: &[String; 3],
list: &[UnitData],
needs: &UnitNeeds,
) -> Result<Vec<u8>, Error> {
let widths: Vec<Width> = Width::ALL
.into_iter()
.filter(|w| needs.widths[*w as usize])
.collect();
let per_specs: Vec<Vec<TemplatePart<'_>>> = widths
.iter()
.map(|w| tpl(&per[*w as usize], 2))
.collect::<Result<_, _>>()?;
let mut specs = Vec::with_capacity(list.len());
for u in list {
let mut blocks = Vec::with_capacity(widths.len());
for w in &widths {
let d = &u.widths[*w as usize];
let patterns = d
.patterns
.iter()
.enumerate()
.map(|(c, p)| Ok((u8::try_from(c).unwrap_or(5), tpl(p, 1)?)))
.collect::<Result<Vec<_>, Error>>()?;
blocks.push(UnitWidthSpec {
name: d.name.as_deref(),
per_unit: d.per.as_deref().map(|p| tpl(p, 1)).transpose()?,
patterns,
});
}
specs.push(UnitSpec {
id: &u.id,
widths: blocks,
});
}
Ok(units(&UnitsSpec {
widths,
names: needs.names,
per: per_specs,
units: specs,
})?)
}