late-java-core 2.2.9

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

/// Mapeo de plataformas Node.js a esquemas de nomenclatura de Mojang
const MOJANG_LIB: &[(&str, &str)] = &[
    ("windows", "windows"),
    ("macos", "osx"),
    ("linux", "linux"),
];

/// Mapeo de arquitecturas Node.js a reemplazos de arquitectura de Mojang
const ARCH: &[(&str, &str)] = &[
    ("x86", "32"),
    ("x86_64", "64"),
    ("aarch64", "64"),
    ("arm", "32"),
];

/// Librería de Minecraft
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MinecraftLibrary {
    pub name: Option<String>,
    pub rules: Option<Vec<Rule>>,
    pub natives: Option<HashMap<String, String>>,
    pub downloads: LibraryDownloads,
}

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

#[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 versión de Minecraft
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MinecraftVersionJson {
    pub id: String,
    pub libraries: Vec<MinecraftLibrary>,
    pub downloads: VersionDownloads,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VersionDownloads {
    pub client: ClientDownload,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClientDownload {
    pub sha1: String,
    pub size: u64,
    pub url: String,
}

/// Item de asset personalizado
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomAssetItem {
    pub path: String,
    pub hash: String,
    pub size: u64,
    pub url: String,
}

/// Opciones de librerías
#[derive(Debug, Clone)]
pub struct LibrariesOptions {
    pub path: String,
    pub instance: Option<String>,
    pub headers: Option<HashMap<String, String>>,
    pub token: Option<String>,
}

/// Descarga de librería
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LibraryDownload {
    pub sha1: Option<String>,
    pub size: Option<u64>,
    pub path: String,
    pub r#type: String,
    pub url: Option<String>,
    pub content: Option<String>,
}

/// Clase para manejar librerías de Minecraft
pub struct Libraries {
    json: Option<MinecraftVersionJson>,
    options: LibrariesOptions,
}

impl Libraries {
    pub fn new(options: LibrariesOptions) -> Self {
        Self {
            json: None,
            options,
        }
    }

    /// Obtener librerías desde el JSON de versión
    pub async fn get_libraries(&mut self, json: MinecraftVersionJson) -> Result<Vec<LibraryDownload>> {
        self.json = Some(json.clone());
        let mut libraries = Vec::new();

        for lib in &json.libraries {
            let mut artifact: Option<Artifact> = None;
            let mut lib_type = "Libraries".to_string();

            if let Some(natives) = &lib.natives {
                // Si esta librería tiene nativos del OS, elegir el clasificador correcto
                let current_os = std::env::consts::OS;
                let mojang_os = MOJANG_LIB.iter()
                    .find(|(node_os, _)| *node_os == current_os)
                    .map(|(_, mojang_os)| *mojang_os)
                    .unwrap_or(current_os);

                if let Some(native) = natives.get(mojang_os) {
                    lib_type = "Native".to_string();
                    let current_arch = std::env::consts::ARCH;
                    let arch_replacement = ARCH.iter()
                        .find(|(arch, _)| *arch == current_arch)
                        .map(|(_, replacement)| *replacement)
                        .unwrap_or("64");

                    let arch_replaced = native.replace("${arch}", arch_replacement);
                    if let Some(classifiers) = &lib.downloads.classifiers {
                        artifact = classifiers.get(&arch_replaced).cloned();
                    }
                } else {
                    // No hay nativo válido para la plataforma actual
                    continue;
                }
            } else {
                // Si hay reglas que restringen el OS, omitir si no coincide
                if let Some(rules) = &lib.rules {
                    if let Some(first_rule) = rules.first() {
                        if let Some(os_rule) = &first_rule.os {
                            if let Some(os_name) = &os_rule.name {
                                let current_os = std::env::consts::OS;
                                let mojang_os = MOJANG_LIB.iter()
                                    .find(|(node_os, _)| *node_os == current_os)
                                    .map(|(_, mojang_os)| *mojang_os)
                                    .unwrap_or(current_os);
                                
                                if os_name != mojang_os {
                                    continue;
                                }
                            }
                        }
                    }
                }
                artifact = lib.downloads.artifact.clone();
            }

            if let Some(artifact) = artifact {
                libraries.push(LibraryDownload {
                    sha1: artifact.sha1,
                    size: artifact.size,
                    path: format!("libraries/{}", artifact.path.unwrap_or_default()),
                    r#type: lib_type,
                    url: artifact.url,
                    content: None,
                });
            }
        }

        // Agregar el JAR principal del cliente de Minecraft a la lista
        libraries.push(LibraryDownload {
            sha1: Some(json.downloads.client.sha1),
            size: Some(json.downloads.client.size),
            path: format!("versions/{}/{}.jar", json.id, json.id),
            r#type: "Libraries".to_string(),
            url: Some(json.downloads.client.url),
            content: None,
        });

        // Agregar el archivo JSON para esta versión como "CFILE"
        libraries.push(LibraryDownload {
            path: format!("versions/{}/{}.json", json.id, json.id),
            r#type: "CFILE".to_string(),
            content: Some(serde_json::to_string(&json)?),
            sha1: None,
            size: None,
            url: None,
        });

        Ok(libraries)
    }

    /// Obtener assets personalizados desde una URL remota
    pub async fn get_assets_others(&self, url: Option<&str>) -> Result<Vec<LibraryDownload>> {
        if url.is_none() {
            return Ok(Vec::new());
        }

        let url = url.unwrap();

        // Preparar headers para la request
        let mut headers = self.options.headers.clone();
        if self.options.token.is_some() && headers.is_none() {
            let mut auth_headers = HashMap::new();
            auth_headers.insert("Authorization".to_string(), self.options.token.as_ref().unwrap().clone());
            headers = Some(auth_headers);
        }

        let mut request = reqwest::Client::new().get(url);
        if let Some(headers) = headers {
            for (key, value) in headers {
                request = request.header(&key, &value);
            }
        }

        let response = request.send().await?;
        let data: Vec<CustomAssetItem> = response.json().await?;

        let mut assets = Vec::new();
        for asset in data {
            if asset.path.is_empty() {
                continue;
            }

            // El 'tipo' se deduce de la primera parte de la ruta
            let file_type = asset.path.split('/').next().unwrap_or("unknown");

            assets.push(LibraryDownload {
                sha1: Some(asset.hash),
                size: Some(asset.size),
                r#type: file_type.to_string(),
                path: if let Some(instance) = &self.options.instance {
                    format!("instances/{}/{}", instance, asset.path)
                } else {
                    asset.path
                },
                url: Some(asset.url),
                content: None,
            });
        }

        Ok(assets)
    }

    /// Extraer librerías nativas
    pub async fn natives(&self, bundle: &[LibraryDownload]) -> Result<Vec<String>> {
        let json = self.json.as_ref()
            .ok_or_else(|| LateJavaCoreError::Minecraft("No version JSON loaded".to_string()))?;

        // Recopilar solo los archivos de librerías nativas
        let natives: Vec<String> = bundle
            .iter()
            .filter(|item| item.r#type == "Native")
            .map(|item| item.path.clone())
            .collect();

        if natives.is_empty() {
            return Ok(Vec::new());
        }

        // Crear la carpeta de nativos si no existe
        let natives_folder = format!("{}/versions/{}/natives", self.options.path, json.id);
        std::fs::create_dir_all(&natives_folder)?;

        // Para cada jar nativo, extraer su contenido (excluyendo META-INF)
        for native in natives {
            let full_path = format!("{}/{}", self.options.path, native);
            let entries = get_file_from_archive(&full_path, None, None, true).await?;

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

                let entry_path = format!("{}/{}", natives_folder, entry.name);

                if entry.is_directory {
                    std::fs::create_dir_all(&entry_path)?;
                } else {
                    // Escribir el archivo en la carpeta de nativos
                    std::fs::write(&entry_path, entry.data)?;
                }
            }
        }

        Ok(natives)
    }
}