use std::collections::HashSet;
use crate::model::{
Archived, Assignments, DueFilter, Filter, Id, LinkKind, Notes, Schedule, Status, Tags, Title,
};
use crate::ports::{ComponentStore, LinkRepository, TaskEntityStore};
use crate::temporal::Date;
pub async fn select_matching<St>(store: &St, filter: &Filter, today: Date) -> Vec<Id>
where
St: ComponentStore + TaskEntityStore + LinkRepository,
{
let within = match &filter.within {
Some(root) => Some(descendants(store, root).await),
None => None,
};
let mut out = Vec::new();
for id in store.all().await {
if matches(store, filter, today, within.as_ref(), &id).await {
out.push(id);
}
}
out
}
async fn matches<St>(
store: &St,
f: &Filter,
today: Date,
within: Option<&HashSet<Id>>,
id: &Id,
) -> bool
where
St: ComponentStore + TaskEntityStore + LinkRepository,
{
if let Some(set) = within
&& !set.contains(id)
{
return false;
}
if let Some(text) = &f.text {
let needle = text.to_lowercase();
let title = store
.get::<Title>(id)
.await
.map(|t| t.0)
.unwrap_or_default();
let notes = store
.get::<Notes>(id)
.await
.map(|n| n.0)
.unwrap_or_default();
if !format!("{title} {notes}").to_lowercase().contains(&needle) {
return false;
}
}
if !f.status.is_empty() {
let st = store.get::<Status>(id).await.unwrap_or(Status::Draft);
if !f.status.contains(&st) {
return false;
}
}
if f.assignee.is_some() || f.claimed.is_some() {
let asg = store.get::<Assignments>(id).await.unwrap_or_default().0;
if let Some(a) = &f.assignee
&& !asg.iter().any(|x| &x.actor == a)
{
return false;
}
if let Some(claimed) = f.claimed
&& asg.iter().any(|x| x.claimed) != claimed
{
return false;
}
}
if !f.tags.is_empty() {
let tags = store.get::<Tags>(id).await.unwrap_or_default().0;
if !f.tags.iter().all(|t| tags.contains(t)) {
return false;
}
}
if let Some(due) = &f.due {
let date = store.get::<Schedule>(id).await.map(|s| s.0.date);
if !due_matches(date, due, today) {
return false;
}
}
if let Some(archived) = f.archived {
let is_archived = store.get::<Archived>(id).await.is_some();
if is_archived != archived {
return false;
}
}
true
}
fn due_matches(due: Option<Date>, filter: &DueFilter, today: Date) -> bool {
let Some(d) = due else { return false };
match filter {
DueFilter::Today => d == today,
DueFilter::Overdue => d < today,
DueFilter::Before(x) => d < *x,
DueFilter::On(x) => d == *x,
DueFilter::After(x) => d > *x,
}
}
pub async fn descendants<St: LinkRepository + ?Sized>(store: &St, id: &Id) -> HashSet<Id> {
let mut seen = HashSet::new();
let mut stack: Vec<Id> = store
.outgoing(id, LinkKind::Child)
.await
.into_iter()
.map(|l| l.to)
.collect();
while let Some(cur) = stack.pop() {
if seen.insert(cur.clone()) {
stack.extend(
store
.outgoing(&cur, LinkKind::Child)
.await
.into_iter()
.map(|l| l.to),
);
}
}
seen
}