use std::{
cell::{RefCell, RefMut},
collections::{HashMap, HashSet},
io::{BufWriter, Write},
};
use crate::{
Cache, Error, Mod,
args::Cli,
cache::CacheId,
format::Formatter,
get_curseforge_mods, get_modrinth_mods,
parser::{ParsedCurseForgeId, ParsedModrinthId, Parser},
request::{CurseForgeId, ModrinthId},
};
fn warn_missing(source: &str, requested: &HashMap<String, CacheId>, returned: &HashSet<String>) {
for id in requested.keys() {
if !returned.contains(id) {
log::warn!(
"{source} returned no result for \"{id}\" -- it will be missing from the list"
);
}
}
}
pub struct App {
cache: RefCell<Cache>,
modrinth_mods: Vec<ParsedModrinthId>,
curseforge_mods: Vec<ParsedCurseForgeId>,
}
impl App {
pub fn new<P>(cache: Cache, parser: P) -> Self
where
P: Parser,
{
let (modrinth_mods, curseforge_mods) = parser.get_mods_owned();
Self {
cache: RefCell::new(cache),
modrinth_mods,
curseforge_mods,
}
}
fn get_mods(&self) -> Result<Vec<Mod>, Error> {
let mut cache = self.cache.borrow_mut();
let mut mods =
Vec::<Mod>::with_capacity(self.modrinth_mods.len() + self.curseforge_mods.len());
let mut mr_mods_ids = Vec::<ModrinthId>::with_capacity(self.modrinth_mods.len());
let mut cf_mods_ids = Vec::<CurseForgeId>::with_capacity(self.curseforge_mods.len());
let mut mr_id_map = HashMap::<String, CacheId>::with_capacity(mr_mods_ids.capacity());
let mut cf_id_map = HashMap::<String, CacheId>::with_capacity(cf_mods_ids.capacity());
for id in self.modrinth_mods.iter().cloned() {
match cache.get_mod(id.clone()).cloned() {
None => {
mr_id_map.insert(id.id.clone(), id.clone().into());
mr_mods_ids.push(id.into());
}
Some(m) => mods.push(m),
}
}
for id in self.curseforge_mods.iter().cloned() {
match cache.get_mod(id.clone()).cloned() {
None => {
cf_id_map.insert(id.id.to_string(), id.clone().into());
cf_mods_ids.push(id.into());
}
Some(m) => mods.push(m),
}
}
if !mr_mods_ids.is_empty() {
let fetched = get_modrinth_mods(mr_mods_ids)?;
let mut returned = HashSet::<String>::with_capacity(fetched.len());
for m in fetched {
returned.insert(m.id.clone());
match mr_id_map.get(&m.id).cloned() {
Some(id) => {
cache.set_mod(id, m.clone());
mods.push(m);
}
None => {
log::warn!(
"Modrinth returned unrequested project \"{}\"; ignoring",
m.id
)
}
}
}
warn_missing("Modrinth", &mr_id_map, &returned);
}
if !cf_mods_ids.is_empty() {
let fetched = get_curseforge_mods(cf_mods_ids)?;
let mut returned = HashSet::<String>::with_capacity(fetched.len());
for m in fetched.into_iter().map(Mod::from) {
returned.insert(m.id.clone());
match cf_id_map.get(&m.id).cloned() {
Some(id) => {
cache.set_mod(id, m.clone());
mods.push(m);
}
None => {
log::warn!(
"CurseForge returned unrequested project \"{}\"; ignoring",
m.id
)
}
}
}
warn_missing("CurseForge", &cf_id_map, &returned);
}
Ok(mods)
}
pub fn sorted_mods(&self) -> Result<Vec<Mod>, Error> {
let mut mods = self.get_mods()?;
mods.sort_by(|a, b| {
a.title
.to_lowercase()
.cmp(&b.title.to_lowercase())
.then_with(|| a.id.cmp(&b.id))
});
Ok(mods)
}
pub fn run(&self, cli: Cli) -> Result<(), Error> {
let formatter = Formatter::new(&cli.format)?;
let mods = self.sorted_mods()?;
let stdout = std::io::stdout();
let mut out = BufWriter::new(stdout.lock());
formatter.write_all(&mut out, &mods)?;
out.flush()?;
log::info!("listed {} mod(s)", mods.len());
Ok(())
}
pub fn close(&self) -> Result<(), Error> {
let mut cache = self.cache.borrow_mut();
let installed: HashSet<String> = self
.modrinth_mods
.iter()
.map(|m| m.id.clone())
.chain(self.curseforge_mods.iter().map(|m| m.id.to_string()))
.collect();
let pruned = cache.retain_only(&installed);
if pruned > 0 {
log::debug!("pruned {pruned} cached mod(s) no longer in the pack");
}
cache.save()
}
}