1#![allow(dead_code)]
8#![allow(clippy::cast_precision_loss)]
9#![allow(clippy::too_many_arguments)]
10
11use std::collections::{HashMap, HashSet};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum LockLevel {
16 Unlocked,
18 PositionLocked,
20 ContentLocked,
22 FullyLocked,
24}
25
26impl LockLevel {
27 #[must_use]
29 pub fn blocks_position(&self) -> bool {
30 matches!(self, Self::PositionLocked | Self::FullyLocked)
31 }
32
33 #[must_use]
35 pub fn blocks_content(&self) -> bool {
36 matches!(self, Self::ContentLocked | Self::FullyLocked)
37 }
38
39 #[must_use]
41 pub fn is_locked(&self) -> bool {
42 !matches!(self, Self::Unlocked)
43 }
44}
45
46#[derive(Debug, Clone)]
48pub struct TrackLock {
49 pub track_index: u32,
51 pub level: LockLevel,
53 pub locked_by: String,
55 pub reason: String,
57}
58
59impl TrackLock {
60 #[must_use]
62 pub fn new(track_index: u32, level: LockLevel, locked_by: &str) -> Self {
63 Self {
64 track_index,
65 level,
66 locked_by: locked_by.to_string(),
67 reason: String::new(),
68 }
69 }
70
71 #[must_use]
73 pub fn with_reason(mut self, reason: &str) -> Self {
74 self.reason = reason.to_string();
75 self
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct ClipLock {
82 pub clip_id: u64,
84 pub level: LockLevel,
86 pub locked_by: String,
88}
89
90impl ClipLock {
91 #[must_use]
93 pub fn new(clip_id: u64, level: LockLevel, locked_by: &str) -> Self {
94 Self {
95 clip_id,
96 level,
97 locked_by: locked_by.to_string(),
98 }
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq)]
104pub enum LockCheckResult {
105 Allowed,
107 BlockedByTrack {
109 track: u32,
111 level: LockLevel,
113 },
114 BlockedByClip {
116 clip_id: u64,
118 level: LockLevel,
120 },
121}
122
123impl LockCheckResult {
124 #[must_use]
126 pub fn is_allowed(&self) -> bool {
127 matches!(self, Self::Allowed)
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum OperationKind {
134 Move,
136 ContentEdit,
138 Delete,
140 Add,
142}
143
144impl OperationKind {
145 #[must_use]
147 pub fn is_position_change(&self) -> bool {
148 matches!(self, Self::Move | Self::Delete)
149 }
150
151 #[must_use]
153 pub fn is_content_change(&self) -> bool {
154 matches!(self, Self::ContentEdit | Self::Delete)
155 }
156}
157
158#[derive(Debug, Clone, Default)]
160pub struct LockManager {
161 track_locks: HashMap<u32, TrackLock>,
163 clip_locks: HashMap<u64, ClipLock>,
165 pinned_clips: HashSet<u64>,
167}
168
169impl LockManager {
170 #[must_use]
172 pub fn new() -> Self {
173 Self::default()
174 }
175
176 pub fn lock_track(&mut self, lock: TrackLock) {
178 self.track_locks.insert(lock.track_index, lock);
179 }
180
181 pub fn unlock_track(&mut self, track_index: u32) -> bool {
183 self.track_locks.remove(&track_index).is_some()
184 }
185
186 #[must_use]
188 pub fn get_track_lock(&self, track_index: u32) -> Option<&TrackLock> {
189 self.track_locks.get(&track_index)
190 }
191
192 pub fn lock_clip(&mut self, lock: ClipLock) {
194 self.clip_locks.insert(lock.clip_id, lock);
195 }
196
197 pub fn unlock_clip(&mut self, clip_id: u64) -> bool {
199 self.clip_locks.remove(&clip_id).is_some()
200 }
201
202 #[must_use]
204 pub fn get_clip_lock(&self, clip_id: u64) -> Option<&ClipLock> {
205 self.clip_locks.get(&clip_id)
206 }
207
208 pub fn pin_clip(&mut self, clip_id: u64) {
210 self.pinned_clips.insert(clip_id);
211 }
212
213 pub fn unpin_clip(&mut self, clip_id: u64) -> bool {
215 self.pinned_clips.remove(&clip_id)
216 }
217
218 #[must_use]
220 pub fn is_pinned(&self, clip_id: u64) -> bool {
221 self.pinned_clips.contains(&clip_id)
222 }
223
224 #[must_use]
226 pub fn check(&self, track_index: u32, clip_id: u64, op: OperationKind) -> LockCheckResult {
227 if let Some(tl) = self.track_locks.get(&track_index) {
229 if Self::does_lock_block(tl.level, op) {
230 return LockCheckResult::BlockedByTrack {
231 track: track_index,
232 level: tl.level,
233 };
234 }
235 }
236 if let Some(cl) = self.clip_locks.get(&clip_id) {
238 if Self::does_lock_block(cl.level, op) {
239 return LockCheckResult::BlockedByClip {
240 clip_id,
241 level: cl.level,
242 };
243 }
244 }
245 LockCheckResult::Allowed
246 }
247
248 fn does_lock_block(level: LockLevel, op: OperationKind) -> bool {
250 match level {
251 LockLevel::Unlocked => false,
252 LockLevel::FullyLocked => true,
253 LockLevel::PositionLocked => op.is_position_change(),
254 LockLevel::ContentLocked => op.is_content_change(),
255 }
256 }
257
258 #[must_use]
260 pub fn locked_track_count(&self) -> usize {
261 self.track_locks.len()
262 }
263
264 #[must_use]
266 pub fn locked_clip_count(&self) -> usize {
267 self.clip_locks.len()
268 }
269
270 #[must_use]
272 pub fn pinned_clip_count(&self) -> usize {
273 self.pinned_clips.len()
274 }
275
276 pub fn clear_all(&mut self) {
278 self.track_locks.clear();
279 self.clip_locks.clear();
280 self.pinned_clips.clear();
281 }
282
283 #[must_use]
285 pub fn locked_tracks(&self) -> Vec<u32> {
286 self.track_locks.keys().copied().collect()
287 }
288
289 #[must_use]
291 pub fn locked_clips(&self) -> Vec<u64> {
292 self.clip_locks.keys().copied().collect()
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::*;
299
300 #[test]
301 fn test_lock_level_blocks_position() {
302 assert!(!LockLevel::Unlocked.blocks_position());
303 assert!(LockLevel::PositionLocked.blocks_position());
304 assert!(!LockLevel::ContentLocked.blocks_position());
305 assert!(LockLevel::FullyLocked.blocks_position());
306 }
307
308 #[test]
309 fn test_lock_level_blocks_content() {
310 assert!(!LockLevel::Unlocked.blocks_content());
311 assert!(!LockLevel::PositionLocked.blocks_content());
312 assert!(LockLevel::ContentLocked.blocks_content());
313 assert!(LockLevel::FullyLocked.blocks_content());
314 }
315
316 #[test]
317 fn test_lock_level_is_locked() {
318 assert!(!LockLevel::Unlocked.is_locked());
319 assert!(LockLevel::PositionLocked.is_locked());
320 assert!(LockLevel::ContentLocked.is_locked());
321 assert!(LockLevel::FullyLocked.is_locked());
322 }
323
324 #[test]
325 fn test_track_lock_new() {
326 let tl = TrackLock::new(0, LockLevel::FullyLocked, "alice").with_reason("final mix");
327 assert_eq!(tl.track_index, 0);
328 assert_eq!(tl.locked_by, "alice");
329 assert_eq!(tl.reason, "final mix");
330 }
331
332 #[test]
333 fn test_clip_lock_new() {
334 let cl = ClipLock::new(42, LockLevel::ContentLocked, "bob");
335 assert_eq!(cl.clip_id, 42);
336 assert_eq!(cl.level, LockLevel::ContentLocked);
337 }
338
339 #[test]
340 fn test_manager_lock_unlock_track() {
341 let mut mgr = LockManager::new();
342 mgr.lock_track(TrackLock::new(0, LockLevel::FullyLocked, "u"));
343 assert_eq!(mgr.locked_track_count(), 1);
344 assert!(mgr.get_track_lock(0).is_some());
345 assert!(mgr.unlock_track(0));
346 assert_eq!(mgr.locked_track_count(), 0);
347 }
348
349 #[test]
350 fn test_manager_lock_unlock_clip() {
351 let mut mgr = LockManager::new();
352 mgr.lock_clip(ClipLock::new(10, LockLevel::PositionLocked, "u"));
353 assert_eq!(mgr.locked_clip_count(), 1);
354 assert!(mgr.get_clip_lock(10).is_some());
355 assert!(mgr.unlock_clip(10));
356 assert_eq!(mgr.locked_clip_count(), 0);
357 }
358
359 #[test]
360 fn test_manager_pin_unpin() {
361 let mut mgr = LockManager::new();
362 mgr.pin_clip(5);
363 assert!(mgr.is_pinned(5));
364 assert_eq!(mgr.pinned_clip_count(), 1);
365 assert!(mgr.unpin_clip(5));
366 assert!(!mgr.is_pinned(5));
367 }
368
369 #[test]
370 fn test_check_fully_locked_track() {
371 let mut mgr = LockManager::new();
372 mgr.lock_track(TrackLock::new(0, LockLevel::FullyLocked, "u"));
373 let r = mgr.check(0, 1, OperationKind::Move);
374 assert!(!r.is_allowed());
375 assert!(matches!(
376 r,
377 LockCheckResult::BlockedByTrack { track: 0, .. }
378 ));
379 }
380
381 #[test]
382 fn test_check_position_locked_allows_content_edit() {
383 let mut mgr = LockManager::new();
384 mgr.lock_track(TrackLock::new(0, LockLevel::PositionLocked, "u"));
385 assert!(mgr.check(0, 1, OperationKind::ContentEdit).is_allowed());
387 assert!(!mgr.check(0, 1, OperationKind::Move).is_allowed());
389 }
390
391 #[test]
392 fn test_check_content_locked_allows_move() {
393 let mut mgr = LockManager::new();
394 mgr.lock_track(TrackLock::new(0, LockLevel::ContentLocked, "u"));
395 assert!(mgr.check(0, 1, OperationKind::Move).is_allowed());
396 assert!(!mgr.check(0, 1, OperationKind::ContentEdit).is_allowed());
397 }
398
399 #[test]
400 fn test_check_clip_lock_overrides() {
401 let mut mgr = LockManager::new();
402 mgr.lock_clip(ClipLock::new(42, LockLevel::FullyLocked, "u"));
404 let r = mgr.check(0, 42, OperationKind::Add);
405 assert!(!r.is_allowed());
406 assert!(matches!(
407 r,
408 LockCheckResult::BlockedByClip { clip_id: 42, .. }
409 ));
410 }
411
412 #[test]
413 fn test_check_unlocked() {
414 let mgr = LockManager::new();
415 assert!(mgr.check(0, 1, OperationKind::Move).is_allowed());
416 assert!(mgr.check(0, 1, OperationKind::Delete).is_allowed());
417 }
418
419 #[test]
420 fn test_clear_all() {
421 let mut mgr = LockManager::new();
422 mgr.lock_track(TrackLock::new(0, LockLevel::FullyLocked, "u"));
423 mgr.lock_clip(ClipLock::new(1, LockLevel::FullyLocked, "u"));
424 mgr.pin_clip(2);
425 mgr.clear_all();
426 assert_eq!(mgr.locked_track_count(), 0);
427 assert_eq!(mgr.locked_clip_count(), 0);
428 assert_eq!(mgr.pinned_clip_count(), 0);
429 }
430
431 #[test]
432 fn test_operation_kind_classification() {
433 assert!(OperationKind::Move.is_position_change());
434 assert!(OperationKind::Delete.is_position_change());
435 assert!(!OperationKind::ContentEdit.is_position_change());
436 assert!(!OperationKind::Add.is_position_change());
437
438 assert!(OperationKind::ContentEdit.is_content_change());
439 assert!(OperationKind::Delete.is_content_change());
440 assert!(!OperationKind::Move.is_content_change());
441 }
442}