1use std::collections::{HashMap, VecDeque};
4use std::path::{Path, PathBuf};
5
6use crate::history::History;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum SelectionMode {
11 Random,
12 Sequential,
13}
14
15pub struct PresetManager {
18 preset_queue: VecDeque<PathBuf>,
20
21 current_index: usize,
24
25 transition: TransitionState,
27
28 history: History<PathBuf>,
32
33 locked: bool,
36
37 mode: SelectionMode,
39
40 ratings: HashMap<PathBuf, u8>,
43
44 ratings_path: Option<PathBuf>,
46}
47
48#[derive(Debug, Clone)]
50pub enum TransitionState {
51 None,
53
54 Transitioning {
56 progress: f32,
58
59 duration: f32,
61
62 elapsed: f32,
64 },
65}
66
67pub const DEFAULT_RATING: u8 = 3;
71
72pub const DEFAULT_HISTORY_SIZE: usize = 64;
74
75impl PresetManager {
76 pub fn new() -> Self {
78 Self::with_history_size(DEFAULT_HISTORY_SIZE)
79 }
80
81 pub fn with_history_size(history_size: usize) -> Self {
83 Self {
84 preset_queue: VecDeque::new(),
85 current_index: 0,
86 transition: TransitionState::None,
87 history: History::new(history_size.max(1)),
88 locked: false,
89 mode: SelectionMode::Random,
90 ratings: HashMap::new(),
91 ratings_path: None,
92 }
93 }
94
95 pub fn attach_ratings_file(&mut self, path: PathBuf) {
99 self.ratings = load_ratings(&path);
100 self.ratings_path = Some(path);
101 }
102
103 pub fn add_preset<P: AsRef<Path>>(&mut self, path: P) {
105 self.preset_queue.push_back(path.as_ref().to_path_buf());
106 }
107
108 pub fn add_presets<P: AsRef<Path>>(&mut self, paths: &[P]) {
110 for path in paths {
111 self.add_preset(path);
112 }
113 }
114
115 pub fn next_preset(&mut self) -> Option<&Path> {
117 if self.preset_queue.is_empty() {
118 return None;
119 }
120
121 self.current_index = (self.current_index + 1) % self.preset_queue.len();
122 self.preset_queue
123 .get(self.current_index)
124 .map(|p| p.as_path())
125 }
126
127 pub fn prev_preset(&mut self) -> Option<&Path> {
129 if self.preset_queue.is_empty() {
130 return None;
131 }
132
133 if self.current_index == 0 {
134 self.current_index = self.preset_queue.len() - 1;
135 } else {
136 self.current_index -= 1;
137 }
138
139 self.preset_queue
140 .get(self.current_index)
141 .map(|p| p.as_path())
142 }
143
144 pub fn current_preset(&self) -> Option<&Path> {
146 self.preset_queue
147 .get(self.current_index)
148 .map(|p| p.as_path())
149 }
150
151 pub fn random_preset(&mut self) -> Option<&Path> {
155 if self.preset_queue.is_empty() {
156 return None;
157 }
158 let pick = quick_random_seed() % self.preset_queue.len();
159 self.current_index = pick;
160 self.preset_queue
161 .get(self.current_index)
162 .map(|p| p.as_path())
163 }
164
165 pub fn random_weighted_by_rating(&mut self) -> Option<&Path> {
170 if self.preset_queue.is_empty() {
171 return None;
172 }
173
174 let weights: Vec<u64> = self
175 .preset_queue
176 .iter()
177 .map(|p| {
178 let r = self.ratings.get(p).copied().unwrap_or(DEFAULT_RATING);
179 1u64 << r.min(5)
180 })
181 .collect();
182 let total: u64 = weights.iter().sum();
183 if total == 0 {
184 return self.random_preset();
185 }
186 let mut pick = (quick_random_seed() as u64) % total;
187 for (i, &w) in weights.iter().enumerate() {
188 if pick < w {
189 self.current_index = i;
190 return self.preset_queue.get(i).map(|p| p.as_path());
191 }
192 pick -= w;
193 }
194 self.current_index = self.preset_queue.len() - 1;
197 self.preset_queue
198 .get(self.current_index)
199 .map(|p| p.as_path())
200 }
201
202 pub fn advance(&mut self) -> Option<PathBuf> {
207 if self.locked {
208 return None;
209 }
210 let picked = match self.mode {
211 SelectionMode::Random => self.random_weighted_by_rating().map(Path::to_path_buf),
212 SelectionMode::Sequential => self.next_preset().map(Path::to_path_buf),
213 };
214 if let Some(ref p) = picked {
215 self.history.push(p.clone());
216 }
217 picked
218 }
219
220 pub fn retreat(&mut self) -> Option<PathBuf> {
225 if self.locked {
226 return None;
227 }
228 if self.history.can_go_back() {
233 let back = self.history.back().cloned();
234 if let Some(b) = &back
236 && self.current_preset().map(Path::to_path_buf).as_ref() == Some(b)
237 && self.history.can_go_back()
238 {
239 return self.history.back().cloned();
240 }
241 return back;
242 }
243 self.prev_preset().map(Path::to_path_buf)
244 }
245
246 pub fn is_locked(&self) -> bool {
250 self.locked
251 }
252
253 pub fn set_locked(&mut self, locked: bool) {
256 self.locked = locked;
257 }
258
259 pub fn toggle_lock(&mut self) -> bool {
261 self.locked = !self.locked;
262 self.locked
263 }
264
265 pub fn mode(&self) -> SelectionMode {
268 self.mode
269 }
270
271 pub fn set_mode(&mut self, mode: SelectionMode) {
272 self.mode = mode;
273 }
274
275 pub fn cycle_mode(&mut self) -> SelectionMode {
277 self.mode = match self.mode {
278 SelectionMode::Random => SelectionMode::Sequential,
279 SelectionMode::Sequential => SelectionMode::Random,
280 };
281 self.mode
282 }
283
284 pub fn rating(&self, path: &Path) -> u8 {
288 self.ratings.get(path).copied().unwrap_or(DEFAULT_RATING)
289 }
290
291 pub fn current_rating(&self) -> Option<u8> {
293 self.current_preset().map(|p| self.rating(p))
294 }
295
296 pub fn set_rating(&mut self, path: &Path, rating: u8) {
298 let clamped = rating.min(5);
299 self.ratings.insert(path.to_path_buf(), clamped);
300 self.persist_ratings();
301 }
302
303 pub fn nudge_current_rating(&mut self, delta: i32) -> Option<u8> {
306 let path = self.current_preset()?.to_path_buf();
307 let cur = self.rating(&path) as i32;
308 let next = (cur + delta).clamp(0, 5) as u8;
309 self.ratings.insert(path, next);
310 self.persist_ratings();
311 Some(next)
312 }
313
314 fn persist_ratings(&self) {
315 let Some(path) = self.ratings_path.as_ref() else {
316 return;
317 };
318 save_ratings(path, &self.ratings);
319 }
320
321 pub fn start_transition(&mut self, duration: f32) {
325 self.transition = TransitionState::Transitioning {
326 progress: 0.0,
327 duration,
328 elapsed: 0.0,
329 };
330 }
331
332 pub fn update_transition(&mut self, delta_time: f32) -> bool {
334 match &mut self.transition {
335 TransitionState::Transitioning {
336 progress,
337 duration,
338 elapsed,
339 } => {
340 *elapsed += delta_time;
341 *progress = (*elapsed / *duration).min(1.0);
342
343 if *progress >= 1.0 {
344 self.transition = TransitionState::None;
345 true } else {
347 false
348 }
349 }
350 TransitionState::None => true,
351 }
352 }
353
354 pub fn transition_progress(&self) -> f32 {
356 match &self.transition {
357 TransitionState::Transitioning { progress, .. } => *progress,
358 TransitionState::None => 1.0,
359 }
360 }
361
362 pub fn is_transitioning(&self) -> bool {
364 matches!(self.transition, TransitionState::Transitioning { .. })
365 }
366
367 pub fn clear(&mut self) {
369 self.preset_queue.clear();
370 self.current_index = 0;
371 self.transition = TransitionState::None;
372 self.history.clear();
373 }
374
375 pub fn preset_count(&self) -> usize {
377 self.preset_queue.len()
378 }
379
380 pub fn shuffle(&mut self) {
382 if self.preset_queue.len() <= 1 {
383 return;
384 }
385 let seed = quick_random_seed();
386 let mut indices: Vec<usize> = (0..self.preset_queue.len()).collect();
387 for i in (1..indices.len()).rev() {
389 let j = (seed.wrapping_add(i)) % (i + 1);
390 indices.swap(i, j);
391 }
392 let mut new_queue = VecDeque::with_capacity(self.preset_queue.len());
393 for idx in indices {
394 if let Some(preset) = self.preset_queue.get(idx) {
395 new_queue.push_back(preset.clone());
396 }
397 }
398 self.preset_queue = new_queue;
399 self.current_index = 0;
400 }
401}
402
403impl Default for PresetManager {
404 fn default() -> Self {
405 Self::new()
406 }
407}
408
409fn quick_random_seed() -> usize {
413 use std::time::{SystemTime, UNIX_EPOCH};
414 SystemTime::now()
415 .duration_since(UNIX_EPOCH)
416 .map(|d| d.as_nanos() as usize)
417 .unwrap_or(0)
418}
419
420#[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
424struct RatingsFile {
425 #[serde(default)]
426 ratings: HashMap<String, u8>,
427}
428
429fn load_ratings(path: &Path) -> HashMap<PathBuf, u8> {
430 let text = match std::fs::read_to_string(path) {
431 Ok(s) => s,
432 Err(_) => return HashMap::new(),
433 };
434 let parsed: RatingsFile = match toml::from_str(&text) {
435 Ok(r) => r,
436 Err(e) => {
437 log::warn!(
438 "Failed to parse ratings file {}: {} — starting fresh",
439 path.display(),
440 e
441 );
442 return HashMap::new();
443 }
444 };
445 parsed
446 .ratings
447 .into_iter()
448 .map(|(k, v)| (PathBuf::from(k), v.min(5)))
449 .collect()
450}
451
452fn save_ratings(path: &Path, ratings: &HashMap<PathBuf, u8>) {
453 if let Some(parent) = path.parent()
454 && let Err(e) = std::fs::create_dir_all(parent)
455 {
456 log::warn!(
457 "Failed to create {} for ratings save: {}",
458 parent.display(),
459 e
460 );
461 return;
462 }
463 let file = RatingsFile {
464 ratings: ratings
465 .iter()
466 .filter_map(|(k, v)| k.to_str().map(|s| (s.to_string(), *v)))
467 .collect(),
468 };
469 match toml::to_string_pretty(&file) {
470 Ok(s) => {
471 if let Err(e) = std::fs::write(path, s) {
472 log::warn!("Failed to write ratings to {}: {}", path.display(), e);
473 }
474 }
475 Err(e) => log::warn!("Failed to serialise ratings: {}", e),
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn test_preset_manager_creation() {
485 let manager = PresetManager::new();
486 assert_eq!(manager.preset_count(), 0);
487 }
488
489 #[test]
490 fn test_add_presets() {
491 let mut manager = PresetManager::new();
492
493 manager.add_preset("preset1.milk");
494 manager.add_preset("preset2.milk");
495 manager.add_preset("preset3.milk");
496
497 assert_eq!(manager.preset_count(), 3);
498 }
499
500 #[test]
501 fn test_navigation() {
502 let mut manager = PresetManager::new();
503
504 manager.add_preset("preset1.milk");
505 manager.add_preset("preset2.milk");
506 manager.add_preset("preset3.milk");
507
508 assert_eq!(
510 manager.current_preset().unwrap().to_str().unwrap(),
511 "preset1.milk"
512 );
513
514 manager.next_preset();
516 assert_eq!(
517 manager.current_preset().unwrap().to_str().unwrap(),
518 "preset2.milk"
519 );
520
521 manager.next_preset();
523 assert_eq!(
524 manager.current_preset().unwrap().to_str().unwrap(),
525 "preset3.milk"
526 );
527
528 manager.next_preset();
530 assert_eq!(
531 manager.current_preset().unwrap().to_str().unwrap(),
532 "preset1.milk"
533 );
534
535 manager.prev_preset();
537 assert_eq!(
538 manager.current_preset().unwrap().to_str().unwrap(),
539 "preset3.milk"
540 );
541 }
542
543 #[test]
544 fn test_transition() {
545 let mut manager = PresetManager::new();
546
547 manager.start_transition(1.0);
548 assert!(manager.is_transitioning());
549 assert_eq!(manager.transition_progress(), 0.0);
550
551 manager.update_transition(0.5);
553 assert!(manager.is_transitioning());
554 assert!((manager.transition_progress() - 0.5).abs() < 0.01);
555
556 manager.update_transition(0.5);
558 assert!(!manager.is_transitioning());
559 assert_eq!(manager.transition_progress(), 1.0);
560 }
561
562 #[test]
563 fn test_shuffle() {
564 let mut manager = PresetManager::new();
565
566 for i in 0..10 {
567 manager.add_preset(format!("preset{}.milk", i));
568 }
569
570 let _original_first = manager.current_preset().unwrap().to_path_buf();
571
572 manager.shuffle();
573
574 assert_eq!(manager.preset_count(), 10);
576
577 assert!(manager.current_preset().is_some());
580 }
581
582 #[test]
583 fn lock_blocks_advance_and_retreat() {
584 let mut m = PresetManager::new();
585 m.add_preset("a.milk");
586 m.add_preset("b.milk");
587 m.set_locked(true);
588 assert!(m.advance().is_none(), "advance must respect lock");
589 assert!(m.retreat().is_none(), "retreat must respect lock");
590 m.toggle_lock();
591 assert!(!m.is_locked());
592 assert!(m.advance().is_some(), "advance must work once unlocked");
593 }
594
595 #[test]
596 fn cycle_mode_alternates_random_and_sequential() {
597 let mut m = PresetManager::new();
598 assert_eq!(m.mode(), SelectionMode::Random);
599 assert_eq!(m.cycle_mode(), SelectionMode::Sequential);
600 assert_eq!(m.cycle_mode(), SelectionMode::Random);
601 }
602
603 #[test]
604 fn ratings_clamp_to_zero_through_five() {
605 let mut m = PresetManager::new();
606 m.add_preset("a.milk");
607 let path = m.current_preset().unwrap().to_path_buf();
608 m.set_rating(&path, 99);
609 assert_eq!(m.rating(&path), 5);
610 m.set_rating(&path, 0);
611 assert_eq!(m.rating(&path), 0);
612 }
613
614 #[test]
615 fn nudge_current_rating_walks_within_range() {
616 let mut m = PresetManager::new();
617 m.add_preset("a.milk");
618 assert_eq!(m.nudge_current_rating(1), Some(DEFAULT_RATING + 1));
619 assert_eq!(m.nudge_current_rating(-10), Some(0));
620 assert_eq!(m.nudge_current_rating(99), Some(5));
621 }
622
623 #[test]
624 fn sequential_advance_walks_history_forward() {
625 let mut m = PresetManager::new();
626 m.add_preset("a.milk");
627 m.add_preset("b.milk");
628 m.add_preset("c.milk");
629 m.set_mode(SelectionMode::Sequential);
630 let first = m.advance().unwrap();
631 assert_eq!(first.to_str().unwrap(), "b.milk");
632 let second = m.advance().unwrap();
633 assert_eq!(second.to_str().unwrap(), "c.milk");
634 let back = m.retreat().unwrap();
636 assert_eq!(back.to_str().unwrap(), "b.milk");
637 }
638
639 #[test]
640 fn weighted_random_falls_back_to_uniform_with_empty_ratings() {
641 let mut m = PresetManager::new();
642 m.add_preset("a.milk");
643 m.add_preset("b.milk");
644 assert!(m.random_weighted_by_rating().is_some());
647 }
648
649 #[test]
650 fn ratings_roundtrip_through_file() {
651 let tmp =
652 std::env::temp_dir().join(format!("onedrop-test-ratings-{}.toml", quick_random_seed()));
653 {
655 let mut m = PresetManager::new();
656 m.attach_ratings_file(tmp.clone());
657 m.add_preset("/abs/path/a.milk");
658 m.set_rating(Path::new("/abs/path/a.milk"), 4);
659 }
660 let mut m2 = PresetManager::new();
662 m2.attach_ratings_file(tmp.clone());
663 assert_eq!(m2.rating(Path::new("/abs/path/a.milk")), 4);
664 let _ = std::fs::remove_file(tmp);
665 }
666}