1mod old_default_nes_palette;
2mod v0_10;
3mod v0_11;
4mod v0_12;
5mod v0_13;
6mod v0_14;
7mod v0_8;
8
9use crate::AppConfig;
10use std::cmp::Ordering;
11use std::fmt::{Display, Formatter};
12use std::str::FromStr;
13use toml_edit::DocumentMut;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16struct SemVer {
17    major: u32,
18    minor: u32,
19    patch: u32,
20}
21
22impl SemVer {
23    const fn new(major: u32, minor: u32, patch: u32) -> Self {
24        Self { major, minor, patch }
25    }
26}
27
28impl PartialOrd for SemVer {
29    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
30        Some(self.cmp(other))
31    }
32}
33
34impl Ord for SemVer {
35    fn cmp(&self, other: &Self) -> Ordering {
36        self.major
37            .cmp(&other.major)
38            .then(self.minor.cmp(&other.minor))
39            .then(self.patch.cmp(&other.patch))
40    }
41}
42
43impl Display for SemVer {
44    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
45        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
46    }
47}
48
49impl FromStr for SemVer {
50    type Err = String;
51
52    fn from_str(s: &str) -> Result<Self, Self::Err> {
53        let err_fn = || format!("Invalid semver string: {s}");
54
55        let Ok(split) = s.split('.').map(str::parse::<u32>).collect::<Result<Vec<_>, _>>() else {
56            return Err(err_fn());
57        };
58
59        match split.as_slice() {
60            &[major, minor, patch] => Ok(Self { major, minor, patch }),
61            _ => Err(err_fn()),
62        }
63    }
64}
65
66pub fn migrate_config_str(config_str: &mut String) {
67    let Ok(mut document) = config_str.parse::<DocumentMut>() else { return };
68
69    let mut changed = false;
70
71    changed |= v0_12::migrate_document(&mut document);
72    changed |= v0_13::migrate_document(&mut document);
73
74    if changed {
75        *config_str = document.to_string();
76    }
77}
78
79pub(crate) const fn current_config_version() -> &'static str {
80    env!("CARGO_PKG_VERSION")
81}
82
83#[must_use]
84pub fn migrate_config(config: &AppConfig, config_str: &str) -> Option<AppConfig> {
85    const MIN_VERSION_NO_MIGRATION: SemVer = SemVer::new(0, 14, 0);
86
87    if config
88        .config_version
89        .as_ref()
90        .and_then(|version| SemVer::from_str(version).ok())
91        .is_some_and(|version| version >= MIN_VERSION_NO_MIGRATION)
92    {
93        return None;
94    }
95
96    let old_version = config
97        .config_version
98        .as_ref()
99        .and_then(|s| s.parse::<SemVer>().ok())
100        .unwrap_or(SemVer::new(0, 0, 0));
101
102    log::info!("Migrating config from version {old_version} to {}", current_config_version());
103
104    let mut new_config = config.clone();
105    if old_version < SemVer::new(0, 8, 3) {
106        v0_8::migrate_config_0_8_3(&mut new_config, config_str);
107    }
108
109    if old_version < SemVer::new(0, 8, 4) {
110        v0_8::migrate_config_0_8_4(&mut new_config);
111    }
112
113    if old_version < SemVer::new(0, 10, 2) {
114        v0_10::migrate_config_0_10_2(&mut new_config, config_str);
115    }
116
117    if old_version < SemVer::new(0, 11, 4) {
118        v0_11::migrate_config_0_11_4(&mut new_config, config_str);
119    }
120
121    if old_version < SemVer::new(0, 14, 0) {
122        v0_14::migrate_config_0_14_0(&mut new_config, config_str);
123    }
124
125    new_config.config_version = Some(current_config_version().into());
126
127    Some(new_config)
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn migrate_empty_string_does_not_panic() {
136        migrate_config_str(&mut String::new());
137    }
138}