1use std::collections::HashMap;
6
7use crate::preset::edits::{EditError, EditErrors, Resolved};
8use crate::preset::format::{FormatError, PresetFormat, PresetModel, Route, RoutePolarity, Stored};
9use crate::preset::registry::ParamRegistry;
10
11#[derive(Clone, Debug, PartialEq)]
13pub struct Change {
14 pub target: String,
16 pub value: String,
18 pub uncalibrated: bool,
20 pub raw: bool,
22 pub reason: Option<String>,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct Skipped {
29 pub target: String,
31 pub why: String,
33}
34
35#[derive(Clone, Debug, Default, PartialEq)]
37pub struct ApplyReport {
38 pub changes: Vec<Change>,
40 pub skipped: Vec<Skipped>,
42 pub warnings: Vec<String>,
44}
45
46fn as_number(value: &Stored) -> Option<f64> {
47 match value {
48 Stored::Float(number) => Some(*number),
49 Stored::Int(number) => Some(crate::cast::i64_to_f64(*number)),
50 Stored::Flag(_) | Stored::Text(_) => None,
51 }
52}
53
54pub fn place_route(model: &mut dyn PresetModel, route: Route) -> Result<usize, FormatError> {
62 let routes = model.routes();
63 let same = routes.iter().position(|slot| {
64 slot.as_ref().is_some_and(|have| {
65 have.source == route.source && have.destination == route.destination
66 })
67 });
68 let slot = same
69 .or_else(|| routes.iter().position(Option::is_none))
70 .ok_or_else(|| {
71 FormatError::new(
72 "cannot add the route",
73 format!("all {} modulation slots are taken", routes.len()),
74 "Remove a route, or reuse a slot with the same source and destination",
75 )
76 })?;
77 model.set_route(slot, Some(route))?;
78 Ok(slot)
79}
80
81pub fn apply(
96 format: &dyn PresetFormat,
97 registry: &dyn ParamRegistry,
98 model: &mut dyn PresetModel,
99 resolved: &[Resolved],
100) -> Result<ApplyReport, EditErrors> {
101 let mut report = ApplyReport::default();
102 let mut errors = Vec::new();
103 let held: HashMap<&str, Option<Stored>> = resolved
106 .iter()
107 .filter_map(|edit| match edit {
108 Resolved::Param { key, .. } => Some((key.as_str(), model.get(key))),
109 Resolved::Route { .. } | Resolved::Raw { .. } => None,
110 })
111 .collect();
112 let routes_held = model.routes();
113 for (index, edit) in resolved.iter().enumerate() {
114 let result = match edit {
115 Resolved::Param {
116 key,
117 stored,
118 uncalibrated,
119 reason,
120 } => apply_param(
121 format,
122 registry,
123 model,
124 key,
125 *stored,
126 held.get(key.as_str()),
127 &mut report,
128 )
129 .map(|changed| {
130 if changed {
131 let physical = registry.get(key).map_or_else(String::new, |spec| {
132 shown_value(spec, *stored)
133 .map_or_else(String::new, |text| format!("{text}, "))
134 });
135 report.changes.push(Change {
136 target: key.clone(),
137 value: format!("{physical}stored {stored:?}"),
138 uncalibrated: *uncalibrated,
139 raw: false,
140 reason: reason.clone(),
141 });
142 }
143 }),
144 Resolved::Route {
145 source,
146 destination,
147 amount,
148 polarity,
149 reason,
150 } => apply_route(format, model, source, destination, *amount, *polarity).map(|slot| {
151 match slot {
152 Some(slot) => report.changes.push(Change {
153 target: format!("slot {}", slot + 1),
154 value: format!("{source} -> {destination}, amount {amount:?}"),
155 uncalibrated: false,
156 raw: false,
157 reason: reason.clone(),
158 }),
159 None => report.skipped.push(Skipped {
160 target: format!("{source} -> {destination}"),
161 why: route_skip(&routes_held, &model.routes(), source, destination),
162 }),
163 }
164 }),
165 Resolved::Raw { key, value, reason } => model.set(key, value.clone()).map(|()| {
166 report.changes.push(Change {
167 target: key.clone(),
168 value: value.to_string(),
169 uncalibrated: false,
170 raw: true,
171 reason: reason.clone(),
172 });
173 }),
174 };
175 if let Err(error) = result {
176 errors.push(EditError {
177 place: format!("edit {}", index + 1),
178 cause: format!("{}: {}", error.what, error.cause),
179 fix: error.fix,
180 });
181 }
182 }
183 if !errors.is_empty() {
184 return Err(EditErrors(errors));
185 }
186 let edited: Vec<String> = resolved
187 .iter()
188 .filter_map(|edit| match edit {
189 Resolved::Param { key, .. } | Resolved::Raw { key, .. } => Some(key.clone()),
190 Resolved::Route { .. } => None,
191 })
192 .collect();
193 let edited: Vec<&str> = edited.iter().map(String::as_str).collect();
194 report.warnings = format.scan(model, &edited).map_err(|error| {
195 EditErrors(vec![EditError {
196 place: "the edited preset".to_owned(),
197 cause: format!("{}: {}", error.what, error.cause),
198 fix: error.fix,
199 }])
200 })?;
201 Ok(report)
202}
203
204fn shown_value(spec: &crate::preset::registry::ParamSpec, stored: f64) -> Option<String> {
207 use crate::preset::registry::ParamKind;
208 match spec.kind {
209 ParamKind::Flag => Some(if stored == 0.0 { "off" } else { "on" }.to_owned()),
210 ParamKind::Choice => {
211 let index = (stored - spec.stored_min).max(0.0);
212 spec.choices
213 .iter()
214 .zip(0u32..)
215 .find(|(_, i)| (f64::from(*i) - index).abs() < 0.5)
216 .map(|(label, _)| label.clone())
217 }
218 ParamKind::Float | ParamKind::Count => spec.physical(stored).ok().map(|value| {
219 let tidy = format!("{:.11e}", value.get())
222 .parse()
223 .unwrap_or(value.get());
224 crate::preset::registry::Physical::new(value.kind(), tidy)
225 .map_or_else(|_| value.to_string(), |tidy| tidy.to_string())
226 }),
227 }
228}
229
230fn skip_why(held_before: bool) -> String {
233 if held_before {
234 "equals the template"
235 } else {
236 "equals an earlier edit"
237 }
238 .to_owned()
239}
240
241fn route_skip(
245 before: &[Option<Route>],
246 now: &[Option<Route>],
247 source: &str,
248 destination: &str,
249) -> String {
250 let find = |routes: &[Option<Route>]| {
251 routes
252 .iter()
253 .flatten()
254 .find(|have| have.source == source && have.destination == destination)
255 .map(|have| (have.amount.to_bits(), have.polarity))
256 };
257 skip_why(find(before).is_some() && find(before) == find(now))
258}
259
260fn apply_param(
261 format: &dyn PresetFormat,
262 registry: &dyn ParamRegistry,
263 model: &mut dyn PresetModel,
264 key: &str,
265 stored: f64,
266 held: Option<&Option<Stored>>,
267 report: &mut ApplyReport,
268) -> Result<bool, FormatError> {
269 format.check_value(key, stored)?;
270 let current = model.get(key);
271 if current
272 .as_ref()
273 .and_then(as_number)
274 .is_some_and(|value| value.to_bits() == stored.to_bits())
275 {
276 report.skipped.push(Skipped {
277 target: key.to_owned(),
278 why: skip_why(held.is_none_or(|held| *held == current)),
279 });
280 return Ok(false);
281 }
282 let is_default = registry
283 .get(key)
284 .is_some_and(|spec| spec.default.to_bits() == stored.to_bits());
285 if current.is_none() && is_default && format.absent_means_default(key) {
286 report.skipped.push(Skipped {
287 target: key.to_owned(),
288 why: "equals the default".to_owned(),
289 });
290 return Ok(false);
291 }
292 model.set(key, Stored::Float(stored))?;
293 Ok(true)
294}
295
296fn apply_route(
297 format: &dyn PresetFormat,
298 model: &mut dyn PresetModel,
299 source: &str,
300 destination: &str,
301 amount: f64,
302 polarity: Option<RoutePolarity>,
303) -> Result<Option<usize>, FormatError> {
304 if !format.route_sources().contains(&source) {
305 return Err(FormatError::new(
306 "cannot add the route",
307 format!("{source:?} is not a modulation source"),
308 format!("Use one of: {}", format.route_sources().join(", ")),
309 ));
310 }
311 let existing = model
314 .routes()
315 .into_iter()
316 .flatten()
317 .find(|have| have.source == source && have.destination == destination);
318 let polarity = polarity
319 .or(existing.as_ref().map(|have| have.polarity))
320 .unwrap_or_else(|| format.default_polarity(source));
321 if existing
322 .as_ref()
323 .is_some_and(|have| have.amount.to_bits() == amount.to_bits() && have.polarity == polarity)
324 {
325 return Ok(None);
326 }
327 place_route(
328 model,
329 Route {
330 source: source.to_owned(),
331 destination: destination.to_owned(),
332 amount,
333 polarity,
334 extras: existing.map(|have| have.extras).unwrap_or_default(),
335 },
336 )
337 .map(Some)
338}