use std::collections::HashMap;
use indexmap::IndexMap;
use crate::elements::{Element, ElementKind};
pub struct RefResolver {
epoch_to_human: HashMap<String, String>,
human_to_epoch: HashMap<String, String>,
}
impl RefResolver {
pub fn new(elements: &IndexMap<String, Element>) -> Self {
let mut r = RefResolver {
epoch_to_human: HashMap::new(),
human_to_epoch: HashMap::new(),
};
r.build_maps(elements);
r
}
pub fn to_human(&self, epoch_ref: &str) -> Option<&str> {
self.epoch_to_human.get(epoch_ref).map(|s| s.as_str())
}
pub fn to_epoch(&self, human_ref: &str) -> Option<&str> {
self.human_to_epoch.get(human_ref).map(|s| s.as_str())
}
#[allow(dead_code)]
pub fn resolve<'a>(&'a self, ref_str: &'a str) -> Option<&'a str> {
if let Some(epoch) = self.human_to_epoch.get(ref_str) {
return Some(epoch.as_str());
}
if self.epoch_to_human.contains_key(ref_str) {
return Some(ref_str);
}
None
}
fn build_maps(&mut self, elements: &IndexMap<String, Element>) {
if elements.is_empty() { return; }
let mut sorted: Vec<(&String, &Element)> = elements.iter().collect();
sorted.sort_by(|(a, _), (b, _)| {
let a_parts: Vec<u64> = a.split('.').filter_map(|s| s.parse().ok()).collect();
let b_parts: Vec<u64> = b.split('.').filter_map(|s| s.parse().ok()).collect();
a_parts.cmp(&b_parts)
});
let root_depth = sorted.first().map(|(k, _)| k.split('.').count()).unwrap_or(1);
let mut type_counters: HashMap<String, usize> = HashMap::new();
for (epoch_ref, el) in &sorted {
let seg_count = epoch_ref.split('.').count();
let depth = seg_count - root_depth;
if depth == 0 { continue; }
let human = if depth == 1 {
if el.kind() == ElementKind::Unknown { continue; }
let type_name = el.sign().to_string();
let n = type_counters.entry(type_name.clone()).or_insert(0);
*n += 1;
format!("{}-{}", type_name, n)
} else {
let parts: Vec<&str> = epoch_ref.splitn(seg_count, '.').collect();
let parent_epoch = parts[..parts.len() - 1].join(".");
let child_idx = parts[parts.len() - 1];
match self.epoch_to_human.get(&parent_epoch) {
Some(parent_human) => format!("{}.{}", parent_human, child_idx),
None => continue, }
};
self.epoch_to_human.insert(epoch_ref.to_string(), human.clone());
self.human_to_epoch.insert(human, epoch_ref.to_string());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser;
fn parse(content: &str) -> IndexMap<String, Element> {
let wrapped = format!("@mps[]{{\n{}\n}}", content);
parser::parse_wrapped(&wrapped, 20260101).unwrap()
}
#[test]
fn test_empty_elements() {
let els = parse("");
let r = RefResolver::new(&els);
assert!(r.epoch_to_human.is_empty());
}
#[test]
fn test_top_level_task() {
let els = parse("@task[work]{ Do thing }");
let r = RefResolver::new(&els);
assert_eq!(r.to_human("20260101.1"), Some("task-1"));
assert_eq!(r.to_epoch("task-1"), Some("20260101.1"));
}
#[test]
fn test_sequential_types() {
let els = parse("@task{ A }\n@note{ B }\n@task{ C }");
let r = RefResolver::new(&els);
assert_eq!(r.to_human("20260101.1"), Some("task-1"));
assert_eq!(r.to_human("20260101.2"), Some("note-1"));
assert_eq!(r.to_human("20260101.3"), Some("task-2"));
}
#[test]
fn test_nested_inside_mps() {
let els = parse("@mps{\n @task{ Nested }\n @note{ also }\n}");
let r = RefResolver::new(&els);
assert_eq!(r.to_human("20260101.1"), Some("mps-1"));
assert_eq!(r.to_human("20260101.1.1"), Some("mps-1.1"));
assert_eq!(r.to_human("20260101.1.2"), Some("mps-1.2"));
}
#[test]
fn test_resolve_accepts_both_forms() {
let els = parse("@task{ A }");
let r = RefResolver::new(&els);
assert_eq!(r.resolve("task-1"), Some("20260101.1"));
assert_eq!(r.resolve("20260101.1"), Some("20260101.1"));
assert_eq!(r.resolve("bogus"), None);
}
#[test]
fn test_unknown_element_skipped() {
let els = parse("@widget{ unknown }\n@task{ real }");
let r = RefResolver::new(&els);
assert!(r.to_human("20260101.1").is_none());
assert_eq!(r.to_human("20260101.2"), Some("task-1"));
}
#[test]
fn test_roundtrip() {
let els = parse("@task[work]{ A }\n@note{ B }");
let r = RefResolver::new(&els);
for epoch_ref in r.epoch_to_human.keys() {
let human = r.to_human(epoch_ref).unwrap();
let back = r.to_epoch(human).unwrap();
assert_eq!(back, epoch_ref.as_str());
}
}
}