use crate::app::Snapshot;
use crate::projects::balls::Ball;
use crate::spend::{Attribution, Figure, Prices};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::PathBuf;
pub fn descendants(id: &str, by_id: &HashMap<&str, &Ball>) -> Vec<String> {
let mut children: BTreeMap<&str, Vec<&str>> = BTreeMap::new();
for ball in by_id.values() {
if let Some(parent) = ball.parent.as_deref() {
children.entry(parent).or_default().push(&ball.id);
}
}
let mut seen: BTreeSet<String> = BTreeSet::new();
let mut frontier = vec![id];
while let Some(next) = frontier.pop() {
for child in children.get(next).into_iter().flatten() {
if seen.insert((*child).to_owned()) {
frontier.push(child);
}
}
}
seen.into_iter().collect()
}
pub(super) fn of(
snap: &Snapshot,
prices: &Prices,
id: &str,
by_id: &HashMap<&str, &Ball>,
) -> Option<Figure> {
let mut members = descendants(id, by_id);
if members.is_empty() {
return None;
}
members.push(id.to_owned());
fold(snap, prices, &members)
}
#[derive(Default)]
struct Slice {
whole: bool,
roots: BTreeSet<String>,
}
fn fold(snap: &Snapshot, prices: &Prices, members: &[String]) -> Option<Figure> {
let mut slices: BTreeMap<PathBuf, Slice> = BTreeMap::new();
for row in &snap.join_rows {
let Some(ws) = row.workspace.clone() else {
continue;
};
if !members.contains(&row.ball_id) {
continue;
}
let roots = super::stamped_roots(&snap.trees, &ws, &row.ball_id);
let slice = slices.entry(ws).or_default();
if roots.is_empty() {
slice.whole = true;
}
slice.roots.extend(roots);
}
if slices.is_empty() {
return None;
}
let mut bills = Vec::new();
let mut roots = 0;
let mut whole = false;
for (ws, slice) in &slices {
let keep: Vec<String> = if slice.whole {
whole = true;
Vec::new()
} else {
roots += slice.roots.len();
slice.roots.iter().cloned().collect()
};
bills.extend(crate::spend::select(
&super::rows::bills_of(snap, ws),
&keep,
));
}
let attribution = if whole {
Attribution::Workspace
} else {
Attribution::Conversations(roots)
};
Some(crate::spend::figure(&bills, prices, attribution))
}