late-java-core 2.2.9

A Rust library for launching Minecraft Java Edition
use crate::error::{Result, LateJavaCoreError};
use crate::downloader::Downloader;
use crate::utils::get_file_from_archive;
use serde::{Deserialize, Serialize};
use std::path::Path;

/// Opciones del descargador de Java
#[derive(Debug, Clone)]
pub struct JavaDownloaderOptions {
    pub path: String,
    pub java: JavaOptions,
    pub intel_enabled_mac: Option<bool>,
}

#[derive(Debug, Clone)]
pub struct JavaOptions {
    pub version: Option<String>,
    pub java_type: String,
}

/// JSON de versión de Minecraft
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MinecraftVersionJson {
    pub java_version: Option<JavaVersion>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JavaVersion {
    pub component: Option<String>,
    pub major_version: Option<u32>,
}

/// Resultado de descarga de Java
#[derive(Debug, Clone)]
pub struct JavaDownloadResult {
    pub files: Vec<JavaFileItem>,
    pub path: String,
    pub error: Option<bool>,
    pub message: Option<String>,
}

/// Item de archivo de Java
#[derive(Debug, Clone)]
pub struct JavaFileItem {
    pub path: String,
    pub executable: Option<bool>,
    pub sha1: Option<String>,
    pub size: Option<u64>,
    pub url: Option<String>,
    pub r#type: Option<String>,
}

/// Descargador de Java
pub struct JavaDownloader {
    options: JavaDownloaderOptions,
}

impl JavaDownloader {
    pub fn new(options: JavaDownloaderOptions) -> Self {
        Self { options }
    }

    /// Obtener archivos de Java desde los metadatos de runtime de Mojang
    pub async fn get_java_files(&self, json_version: &MinecraftVersionJson) -> Result<JavaDownloadResult> {
        // Si se fuerza una versión específica, delegar a get_java_other() inmediatamente
        if self.options.java.version.is_some() {
            return self.get_java_other(json_version, self.options.java.version.as_deref()).await;
        }

        // Mapeo de OS a arquitectura para Java curado de Mojang
        let arch_mapping: std::collections::HashMap<&str, std::collections::HashMap<&str, &str>> = [
            ("windows", [("x86_64", "windows-x64"), ("x86", "windows-x86"), ("aarch64", "windows-arm64")].iter().cloned().collect()),
            ("macos", [("x86_64", "mac-os"), ("aarch64", if self.options.intel_enabled_mac.unwrap_or(false) { "mac-os" } else { "mac-os-arm64" })].iter().cloned().collect()),
            ("linux", [("x86_64", "linux"), ("x86", "linux-i386")].iter().cloned().collect()),
        ].iter().cloned().collect();

        let os_platform = std::env::consts::OS;
        let arch = std::env::consts::ARCH;

        let java_version_name = json_version.java_version
            .as_ref()
            .and_then(|jv| jv.component.as_deref())
            .unwrap_or("jre-legacy");

        let os_arch_mapping = arch_mapping.get(os_platform);
        if os_arch_mapping.is_none() {
            return self.get_java_other(json_version, None).await;
        }

        let os_arch_mapping = os_arch_mapping.unwrap();
        let arch_os = os_arch_mapping.get(arch);
        if arch_os.is_none() {
            return self.get_java_other(json_version, None).await;
        }

        let arch_os = arch_os.unwrap();

        // Obtener metadatos de runtime de Java de Mojang
        let url = "https://launchermeta.mojang.com/v1/products/java-runtime/2ec0cc96c44e5a76b9c8b7c39df7210883d12871/all.json";
        let response = reqwest::get(url).await?;
        let java_versions_json: serde_json::Value = response.json().await?;

        let version_name = java_versions_json[arch_os][java_version_name][0]["version"]["name"]
            .as_str()
            .ok_or_else(|| LateJavaCoreError::Java("No version name found".to_string()))?;

        // Obtener el manifest del runtime que lista archivos individuales
        let manifest_url = java_versions_json[arch_os][java_version_name][0]["manifest"]["url"]
            .as_str()
            .ok_or_else(|| LateJavaCoreError::Java("No manifest URL found".to_string()))?;

        let manifest_response = reqwest::get(manifest_url).await?;
        let manifest: serde_json::Value = manifest_response.json().await?;

        let manifest_entries: Vec<(String, serde_json::Value)> = manifest["files"]
            .as_object()
            .unwrap()
            .iter()
            .map(|(k, v)| (k.clone(), v.clone()))
            .collect();

        // Identificar el ejecutable de Java en el manifest
        let java_exe_key = if std::env::consts::OS == "windows" {
            "bin/javaw.exe"
        } else {
            "bin/java"
        };

        let java_entry = manifest_entries.iter()
            .find(|(rel_path, _)| rel_path.ends_with(java_exe_key))
            .ok_or_else(|| LateJavaCoreError::Java("Java executable not found in manifest".to_string()))?;

        let to_delete = java_entry.0.replace(java_exe_key, "");
        let mut files = Vec::new();

        for (rel_path, info) in manifest_entries {
            if info["type"] == "directory" {
                continue;
            }

            if info["downloads"].is_null() {
                continue;
            }

            files.push(JavaFileItem {
                path: format!("runtime/jre-{}-{}/{}", version_name, arch_os, rel_path.replace(&to_delete, "")),
                executable: info["executable"].as_bool(),
                sha1: info["downloads"]["raw"]["sha1"].as_str().map(|s| s.to_string()),
                size: info["downloads"]["raw"]["size"].as_u64(),
                url: info["downloads"]["raw"]["url"].as_str().map(|s| s.to_string()),
                r#type: Some("Java".to_string()),
            });
        }

        let java_path = Path::new(&self.options.path)
            .join(format!("runtime/jre-{}-{}", version_name, arch_os))
            .join("bin")
            .join(if std::env::consts::OS == "windows" { "javaw.exe" } else { "java" });

        Ok(JavaDownloadResult {
            files,
            path: java_path.to_string_lossy().to_string(),
            error: None,
            message: None,
        })
    }

    /// Método de respaldo para descargar Java desde Adoptium
    pub async fn get_java_other(&self, json_version: &MinecraftVersionJson, version_download: Option<&str>) -> Result<JavaDownloadResult> {
        let (platform, arch) = self.get_platform_arch();
        let major_version = version_download
            .and_then(|v| v.parse().ok())
            .or_else(|| json_version.java_version.as_ref().and_then(|jv| jv.major_version))
            .unwrap_or(8);

        let path_folder = Path::new(&self.options.path).join(format!("runtime/jre-{}", major_version));

        // Construir la consulta API para obtener la versión de Java
        let mut query_params = std::collections::HashMap::new();
        query_params.insert("java_version", major_version.to_string());
        query_params.insert("os", platform);
        query_params.insert("arch", arch);
        query_params.insert("archive_type", "zip");
        query_params.insert("java_package_type", &self.options.java.java_type);

        let query_string: String = query_params.iter()
            .map(|(k, v)| format!("{}={}", k, v))
            .collect::<Vec<_>>()
            .join("&");

        let java_version_url = format!("https://api.azul.com/metadata/v1/zulu/packages/?{}", query_string);
        let response = reqwest::get(&java_version_url).await?;
        let java_versions: serde_json::Value = response.json().await?;

        let java_versions = java_versions.as_array()
            .and_then(|arr| arr.first())
            .ok_or_else(|| LateJavaCoreError::Java("No Java versions found for the specified parameters".to_string()))?;

        let name = java_versions["name"]
            .as_str()
            .ok_or_else(|| LateJavaCoreError::Java("No name in Java version response".to_string()))?;

        let download_url = java_versions["download_url"]
            .as_str()
            .ok_or_else(|| LateJavaCoreError::Java("No download URL in Java version response".to_string()))?;

        let mut java_exe_path = path_folder.join(name.replace(".zip", "")).join("bin").join("java");

        if platform == "macos" {
            let bin_path = path_folder.join(name.replace(".zip", "")).join("bin");
            if bin_path.exists() {
                if let Ok(bin_content) = std::fs::read_to_string(&bin_path) {
                    java_exe_path = path_folder.join(name.replace(".zip", "")).join(bin_content.trim()).join("java");
                }
            }
        }

        if !java_exe_path.exists() {
            let zip_path = path_folder.join(name);
            self.verify_and_download_file(&zip_path, &path_folder, name, download_url).await?;

            let entries = get_file_from_archive(&zip_path.to_string_lossy(), None, None, true).await?;

            for entry in entries {
                if entry.name.starts_with("META-INF") {
                    continue;
                }

                let entry_path = path_folder.join(&entry.name);

                if entry.is_directory {
                    std::fs::create_dir_all(&entry_path)?;
                } else {
                    std::fs::write(&entry_path, entry.data)?;
                }
            }

            if platform == "macos" {
                let bin_path = path_folder.join(name.replace(".zip", "")).join("bin");
                if bin_path.exists() {
                    if let Ok(bin_content) = std::fs::read_to_string(&bin_path) {
                        java_exe_path = path_folder.join(name.replace(".zip", "")).join(bin_content.trim()).join("java");
                    }
                }
            }
        }

        Ok(JavaDownloadResult {
            files: Vec::new(),
            path: java_exe_path.to_string_lossy().to_string(),
            error: None,
            message: None,
        })
    }

    /// Mapear plataforma y arquitectura de Node a formato esperado por Adoptium
    fn get_platform_arch(&self) -> (String, String) {
        let platform_map: std::collections::HashMap<&str, &str> = [
            ("windows", "windows"),
            ("macos", "macos"),
            ("linux", "linux"),
        ].iter().cloned().collect();

        let arch_map: std::collections::HashMap<&str, &str> = [
            ("x86_64", "x64"),
            ("x86", "x32"),
            ("aarch64", "aarch64"),
            ("arm", "arm"),
        ].iter().cloned().collect();

        let current_os = std::env::consts::OS;
        let current_arch = std::env::consts::ARCH;

        let mapped_platform = platform_map.get(current_os).unwrap_or(&current_os);
        let mut mapped_arch = arch_map.get(current_arch).unwrap_or(&current_arch);

        // Forzar x64 si Apple Silicon pero el usuario quiere usar Java basado en Intel
        if current_os == "macos" && current_arch == "aarch64" && self.options.intel_enabled_mac.unwrap_or(false) {
            mapped_arch = &"x64";
        }

        (mapped_platform.to_string(), mapped_arch.to_string())
    }

    /// Verificar y descargar archivo
    async fn verify_and_download_file(&self, file_path: &Path, path_folder: &Path, file_name: &str, url: &str) -> Result<()> {
        if !file_path.exists() {
            std::fs::create_dir_all(path_folder)?;
            let downloader = Downloader::new();
            downloader.download_file(url, &path_folder.to_string_lossy(), file_name).await?;
        }
        Ok(())
    }
}