use std::collections::BTreeSet;
use std::fmt::Write as _;
use std::sync::OnceLock;
use crate::substitution::{
CrudEvent, LinkPattern, SubstitutionAction, SubstitutionGraph, SubstitutionRule,
SubstitutionRuleSet, SubstitutionTraceReport,
};
pub const TASK_NODE: &str = "request:task";
pub const MODIFIER_NODE: &str = "request:modifier";
pub const PROGRAM_PLAN_RULES_LINO: &str = crate::seed::PROGRAM_PLAN_RULES_LINO;
#[must_use]
pub fn rules() -> &'static SubstitutionRuleSet {
static RULES: OnceLock<SubstitutionRuleSet> = OnceLock::new();
RULES.get_or_init(|| {
let mut set = SubstitutionRuleSet::from_links_notation(PROGRAM_PLAN_RULES_LINO)
.expect("embedded program-plan rules must parse");
let derived = derive_inverse_rules(
&set.rules,
&crate::seed::operation_vocabulary().inverse_pairs(),
);
set.rules.extend(derived);
set.rules
.sort_by(|left, right| left.order.cmp(&right.order).then(left.id.cmp(&right.id)));
set
})
}
fn derive_inverse_rules(
base_rules: &[SubstitutionRule],
inverse_pairs: &[(String, String)],
) -> Vec<SubstitutionRule> {
let mut derived = Vec::new();
for (cancel_op, base_op) in inverse_pairs {
for rule in base_rules {
let Some(condition_index) = rule.conditions.iter().position(|condition| {
condition.literal_pair() == Some((MODIFIER_NODE, base_op.as_str()))
}) else {
continue;
};
let [action] = rule.actions.as_slice() else {
continue;
};
let [added] = action.add.as_slice() else {
continue;
};
let conditions = rule
.conditions
.iter()
.enumerate()
.map(|(index, condition)| {
if index == condition_index {
LinkPattern::parse(&format!("{MODIFIER_NODE} -> {cancel_op}"))
.expect("modifier condition pattern is well-formed")
} else {
condition.clone()
}
})
.collect();
derived.push(SubstitutionRule {
id: format!("{cancel_op}__{}", rule.id),
order: rule.order,
events: rule.events.clone(),
conditions,
actions: vec![SubstitutionAction {
remove: added.clone(),
add: vec![action.remove.clone()],
}],
});
}
}
derived
}
#[must_use]
pub(crate) fn modifier_slugs() -> &'static BTreeSet<String> {
static MODIFIER_SLUGS: OnceLock<BTreeSet<String>> = OnceLock::new();
MODIFIER_SLUGS.get_or_init(|| {
rules()
.rules
.iter()
.flat_map(|rule| &rule.conditions)
.filter_map(|condition| condition.literal_pair())
.filter(|(from, _)| *from == MODIFIER_NODE)
.map(|(_, to)| to.to_owned())
.collect()
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProgramPlan {
pub base_task: String,
pub modifiers: Vec<String>,
pub resolved_task: String,
pub graph: SubstitutionGraph,
pub report: SubstitutionTraceReport,
}
impl ProgramPlan {
#[must_use]
pub fn was_modified(&self) -> bool {
self.resolved_task != self.base_task
}
#[must_use]
pub fn links_notation(&self) -> String {
let mut out = String::new();
out.push_str("program_plan\n");
let _ = writeln!(out, " base_task {}", self.base_task);
let _ = writeln!(out, " resolved_task {}", self.resolved_task);
for modifier in &self.modifiers {
let _ = writeln!(out, " modifier {modifier}");
}
for line in self.graph.links_notation().lines() {
out.push_str(" ");
out.push_str(line);
out.push('\n');
}
for line in self.report.links_notation().lines() {
out.push_str(" ");
out.push_str(line);
out.push('\n');
}
out.trim_end().to_owned()
}
}
#[must_use]
pub fn lower(base_task: &str, modifiers: &[String]) -> ProgramPlan {
lower_with_rules(rules(), base_task, modifiers)
}
#[must_use]
pub fn lower_with_rules(
rules: &SubstitutionRuleSet,
base_task: &str,
modifiers: &[String],
) -> ProgramPlan {
let mut graph = SubstitutionGraph::new().with_link(TASK_NODE, base_task);
for modifier in modifiers {
graph.insert_link(MODIFIER_NODE, modifier);
}
let report = graph.apply_rules(rules, CrudEvent::Manual);
let resolved_task = resolved_task_from_graph(&graph).unwrap_or_else(|| base_task.to_owned());
ProgramPlan {
base_task: base_task.to_owned(),
modifiers: modifiers.to_vec(),
resolved_task,
graph,
report,
}
}
#[must_use]
pub fn resolve_task(base_task: &str, modifiers: &[String]) -> String {
lower(base_task, modifiers).resolved_task
}
fn resolved_task_from_graph(graph: &SubstitutionGraph) -> Option<String> {
graph
.links()
.into_iter()
.find(|link| link.from == TASK_NODE)
.map(|link| link.to)
}