use std::{path::{PathBuf, Path}, fs, process};
use anyhow::Result;
use clap::Parser;
use json::Value;

macro_rules! download_if_not_exists {
    ($att:expr, $($url:tt),*) => {
        $(
            let path = $url.rsplitn(2, '/').nth(0).unwrap();
            if fs::metadata(path).is_err() {
                println!("[unmapper] Downloading missing `{}`...", path);
                save_to(path, $url, $att)?;
            }
        )*
    };
}

macro_rules! clean_up_files {
    ($($file:tt),*) => {
        $(
            let _ = std::fs::remove_file($file);
        )*
    };
}

#[derive(Parser, Debug)]
#[clap(version, about, author)]
struct Args {
    #[clap(short, long, help = "Minecraft version tag commonly found in `.minecraft/versions` folder")]
    tag: String,
    #[clap(short, long, default_value_t = 3, help = "How many times should unmapper try to download required resource")]
    attempts: u8,
    #[clap(short = 'C', long, help = "Tells unmapper not to deobfuscate client.jar")]
    no_client: bool,
    #[clap(short, long, help = "Tells unmapper to deobfuscate server.jar")]
    server: bool,
    #[clap(short = 'U', long, help = "Doesn't remove downloaded dependencies and artifacts afterwards")]
    no_clean_up: bool,
    #[clap(long, help = "Stops deobfuscator from remapping local variable names")]
    kill_lvt: bool,
    #[clap(long, help = "Overrides path to `.minecraft/versions` folder")]
    versions: Option<String>
}

struct Mapping<'a> {
    jar: &'a str,
    map: &'a str
}

struct ClenupGuard(bool);
impl Drop for ClenupGuard {
    fn drop(&mut self) {
        if !self.0 { return }

        println!("[unmapper] Cleaning up...");

        clean_up_files!(
            "SpecialSource-1.10.0.jar",
            "guava-20.0.jar",
            "jopt-simple-4.9.jar",
            "asm-commons-9.1.jar",
            "asm-9.1.jar",
            "asm-tree-9.1.jar",
            "asm-analysis-9.1.jar",
            "client.jar",
            "client.deob.mappings",
            "server.jar",
            "server.deob.mappings"
        );
    }
}

fn main() -> Result<()> {
    let args = Args::parse();

    let _cleanup = ClenupGuard(!args.no_clean_up);

    let mut version = args.versions
        .map(|s| PathBuf::from(s))
        .unwrap_or_else(|| {
            let mut cfg = if cfg!(windows) {
                dirs::config_dir()
                    .expect("Failed to get config folder. Try specifying `--versions` flag")
            } else if cfg!(unix) {
                dirs::home_dir()
                    .expect("Failed to get home directory. Try specifying `--versions` flags")
            } else {
                panic!("Unsupported platform")
            };
            
            cfg.push(".minecraft");
            cfg.push("versions");
            cfg.push(&args.tag);
            cfg
        });

    let (client_jar_path, json_path) = (
        { let mut tmp = version.clone(); tmp.push(format!("{}.jar", &args.tag)); tmp },
        { version.push(format!("{}.json", &args.tag)); version }
    );

    if fs::metadata(&client_jar_path).is_err() || fs::metadata(&json_path).is_err() {
        panic!("Specified tag's jar and/or json files wasn't found in it's version folder or versions folder doesn't exists.");
    }
    
    let tag = json::from_str::<Value>(&fs::read_to_string(&json_path)?)?;

    let client = if !args.no_client {
        Some(Mapping {
            jar: tag["downloads"]["client"]["url"].as_str().expect("Invalid json format"),
            map: tag["downloads"]["client_mappings"]["url"].as_str().expect("Invalid json format"),
        })
    } else {
        None
    };
    
    let server = if args.server {
        Some(Mapping {
            jar: tag["downloads"]["server"]["url"].as_str().expect("Invalid json format"),
            map: tag["downloads"]["server_mappings"]["url"].as_str().expect("Invalid json format"),
        })
    } else {
        None
    };

    if client.is_none() && server.is_none() {
        println!("[unmapper] Aborting. Nor client or server was specified.");
        return Ok(());
    }

    if let Some(c) = &client {
        println!("[unmapper] Downloading `client.jar`...");
        save_to("client.jar", c.jar, args.attempts)?;
        println!("[unmapper] Downloading client mappings...");
        save_to("client.deob.mappings", c.map, args.attempts)?;
    }

    if let Some(s) = &server {
        println!("[unmapper] Downloading `server.jar`...");
        save_to("server.jar", s.jar, args.attempts)?;
        println!("[unmapper] Downloading server mappings...");
        save_to("server.deob.mappings", s.map, args.attempts)?;
    }

    download_if_not_exists!(
        args.attempts,
        "https://repo1.maven.org/maven2/net/md-5/SpecialSource/1.10.0/SpecialSource-1.10.0.jar",
        "https://repo1.maven.org/maven2/com/google/guava/guava/20.0/guava-20.0.jar",
        "https://repo1.maven.org/maven2/net/sf/jopt-simple/jopt-simple/4.9/jopt-simple-4.9.jar",
        "https://repo1.maven.org/maven2/org/ow2/asm/asm-commons/9.1/asm-commons-9.1.jar",
        "https://repo1.maven.org/maven2/org/ow2/asm/asm/9.1/asm-9.1.jar",
        "https://repo1.maven.org/maven2/org/ow2/asm/asm-tree/9.1/asm-tree-9.1.jar",
        "https://repo1.maven.org/maven2/org/ow2/asm/asm-analysis/9.1/asm-analysis-9.1.jar"
    );

    if client.is_some() {
        println!("[unmapper] Started `client.jar` deobfuscation.");
        
        let mut p = process::Command::new("java");
        p.args(&[
            "-cp", "SpecialSource-1.10.0.jar;*;.", "net.md_5.specialsource.SpecialSource",
            "--in-jar", "client.jar",
            "--out-jar", "client.out.jar",
            "--srg-in", "client.deob.mappings",
        ]); if args.kill_lvt { p.arg("--kill-lvt"); }
        p.spawn()?.wait()?;
    }

    if server.is_some() {
        println!("[unmapper] Started `server.jar` deobfuscation.");
        
        let mut p = process::Command::new("java");
        p.args(&[
            "-cp", "SpecialSource-1.10.0.jar;*;.", "net.md_5.specialsource.SpecialSource",
            "--in-jar", "server.jar",
            "--out-jar", "server.out.jar",
            "--srg-in", "server.deob.mappings",
        ]); if args.kill_lvt { p.arg("--kill-lvt"); }
        p.spawn()?.wait()?;
    }
    println!("[unmapper] Done.");

    Ok(())
}

fn save_to(path: impl AsRef<Path>, url: impl AsRef<str>, attempts: u8) -> Result<()> {
    for i in 1..attempts {
        print!("[unmapper] Attempting {}/{}: ", i, attempts);

        if let Ok(res) = reqwest::blocking::get(url.as_ref()) {
            fs::write(path, res.bytes()?)?;
            println!("Success");
            return Ok(())
        } else {
            println!("Failed");
        }
    }
    Ok(())
}
