use std::collections::BTreeMap;
use std::sync::Arc;
use sim_lib_pitch_serial::RowForm;
use thiserror::Error;
use crate::{
BuiltInPracticeRule, EventPlacement, OrdinalRef, PlannedSerialEvent, PracticeRule,
PracticeRuleId, RowInstanceId, SerialEventId, SerialOrigin, SerialPlan, SerialPractice,
SerialRole, StructuralLicense, VoiceId,
};
#[derive(Clone, Debug, PartialEq, Eq, Error)]
pub enum SerialDeployError {
#[error("{0}")]
InvalidTechniqueId(String),
#[error("technique {0} must contain at least one deployer")]
EmptyTechnique(String),
#[error("deployer {deployer} references unknown row {row_id}")]
UnknownRow {
deployer: String,
row_id: RowInstanceId,
},
#[error("deployer {deployer} attempted to reuse event id {event_id}")]
DuplicateEventId {
deployer: String,
event_id: SerialEventId,
},
#[error("deployer {deployer} expected {expected} voices but received {actual}")]
VoiceCountMismatch {
deployer: String,
expected: usize,
actual: usize,
},
#[error("aggregate rotation block lengths must sum to 12, received {0}")]
InvalidRotationCoverage(usize),
#[error("deployer {deployer} requires an interlocking partition witness")]
NotInterlocking {
deployer: String,
},
#[error(
"rows {source_row_id} and {partner_row_id} are not combinatorial at block size {block_size}"
)]
NotCombinatorial {
source_row_id: RowInstanceId,
partner_row_id: RowInstanceId,
block_size: usize,
},
#[error("partition build failed: {0}")]
Partition(String),
#[error("serial plan validation failed: {0}")]
Plan(String),
}
pub(crate) fn validate_technique_id(value: impl Into<String>) -> Result<String, SerialDeployError> {
let value = value.into();
if value.trim().is_empty() {
return Err(SerialDeployError::InvalidTechniqueId(
"technique id cannot be empty".to_owned(),
));
}
if value
.chars()
.any(|ch| !(ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '_' | '.')))
{
return Err(SerialDeployError::InvalidTechniqueId(
"technique id must use ASCII letters, digits, /, -, _, or .".to_owned(),
));
}
Ok(value)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SerialDeployerParameter {
pub name: String,
pub value: String,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SerialDeployerKind {
CompleteHorizontalStatement,
MotivicPartition,
VerticalBlocks,
InterlockingPartition,
MelodyAccompanimentDistribution,
AggregateRotation,
SimultaneousForms,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SerialDeployerSpec {
pub kind: SerialDeployerKind,
pub label: String,
pub expected_fact: String,
pub parameters: Vec<SerialDeployerParameter>,
}
#[derive(Clone)]
pub(crate) enum SerialDeployerInner {
Horizontal(super::components::HorizontalStatementSpec),
Motivic(super::components::MotivicPartitionSpec),
Vertical(super::components::VerticalBlocksSpec),
Interlocking(super::components::InterlockingPartitionSpec),
MelodyAccompaniment(super::components::MelodyAccompanimentSpec),
AggregateRotation(super::components::AggregateRotationSpec),
SimultaneousForms(super::components::SimultaneousFormsSpec),
}
#[derive(Clone)]
pub struct SerialDeployer {
pub(crate) inner: SerialDeployerInner,
}
impl SerialDeployer {
pub(crate) fn spec(&self) -> SerialDeployerSpec {
match &self.inner {
SerialDeployerInner::Horizontal(spec) => SerialDeployerSpec {
kind: SerialDeployerKind::CompleteHorizontalStatement,
label: spec.event_id.as_str().to_owned(),
expected_fact: "one complete row sounds in sequence".to_owned(),
parameters: vec![
param("row", spec.row_id.as_str()),
param("voice", &spec.voice.to_string()),
param("reading", spec.license.reading_id.as_str()),
],
},
SerialDeployerInner::Motivic(spec) => SerialDeployerSpec {
kind: SerialDeployerKind::MotivicPartition,
label: spec.event_prefix.clone(),
expected_fact: "partition blocks sound sequentially as inspectable motives"
.to_owned(),
parameters: vec![
param("row", spec.row_id.as_str()),
param("blocks", &spec.partition.block_count().to_string()),
param("reading", spec.license.reading_id.as_str()),
],
},
SerialDeployerInner::Vertical(spec) => SerialDeployerSpec {
kind: SerialDeployerKind::VerticalBlocks,
label: spec.event_prefix.clone(),
expected_fact: "selected blocks sound as chordal vertical events".to_owned(),
parameters: vec![
param("row", spec.row_id.as_str()),
param("blocks", &format!("{:?}", spec.selected_blocks)),
param("reading", spec.license.reading_id.as_str()),
],
},
SerialDeployerInner::Interlocking(spec) => SerialDeployerSpec {
kind: SerialDeployerKind::InterlockingPartition,
label: spec.event_prefix.clone(),
expected_fact: "interlocking block evidence licenses sequential partition exchange"
.to_owned(),
parameters: vec![
param("row", spec.row_id.as_str()),
param("blocks", &spec.partition.block_count().to_string()),
param("reading", spec.license.reading_id.as_str()),
],
},
SerialDeployerInner::MelodyAccompaniment(spec) => SerialDeployerSpec {
kind: SerialDeployerKind::MelodyAccompanimentDistribution,
label: spec.event_prefix.clone(),
expected_fact: "each block splits into melody lead and accompaniment residue"
.to_owned(),
parameters: vec![
param("row", spec.row_id.as_str()),
param("blocks", &spec.partition.block_count().to_string()),
param("reading", spec.license.reading_id.as_str()),
],
},
SerialDeployerInner::AggregateRotation(spec) => SerialDeployerSpec {
kind: SerialDeployerKind::AggregateRotation,
label: spec.event_prefix.clone(),
expected_fact: "a rotated aggregate is reblocked without losing row coverage"
.to_owned(),
parameters: vec![
param("row", spec.row_id.as_str()),
param("rotation", &spec.rotation.to_string()),
param("reading", spec.license.reading_id.as_str()),
],
},
SerialDeployerInner::SimultaneousForms(spec) => SerialDeployerSpec {
kind: SerialDeployerKind::SimultaneousForms,
label: spec.event_prefix.clone(),
expected_fact: "simultaneous form blocks preserve each form identity and alignment"
.to_owned(),
parameters: vec![
param(
"forms",
&spec
.row_ids
.iter()
.map(RowInstanceId::as_str)
.collect::<Vec<_>>()
.join(","),
),
param("block-size", &spec.block_size.to_string()),
param("reading", spec.license.reading_id.as_str()),
],
},
}
}
pub(crate) fn apply(
&self,
rows: &BTreeMap<RowInstanceId, RowForm>,
builder: &mut PlanBuilder,
) -> Result<(), SerialDeployError> {
match &self.inner {
SerialDeployerInner::Horizontal(spec) => spec.apply(rows, builder),
SerialDeployerInner::Motivic(spec) => spec.apply(rows, builder),
SerialDeployerInner::Vertical(spec) => spec.apply(rows, builder),
SerialDeployerInner::Interlocking(spec) => spec.apply(rows, builder),
SerialDeployerInner::MelodyAccompaniment(spec) => spec.apply(rows, builder),
SerialDeployerInner::AggregateRotation(spec) => spec.apply(rows, builder),
SerialDeployerInner::SimultaneousForms(spec) => spec.apply(rows, builder),
}
}
}
pub(crate) fn param(name: &str, value: &str) -> SerialDeployerParameter {
SerialDeployerParameter {
name: name.to_owned(),
value: value.to_owned(),
}
}
#[derive(Clone, Debug)]
pub(crate) struct PlanBuilder {
pub(crate) events: BTreeMap<SerialEventId, PlannedSerialEvent>,
pub(crate) precedence: Vec<(SerialEventId, SerialEventId)>,
}
impl PlanBuilder {
pub(crate) fn add_event(
&mut self,
deployer: &str,
event: PlannedSerialEvent,
) -> Result<(), SerialDeployError> {
if self.events.contains_key(&event.id) {
return Err(SerialDeployError::DuplicateEventId {
deployer: deployer.to_owned(),
event_id: event.id,
});
}
self.events.insert(event.id.clone(), event);
Ok(())
}
pub(crate) fn add_precedence(&mut self, before: &SerialEventId, after: &SerialEventId) {
self.precedence.push((before.clone(), after.clone()));
}
}
#[derive(Clone)]
pub struct TechniquePlan {
id: String,
rules: Vec<Arc<dyn PracticeRule>>,
deployers: Vec<SerialDeployer>,
}
impl TechniquePlan {
pub fn builder(id: impl Into<String>) -> Result<TechniquePlanBuilder, SerialDeployError> {
Ok(TechniquePlanBuilder {
id: validate_technique_id(id)?,
rules: Vec::new(),
deployers: Vec::new(),
})
}
pub fn id(&self) -> &str {
&self.id
}
pub fn rule_specs(&self) -> Vec<crate::PracticeRuleSpec> {
self.practice().rule_specs()
}
pub fn deployer_specs(&self) -> Vec<SerialDeployerSpec> {
self.deployers.iter().map(SerialDeployer::spec).collect()
}
pub fn practice(&self) -> SerialPractice {
SerialPractice::new(
crate::PracticeId::new(self.id.clone()).expect("validated technique id"),
self.rules.clone(),
)
}
pub fn deploy(
&self,
rows: BTreeMap<RowInstanceId, RowForm>,
) -> Result<SerialPlan, SerialDeployError> {
let mut builder = PlanBuilder {
events: BTreeMap::new(),
precedence: Vec::new(),
};
for deployer in &self.deployers {
deployer.apply(&rows, &mut builder)?;
}
SerialPlan::try_new(rows, builder.events, builder.precedence)
.map_err(|error| SerialDeployError::Plan(error.to_string()))
}
}
pub struct TechniquePlanBuilder {
id: String,
rules: Vec<Arc<dyn PracticeRule>>,
deployers: Vec<SerialDeployer>,
}
impl TechniquePlanBuilder {
pub fn rule(mut self, rule: Arc<dyn PracticeRule>) -> Self {
self.rules.push(rule);
self
}
pub fn deployer(mut self, deployer: SerialDeployer) -> Self {
self.deployers.push(deployer);
self
}
pub fn build(self) -> Result<TechniquePlan, SerialDeployError> {
if self.deployers.is_empty() {
return Err(SerialDeployError::EmptyTechnique(self.id));
}
Ok(TechniquePlan {
id: self.id,
rules: self.rules,
deployers: self.deployers,
})
}
}
pub fn strict_aggregate() -> Arc<dyn PracticeRule> {
Arc::new(BuiltInPracticeRule::aggregate(
PracticeRuleId::new("rule/aggregate").expect("static rule id"),
))
}
pub(crate) fn require_row(
deployer: &str,
rows: &BTreeMap<RowInstanceId, RowForm>,
row_id: &RowInstanceId,
) -> Result<(), SerialDeployError> {
if rows.contains_key(row_id) {
Ok(())
} else {
Err(SerialDeployError::UnknownRow {
deployer: deployer.to_owned(),
row_id: row_id.clone(),
})
}
}
pub(crate) fn require_voices(
deployer: &str,
expected: usize,
actual: usize,
) -> Result<(), SerialDeployError> {
if expected == actual {
Ok(())
} else {
Err(SerialDeployError::VoiceCountMismatch {
deployer: deployer.to_owned(),
expected,
actual,
})
}
}
pub(crate) fn structural_event(
event_id: SerialEventId,
ordinals: &[u8],
row_id: RowInstanceId,
voice: VoiceId,
rationale: String,
license: StructuralLicense,
placement: EventPlacement,
) -> PlannedSerialEvent {
PlannedSerialEvent {
id: event_id,
ordinals: ordinals
.iter()
.copied()
.map(|ordinal| OrdinalRef::new(row_id.clone(), usize::from(ordinal)))
.collect(),
role: SerialRole::Structural,
origin: SerialOrigin::Structural { rationale },
voice,
placement,
parents: Vec::new(),
licenses: vec![license],
}
}