use crate::plan::{Change, Plan};
use std::collections::{HashMap, HashSet};
#[derive(Debug, thiserror::Error, Clone, PartialEq, Eq)]
pub enum ToposortError {
#[error("cycle detected involving keyword `{0}` — declarations form a loop")]
Cycle(String),
}
pub fn ordered_apply(plan: &Plan) -> Result<Vec<Change>, ToposortError> {
let mut keywords: Vec<&str> = Vec::new();
for c in plan.iter_actionable() {
let kw = c.id().keyword.as_str();
if !keywords.contains(&kw) {
keywords.push(kw);
}
}
let order = topo_sort(&keywords)?;
let kw_index: HashMap<&str, usize> = order
.iter()
.enumerate()
.map(|(i, k)| (*k, i))
.collect();
let mut removes = plan.removes.clone();
removes.sort_by_key(|c| {
std::cmp::Reverse(*kw_index.get(c.id().keyword.as_str()).unwrap_or(&usize::MAX))
});
let mut adds = plan.adds.clone();
adds.sort_by_key(|c| *kw_index.get(c.id().keyword.as_str()).unwrap_or(&usize::MAX));
let mut changes = plan.changes.clone();
changes.sort_by_key(|c| *kw_index.get(c.id().keyword.as_str()).unwrap_or(&usize::MAX));
let mut out = Vec::with_capacity(plan.actionable_count());
out.extend(removes);
out.extend(adds);
out.extend(changes);
Ok(out)
}
fn topo_sort<'a>(keywords: &'a [&str]) -> Result<Vec<&'a str>, ToposortError> {
let kw_set: HashSet<&str> = keywords.iter().copied().collect();
let mut deps: HashMap<&str, Vec<&str>> = HashMap::new();
for kw in keywords {
let edges: Vec<&str> = tatara_lisp::domain::lookup_deps(kw)
.map(|h| {
h.depends_on
.iter()
.copied()
.filter(|d| kw_set.contains(d))
.collect()
})
.unwrap_or_default();
deps.insert(kw, edges);
}
let mut in_degree: HashMap<&str, usize> =
keywords.iter().map(|k| (*k, 0)).collect();
for (_kw, edges) in &deps {
for &dep in edges {
*in_degree.entry(dep).or_insert(0) += 0; }
}
for kw in keywords {
in_degree.insert(kw, 0);
}
for (kw, edges) in &deps {
in_degree.insert(kw, edges.len());
}
let mut queue: Vec<&str> = keywords
.iter()
.filter(|k| in_degree.get(*k).copied().unwrap_or(0) == 0)
.copied()
.collect();
let mut out = Vec::with_capacity(keywords.len());
while let Some(kw) = queue.pop() {
out.push(kw);
let kws: Vec<&str> = deps.keys().copied().collect();
for other in kws {
if other == kw {
continue;
}
if let Some(edges) = deps.get_mut(other) {
if let Some(pos) = edges.iter().position(|d| *d == kw) {
edges.remove(pos);
let degree = in_degree.entry(other).or_insert(0);
if *degree > 0 {
*degree -= 1;
}
if *degree == 0 && !out.contains(&other) && !queue.contains(&other) {
queue.push(other);
}
}
}
}
}
if out.len() != keywords.len() {
let stuck = keywords
.iter()
.find(|k| !out.contains(k))
.copied()
.unwrap_or("?");
return Err(ToposortError::Cycle(stuck.to_string()));
}
Ok(out)
}