late-java-core 2.2.9

A Rust library for launching Minecraft Java Edition
use crate::error::{Result, LateJavaCoreError};
use crate::launcher::LaunchOptions;
use crate::auth::AuthResponse;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Argumentos de lanzamiento de Minecraft
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LaunchArguments {
    pub game: Vec<String>,
    pub jvm: Vec<String>,
    pub classpath: Vec<String>,
    pub main_class: Option<String>,
}

/// JSON de versión de Minecraft
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionJson {
    pub id: String,
    pub r#type: String,
    pub asset_index: AssetIndex,
    pub assets: Option<String>,
    pub main_class: Option<String>,
    pub minecraft_arguments: Option<String>,
    pub arguments: Option<Arguments>,
    pub libraries: Option<Vec<Library>>,
    pub natives_list: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssetIndex {
    pub id: String,
    pub url: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Arguments {
    pub game: Option<Vec<serde_json::Value>>,
    pub jvm: Option<Vec<serde_json::Value>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Library {
    pub name: String,
    pub loader: Option<String>,
    pub natives: Option<HashMap<String, String>>,
    pub rules: Option<Vec<Rule>>,
    pub downloads: Option<LibraryDownloads>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Rule {
    pub action: String,
    pub os: Option<OsRule>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OsRule {
    pub name: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LibraryDownloads {
    pub artifact: Option<Artifact>,
    pub classifiers: Option<HashMap<String, Artifact>>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Artifact {
    pub sha1: Option<String>,
    pub size: Option<u64>,
    pub path: Option<String>,
    pub url: Option<String>,
}

/// JSON de loader (Forge, Fabric, etc.)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoaderJson {
    pub id: Option<String>,
    pub main_class: Option<String>,
    pub libraries: Option<Vec<Library>>,
    pub minecraft_arguments: Option<String>,
    pub is_old_forge: Option<bool>,
    pub jar_path: Option<String>,
    pub arguments: Option<Arguments>,
}

/// Constructor de argumentos de Minecraft
pub struct MinecraftArguments {
    options: LaunchOptions,
    authenticator: AuthResponse,
}

impl MinecraftArguments {
    pub fn new(options: LaunchOptions, authenticator: AuthResponse) -> Self {
        Self {
            options,
            authenticator,
        }
    }

    /// Obtener todos los argumentos de lanzamiento
    pub async fn get_arguments(&self, version_json: &VersionJson, loader_json: Option<&LoaderJson>) -> Result<LaunchArguments> {
        let game_arguments = self.get_game_arguments(version_json, loader_json).await?;
        let jvm_arguments = self.get_jvm_arguments(version_json).await?;
        let classpath_data = self.get_classpath(version_json, loader_json).await?;

        Ok(LaunchArguments {
            game: game_arguments,
            jvm: jvm_arguments,
            classpath: classpath_data.classpath,
            main_class: classpath_data.main_class,
        })
    }

    /// Construir argumentos del juego
    pub async fn get_game_arguments(&self, version_json: &VersionJson, loader_json: Option<&LoaderJson>) -> Result<Vec<String>> {
        let mut game_args = if let Some(minecraft_args) = &version_json.minecraft_arguments {
            minecraft_args.split(' ').map(|s| s.to_string()).collect()
        } else if let Some(args) = &version_json.arguments {
            args.game.as_ref()
                .map(|g| g.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect())
                .unwrap_or_default()
        } else {
            Vec::new()
        };

        // Fusionar argumentos del loader si se proporciona
        if let Some(loader) = loader_json {
            if let Some(loader_args) = &loader.minecraft_arguments {
                let loader_game_args: Vec<String> = loader_args.split(' ')
                    .map(|s| s.to_string())
                    .collect();
                game_args.extend(loader_game_args);
                
                // Remover argumentos duplicados
                game_args.sort();
                game_args.dedup();
            }
        }

        // Determinar tipo de usuario
        let user_type = if version_json.id.starts_with("1.16") {
            "Xbox"
        } else {
            match self.authenticator.meta.auth_type.as_str() {
                "Xbox" => "msa",
                _ => &self.authenticator.meta.auth_type,
            }
        };

        // Mapa de placeholders a valores reales
        let placeholder_map: HashMap<String, String> = [
            ("${auth_access_token}", self.authenticator.access_token.clone()),
            ("${auth_session}", self.authenticator.access_token.clone()),
            ("${auth_player_name}", self.authenticator.name.clone()),
            ("${auth_uuid}", self.authenticator.uuid.clone()),
            ("${auth_xuid}", self.authenticator.xbox_account.as_ref()
                .map(|x| x.xuid.clone())
                .unwrap_or_else(|| self.authenticator.access_token.clone())),
            ("${user_properties}", self.authenticator.user_properties.clone()),
            ("${user_type}", user_type.to_string()),
            ("${version_name}", loader_json
                .and_then(|l| l.id.as_ref())
                .unwrap_or(&version_json.id)
                .clone()),
            ("${assets_index_name}", version_json.asset_index.id.clone()),
            ("${game_directory}", if let Some(instance) = &self.options.instance {
                format!("{}/instances/{}", self.options.path, instance)
            } else {
                self.options.path.clone()
            }),
            ("${assets_root}", if self.is_old(version_json) {
                format!("{}/resources", self.options.path)
            } else {
                format!("{}/assets", self.options.path)
            }),
            ("${game_assets}", if self.is_old(version_json) {
                format!("{}/resources", self.options.path)
            } else {
                format!("{}/assets", self.options.path)
            }),
            ("${version_type}", version_json.r#type.clone()),
            ("${clientid}", self.authenticator.client_token.clone()),
        ].iter().cloned().collect();

        // Reemplazar placeholders en los argumentos del juego
        for arg in &mut game_args {
            if let Some(replacement) = placeholder_map.get(arg) {
                *arg = replacement.clone();
            }
        }

        // Agregar opciones de pantalla si se proporcionan
        if let Some(width) = self.options.screen.width {
            if let Some(height) = self.options.screen.height {
                game_args.push("--width".to_string());
                game_args.push(width.to_string());
                game_args.push("--height".to_string());
                game_args.push(height.to_string());
            }
        }

        // Agregar argumentos del juego personalizados
        game_args.extend(self.options.game_args.clone());

        // Filtrar argumentos no válidos
        game_args.retain(|arg| !arg.is_empty());

        Ok(game_args)
    }

    /// Construir argumentos JVM
    pub async fn get_jvm_arguments(&self, version_json: &VersionJson) -> Result<Vec<String>> {
        let mut jvm_args = vec![
            format!("-Xms{}", self.options.memory.min),
            format!("-Xmx{}", self.options.memory.max),
            "-XX:+UnlockExperimentalVMOptions".to_string(),
            "-XX:G1NewSizePercent=20".to_string(),
            "-XX:G1ReservePercent=20".to_string(),
            "-XX:MaxGCPauseMillis=50".to_string(),
            "-XX:G1HeapRegionSize=32M".to_string(),
            "-Dfml.ignoreInvalidMinecraftCertificates=true".to_string(),
            format!("-Djna.tmpdir={}/versions/{}/natives", self.options.path, version_json.id),
            format!("-Dorg.lwjgl.system.SharedLibraryExtractPath={}/versions/{}/natives", self.options.path, version_json.id),
            format!("-Dio.netty.native.workdir={}/versions/{}/natives", self.options.path, version_json.id),
        ];

        // Argumentos específicos del OS
        let os_specific_opts: HashMap<&str, &str> = [
            ("windows", "-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump"),
            ("macos", "-XstartOnFirstThread"),
            ("linux", "-Xss1M"),
        ].iter().cloned().collect();

        // Para versiones más nuevas que usan "arguments.game"
        if version_json.minecraft_arguments.is_none() {
            let current_os = std::env::consts::OS;
            if let Some(opt) = os_specific_opts.get(current_os) {
                jvm_args.push(opt.to_string());
            }
        }

        // Omitir modo offline para multijugador
        if self.options.bypass_offline.unwrap_or(false) {
            jvm_args.push("-Dminecraft.api.auth.host=https://nope.invalid/".to_string());
            jvm_args.push("-Dminecraft.api.account.host=https://nope.invalid/".to_string());
            jvm_args.push("-Dminecraft.api.session.host=https://nope.invalid/".to_string());
            jvm_args.push("-Dminecraft.api.services.host=https://nope.invalid/".to_string());
        }

        // Si se especifican nativos, agregar la ruta de la librería nativa
        if version_json.natives_list.unwrap_or(false) {
            jvm_args.push(format!("-Djava.library.path={}/versions/{}/natives", self.options.path, version_json.id));
        }

        // Manejo especial para macOS (configurar icono del dock)
        if std::env::consts::OS == "macos" {
            if let Some(assets) = &version_json.assets {
                let assets_path = format!("{}/assets/indexes/{}.json", self.options.path, assets);
                if std::path::Path::new(&assets_path).exists() {
                    if let Ok(assets_content) = std::fs::read_to_string(&assets_path) {
                        if let Ok(assets_json) = serde_json::from_str::<serde_json::Value>(&assets_content) {
                            if let Some(icon_hash) = assets_json["objects"]["icons/minecraft.icns"]["hash"].as_str() {
                                jvm_args.push("-Xdock:name=Minecraft".to_string());
                                jvm_args.push(format!("-Xdock:icon={}/assets/objects/{}/{}", 
                                    self.options.path, 
                                    &icon_hash[0..2], 
                                    icon_hash));
                            }
                        }
                    }
                }
            }
        }

        // Agregar argumentos JVM personalizados
        jvm_args.extend(self.options.jvm_args.clone());

        Ok(jvm_args)
    }

    /// Construir classpath
    pub async fn get_classpath(&self, version_json: &VersionJson, loader_json: Option<&LoaderJson>) -> Result<ClasspathData> {
        let mut combined_libraries = version_json.libraries.clone().unwrap_or_default();

        // Si se proporciona un JSON de loader, fusionar sus librerías
        if let Some(loader) = loader_json {
            if let Some(loader_libs) = &loader.libraries {
                combined_libraries.extend(loader_libs.clone());
            }
        }

        let mut libraries_list = Vec::new();

        for lib in &combined_libraries {
            // Verificar reglas de la librería
            if self.should_skip_library(lib) {
                continue;
            }

            // Construir la ruta para esta librería
            let lib_path = self.get_library_path(lib);
            if let Some(loader_path) = &lib.loader {
                libraries_list.push(format!("{}/libraries/{}", loader_path, lib_path));
            } else {
                libraries_list.push(format!("{}/libraries/{}", self.options.path, lib_path));
            }
        }

        // Agregar el JAR principal de Minecraft
        if let Some(loader) = loader_json {
            if loader.is_old_forge.unwrap_or(false) {
                if let Some(jar_path) = &loader.jar_path {
                    libraries_list.push(jar_path.clone());
                }
            }
        } else if let Some(mcp) = &self.options.mcp {
            libraries_list.push(mcp.clone());
        } else {
            libraries_list.push(format!("{}/versions/{}/{}.jar", self.options.path, version_json.id, version_json.id));
        }

        // Filtrar duplicados
        libraries_list.sort();
        libraries_list.dedup();

        // El argumento classpath final depende del OS
        let cp_separator = if std::env::consts::OS == "windows" { ";" } else { ":" };
        let cp_argument = if !libraries_list.is_empty() {
            libraries_list.join(cp_separator)
        } else {
            String::new()
        };

        Ok(ClasspathData {
            classpath: vec!["-cp".to_string(), cp_argument],
            main_class: loader_json
                .and_then(|l| l.main_class.clone())
                .or_else(|| version_json.main_class.clone()),
        })
    }

    fn is_old(&self, version_json: &VersionJson) -> bool {
        version_json.assets.as_ref()
            .map(|assets| assets == "legacy" || assets == "pre-1.6")
            .unwrap_or(false)
    }

    fn should_skip_library(&self, lib: &Library) -> bool {
        if let Some(rules) = &lib.rules {
            let current_os = std::env::consts::OS;
            let mojang_os = match current_os {
                "windows" => "windows",
                "macos" => "osx",
                "linux" => "linux",
                _ => current_os,
            };

            for rule in rules {
                if let Some(os_rule) = &rule.os {
                    if let Some(os_name) = &os_rule.name {
                        match rule.action.as_str() {
                            "allow" => {
                                if os_name != mojang_os {
                                    return true;
                                }
                            }
                            "disallow" => {
                                if os_name == mojang_os {
                                    return true;
                                }
                            }
                            _ => {}
                        }
                    }
                }
            }
        }
        false
    }

    fn get_library_path(&self, lib: &Library) -> String {
        let parts: Vec<&str> = lib.name.split(':').collect();
        if parts.len() < 3 {
            return lib.name.clone();
        }

        let group = parts[0].replace('.', "/");
        let artifact = parts[1];
        let version = parts[2];

        format!("{}/{}/{}/{}-{}.jar", group, artifact, version, artifact, version)
    }
}

#[derive(Debug, Clone)]
pub struct ClasspathData {
    pub classpath: Vec<String>,
    pub main_class: Option<String>,
}