lr2-oxytabler 0.8.2

Table manager for Lunatic Rave 2
use crate::TableEntry;
use anyhow::Result;

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TableUpdateChangelog {
    full: String,
    summary: String,
}

impl TableUpdateChangelog {
    pub fn try_new(
        db: &rusqlite::Connection,
        old: &[TableEntry],
        new: &[TableEntry],
        folder_order: &[String],
        symbol: &str,
    ) -> Result<Self> {
        let raw = RawTableUpdateChangelog::new(old, new, folder_order);
        Ok(Self {
            full: raw.full(db, symbol)?,
            summary: raw.summary(),
        })
    }

    pub fn full(&'_ self) -> &'_ str {
        &self.full
    }

    pub fn summary(&'_ self) -> &'_ str {
        &self.summary
    }
}

#[derive(Clone, Debug, Default, PartialEq)]
struct RawTableUpdateChangelog {
    changed: Vec<(String, String, String)>,
    deleted: Vec<(String, String)>,
    new: Vec<(String, String)>,
}

impl RawTableUpdateChangelog {
    fn new(old: &[TableEntry], new: &[TableEntry], folder_order: &[String]) -> Self {
        use alphanumeric_sort::compare_str as acmp;

        let mut changelog = Self {
            changed: vec![],
            deleted: vec![],
            new: vec![],
        };

        for e in old {
            #[derive(PartialEq)]
            enum Found {
                Not,
                DifferentLv(String),
                SameLv,
            }
            let mut found = Found::Not;
            for ee in new.iter().filter(|ee| e.md5 == ee.md5) {
                if e.level == ee.level {
                    found = Found::SameLv;
                    break;
                }
                found = Found::DifferentLv(ee.level.clone());
            }
            match found {
                Found::Not => changelog.deleted.push((e.md5.clone(), e.level.clone())),
                Found::DifferentLv(to) => {
                    changelog.changed.push((e.md5.clone(), e.level.clone(), to));
                }
                Found::SameLv => {}
            }
        }

        for e in new {
            if !old.iter().any(|ee| e.md5 == ee.md5) {
                changelog.new.push((e.md5.clone(), e.level.clone()));
            }
        }

        let key_by_order = |e: &str| {
            folder_order
                .iter()
                .position(|o| *o == *e)
                .unwrap_or(usize::MAX)
        };
        let ocmp = |a: &String, b: &String| -> std::cmp::Ordering {
            key_by_order(a).cmp(&key_by_order(b))
        };

        changelog.new.sort_unstable_by(|(lh, ll), (rh, rl)| {
            ocmp(ll, rl)
                .then_with(|| acmp(ll, rl))
                .then_with(|| lh.cmp(rh))
        });
        changelog.deleted.sort_unstable_by(|(lh, ll), (rh, rl)| {
            ocmp(ll, rl)
                .then_with(|| acmp(ll, rl))
                .then_with(|| lh.cmp(rh))
        });
        changelog
            .changed
            .sort_unstable_by(|(lh, _, ll), (rh, _, rl)| {
                ocmp(ll, rl)
                    .then_with(|| acmp(ll, rl))
                    .then_with(|| lh.cmp(rh))
            });

        changelog
    }

    fn full(&self, db: &rusqlite::Connection, symbol: &str) -> Result<String> {
        use crate::db::load_song;

        let mut out = String::new();
        if !self.new.is_empty() {
            for (md5, level) in &self.new {
                out += "+";
                out += symbol;
                out += level;
                out += " ";
                out += load_song(db, md5)?.as_deref().unwrap_or(md5);
                out += "\n";
            }
        }
        if !self.changed.is_empty() {
            if !out.is_empty() {
                out += "\n";
            }
            for (md5, from, to) in &self.changed {
                out += "~";
                out += symbol;
                out += to;
                out += " <- ";
                out += symbol;
                out += from;
                out += " ";
                out += load_song(db, md5)?.as_deref().unwrap_or(md5);
                out += "\n";
            }
        }
        if !self.deleted.is_empty() {
            if !out.is_empty() {
                out += "\n";
            }
            for (md5, level) in &self.deleted {
                out += "-";
                out += symbol;
                out += level;
                out += " ";
                out += load_song(db, md5)?.as_deref().unwrap_or(md5);
                out += "\n";
            }
        }
        out.pop();
        Ok(out)
    }

    fn summary(&self) -> String {
        use std::fmt::Write as _;
        let mut out = String::new();
        if !self.new.is_empty() {
            write!(out, "+{} ", self.new.len()).unwrap();
        }
        if !self.changed.is_empty() {
            write!(out, "~{} ", self.changed.len()).unwrap();
        }
        if !self.deleted.is_empty() {
            write!(out, "-{} ", self.deleted.len()).unwrap();
        }
        out.pop();
        out
    }
}

#[cfg(test)]
mod tests {
    use test_log::test;

    #[test]
    fn calculate_diff() {
        use super::RawTableUpdateChangelog;
        use crate::Table;
        assert_eq!(
            RawTableUpdateChangelog::new(
                &Table::empty()
                    .with_entry_leveled_plus(1, 1)
                    .with_entry_leveled_plus(1, 1)
                    .with_entry_leveled_plus(69, 1)
                    .with_entry_leveled_plus(69, 2)
                    .with_entry_leveled_plus(420, 1)
                    .with_entry_leveled_plus(1234, 1)
                    .0
                    .entries,
                &Table::empty()
                    .with_entry_leveled_plus(0, 10)
                    .with_entry_leveled_plus(1, 1)
                    .with_entry_leveled_plus(2, 1)
                    .with_entry_leveled_plus(2, 1)
                    .with_entry_leveled_plus(69, 1)
                    .with_entry_leveled_plus(69, 2)
                    .with_entry_leveled_plus(420, 10)
                    .0
                    .entries,
                &[]
            ),
            RawTableUpdateChangelog {
                changed: vec![(
                    "foodfoodfoodfoodfoodfoodfood 420".to_string(),
                    "1".to_string(),
                    "10".to_string()
                )],
                deleted: vec![
                    // ( // not in here and that is fine
                    //     "foodfoodfoodfoodfoodfoodfood   1".to_string(),
                    //     "1".to_string()
                    // ),
                    (
                        "foodfoodfoodfoodfoodfoodfood1234".to_string(),
                        "1".to_string()
                    ),
                ],
                new: vec![
                    (
                        "foodfoodfoodfoodfoodfoodfood   2".to_string(),
                        "1".to_string()
                    ),
                    (
                        "foodfoodfoodfoodfoodfoodfood   2".to_string(),
                        "1".to_string()
                    ),
                    (
                        "foodfoodfoodfoodfoodfoodfood   0".to_string(),
                        "10".to_string()
                    ),
                ]
            }
        );
    }
}