late-java-core 2.2.9

A Rust library for launching Minecraft Java Edition
use crate::error::{Result, LateJavaCoreError};
use crate::utils::get_file_hash;
use std::path::Path;

/// Item de bundle
#[derive(Debug, Clone)]
pub struct BundleItem {
    pub r#type: Option<String>,
    pub path: String,
    pub folder: Option<String>,
    pub content: Option<String>,
    pub sha1: Option<String>,
    pub size: Option<u64>,
    pub url: Option<String>,
}

/// Opciones de MinecraftBundle
#[derive(Debug, Clone)]
pub struct MinecraftBundleOptions {
    pub path: String,
    pub instance: Option<String>,
    pub ignored: Vec<String>,
}

/// Clase para manejar bundles de Minecraft
pub struct MinecraftBundle {
    options: MinecraftBundleOptions,
}

impl MinecraftBundle {
    pub fn new(options: MinecraftBundleOptions) -> Self {
        Self { options }
    }

    /// Verificar cada item en el bundle para ver si necesita ser descargado o actualizado
    pub async fn check_bundle(&self, bundle: &mut [BundleItem]) -> Result<Vec<BundleItem>> {
        let mut to_download = Vec::new();

        for file in bundle.iter_mut() {
            if file.path.is_empty() {
                continue;
            }

            // Convertir ruta a absoluta, formato consistente
            file.path = Path::new(&self.options.path)
                .join(&file.path)
                .to_string_lossy()
                .replace('\\', "/");
            
            file.folder = file.path.split('/')
                .take_while(|part| *part != "")
                .collect::<Vec<_>>()
                .join("/");

            // Si es un archivo de contenido directo (CFILE), crear/escribir el contenido inmediatamente
            if file.r#type.as_ref().map(|t| t == "CFILE").unwrap_or(false) {
                if let Some(folder) = &file.folder {
                    std::fs::create_dir_all(folder)?;
                }
                std::fs::write(&file.path, file.content.as_deref().unwrap_or(""))?;
                continue;
            }

            // Si el archivo se supone que tiene un cierto hash, verificarlo
            if Path::new(&file.path).exists() {
                // Construir el prefijo de ruta de instancia para ignorar verificaciones
                let replace_name = if let Some(instance) = &self.options.instance {
                    format!("{}/instances/{}/", self.options.path, instance)
                } else {
                    format!("{}/", self.options.path)
                };

                // Si el archivo está en la lista "ignored", omitir verificaciones
                let relative_path = file.path.replace(&replace_name, "");
                if self.options.ignored.contains(&relative_path) {
                    continue;
                }

                // Si el archivo tiene un hash y no coincide, marcarlo para descarga
                if let Some(expected_hash) = &file.sha1 {
                    let local_hash = get_file_hash(&file.path).await?;
                    if local_hash != *expected_hash {
                        to_download.push(file.clone());
                    }
                }
            } else {
                // El archivo no existe en absoluto, marcarlo para descarga
                to_download.push(file.clone());
            }
        }

        Ok(to_download)
    }

    /// Calcular el tamaño total de descarga de todos los archivos en el bundle
    pub async fn get_total_size(&self, bundle: &[BundleItem]) -> Result<u64> {
        let mut total_size = 0;
        for file in bundle {
            if let Some(size) = file.size {
                total_size += size;
            }
        }
        Ok(total_size)
    }

    /// Remover archivos o directorios que no deberían estar presentes
    pub async fn check_files(&self, bundle: &[BundleItem]) -> Result<()> {
        // Si se usan instancias, asegurar que el directorio 'instances' existe
        let instance_path = if let Some(instance) = &self.options.instance {
            let instances_dir = format!("{}/instances", self.options.path);
            if !Path::new(&instances_dir).exists() {
                std::fs::create_dir_all(&instances_dir)?;
            }
            format!("/instances/{}", instance)
        } else {
            String::new()
        };

        // Recopilar todos los archivos existentes en el directorio relevante
        let all_files = if self.options.instance.is_some() {
            self.get_files(&format!("{}{}", self.options.path, instance_path))
        } else {
            self.get_files(&self.options.path)
        };

        // También recopilar archivos de directorios "loader" y "runtime" para ignorar
        let mut ignored_files = Vec::new();
        ignored_files.extend(self.get_files(&format!("{}/loader", self.options.path)));
        ignored_files.extend(self.get_files(&format!("{}/runtime", self.options.path)));

        // Convertir rutas ignoradas personalizadas a rutas de archivo reales
        for ignored_path in &self.options.ignored {
            let full_ignored_path = format!("{}{}/{}", self.options.path, instance_path, ignored_path);
            if Path::new(&full_ignored_path).exists() {
                if Path::new(&full_ignored_path).is_dir() {
                    // Si es un directorio, agregar todos los archivos dentro de él
                    ignored_files.extend(self.get_files(&full_ignored_path));
                } else {
                    // Si es un archivo único, solo agregar ese archivo
                    ignored_files.push(full_ignored_path);
                }
            }
        }

        // Marcar rutas de bundle como ignoradas (para que no las eliminemos)
        for file in bundle {
            ignored_files.push(file.path.clone());
        }

        // Filtrar todos los archivos ignorados de la lista principal de archivos
        let files_to_delete: Vec<String> = all_files.into_iter()
            .filter(|file| !ignored_files.contains(file))
            .collect();

        // Remover cada archivo o directorio
        for file_path in files_to_delete {
            if let Ok(metadata) = std::fs::metadata(&file_path) {
                if metadata.is_dir() {
                    std::fs::remove_dir_all(&file_path)?;
                } else {
                    std::fs::remove_file(&file_path)?;

                    // Limpiar carpetas vacías subiendo hasta que lleguemos a la ruta principal
                    let mut current_dir = Path::new(&file_path).parent();
                    while let Some(dir) = current_dir {
                        if dir.to_string_lossy() == self.options.path {
                            break;
                        }
                        
                        if let Ok(entries) = std::fs::read_dir(dir) {
                            if entries.count() == 0 {
                                std::fs::remove_dir(dir)?;
                            }
                        }
                        
                        current_dir = dir.parent();
                    }
                }
            }
        }

        Ok(())
    }

    /// Recursivamente recopilar todos los archivos en una ruta de directorio dada
    fn get_files(&self, dir_path: &str) -> Vec<String> {
        let mut collected_files = Vec::new();
        self.get_files_recursive(dir_path, &mut collected_files);
        collected_files
    }

    fn get_files_recursive(&self, dir_path: &str, collected_files: &mut Vec<String>) {
        if Path::new(dir_path).exists() {
            if let Ok(entries) = std::fs::read_dir(dir_path) {
                let entries: Vec<_> = entries.collect();
                
                // Si el directorio está vacío, almacenarlo como "archivo" para que pueda ser procesado
                if entries.is_empty() {
                    collected_files.push(dir_path.to_string());
                }
                
                // Explorar cada entrada hija
                for entry in entries {
                    if let Ok(entry) = entry {
                        let full_path = entry.path().to_string_lossy().to_string();
                        if entry.metadata().map(|m| m.is_dir()).unwrap_or(false) {
                            self.get_files_recursive(&full_path, collected_files);
                        } else {
                            collected_files.push(full_path);
                        }
                    }
                }
            }
        }
    }
}