use std::collections::{HashMap, VecDeque};
use anyhow::Result;
use rusqlite::Connection;
use uuid::Uuid;
use crate::infrastructure::db;
use crate::infrastructure::model::{Status, Task};
use super::types::{BoardState, Feature};
pub(super) fn build_state(conn: &Connection, project: String) -> Result<BoardState> {
let all = db::list_tasks_for_board(conn, &project)?;
let edges = db::dependency_edges_for_project(conn, &project)?;
let pos: HashMap<Uuid, usize> = all.iter().enumerate().map(|(i, t)| (t.uuid, i)).collect();
let n = all.len();
let mut dependents: HashMap<usize, Vec<usize>> = HashMap::new();
let mut indeg = vec![0usize; n];
let mut neighbors: Vec<Vec<usize>> = vec![Vec::new(); n];
for (task, dep) in &edges {
if let (Some(&ti), Some(&di)) = (pos.get(task), pos.get(dep)) {
dependents.entry(di).or_default().push(ti);
indeg[ti] += 1;
neighbors[di].push(ti);
neighbors[ti].push(di);
}
}
let mut visited = vec![false; n];
let mut components: Vec<Vec<usize>> = Vec::new();
for start in 0..n {
if visited[start] {
continue;
}
let mut comp = Vec::new();
let mut queue = VecDeque::new();
queue.push_back(start);
visited[start] = true;
while let Some(node) = queue.pop_back() {
comp.push(node);
for &nb in &neighbors[node] {
if !visited[nb] {
visited[nb] = true;
queue.push_back(nb);
}
}
}
components.push(comp);
}
let mut features_nodes: Vec<Vec<usize>> = Vec::new();
let mut ungrouped: Vec<usize> = Vec::new();
for comp in components {
if comp.len() >= 2 {
features_nodes.push(topo_order(&comp, &dependents, &indeg));
} else {
ungrouped.push(comp[0]);
}
}
features_nodes.sort_by(|a, b| {
sort_key(b, &all)
.partial_cmp(&sort_key(a, &all))
.unwrap_or(std::cmp::Ordering::Equal)
});
ungrouped.sort_unstable();
let mut tasks: Vec<Task> = Vec::with_capacity(n);
let mut feature_of: Vec<usize> = Vec::with_capacity(n);
let mut features: Vec<Feature> = Vec::new();
for nodes in &features_nodes {
let fi = features.len();
let done = nodes
.iter()
.filter(|&&i| all[i].status == Status::Completed)
.count();
let title = nodes
.last()
.map(|&i| truncate(&all[i].description, 56))
.unwrap_or_else(|| format!("Feature {}", fi + 1));
features.push(Feature {
title,
done,
total: nodes.len(),
grouped: true,
});
for &i in nodes {
tasks.push(all[i].clone());
feature_of.push(fi);
}
}
if !ungrouped.is_empty() {
let fi = features.len();
let done = ungrouped
.iter()
.filter(|&&i| all[i].status == Status::Completed)
.count();
features.push(Feature {
title: "Standalone tasks".to_string(),
done,
total: ungrouped.len(),
grouped: false,
});
for &i in &ungrouped {
tasks.push(all[i].clone());
feature_of.push(fi);
}
}
let pending = tasks.iter().filter(|t| t.status == Status::Pending).count();
let feature_count = features.iter().filter(|f| f.grouped).count();
Ok(BoardState {
project,
done: tasks.len() - pending,
pending,
feature_count,
tasks,
feature_of,
features,
selected: 0,
scroll: 0,
})
}
fn topo_order(
nodes: &[usize],
dependents: &HashMap<usize, Vec<usize>>,
global_indeg: &[usize],
) -> Vec<usize> {
let mut indeg: HashMap<usize, usize> = nodes.iter().map(|&i| (i, global_indeg[i])).collect();
let mut out = Vec::with_capacity(nodes.len());
while out.len() < nodes.len() {
let mut ready: Vec<usize> = indeg
.iter()
.filter(|&(_, &d)| d == 0)
.map(|(&i, _)| i)
.collect();
if ready.is_empty() {
break; }
ready.sort_unstable();
for i in ready {
indeg.remove(&i);
out.push(i);
if let Some(deps) = dependents.get(&i) {
for &j in deps {
if let Some(d) = indeg.get_mut(&j) {
*d = d.saturating_sub(1);
}
}
}
}
}
out
}
fn sort_key(nodes: &[usize], all: &[Task]) -> (bool, f64) {
nodes
.iter()
.filter_map(|&i| (all[i].status == Status::Pending).then_some(all[i].urgency))
.fold((false, 0.0), |(_, best), u| (true, best.max(u)))
}
fn truncate(s: &str, max: usize) -> String {
if s.chars().count() <= max {
s.to_string()
} else {
let mut out: String = s.chars().take(max - 1).collect();
out.push('…');
out
}
}