use std::collections::{HashMap, HashSet};
use crate::fields::{Field, FieldSpec};
use crate::formats::{ne5, ns4};
use crate::{Entity, Live, Program};
#[derive(Debug)]
pub struct Panel {
pub groups: &'static [Group],
pub exhaustive: bool,
}
#[derive(Debug)]
pub struct Selection {
pub field: &'static str,
pub value: &'static str,
}
impl Selection {
pub fn selected(&self, fields: &[Field]) -> bool {
fields
.iter()
.find(|field| field.path == self.field)
.is_some_and(|field| field.value == self.value)
}
}
#[derive(Debug)]
pub struct Group {
pub title: &'static str,
pub members: &'static [&'static str],
pub groups: &'static [Group],
pub when: Option<Relevance>,
pub selected_by: Option<Selection>,
}
#[derive(Debug)]
pub struct Relevance {
pub any_of: &'static [Match],
}
#[derive(Debug)]
pub struct Match {
pub field: &'static str,
pub is: &'static [&'static str],
}
type Index<'a> = HashMap<&'a str, &'a Field>;
impl Match {
pub fn holds(&self, fields: &[Field]) -> bool {
fields
.iter()
.find(|field| field.path == self.field)
.is_some_and(|field| self.matched(field))
}
fn holds_in(&self, index: &Index) -> bool {
index
.get(self.field)
.is_some_and(|field| self.matched(field))
}
fn matched(&self, field: &Field) -> bool {
self.is.iter().any(|value| *value == field.value)
}
}
impl Relevance {
pub fn holds(&self, fields: &[Field]) -> bool {
self.any_of.is_empty() || self.any_of.iter().any(|m| m.holds(fields))
}
fn holds_in(&self, index: &Index) -> bool {
self.any_of.is_empty() || self.any_of.iter().any(|m| m.holds_in(index))
}
}
impl Group {
pub fn is_relevant(&self, fields: &[Field]) -> bool {
self.when.as_ref().is_none_or(|when| when.holds(fields))
}
pub fn members_of<'a>(&self, specs: &'a [FieldSpec]) -> Vec<&'a str> {
members_in(self.members, specs, |member| {
specs.iter().find(|spec| spec.name == member)
})
.into_iter()
.map(|spec| spec.name.as_str())
.collect()
}
fn walk(&self) -> Vec<&Group> {
let mut out = vec![self];
for group in self.groups {
out.extend(group.walk());
}
out
}
}
trait Placed {
fn path(&self) -> &str;
fn morph_parent(&self) -> Option<String>;
}
impl Placed for FieldSpec {
fn path(&self) -> &str {
&self.name
}
fn morph_parent(&self) -> Option<String> {
FieldSpec::morph_parent(self)
}
}
impl Placed for Field {
fn path(&self) -> &str {
&self.path
}
fn morph_parent(&self) -> Option<String> {
self.spec.morph_parent()
}
}
fn members_in<'a, T: Placed>(
members: &[&str],
items: &'a [T],
find: impl Fn(&str) -> Option<&'a T>,
) -> Vec<&'a T> {
let mut out = Vec::new();
for member in members {
match member.strip_suffix(".*") {
Some(prefix) => out.extend(
items
.iter()
.filter(|item| under(item.path(), prefix))
.filter(|item| item.morph_parent().is_none()),
),
None => out.extend(find(member)),
}
}
out
}
fn unclaimed<T: Placed>(items: &[T], claimed: impl Fn(&str) -> bool) -> Vec<&T> {
items
.iter()
.filter(|item| !claimed(item.path()))
.filter(|item| !item.morph_parent().is_some_and(|parent| claimed(&parent)))
.collect()
}
fn under(path: &str, prefix: &str) -> bool {
path.strip_prefix(prefix)
.and_then(|rest| rest.strip_prefix('.'))
.is_some_and(|leaf| !leaf.contains('.'))
}
impl Panel {
pub fn walk(&self) -> Vec<&Group> {
self.groups.iter().flat_map(Group::walk).collect()
}
pub fn named<'a>(&self, specs: &'a [FieldSpec]) -> Vec<&'a str> {
self.walk()
.into_iter()
.flat_map(|group| group.members_of(specs))
.collect()
}
pub fn leftovers<'a>(&self, specs: &'a [FieldSpec]) -> Vec<&'a str> {
let named = self.named(specs);
unclaimed(specs, |path| named.contains(&path))
.into_iter()
.map(|spec| spec.name.as_str())
.collect()
}
}
pub struct Section<'a> {
pub group: &'a Group,
pub relevant: bool,
pub fields: Vec<&'a Field>,
pub groups: Vec<Section<'a>>,
}
pub struct Resolved<'a> {
pub sections: Vec<Section<'a>>,
pub leftovers: Vec<&'a Field>,
}
impl Panel {
pub fn resolve<'a>(&'a self, fields: &'a [Field]) -> Resolved<'a> {
let index: Index<'a> = fields.iter().map(|f| (f.path.as_str(), f)).collect();
let mut claimed: HashSet<&'a str> = HashSet::new();
let sections = self
.groups
.iter()
.map(|group| resolve_group(group, fields, &index, true, &mut claimed))
.collect();
let leftovers = unclaimed(fields, |path| claimed.contains(path));
Resolved {
sections,
leftovers,
}
}
}
fn resolve_group<'a>(
group: &'a Group,
fields: &'a [Field],
index: &Index<'a>,
parent_relevant: bool,
claimed: &mut HashSet<&'a str>,
) -> Section<'a> {
let relevant = parent_relevant && group.when.as_ref().is_none_or(|when| when.holds_in(index));
let own = members_in(group.members, fields, |member| index.get(member).copied());
claimed.extend(own.iter().map(|field| field.path.as_str()));
let groups = group
.groups
.iter()
.map(|nested| resolve_group(nested, fields, index, relevant, claimed))
.collect();
Section {
group,
relevant,
fields: own,
groups,
}
}
pub fn of(entity: &Entity) -> Option<&'static Panel> {
match entity {
Entity::Program(Program::Electro5(_)) | Entity::Live(Live::Electro5(_)) => {
Some(&ne5::program::PANEL)
}
Entity::Program(Program::Stage4(_)) | Entity::Live(Live::Stage4(_)) => {
Some(&ns4::program::PANEL)
}
_ => None,
}
}
pub struct Authored {
pub name: &'static str,
pub panel: &'static Panel,
pub specs: fn() -> Vec<FieldSpec>,
}
pub const AUTHORED: &[Authored] = &[
Authored {
name: "ne5::Program",
panel: &ne5::program::PANEL,
specs: ne5::Program::field_specs,
},
Authored {
name: "ns4::Program",
panel: &ns4::program::PANEL,
specs: ns4::Program::field_specs,
},
];
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
#[test]
fn every_named_path_is_a_real_field() {
for authored in AUTHORED {
let specs = (authored.specs)();
let known: HashSet<&str> = specs.iter().map(|spec| spec.name.as_str()).collect();
for group in authored.panel.walk() {
for member in group.members {
match member.strip_suffix(".*") {
Some(prefix) => assert!(
specs.iter().any(|spec| under(&spec.name, prefix)),
"{}: {} names no field under {prefix}",
authored.name,
group.title,
),
None => assert!(
known.contains(member),
"{}: {} names {member}, which is not a field",
authored.name,
group.title,
),
}
}
}
}
}
#[test]
fn no_group_names_a_morph_slot_whose_parameter_is_declared() {
for authored in AUTHORED {
let specs = (authored.specs)();
for path in authored.panel.named(&specs) {
let spec = specs.iter().find(|spec| spec.name == path).expect(path);
assert!(
spec.morph_parent().is_none(),
"{}: {path} is a morph slot of {:?}",
authored.name,
spec.morph_parent(),
);
}
}
}
#[test]
fn no_field_is_named_twice() {
for authored in AUTHORED {
let specs = (authored.specs)();
let mut seen = HashSet::new();
for path in authored.panel.named(&specs) {
assert!(
seen.insert(path),
"{}: {path} is in two groups",
authored.name
);
}
}
}
#[test]
fn every_condition_names_a_field_and_values_it_accepts() {
for authored in AUTHORED {
let specs = (authored.specs)();
for group in authored.panel.walk() {
let Some(when) = &group.when else { continue };
for m in when.any_of {
let spec = specs
.iter()
.find(|spec| spec.name == m.field)
.unwrap_or_else(|| {
panic!(
"{}: {} tests {}, which is not a field",
authored.name, group.title, m.field
)
});
let legal = (spec.legal)();
if legal.is_empty() {
continue;
}
for value in m.is {
assert!(
legal.iter().any(|l| l == value),
"{}: {} tests {} for {value}, which it does not accept",
authored.name,
group.title,
m.field,
);
}
}
}
}
}
#[test]
fn every_selection_names_a_field_and_a_value_it_accepts() {
for authored in AUTHORED {
let specs = (authored.specs)();
for group in authored.panel.walk() {
let Some(selection) = &group.selected_by else {
continue;
};
let spec = specs
.iter()
.find(|spec| spec.name == selection.field)
.unwrap_or_else(|| {
panic!(
"{}: {} is selected by {}, which is not a field",
authored.name, group.title, selection.field
)
});
assert!(
(spec.legal)().iter().any(|value| value == selection.value),
"{}: {} does not accept {}",
authored.name,
selection.field,
selection.value,
);
assert!(
!group.members_of(&specs).contains(&selection.field),
"{}: {} contains its own selector {}",
authored.name,
group.title,
selection.field,
);
}
}
}
#[test]
fn an_exhaustive_layout_leaves_nothing_out() {
for authored in AUTHORED {
if !authored.panel.exhaustive {
continue;
}
let specs = (authored.specs)();
assert_eq!(
authored.panel.leftovers(&specs),
Vec::<&str>::new(),
"{} claims to be exhaustive",
authored.name,
);
}
}
#[test]
fn a_prefix_names_one_bodys_fields() {
assert!(under("organ_a.drawbar_1", "organ_a"));
assert!(!under("organ_ab.drawbar_1", "organ_a"));
assert!(!under("organ_a.inner.leaf", "organ_a"));
assert!(!under("drawbar_1", "organ_a"));
}
}