oliver 0.1.2

Lightweight CLI mod manager for Baldur's Gate 3 on Linux
use crate::common::{read_current_installed_mods, settings_file_path, write_file};
use anyhow::Result;
use larian_formats::lspk::{self, DecompressedLspk};
use quick_xml::se::Serializer;
use serde::Serialize;
use std::{
    fs::File,
    path::{Path, PathBuf},
};

pub fn install_all(
    verbose: bool,
    dry_run: bool,
    refresh: bool,
    mod_file_paths: Vec<String>,
    app_data_path: &str,
) -> Result<()> {
    mod_file_paths.into_iter().try_for_each(|mod_file_path| {
        println!("Installing {mod_file_path}");

        install(verbose, dry_run, refresh, mod_file_path, app_data_path)
    })
}

fn install(
    verbose: bool,
    dry_run: bool,
    refresh: bool,
    mut mod_file_path: String,
    app_data_path: &str,
) -> Result<()> {
    if mod_file_path.starts_with('~') {
        let home_dir = std::env::var("HOME")?;
        mod_file_path = mod_file_path.replacen('~', &home_dir, 1);
    }

    let mod_file = File::open(&mod_file_path)?;

    let (name, lspk_files) = read_mod(&mod_file_path, mod_file)?;

    if let Some(name) = name {
        mod_file_path = name;
    }

    let lspk_meta_lsx = lspk_files.extract_meta_lsx()?;
    let parsed_mod_metadata = lspk_meta_lsx.extract_meta_lsx_data()?;

    let mod_info = parsed_mod_metadata.find_node_by_id("ModuleInfo")?;

    let mod_name = mod_info.find_attribute_value_where_id(|id| id == "Name")?;
    let mod_uuid = mod_info.find_attribute_value_where_id(|id| id == "UUID")?;
    let mod_folder = mod_info.find_attribute_value_where_id(|id| id == "Folder")?;

    let lspk_mod_version: u64 = mod_info
        .find_attribute_value_where_id(|id| id == "Version" || id == "Version64")?
        .parse()?;

    let mut parsed_settings = read_current_installed_mods(app_data_path)?;

    let mods = parsed_settings.find_node_mut_by_id("Mods")?;
    let base_id = mods
        .get_uuid_where_name("GustavDev")
        .map(ToString::to_string);

    let existing_version_node =
        mods.get_or_insert_child_mut_where_id(mod_uuid, mod_name, mod_folder);

    match existing_version_node.get_version() {
        Some(existing) if existing > lspk_mod_version => {
            println!("    {mod_name} version {existing} is already installed");
            println!(
                "    The mod file you're trying to install is version {lspk_mod_version}, which \
                 is older."
            );
            println!("    \nNo installation needed!\n");

            return Ok(());
        }
        Some(existing) if existing == lspk_mod_version && !refresh => {
            println!("    {mod_name} version {existing} is already installed");
            println!("    The mod file you're trying to install is the same version");
            println!("\n    No installation needed!\n");

            return Ok(());
        }
        Some(existing) => {
            if verbose {
                println!("    updating {mod_name} from {existing} to {lspk_mod_version}");
            }
        }
        None => {
            if verbose {
                println!("    installing {mod_name} version {lspk_mod_version}");
            }
        }
    }

    let lspk_installed_file_name = mod_file_path;

    let lspk_installation_path: PathBuf = [
        &app_data_path,
        "Local",
        "Larian Studios",
        "Baldur's Gate 3",
        "Mods",
        &lspk_installed_file_name,
    ]
    .into_iter()
    .collect();

    if !dry_run {
        write_file(&lspk_installation_path, lspk_files.original_bytes, false)?;
    }

    existing_version_node.set_attribute_where_id(lspk_mod_version.to_string(), "int64", |id| {
        id == "Version" || id == "Version64"
    });

    let mod_order = parsed_settings.find_node_mut_by_id("ModOrder")?;

    if let Some(base_id) = base_id {
        if dry_run && verbose {
            println!(
                "    non-dry run would ensure that GustavDev ({base_id} is first is mod order"
            );
        } else {
            if verbose {
                println!("    ensuring that GustavDev ({base_id} is first is mod order");
            }

            mod_order.prepend_base(base_id);
        }
    }

    mod_order.ensure_node_exists(mod_uuid);

    let mut xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n".to_string();
    let mut ser = Serializer::new(&mut xml);
    ser.indent(' ', 4);
    parsed_settings.serialize(ser)?;
    xml.push('\n');

    if dry_run {
        print!("    non-dry run would have ");
    } else {
        write_file(settings_file_path(app_data_path), xml.replace("/>\n", " />\n"), true)?;
    }

    println!("    installed {}\n", lspk_installation_path.display());

    Ok(())
}

fn read_mod(mod_file_path: &str, mod_file: File) -> Result<(Option<String>, DecompressedLspk)> {
    let is_zipfile = Path::new(mod_file_path)
        .extension()
        .map_or(false, |e| e.eq_ignore_ascii_case("zip"));

    if is_zipfile {
        let (name, file) = lspk::entry_from_zipfile(&mod_file)?;
        Ok((Some(name), file))
    } else {
        let file = lspk::Reader::new(mod_file)?.read()?;
        Ok((None, file))
    }
}