Skip to main content

maolan_engine/
history.rs

1use crate::{
2    audio::io::AudioIO,
3    kind::Kind,
4    message::{Action, ClipMoveFrom, ClipMoveTo},
5    midi::io::MIDIIO,
6    state::State,
7};
8use std::collections::VecDeque;
9use std::sync::Arc;
10
11#[derive(Clone, Debug)]
12pub struct UndoEntry {
13    pub forward_actions: Vec<Action>,
14    pub inverse_actions: Vec<Action>,
15}
16
17pub struct History {
18    undo_stack: VecDeque<UndoEntry>,
19    redo_stack: VecDeque<UndoEntry>,
20    max_history: usize,
21}
22
23impl History {
24    pub fn new(max_history: usize) -> Self {
25        Self {
26            undo_stack: VecDeque::new(),
27            redo_stack: VecDeque::new(),
28            max_history,
29        }
30    }
31
32    pub fn record(&mut self, entry: UndoEntry) {
33        self.undo_stack.push_back(entry);
34        self.redo_stack.clear(); // Clear redo stack on new action
35
36        // Limit history size
37        if self.undo_stack.len() > self.max_history {
38            self.undo_stack.pop_front();
39        }
40    }
41
42    pub fn undo(&mut self) -> Option<Vec<Action>> {
43        self.undo_stack.pop_back().map(|entry| {
44            let inverse = entry.inverse_actions.clone();
45            self.redo_stack.push_back(entry);
46            inverse
47        })
48    }
49
50    pub fn redo(&mut self) -> Option<Vec<Action>> {
51        self.redo_stack.pop_back().map(|entry| {
52            let forward = entry.forward_actions.clone();
53            self.undo_stack.push_back(entry);
54            forward
55        })
56    }
57
58    pub fn clear(&mut self) {
59        self.undo_stack.clear();
60        self.redo_stack.clear();
61    }
62}
63
64impl Default for History {
65    fn default() -> Self {
66        Self::new(100)
67    }
68}
69
70/// Check if an action should be recorded in history
71pub fn should_record(action: &Action) -> bool {
72    matches!(
73        action,
74        Action::SetTempo(_)
75            | Action::SetLoopEnabled(_)
76            | Action::SetLoopRange(_)
77            | Action::SetPunchEnabled(_)
78            | Action::SetPunchRange(_)
79            | Action::SetMetronomeEnabled(_)
80            | Action::SetTimeSignature { .. }
81            | Action::AddTrack { .. }
82            | Action::RemoveTrack(_)
83            | Action::RenameTrack { .. }
84            | Action::TrackLevel(_, _)
85            | Action::TrackBalance(_, _)
86            | Action::TrackToggleArm(_)
87            | Action::TrackToggleMute(_)
88            | Action::TrackToggleSolo(_)
89            | Action::TrackToggleInputMonitor(_)
90            | Action::TrackToggleDiskMonitor(_)
91            | Action::TrackSetMidiLearnBinding { .. }
92            | Action::SetGlobalMidiLearnBinding { .. }
93            | Action::TrackSetVcaMaster { .. }
94            | Action::TrackSetFrozen { .. }
95            | Action::TrackAddAudioInput(_)
96            | Action::TrackAddAudioOutput(_)
97            | Action::TrackRemoveAudioInput(_)
98            | Action::TrackRemoveAudioOutput(_)
99            | Action::AddClip { .. }
100            | Action::RemoveClip { .. }
101            | Action::RenameClip { .. }
102            | Action::ClipMove { .. }
103            | Action::SetClipFade { .. }
104            | Action::SetClipBounds { .. }
105            | Action::SetClipMuted { .. }
106            | Action::ClearAllMidiLearnBindings
107            | Action::Connect { .. }
108            | Action::Disconnect { .. }
109            | Action::TrackConnectVst3Audio { .. }
110            | Action::TrackDisconnectVst3Audio { .. }
111            | Action::TrackConnectPluginAudio { .. }
112            | Action::TrackDisconnectPluginAudio { .. }
113            | Action::TrackConnectPluginMidi { .. }
114            | Action::TrackDisconnectPluginMidi { .. }
115            | Action::TrackLoadClapPlugin { .. }
116            | Action::TrackUnloadClapPlugin { .. }
117            | Action::TrackLoadLv2Plugin { .. }
118            | Action::TrackUnloadLv2PluginInstance { .. }
119            | Action::TrackLoadVst3Plugin { .. }
120            | Action::TrackUnloadVst3PluginInstance { .. }
121            | Action::TrackSetLv2ControlValue { .. }
122            | Action::TrackSetClapParameter { .. }
123            | Action::TrackSetVst3Parameter { .. }
124            | Action::ModifyMidiNotes { .. }
125            | Action::ModifyMidiControllers { .. }
126            | Action::DeleteMidiControllers { .. }
127            | Action::InsertMidiControllers { .. }
128            | Action::DeleteMidiNotes { .. }
129            | Action::InsertMidiNotes { .. }
130            | Action::SetMidiSysExEvents { .. }
131    )
132}
133
134/// Create an inverse action that will undo the given action
135/// Returns None if the action cannot be inverted
136pub fn create_inverse_action(action: &Action, state: &State) -> Option<Action> {
137    match action {
138        Action::AddTrack { name, .. } => Some(Action::RemoveTrack(name.clone())),
139
140        Action::RemoveTrack(name) => {
141            // Find the track to capture its data
142            let track = state.tracks.get(name)?;
143            let track_lock = track.lock();
144            Some(Action::AddTrack {
145                name: track_lock.name.clone(),
146                audio_ins: track_lock.primary_audio_ins(),
147                midi_ins: track_lock.midi.ins.len(),
148                audio_outs: track_lock.primary_audio_outs(),
149                midi_outs: track_lock.midi.outs.len(),
150            })
151        }
152
153        Action::RenameTrack { old_name, new_name } => Some(Action::RenameTrack {
154            old_name: new_name.clone(),
155            new_name: old_name.clone(),
156        }),
157
158        Action::TrackLevel(name, _new_level) => {
159            // Find current level
160            let track = state.tracks.get(name)?;
161            let track_lock = track.lock();
162            Some(Action::TrackLevel(name.clone(), track_lock.level))
163        }
164
165        Action::TrackBalance(name, _new_balance) => {
166            // Find current balance
167            let track = state.tracks.get(name)?;
168            let track_lock = track.lock();
169            Some(Action::TrackBalance(name.clone(), track_lock.balance))
170        }
171
172        Action::TrackToggleArm(name) => Some(Action::TrackToggleArm(name.clone())),
173        Action::TrackToggleMute(name) => Some(Action::TrackToggleMute(name.clone())),
174        Action::TrackToggleSolo(name) => Some(Action::TrackToggleSolo(name.clone())),
175        Action::TrackToggleInputMonitor(name) => {
176            Some(Action::TrackToggleInputMonitor(name.clone()))
177        }
178        Action::TrackToggleDiskMonitor(name) => Some(Action::TrackToggleDiskMonitor(name.clone())),
179        Action::TrackSetMidiLearnBinding {
180            track_name, target, ..
181        } => {
182            let track = state.tracks.get(track_name)?;
183            let track_lock = track.lock();
184            let binding = match target {
185                crate::message::TrackMidiLearnTarget::Volume => {
186                    track_lock.midi_learn_volume.clone()
187                }
188                crate::message::TrackMidiLearnTarget::Balance => {
189                    track_lock.midi_learn_balance.clone()
190                }
191                crate::message::TrackMidiLearnTarget::Mute => track_lock.midi_learn_mute.clone(),
192                crate::message::TrackMidiLearnTarget::Solo => track_lock.midi_learn_solo.clone(),
193                crate::message::TrackMidiLearnTarget::Arm => track_lock.midi_learn_arm.clone(),
194                crate::message::TrackMidiLearnTarget::InputMonitor => {
195                    track_lock.midi_learn_input_monitor.clone()
196                }
197                crate::message::TrackMidiLearnTarget::DiskMonitor => {
198                    track_lock.midi_learn_disk_monitor.clone()
199                }
200            };
201            Some(Action::TrackSetMidiLearnBinding {
202                track_name: track_name.clone(),
203                target: *target,
204                binding,
205            })
206        }
207        Action::TrackSetVcaMaster { track_name, .. } => {
208            let track = state.tracks.get(track_name)?;
209            let track_lock = track.lock();
210            Some(Action::TrackSetVcaMaster {
211                track_name: track_name.clone(),
212                master_track: track_lock.vca_master(),
213            })
214        }
215        Action::TrackSetFrozen { track_name, .. } => {
216            let track = state.tracks.get(track_name)?;
217            let track_lock = track.lock();
218            Some(Action::TrackSetFrozen {
219                track_name: track_name.clone(),
220                frozen: track_lock.frozen(),
221            })
222        }
223        Action::TrackAddAudioInput(name) => Some(Action::TrackRemoveAudioInput(name.clone())),
224        Action::TrackAddAudioOutput(name) => Some(Action::TrackRemoveAudioOutput(name.clone())),
225        Action::TrackRemoveAudioInput(name) => Some(Action::TrackAddAudioInput(name.clone())),
226        Action::TrackRemoveAudioOutput(name) => Some(Action::TrackAddAudioOutput(name.clone())),
227
228        Action::AddClip {
229            track_name, kind, ..
230        } => {
231            // To undo adding a clip, we need to know which index it will have
232            let track = state.tracks.get(track_name)?;
233            let track_lock = track.lock();
234            let clip_index = match kind {
235                Kind::Audio => track_lock.audio.clips.len(),
236                Kind::MIDI => track_lock.midi.clips.len(),
237            };
238            Some(Action::RemoveClip {
239                track_name: track_name.clone(),
240                kind: *kind,
241                clip_indices: vec![clip_index],
242            })
243        }
244
245        Action::RemoveClip {
246            track_name,
247            kind,
248            clip_indices,
249        } => {
250            // To undo removing clips, we need to capture their data
251            let track = state.tracks.get(track_name)?;
252            let track_lock = track.lock();
253
254            // For now, we only support undoing single clip removal
255            if clip_indices.len() != 1 {
256                return None;
257            }
258
259            let clip_idx = clip_indices[0];
260            match kind {
261                Kind::Audio => {
262                    let clip = track_lock.audio.clips.get(clip_idx)?;
263                    let length = clip.end.saturating_sub(clip.start);
264                    Some(Action::AddClip {
265                        name: clip.name.clone(),
266                        track_name: track_name.clone(),
267                        start: clip.start,
268                        length,
269                        offset: clip.offset,
270                        input_channel: clip.input_channel,
271                        muted: clip.muted,
272                        kind: Kind::Audio,
273                        fade_enabled: clip.fade_enabled,
274                        fade_in_samples: clip.fade_in_samples,
275                        fade_out_samples: clip.fade_out_samples,
276                        source_name: clip.pitch_correction_source_name.clone(),
277                        source_offset: clip.pitch_correction_source_offset,
278                        source_length: clip.pitch_correction_source_length,
279                        preview_name: clip.pitch_correction_preview_name.clone(),
280                        pitch_correction_points: clip.pitch_correction_points.clone(),
281                        pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
282                        pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
283                        pitch_correction_formant_compensation:
284                            clip.pitch_correction_formant_compensation,
285                    })
286                }
287                Kind::MIDI => {
288                    let clip = track_lock.midi.clips.get(clip_idx)?;
289                    let length = clip.end.saturating_sub(clip.start);
290                    Some(Action::AddClip {
291                        name: clip.name.clone(),
292                        track_name: track_name.clone(),
293                        start: clip.start,
294                        length,
295                        offset: clip.offset,
296                        input_channel: clip.input_channel,
297                        muted: clip.muted,
298                        kind: Kind::MIDI,
299                        fade_enabled: true,    // Default value for MIDI clips
300                        fade_in_samples: 240,  // Default value
301                        fade_out_samples: 240, // Default value
302                        source_name: None,
303                        source_offset: None,
304                        source_length: None,
305                        preview_name: None,
306                        pitch_correction_points: vec![],
307                        pitch_correction_frame_likeness: None,
308                        pitch_correction_inertia_ms: None,
309                        pitch_correction_formant_compensation: None,
310                    })
311                }
312            }
313        }
314
315        Action::RenameClip {
316            track_name,
317            kind,
318            clip_index,
319            new_name: _,
320        } => {
321            // Find current name
322            let track = state.tracks.get(track_name)?;
323            let track_lock = track.lock();
324            let old_name = match kind {
325                Kind::Audio => track_lock.audio.clips.get(*clip_index)?.name.clone(),
326                Kind::MIDI => track_lock.midi.clips.get(*clip_index)?.name.clone(),
327            };
328            Some(Action::RenameClip {
329                track_name: track_name.clone(),
330                kind: *kind,
331                clip_index: *clip_index,
332                new_name: old_name,
333            })
334        }
335
336        Action::ClipMove {
337            kind,
338            from,
339            to,
340            copy,
341        } => {
342            let (original_start, original_input_channel) = {
343                let source_track = state.tracks.get(&from.track_name)?;
344                let source_lock = source_track.lock();
345                match kind {
346                    Kind::Audio => {
347                        let clip = source_lock.audio.clips.get(from.clip_index)?;
348                        (clip.start, clip.input_channel)
349                    }
350                    Kind::MIDI => {
351                        let clip = source_lock.midi.clips.get(from.clip_index)?;
352                        (clip.start, clip.input_channel)
353                    }
354                }
355            };
356
357            if *copy {
358                // If it was a copy, we need to remove the newly created clip
359                let dest_track = state.tracks.get(&to.track_name)?;
360                let dest_lock = dest_track.lock();
361                let clip_idx = match kind {
362                    Kind::Audio => dest_lock.audio.clips.len(),
363                    Kind::MIDI => dest_lock.midi.clips.len(),
364                };
365                Some(Action::RemoveClip {
366                    track_name: to.track_name.clone(),
367                    kind: *kind,
368                    clip_indices: vec![clip_idx],
369                })
370            } else {
371                // If it was a move, reverse the move from the destination track.
372                let dest_track = state.tracks.get(&to.track_name)?;
373                let dest_lock = dest_track.lock();
374                let dest_len = match kind {
375                    Kind::Audio => {
376                        if dest_lock.audio.clips.is_empty() {
377                            return None;
378                        }
379                        dest_lock.audio.clips.len()
380                    }
381                    Kind::MIDI => {
382                        if dest_lock.midi.clips.is_empty() {
383                            return None;
384                        }
385                        dest_lock.midi.clips.len()
386                    }
387                };
388                let moved_clip_index = if from.track_name == to.track_name {
389                    dest_len.saturating_sub(1)
390                } else {
391                    dest_len
392                };
393                Some(Action::ClipMove {
394                    kind: *kind,
395                    from: ClipMoveFrom {
396                        track_name: to.track_name.clone(),
397                        clip_index: moved_clip_index,
398                    },
399                    to: ClipMoveTo {
400                        track_name: from.track_name.clone(),
401                        sample_offset: original_start,
402                        input_channel: original_input_channel,
403                    },
404                    copy: false,
405                })
406            }
407        }
408
409        Action::SetClipFade {
410            track_name,
411            clip_index,
412            kind,
413            ..
414        } => {
415            // Capture current fade settings
416            let track = state.tracks.get(track_name)?;
417            let track_lock = track.lock();
418            match kind {
419                Kind::Audio => {
420                    let clip = track_lock.audio.clips.get(*clip_index)?;
421                    Some(Action::SetClipFade {
422                        track_name: track_name.clone(),
423                        clip_index: *clip_index,
424                        kind: *kind,
425                        fade_enabled: clip.fade_enabled,
426                        fade_in_samples: clip.fade_in_samples,
427                        fade_out_samples: clip.fade_out_samples,
428                    })
429                }
430                Kind::MIDI => {
431                    // MIDI clips don't have fade fields in engine, use defaults
432                    Some(Action::SetClipFade {
433                        track_name: track_name.clone(),
434                        clip_index: *clip_index,
435                        kind: *kind,
436                        fade_enabled: true,
437                        fade_in_samples: 240,
438                        fade_out_samples: 240,
439                    })
440                }
441            }
442        }
443        Action::SetClipBounds {
444            track_name,
445            clip_index,
446            kind,
447            ..
448        } => {
449            let track = state.tracks.get(track_name)?;
450            let track_lock = track.lock();
451            match kind {
452                Kind::Audio => {
453                    let clip = track_lock.audio.clips.get(*clip_index)?;
454                    Some(Action::SetClipBounds {
455                        track_name: track_name.clone(),
456                        clip_index: *clip_index,
457                        kind: *kind,
458                        start: clip.start,
459                        length: clip.end.max(1),
460                        offset: clip.offset,
461                    })
462                }
463                Kind::MIDI => {
464                    let clip = track_lock.midi.clips.get(*clip_index)?;
465                    Some(Action::SetClipBounds {
466                        track_name: track_name.clone(),
467                        clip_index: *clip_index,
468                        kind: *kind,
469                        start: clip.start,
470                        length: clip.end.max(1),
471                        offset: clip.offset,
472                    })
473                }
474            }
475        }
476        Action::SetClipMuted {
477            track_name,
478            clip_index,
479            kind,
480            ..
481        } => {
482            let track = state.tracks.get(track_name)?;
483            let track_lock = track.lock();
484            let muted = match kind {
485                Kind::Audio => track_lock.audio.clips.get(*clip_index)?.muted,
486                Kind::MIDI => track_lock.midi.clips.get(*clip_index)?.muted,
487            };
488            Some(Action::SetClipMuted {
489                track_name: track_name.clone(),
490                clip_index: *clip_index,
491                kind: *kind,
492                muted,
493            })
494        }
495        Action::SetClipPitchCorrection {
496            track_name,
497            clip_index,
498            ..
499        } => {
500            let track = state.tracks.get(track_name)?;
501            let track_lock = track.lock();
502            let clip = track_lock.audio.clips.get(*clip_index)?;
503            Some(Action::SetClipPitchCorrection {
504                track_name: track_name.clone(),
505                clip_index: *clip_index,
506                preview_name: clip.pitch_correction_preview_name.clone(),
507                source_name: clip.pitch_correction_source_name.clone(),
508                source_offset: clip.pitch_correction_source_offset,
509                source_length: clip.pitch_correction_source_length,
510                pitch_correction_points: clip.pitch_correction_points.clone(),
511                pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
512                pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
513                pitch_correction_formant_compensation: clip
514                    .pitch_correction_formant_compensation,
515            })
516        }
517        Action::Connect {
518            from_track,
519            from_port,
520            to_track,
521            to_port,
522            kind,
523        } => Some(Action::Disconnect {
524            from_track: from_track.clone(),
525            from_port: *from_port,
526            to_track: to_track.clone(),
527            to_port: *to_port,
528            kind: *kind,
529        }),
530
531        Action::Disconnect {
532            from_track,
533            from_port,
534            to_track,
535            to_port,
536            kind,
537        } => Some(Action::Connect {
538            from_track: from_track.clone(),
539            from_port: *from_port,
540            to_track: to_track.clone(),
541            to_port: *to_port,
542            kind: *kind,
543        }),
544        Action::TrackConnectVst3Audio {
545            track_name,
546            from_node,
547            from_port,
548            to_node,
549            to_port,
550        } => Some(Action::TrackDisconnectVst3Audio {
551            track_name: track_name.clone(),
552            from_node: from_node.clone(),
553            from_port: *from_port,
554            to_node: to_node.clone(),
555            to_port: *to_port,
556        }),
557        Action::TrackDisconnectVst3Audio {
558            track_name,
559            from_node,
560            from_port,
561            to_node,
562            to_port,
563        } => Some(Action::TrackConnectVst3Audio {
564            track_name: track_name.clone(),
565            from_node: from_node.clone(),
566            from_port: *from_port,
567            to_node: to_node.clone(),
568            to_port: *to_port,
569        }),
570        Action::TrackConnectPluginAudio {
571            track_name,
572            from_node,
573            from_port,
574            to_node,
575            to_port,
576        } => Some(Action::TrackDisconnectPluginAudio {
577            track_name: track_name.clone(),
578            from_node: from_node.clone(),
579            from_port: *from_port,
580            to_node: to_node.clone(),
581            to_port: *to_port,
582        }),
583        Action::TrackDisconnectPluginAudio {
584            track_name,
585            from_node,
586            from_port,
587            to_node,
588            to_port,
589        } => Some(Action::TrackConnectPluginAudio {
590            track_name: track_name.clone(),
591            from_node: from_node.clone(),
592            from_port: *from_port,
593            to_node: to_node.clone(),
594            to_port: *to_port,
595        }),
596        Action::TrackConnectPluginMidi {
597            track_name,
598            from_node,
599            from_port,
600            to_node,
601            to_port,
602        } => Some(Action::TrackDisconnectPluginMidi {
603            track_name: track_name.clone(),
604            from_node: from_node.clone(),
605            from_port: *from_port,
606            to_node: to_node.clone(),
607            to_port: *to_port,
608        }),
609        Action::TrackDisconnectPluginMidi {
610            track_name,
611            from_node,
612            from_port,
613            to_node,
614            to_port,
615        } => Some(Action::TrackConnectPluginMidi {
616            track_name: track_name.clone(),
617            from_node: from_node.clone(),
618            from_port: *from_port,
619            to_node: to_node.clone(),
620            to_port: *to_port,
621        }),
622
623        Action::TrackLoadClapPlugin {
624            track_name,
625            plugin_path,
626        } => Some(Action::TrackUnloadClapPlugin {
627            track_name: track_name.clone(),
628            plugin_path: plugin_path.clone(),
629        }),
630
631        Action::TrackUnloadClapPlugin {
632            track_name,
633            plugin_path,
634        } => Some(Action::TrackLoadClapPlugin {
635            track_name: track_name.clone(),
636            plugin_path: plugin_path.clone(),
637        }),
638        Action::TrackLoadLv2Plugin {
639            track_name,
640            plugin_uri: _,
641        } => {
642            let track = state.tracks.get(track_name)?;
643            let track = track.lock();
644            Some(Action::TrackUnloadLv2PluginInstance {
645                track_name: track_name.clone(),
646                instance_id: track.next_lv2_instance_id,
647            })
648        }
649        Action::TrackUnloadLv2PluginInstance {
650            track_name,
651            instance_id,
652        } => {
653            let track = state.tracks.get(track_name)?;
654            let track = track.lock();
655            let plugin_uri = track
656                .loaded_lv2_instances()
657                .into_iter()
658                .find(|(id, _)| *id == *instance_id)
659                .map(|(_, uri)| uri)?;
660            Some(Action::TrackLoadLv2Plugin {
661                track_name: track_name.clone(),
662                plugin_uri,
663            })
664        }
665        Action::TrackLoadVst3Plugin {
666            track_name,
667            plugin_path: _,
668        } => {
669            let track = state.tracks.get(track_name)?;
670            let track = track.lock();
671            Some(Action::TrackUnloadVst3PluginInstance {
672                track_name: track_name.clone(),
673                instance_id: track.next_plugin_instance_id,
674            })
675        }
676        Action::TrackUnloadVst3PluginInstance {
677            track_name,
678            instance_id,
679        } => {
680            let track = state.tracks.get(track_name)?;
681            let track = track.lock();
682            let plugin_path = track
683                .loaded_vst3_instances()
684                .into_iter()
685                .find(|(id, _, _)| *id == *instance_id)
686                .map(|(_, path, _)| path)?;
687            Some(Action::TrackLoadVst3Plugin {
688                track_name: track_name.clone(),
689                plugin_path,
690            })
691        }
692        Action::TrackSetClapParameter {
693            track_name,
694            instance_id,
695            ..
696        } => {
697            let track = state.tracks.get(track_name)?;
698            let track = track.lock();
699            let snapshot = track.clap_snapshot_state(*instance_id).ok()?;
700            Some(Action::TrackClapRestoreState {
701                track_name: track_name.clone(),
702                instance_id: *instance_id,
703                state: snapshot,
704            })
705        }
706        Action::TrackSetVst3Parameter {
707            track_name,
708            instance_id,
709            ..
710        } => {
711            let track = state.tracks.get(track_name)?;
712            let track = track.lock();
713            let snapshot = track.vst3_snapshot_state(*instance_id).ok()?;
714            Some(Action::TrackVst3RestoreState {
715                track_name: track_name.clone(),
716                instance_id: *instance_id,
717                state: snapshot,
718            })
719        }
720        Action::TrackSetLv2ControlValue {
721            track_name,
722            instance_id,
723            ..
724        } => {
725            let track = state.tracks.get(track_name)?;
726            let track = track.lock();
727            let snapshot = track.lv2_snapshot_state(*instance_id).ok()?;
728            Some(Action::TrackSetLv2PluginState {
729                track_name: track_name.clone(),
730                instance_id: *instance_id,
731                state: snapshot,
732            })
733        }
734        Action::ModifyMidiNotes {
735            track_name,
736            clip_index,
737            note_indices,
738            new_notes,
739            old_notes,
740        } => Some(Action::ModifyMidiNotes {
741            track_name: track_name.clone(),
742            clip_index: *clip_index,
743            note_indices: note_indices.clone(),
744            new_notes: old_notes.clone(),
745            old_notes: new_notes.clone(),
746        }),
747        Action::ModifyMidiControllers {
748            track_name,
749            clip_index,
750            controller_indices,
751            new_controllers,
752            old_controllers,
753        } => Some(Action::ModifyMidiControllers {
754            track_name: track_name.clone(),
755            clip_index: *clip_index,
756            controller_indices: controller_indices.clone(),
757            new_controllers: old_controllers.clone(),
758            old_controllers: new_controllers.clone(),
759        }),
760        Action::DeleteMidiControllers {
761            track_name,
762            clip_index,
763            deleted_controllers,
764            ..
765        } => Some(Action::InsertMidiControllers {
766            track_name: track_name.clone(),
767            clip_index: *clip_index,
768            controllers: deleted_controllers.clone(),
769        }),
770        Action::InsertMidiControllers {
771            track_name,
772            clip_index,
773            controllers,
774        } => {
775            let mut controller_indices: Vec<usize> =
776                controllers.iter().map(|(idx, _)| *idx).collect();
777            controller_indices.sort_unstable_by(|a, b| b.cmp(a));
778            Some(Action::DeleteMidiControllers {
779                track_name: track_name.clone(),
780                clip_index: *clip_index,
781                controller_indices,
782                deleted_controllers: controllers.clone(),
783            })
784        }
785
786        Action::DeleteMidiNotes {
787            track_name,
788            clip_index,
789            deleted_notes,
790            ..
791        } => Some(Action::InsertMidiNotes {
792            track_name: track_name.clone(),
793            clip_index: *clip_index,
794            notes: deleted_notes.clone(),
795        }),
796
797        Action::InsertMidiNotes {
798            track_name,
799            clip_index,
800            notes,
801        } => {
802            let mut note_indices: Vec<usize> = notes.iter().map(|(idx, _)| *idx).collect();
803            note_indices.sort_unstable_by(|a, b| b.cmp(a));
804            Some(Action::DeleteMidiNotes {
805                track_name: track_name.clone(),
806                clip_index: *clip_index,
807                note_indices,
808                deleted_notes: notes.clone(),
809            })
810        }
811        Action::SetMidiSysExEvents {
812            track_name,
813            clip_index,
814            new_sysex_events,
815            old_sysex_events,
816        } => Some(Action::SetMidiSysExEvents {
817            track_name: track_name.clone(),
818            clip_index: *clip_index,
819            new_sysex_events: old_sysex_events.clone(),
820            old_sysex_events: new_sysex_events.clone(),
821        }),
822
823        // These are more complex and would need additional state tracking
824        _ => None,
825    }
826}
827
828pub fn create_inverse_actions(action: &Action, state: &State) -> Option<Vec<Action>> {
829    if let Action::ClearAllMidiLearnBindings = action {
830        let mut actions = Vec::<Action>::new();
831        for (track_name, track) in &state.tracks {
832            let t = track.lock();
833            let mut push_if_some =
834                |target: crate::message::TrackMidiLearnTarget,
835                 binding: Option<crate::message::MidiLearnBinding>| {
836                    if binding.is_some() {
837                        actions.push(Action::TrackSetMidiLearnBinding {
838                            track_name: track_name.clone(),
839                            target,
840                            binding,
841                        });
842                    }
843                };
844            push_if_some(
845                crate::message::TrackMidiLearnTarget::Volume,
846                t.midi_learn_volume.clone(),
847            );
848            push_if_some(
849                crate::message::TrackMidiLearnTarget::Balance,
850                t.midi_learn_balance.clone(),
851            );
852            push_if_some(
853                crate::message::TrackMidiLearnTarget::Mute,
854                t.midi_learn_mute.clone(),
855            );
856            push_if_some(
857                crate::message::TrackMidiLearnTarget::Solo,
858                t.midi_learn_solo.clone(),
859            );
860            push_if_some(
861                crate::message::TrackMidiLearnTarget::Arm,
862                t.midi_learn_arm.clone(),
863            );
864            push_if_some(
865                crate::message::TrackMidiLearnTarget::InputMonitor,
866                t.midi_learn_input_monitor.clone(),
867            );
868            push_if_some(
869                crate::message::TrackMidiLearnTarget::DiskMonitor,
870                t.midi_learn_disk_monitor.clone(),
871            );
872        }
873        return Some(actions);
874    }
875
876    if let Action::RemoveTrack(track_name) = action {
877        let mut actions = Vec::new();
878        {
879            let track = state.tracks.get(track_name)?;
880            let track = track.lock();
881            actions.push(Action::AddTrack {
882                name: track.name.clone(),
883                audio_ins: track.primary_audio_ins(),
884                midi_ins: track.midi.ins.len(),
885                audio_outs: track.primary_audio_outs(),
886                midi_outs: track.midi.outs.len(),
887            });
888            for _ in track.primary_audio_ins()..track.audio.ins.len() {
889                actions.push(Action::TrackAddAudioInput(track.name.clone()));
890            }
891            for _ in track.primary_audio_outs()..track.audio.outs.len() {
892                actions.push(Action::TrackAddAudioOutput(track.name.clone()));
893            }
894
895            if track.level != 0.0 {
896                actions.push(Action::TrackLevel(track.name.clone(), track.level));
897            }
898            if track.balance != 0.0 {
899                actions.push(Action::TrackBalance(track.name.clone(), track.balance));
900            }
901            if track.armed {
902                actions.push(Action::TrackToggleArm(track.name.clone()));
903            }
904            if track.muted {
905                actions.push(Action::TrackToggleMute(track.name.clone()));
906            }
907            if track.soloed {
908                actions.push(Action::TrackToggleSolo(track.name.clone()));
909            }
910            if track.input_monitor {
911                actions.push(Action::TrackToggleInputMonitor(track.name.clone()));
912            }
913            if !track.disk_monitor {
914                actions.push(Action::TrackToggleDiskMonitor(track.name.clone()));
915            }
916            if track.midi_learn_volume.is_some() {
917                actions.push(Action::TrackSetMidiLearnBinding {
918                    track_name: track.name.clone(),
919                    target: crate::message::TrackMidiLearnTarget::Volume,
920                    binding: track.midi_learn_volume.clone(),
921                });
922            }
923            if track.midi_learn_balance.is_some() {
924                actions.push(Action::TrackSetMidiLearnBinding {
925                    track_name: track.name.clone(),
926                    target: crate::message::TrackMidiLearnTarget::Balance,
927                    binding: track.midi_learn_balance.clone(),
928                });
929            }
930            if track.midi_learn_mute.is_some() {
931                actions.push(Action::TrackSetMidiLearnBinding {
932                    track_name: track.name.clone(),
933                    target: crate::message::TrackMidiLearnTarget::Mute,
934                    binding: track.midi_learn_mute.clone(),
935                });
936            }
937            if track.midi_learn_solo.is_some() {
938                actions.push(Action::TrackSetMidiLearnBinding {
939                    track_name: track.name.clone(),
940                    target: crate::message::TrackMidiLearnTarget::Solo,
941                    binding: track.midi_learn_solo.clone(),
942                });
943            }
944            if track.midi_learn_arm.is_some() {
945                actions.push(Action::TrackSetMidiLearnBinding {
946                    track_name: track.name.clone(),
947                    target: crate::message::TrackMidiLearnTarget::Arm,
948                    binding: track.midi_learn_arm.clone(),
949                });
950            }
951            if track.midi_learn_input_monitor.is_some() {
952                actions.push(Action::TrackSetMidiLearnBinding {
953                    track_name: track.name.clone(),
954                    target: crate::message::TrackMidiLearnTarget::InputMonitor,
955                    binding: track.midi_learn_input_monitor.clone(),
956                });
957            }
958            if track.midi_learn_disk_monitor.is_some() {
959                actions.push(Action::TrackSetMidiLearnBinding {
960                    track_name: track.name.clone(),
961                    target: crate::message::TrackMidiLearnTarget::DiskMonitor,
962                    binding: track.midi_learn_disk_monitor.clone(),
963                });
964            }
965            if track.vca_master.is_some() {
966                actions.push(Action::TrackSetVcaMaster {
967                    track_name: track.name.clone(),
968                    master_track: track.vca_master(),
969                });
970            }
971            for (other_name, other_track_handle) in &state.tracks {
972                if other_name == track_name {
973                    continue;
974                }
975                let other_track = other_track_handle.lock();
976                if other_track.vca_master.as_deref() == Some(track_name.as_str()) {
977                    actions.push(Action::TrackSetVcaMaster {
978                        track_name: other_name.clone(),
979                        master_track: Some(track_name.clone()),
980                    });
981                }
982            }
983
984            for clip in &track.audio.clips {
985                let length = clip.end.saturating_sub(clip.start).max(1);
986                actions.push(Action::AddClip {
987                    name: clip.name.clone(),
988                    track_name: track.name.clone(),
989                    start: clip.start,
990                    length,
991                    offset: clip.offset,
992                    input_channel: clip.input_channel,
993                    muted: clip.muted,
994                    kind: Kind::Audio,
995                    fade_enabled: clip.fade_enabled,
996                    fade_in_samples: clip.fade_in_samples,
997                    fade_out_samples: clip.fade_out_samples,
998                    source_name: clip.pitch_correction_source_name.clone(),
999                    source_offset: clip.pitch_correction_source_offset,
1000                    source_length: clip.pitch_correction_source_length,
1001                    preview_name: clip.pitch_correction_preview_name.clone(),
1002                    pitch_correction_points: clip.pitch_correction_points.clone(),
1003                    pitch_correction_frame_likeness: clip.pitch_correction_frame_likeness,
1004                    pitch_correction_inertia_ms: clip.pitch_correction_inertia_ms,
1005                    pitch_correction_formant_compensation:
1006                        clip.pitch_correction_formant_compensation,
1007                });
1008            }
1009            for clip in &track.midi.clips {
1010                let length = clip.end.saturating_sub(clip.start).max(1);
1011                actions.push(Action::AddClip {
1012                    name: clip.name.clone(),
1013                    track_name: track.name.clone(),
1014                    start: clip.start,
1015                    length,
1016                    offset: clip.offset,
1017                    input_channel: clip.input_channel,
1018                    muted: clip.muted,
1019                    kind: Kind::MIDI,
1020                    fade_enabled: true,
1021                    fade_in_samples: 240,
1022                    fade_out_samples: 240,
1023                    source_name: None,
1024                    source_offset: None,
1025                    source_length: None,
1026                    preview_name: None,
1027                    pitch_correction_points: vec![],
1028                    pitch_correction_frame_likeness: None,
1029                    pitch_correction_inertia_ms: None,
1030                    pitch_correction_formant_compensation: None,
1031                });
1032            }
1033        }
1034
1035        let mut seen_audio = std::collections::HashSet::<(String, usize, String, usize)>::new();
1036        let mut seen_midi = std::collections::HashSet::<(String, usize, String, usize)>::new();
1037
1038        for (from_name, from_track_handle) in &state.tracks {
1039            let from_track = from_track_handle.lock();
1040            for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1041                let conns: Vec<Arc<AudioIO>> = out.connections.lock().to_vec();
1042                for conn in conns {
1043                    for (to_name, to_track_handle) in &state.tracks {
1044                        let to_track = to_track_handle.lock();
1045                        for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1046                            if Arc::ptr_eq(&conn, to_in)
1047                                && (from_name == track_name || to_name == track_name)
1048                                && seen_audio.insert((
1049                                    from_name.clone(),
1050                                    from_port,
1051                                    to_name.clone(),
1052                                    to_port,
1053                                ))
1054                            {
1055                                actions.push(Action::Connect {
1056                                    from_track: from_name.clone(),
1057                                    from_port,
1058                                    to_track: to_name.clone(),
1059                                    to_port,
1060                                    kind: Kind::Audio,
1061                                });
1062                            }
1063                        }
1064                    }
1065                }
1066            }
1067
1068            for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1069                let conns: Vec<Arc<crate::mutex::UnsafeMutex<Box<MIDIIO>>>> =
1070                    out.lock().connections.to_vec();
1071                for conn in conns {
1072                    for (to_name, to_track_handle) in &state.tracks {
1073                        let to_track = to_track_handle.lock();
1074                        for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1075                            if Arc::ptr_eq(&conn, to_in)
1076                                && (from_name == track_name || to_name == track_name)
1077                                && seen_midi.insert((
1078                                    from_name.clone(),
1079                                    from_port,
1080                                    to_name.clone(),
1081                                    to_port,
1082                                ))
1083                            {
1084                                actions.push(Action::Connect {
1085                                    from_track: from_name.clone(),
1086                                    from_port,
1087                                    to_track: to_name.clone(),
1088                                    to_port,
1089                                    kind: Kind::MIDI,
1090                                });
1091                            }
1092                        }
1093                    }
1094                }
1095            }
1096        }
1097
1098        for (to_name, to_track_handle) in &state.tracks {
1099            if to_name != track_name {
1100                continue;
1101            }
1102            let to_track = to_track_handle.lock();
1103            for (to_port, to_in) in to_track.audio.ins.iter().enumerate() {
1104                for (from_name, from_track_handle) in &state.tracks {
1105                    let from_track = from_track_handle.lock();
1106                    for (from_port, out) in from_track.audio.outs.iter().enumerate() {
1107                        let conns: Vec<Arc<AudioIO>> = out.connections.lock().to_vec();
1108                        if conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1109                            && seen_audio.insert((
1110                                from_name.clone(),
1111                                from_port,
1112                                to_name.clone(),
1113                                to_port,
1114                            ))
1115                        {
1116                            actions.push(Action::Connect {
1117                                from_track: from_name.clone(),
1118                                from_port,
1119                                to_track: to_name.clone(),
1120                                to_port,
1121                                kind: Kind::Audio,
1122                            });
1123                        }
1124                    }
1125                }
1126            }
1127            for (to_port, to_in) in to_track.midi.ins.iter().enumerate() {
1128                for (from_name, from_track_handle) in &state.tracks {
1129                    let from_track = from_track_handle.lock();
1130                    for (from_port, out) in from_track.midi.outs.iter().enumerate() {
1131                        let conns: Vec<Arc<crate::mutex::UnsafeMutex<Box<MIDIIO>>>> =
1132                            out.lock().connections.to_vec();
1133                        if conns.iter().any(|conn| Arc::ptr_eq(conn, to_in))
1134                            && seen_midi.insert((
1135                                from_name.clone(),
1136                                from_port,
1137                                to_name.clone(),
1138                                to_port,
1139                            ))
1140                        {
1141                            actions.push(Action::Connect {
1142                                from_track: from_name.clone(),
1143                                from_port,
1144                                to_track: to_name.clone(),
1145                                to_port,
1146                                kind: Kind::MIDI,
1147                            });
1148                        }
1149                    }
1150                }
1151            }
1152        }
1153
1154        return Some(actions);
1155    }
1156
1157    create_inverse_action(action, state).map(|a| vec![a])
1158}
1159
1160#[cfg(test)]
1161mod tests {
1162    use super::*;
1163    use crate::audio::clip::AudioClip;
1164    use crate::kind::Kind;
1165    #[cfg(all(unix, not(target_os = "macos")))]
1166    use crate::message::Lv2PluginState;
1167    use crate::message::{MidiLearnBinding, TrackMidiLearnTarget};
1168    use crate::mutex::UnsafeMutex;
1169    use crate::track::Track;
1170    use crate::vst3::Vst3PluginState;
1171    use std::sync::Arc;
1172
1173    fn make_state_with_track(track: Track) -> State {
1174        let mut state = State::default();
1175        state.tracks.insert(
1176            track.name.clone(),
1177            Arc::new(UnsafeMutex::new(Box::new(track))),
1178        );
1179        state
1180    }
1181
1182    fn binding(cc: u8) -> MidiLearnBinding {
1183        MidiLearnBinding {
1184            device: Some("midi".to_string()),
1185            channel: 1,
1186            cc,
1187        }
1188    }
1189
1190    #[test]
1191    fn history_record_limits_size_and_clears_redo_on_new_entry() {
1192        let mut history = History::new(2);
1193        let a = UndoEntry {
1194            forward_actions: vec![Action::SetTempo(120.0)],
1195            inverse_actions: vec![Action::SetTempo(110.0)],
1196        };
1197        let b = UndoEntry {
1198            forward_actions: vec![Action::SetLoopEnabled(true)],
1199            inverse_actions: vec![Action::SetLoopEnabled(false)],
1200        };
1201        let c = UndoEntry {
1202            forward_actions: vec![Action::SetMetronomeEnabled(true)],
1203            inverse_actions: vec![Action::SetMetronomeEnabled(false)],
1204        };
1205
1206        history.record(a);
1207        history.record(b.clone());
1208        history.record(c.clone());
1209
1210        let undo = history.undo().unwrap();
1211        assert!(matches!(
1212            undo.as_slice(),
1213            [Action::SetMetronomeEnabled(false)]
1214        ));
1215
1216        let redo = history.redo().unwrap();
1217        assert!(matches!(
1218            redo.as_slice(),
1219            [Action::SetMetronomeEnabled(true)]
1220        ));
1221
1222        history.undo();
1223        history.record(UndoEntry {
1224            forward_actions: vec![Action::SetClipPlaybackEnabled(true)],
1225            inverse_actions: vec![Action::SetClipPlaybackEnabled(false)],
1226        });
1227
1228        assert!(history.redo().is_none());
1229        let undo = history.undo().unwrap();
1230        assert!(matches!(
1231            undo.as_slice(),
1232            [Action::SetClipPlaybackEnabled(false)]
1233        ));
1234        let undo = history.undo().unwrap();
1235        assert!(matches!(undo.as_slice(), [Action::SetLoopEnabled(false)]));
1236        assert!(history.undo().is_none());
1237    }
1238
1239    #[test]
1240    fn history_clear_removes_pending_undo_and_redo_entries() {
1241        let mut history = History::new(4);
1242        history.record(UndoEntry {
1243            forward_actions: vec![Action::SetTempo(120.0)],
1244            inverse_actions: vec![Action::SetTempo(100.0)],
1245        });
1246        history.record(UndoEntry {
1247            forward_actions: vec![Action::SetLoopEnabled(true)],
1248            inverse_actions: vec![Action::SetLoopEnabled(false)],
1249        });
1250
1251        assert!(history.undo().is_some());
1252        assert!(history.redo().is_some());
1253
1254        history.clear();
1255
1256        assert!(history.undo().is_none());
1257        assert!(history.redo().is_none());
1258    }
1259
1260    #[test]
1261    fn history_with_zero_capacity_discards_recorded_entries() {
1262        let mut history = History::new(0);
1263        history.record(UndoEntry {
1264            forward_actions: vec![Action::SetTempo(120.0)],
1265            inverse_actions: vec![Action::SetTempo(100.0)],
1266        });
1267
1268        assert!(history.undo().is_none());
1269        assert!(history.redo().is_none());
1270    }
1271
1272    #[test]
1273    fn should_record_covers_recent_transport_and_lv2_actions() {
1274        assert!(should_record(&Action::SetLoopEnabled(true)));
1275        assert!(should_record(&Action::SetLoopRange(Some((64, 128)))));
1276        assert!(should_record(&Action::SetPunchEnabled(true)));
1277        assert!(should_record(&Action::SetPunchRange(Some((32, 96)))));
1278        assert!(should_record(&Action::SetMetronomeEnabled(true)));
1279        assert!(!should_record(&Action::SetClipPlaybackEnabled(false)));
1280        assert!(!should_record(&Action::SetRecordEnabled(true)));
1281        assert!(should_record(&Action::SetClipBounds {
1282            track_name: "t".to_string(),
1283            clip_index: 0,
1284            kind: Kind::Audio,
1285            start: 64,
1286            length: 32,
1287            offset: 16,
1288        }));
1289        assert!(should_record(&Action::TrackLoadVst3Plugin {
1290            track_name: "t".to_string(),
1291            plugin_path: "/tmp/test.vst3".to_string(),
1292        }));
1293        #[cfg(all(unix, not(target_os = "macos")))]
1294        {
1295            assert!(should_record(&Action::TrackLoadLv2Plugin {
1296                track_name: "t".to_string(),
1297                plugin_uri: "urn:test".to_string(),
1298            }));
1299            assert!(should_record(&Action::TrackSetLv2ControlValue {
1300                track_name: "t".to_string(),
1301                instance_id: 0,
1302                index: 1,
1303                value: 0.5,
1304            }));
1305            assert!(!should_record(&Action::TrackSetLv2PluginState {
1306                track_name: "t".to_string(),
1307                instance_id: 0,
1308                state: Lv2PluginState {
1309                    port_values: vec![],
1310                    properties: vec![],
1311                },
1312            }));
1313        }
1314        assert!(!should_record(&Action::TrackVst3RestoreState {
1315            track_name: "t".to_string(),
1316            instance_id: 0,
1317            state: Vst3PluginState {
1318                plugin_id: "id".to_string(),
1319                component_state: vec![],
1320                controller_state: vec![],
1321            },
1322        }));
1323    }
1324
1325    #[test]
1326    fn create_inverse_action_for_add_clip_targets_next_clip_index() {
1327        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1328        track
1329            .audio
1330            .clips
1331            .push(AudioClip::new("existing".to_string(), 0, 16));
1332        let state = make_state_with_track(track);
1333
1334        let inverse = create_inverse_action(
1335            &Action::AddClip {
1336                name: "new".to_string(),
1337                track_name: "t".to_string(),
1338                start: 32,
1339                length: 16,
1340                offset: 0,
1341                input_channel: 0,
1342                muted: false,
1343                kind: Kind::Audio,
1344                fade_enabled: false,
1345                fade_in_samples: 0,
1346                fade_out_samples: 0,
1347                source_name: None,
1348                source_offset: None,
1349                source_length: None,
1350                preview_name: None,
1351                pitch_correction_points: vec![],
1352                pitch_correction_frame_likeness: None,
1353                pitch_correction_inertia_ms: None,
1354                pitch_correction_formant_compensation: None,
1355            },
1356            &state,
1357        )
1358        .unwrap();
1359
1360        match inverse {
1361            Action::RemoveClip {
1362                track_name,
1363                kind,
1364                clip_indices,
1365            } => {
1366                assert_eq!(track_name, "t");
1367                assert_eq!(kind, Kind::Audio);
1368                assert_eq!(clip_indices, vec![1]);
1369            }
1370            other => panic!("unexpected inverse action: {other:?}"),
1371        }
1372    }
1373
1374    #[test]
1375    fn create_inverse_action_for_set_clip_bounds_restores_previous_audio_bounds() {
1376        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1377        let mut clip = AudioClip::new("clip".to_string(), 10, 30);
1378        clip.offset = 7;
1379        track.audio.clips.push(clip);
1380        let state = make_state_with_track(track);
1381
1382        let inverse = create_inverse_action(
1383            &Action::SetClipBounds {
1384                track_name: "t".to_string(),
1385                clip_index: 0,
1386                kind: Kind::Audio,
1387                start: 14,
1388                length: 22,
1389                offset: 11,
1390            },
1391            &state,
1392        )
1393        .expect("inverse action");
1394
1395        match inverse {
1396            Action::SetClipBounds {
1397                track_name,
1398                clip_index,
1399                kind,
1400                start,
1401                length,
1402                offset,
1403            } => {
1404                assert_eq!(track_name, "t");
1405                assert_eq!(clip_index, 0);
1406                assert_eq!(kind, Kind::Audio);
1407                assert_eq!(start, 10);
1408                assert_eq!(length, 30);
1409                assert_eq!(offset, 7);
1410            }
1411            other => panic!("unexpected inverse action: {other:?}"),
1412        }
1413    }
1414
1415    #[test]
1416    fn create_inverse_action_for_set_clip_bounds_restores_previous_midi_bounds() {
1417        let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
1418        track.midi.clips.push(crate::midi::clip::MIDIClip {
1419            name: "pattern.mid".to_string(),
1420            start: 24,
1421            end: 120,
1422            offset: 9,
1423            ..Default::default()
1424        });
1425        let state = make_state_with_track(track);
1426
1427        let inverse = create_inverse_action(
1428            &Action::SetClipBounds {
1429                track_name: "t".to_string(),
1430                clip_index: 0,
1431                kind: Kind::MIDI,
1432                start: 32,
1433                length: 48,
1434                offset: 4,
1435            },
1436            &state,
1437        )
1438        .expect("inverse action");
1439
1440        match inverse {
1441            Action::SetClipBounds {
1442                track_name,
1443                clip_index,
1444                kind,
1445                start,
1446                length,
1447                offset,
1448            } => {
1449                assert_eq!(track_name, "t");
1450                assert_eq!(clip_index, 0);
1451                assert_eq!(kind, Kind::MIDI);
1452                assert_eq!(start, 24);
1453                assert_eq!(length, 120);
1454                assert_eq!(offset, 9);
1455            }
1456            other => panic!("unexpected inverse action: {other:?}"),
1457        }
1458    }
1459
1460    #[test]
1461    fn create_inverse_action_for_set_clip_muted_restores_audio_and_midi_flags() {
1462        let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
1463        let mut audio_clip = AudioClip::new("audio.wav".to_string(), 0, 16);
1464        audio_clip.muted = true;
1465        track.audio.clips.push(audio_clip);
1466        let midi_clip = crate::midi::clip::MIDIClip {
1467            name: "pattern.mid".to_string(),
1468            muted: false,
1469            ..Default::default()
1470        };
1471        track.midi.clips.push(midi_clip);
1472        let state = make_state_with_track(track);
1473
1474        let audio_inverse = create_inverse_action(
1475            &Action::SetClipMuted {
1476                track_name: "t".to_string(),
1477                clip_index: 0,
1478                kind: Kind::Audio,
1479                muted: false,
1480            },
1481            &state,
1482        )
1483        .expect("audio inverse");
1484        let midi_inverse = create_inverse_action(
1485            &Action::SetClipMuted {
1486                track_name: "t".to_string(),
1487                clip_index: 0,
1488                kind: Kind::MIDI,
1489                muted: true,
1490            },
1491            &state,
1492        )
1493        .expect("midi inverse");
1494
1495        assert!(matches!(
1496            audio_inverse,
1497            Action::SetClipMuted { muted: true, kind: Kind::Audio, .. }
1498        ));
1499        assert!(matches!(
1500            midi_inverse,
1501            Action::SetClipMuted { muted: false, kind: Kind::MIDI, .. }
1502        ));
1503    }
1504
1505    #[test]
1506    fn create_inverse_action_for_rename_clip_restores_previous_name() {
1507        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1508        track
1509            .audio
1510            .clips
1511            .push(AudioClip::new("before.wav".to_string(), 0, 16));
1512        let state = make_state_with_track(track);
1513
1514        let inverse = create_inverse_action(
1515            &Action::RenameClip {
1516                track_name: "t".to_string(),
1517                kind: Kind::Audio,
1518                clip_index: 0,
1519                new_name: "after.wav".to_string(),
1520            },
1521            &state,
1522        )
1523        .expect("inverse action");
1524
1525        assert!(matches!(
1526            inverse,
1527            Action::RenameClip { new_name, kind: Kind::Audio, .. } if new_name == "before.wav"
1528        ));
1529    }
1530
1531    #[test]
1532    fn create_inverse_action_for_track_set_vca_master_restores_none() {
1533        let track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1534        let state = make_state_with_track(track);
1535
1536        let inverse = create_inverse_action(
1537            &Action::TrackSetVcaMaster {
1538                track_name: "t".to_string(),
1539                master_track: Some("bus".to_string()),
1540            },
1541            &state,
1542        )
1543        .expect("inverse action");
1544
1545        assert!(matches!(
1546            inverse,
1547            Action::TrackSetVcaMaster { track_name, master_track: None } if track_name == "t"
1548        ));
1549    }
1550
1551    #[test]
1552    fn create_inverse_action_for_remove_midi_clip_restores_clip_with_default_fades() {
1553        let mut track = Track::new("t".to_string(), 0, 0, 1, 1, 64, 48_000.0);
1554        track.midi.clips.push(crate::midi::clip::MIDIClip {
1555            name: "pattern.mid".to_string(),
1556            start: 48,
1557            end: 144,
1558            offset: 12,
1559            input_channel: 3,
1560            muted: true,
1561            ..Default::default()
1562        });
1563        let state = make_state_with_track(track);
1564
1565        let inverse = create_inverse_action(
1566            &Action::RemoveClip {
1567                track_name: "t".to_string(),
1568                kind: Kind::MIDI,
1569                clip_indices: vec![0],
1570            },
1571            &state,
1572        )
1573        .expect("inverse action");
1574
1575        match inverse {
1576            Action::AddClip {
1577                name,
1578                track_name,
1579                start,
1580                length,
1581                offset,
1582                input_channel,
1583                muted,
1584                kind,
1585                fade_enabled,
1586                fade_in_samples,
1587                fade_out_samples,
1588                ..
1589            } => {
1590                assert_eq!(name, "pattern.mid");
1591                assert_eq!(track_name, "t");
1592                assert_eq!(start, 48);
1593                assert_eq!(length, 96);
1594                assert_eq!(offset, 12);
1595                assert_eq!(input_channel, 3);
1596                assert!(muted);
1597                assert_eq!(kind, Kind::MIDI);
1598                assert!(fade_enabled);
1599                assert_eq!(fade_in_samples, 240);
1600                assert_eq!(fade_out_samples, 240);
1601            }
1602            other => panic!("unexpected inverse action: {other:?}"),
1603        }
1604    }
1605
1606    #[test]
1607    fn create_inverse_action_for_clip_copy_targets_new_destination_clip() {
1608        let mut source = Track::new("src".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1609        source
1610            .audio
1611            .clips
1612            .push(AudioClip::new("source.wav".to_string(), 12, 48));
1613        let mut dest = Track::new("dst".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1614        dest.audio
1615            .clips
1616            .push(AudioClip::new("existing.wav".to_string(), 0, 24));
1617
1618        let mut state = State::default();
1619        state.tracks.insert(
1620            source.name.clone(),
1621            Arc::new(UnsafeMutex::new(Box::new(source))),
1622        );
1623        state.tracks.insert(
1624            dest.name.clone(),
1625            Arc::new(UnsafeMutex::new(Box::new(dest))),
1626        );
1627
1628        let inverse = create_inverse_action(
1629            &Action::ClipMove {
1630                kind: Kind::Audio,
1631                from: ClipMoveFrom {
1632                    track_name: "src".to_string(),
1633                    clip_index: 0,
1634                },
1635                to: ClipMoveTo {
1636                    track_name: "dst".to_string(),
1637                    sample_offset: 96,
1638                    input_channel: 0,
1639                },
1640                copy: true,
1641            },
1642            &state,
1643        )
1644        .expect("inverse action");
1645
1646        match inverse {
1647            Action::RemoveClip {
1648                track_name,
1649                kind,
1650                clip_indices,
1651            } => {
1652                assert_eq!(track_name, "dst");
1653                assert_eq!(kind, Kind::Audio);
1654                assert_eq!(clip_indices, vec![1]);
1655            }
1656            other => panic!("unexpected inverse action: {other:?}"),
1657        }
1658    }
1659
1660    #[test]
1661    fn create_inverse_action_for_same_track_clip_move_reverses_last_destination_clip() {
1662        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1663        let mut original = AudioClip::new("clip.wav".to_string(), 20, 40);
1664        original.input_channel = 2;
1665        let moved = AudioClip::new("moved.wav".to_string(), 80, 32);
1666        track.audio.clips.push(original);
1667        track.audio.clips.push(moved);
1668        let state = make_state_with_track(track);
1669
1670        let inverse = create_inverse_action(
1671            &Action::ClipMove {
1672                kind: Kind::Audio,
1673                from: ClipMoveFrom {
1674                    track_name: "t".to_string(),
1675                    clip_index: 0,
1676                },
1677                to: ClipMoveTo {
1678                    track_name: "t".to_string(),
1679                    sample_offset: 80,
1680                    input_channel: 1,
1681                },
1682                copy: false,
1683            },
1684            &state,
1685        )
1686        .expect("inverse action");
1687
1688        match inverse {
1689            Action::ClipMove { kind, from, to, copy } => {
1690                assert_eq!(kind, Kind::Audio);
1691                assert_eq!(from.track_name, "t");
1692                assert_eq!(from.clip_index, 1);
1693                assert_eq!(to.track_name, "t");
1694                assert_eq!(to.sample_offset, 20);
1695                assert_eq!(to.input_channel, 2);
1696                assert!(!copy);
1697            }
1698            other => panic!("unexpected inverse action: {other:?}"),
1699        }
1700    }
1701
1702    #[test]
1703    fn create_inverse_action_for_track_midi_binding_restores_previous_binding() {
1704        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1705        track.midi_learn_volume = Some(binding(7));
1706        let state = make_state_with_track(track);
1707
1708        let inverse = create_inverse_action(
1709            &Action::TrackSetMidiLearnBinding {
1710                track_name: "t".to_string(),
1711                target: TrackMidiLearnTarget::Volume,
1712                binding: Some(binding(9)),
1713            },
1714            &state,
1715        )
1716        .unwrap();
1717
1718        match inverse {
1719            Action::TrackSetMidiLearnBinding {
1720                track_name,
1721                target,
1722                binding,
1723            } => {
1724                assert_eq!(track_name, "t");
1725                assert_eq!(target, TrackMidiLearnTarget::Volume);
1726                assert_eq!(binding.unwrap().cc, 7);
1727            }
1728            other => panic!("unexpected inverse action: {other:?}"),
1729        }
1730    }
1731
1732    #[test]
1733    fn create_inverse_action_for_vst3_load_uses_next_runtime_instance_id() {
1734        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1735        track.next_plugin_instance_id = 42;
1736        let state = make_state_with_track(track);
1737
1738        let inverse = create_inverse_action(
1739            &Action::TrackLoadVst3Plugin {
1740                track_name: "t".to_string(),
1741                plugin_path: "/tmp/test.vst3".to_string(),
1742            },
1743            &state,
1744        )
1745        .unwrap();
1746
1747        match inverse {
1748            Action::TrackUnloadVst3PluginInstance {
1749                track_name,
1750                instance_id,
1751            } => {
1752                assert_eq!(track_name, "t");
1753                assert_eq!(instance_id, 42);
1754            }
1755            other => panic!("unexpected inverse action: {other:?}"),
1756        }
1757    }
1758
1759    #[test]
1760    #[cfg(all(unix, not(target_os = "macos")))]
1761    fn create_inverse_action_for_lv2_load_uses_next_runtime_instance_id() {
1762        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1763        track.next_lv2_instance_id = 5;
1764        let state = make_state_with_track(track);
1765
1766        let inverse = create_inverse_action(
1767            &Action::TrackLoadLv2Plugin {
1768                track_name: "t".to_string(),
1769                plugin_uri: "urn:test".to_string(),
1770            },
1771            &state,
1772        )
1773        .unwrap();
1774
1775        match inverse {
1776            Action::TrackUnloadLv2PluginInstance {
1777                track_name,
1778                instance_id,
1779            } => {
1780                assert_eq!(track_name, "t");
1781                assert_eq!(instance_id, 5);
1782            }
1783            other => panic!("unexpected inverse action: {other:?}"),
1784        }
1785    }
1786
1787    #[test]
1788    fn create_inverse_actions_for_clear_all_midi_learn_bindings_restores_only_existing_bindings() {
1789        let mut track = Track::new("t".to_string(), 1, 1, 0, 0, 64, 48_000.0);
1790        track.midi_learn_volume = Some(binding(7));
1791        track.midi_learn_disk_monitor = Some(binding(64));
1792        let state = make_state_with_track(track);
1793
1794        let inverses = create_inverse_actions(&Action::ClearAllMidiLearnBindings, &state).unwrap();
1795
1796        assert_eq!(inverses.len(), 2);
1797        assert!(inverses.iter().any(|action| {
1798            matches!(
1799                action,
1800                Action::TrackSetMidiLearnBinding {
1801                    target: TrackMidiLearnTarget::Volume,
1802                    binding: Some(MidiLearnBinding { cc: 7, .. }),
1803                    ..
1804                }
1805            )
1806        }));
1807        assert!(inverses.iter().any(|action| {
1808            matches!(
1809                action,
1810                Action::TrackSetMidiLearnBinding {
1811                    target: TrackMidiLearnTarget::DiskMonitor,
1812                    binding: Some(MidiLearnBinding { cc: 64, .. }),
1813                    ..
1814                }
1815            )
1816        }));
1817    }
1818
1819    #[test]
1820    fn create_inverse_actions_for_remove_track_restores_io_flags_and_bindings() {
1821        let mut track = Track::new("t".to_string(), 1, 1, 1, 1, 64, 48_000.0);
1822        track.level = -3.0;
1823        track.balance = 0.25;
1824        track.armed = true;
1825        track.muted = true;
1826        track.soloed = true;
1827        track.input_monitor = true;
1828        track.disk_monitor = false;
1829        track.midi_learn_volume = Some(binding(10));
1830        track.vca_master = Some("bus".to_string());
1831        track.audio.ins.push(Arc::new(AudioIO::new(64)));
1832        track.audio.outs.push(Arc::new(AudioIO::new(64)));
1833        let state = make_state_with_track(track);
1834
1835        let inverses =
1836            create_inverse_actions(&Action::RemoveTrack("t".to_string()), &state).unwrap();
1837
1838        assert!(matches!(
1839            inverses.first(),
1840            Some(Action::AddTrack {
1841                name,
1842                audio_ins: 1,
1843                audio_outs: 1,
1844                midi_ins: 1,
1845                midi_outs: 1,
1846            }) if name == "t"
1847        ));
1848        assert!(
1849            inverses
1850                .iter()
1851                .any(|action| matches!(action, Action::TrackAddAudioInput(name) if name == "t"))
1852        );
1853        assert!(
1854            inverses
1855                .iter()
1856                .any(|action| matches!(action, Action::TrackAddAudioOutput(name) if name == "t"))
1857        );
1858        assert!(
1859            inverses.iter().any(
1860                |action| matches!(action, Action::TrackToggleInputMonitor(name) if name == "t")
1861            )
1862        );
1863        assert!(
1864            inverses.iter().any(
1865                |action| matches!(action, Action::TrackToggleDiskMonitor(name) if name == "t")
1866            )
1867        );
1868        assert!(inverses.iter().any(|action| {
1869            matches!(
1870                action,
1871                Action::TrackSetMidiLearnBinding {
1872                    target: TrackMidiLearnTarget::Volume,
1873                    binding: Some(MidiLearnBinding { cc: 10, .. }),
1874                    ..
1875                }
1876            )
1877        }));
1878        assert!(inverses.iter().any(|action| {
1879            matches!(
1880                action,
1881                Action::TrackSetVcaMaster {
1882                    track_name,
1883                    master_track: Some(master),
1884                } if track_name == "t" && master == "bus"
1885            )
1886        }));
1887    }
1888}