use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};

use crate::config::{Channel, ConfigInfo};
use crate::utils::{
    create_dir, get_sysroot_dir, run_command, run_command_with_output_and_env, walk_dir,
};

#[derive(Default)]
struct BuildArg {
    flags: Vec<String>,
    config_info: ConfigInfo,
    build_sysroot: bool,
}

impl BuildArg {
    /// Creates a new `BuildArg` instance by parsing command-line arguments.
    fn new() -> Result<Option<Self>, String> {
        let mut build_arg = Self::default();
        // Skip binary name and the `build` command.
        let mut args = std::env::args().skip(2);

        while let Some(arg) = args.next() {
            match arg.as_str() {
                "--sysroot" => {
                    build_arg.build_sysroot = true;
                }
                "--help" => {
                    Self::usage();
                    return Ok(None);
                }
                arg => {
                    if !build_arg.config_info.parse_argument(arg, &mut args)? {
                        return Err(format!("Unknown argument `{arg}`"));
                    }
                }
            }
        }
        Ok(Some(build_arg))
    }

    fn usage() {
        println!(
            r#"
`build` command help:
    --sysroot              : When used on its own, build backend in dev mode with optimized dependencies
                             and sysroot in dev mode (unoptimized)
                             When used together with --release, build backend in release mode with optimized dependencies
                             When used together with --release-sysroot,
                             build the sysroot in release mode with optimized dependencies instead of in dev mode
    --release-sysroot      : When combined with --sysroot, additionally
                             build the sysroot in release mode with optimized dependencies.
                             It has no effect if `--sysroot` is not specified.
                             It should not be used on its own.
    --sysroot-panic-abort  : Build the sysroot without unwinding support"#
        );
        ConfigInfo::show_usage();
        println!("    --help                 : Show this help");
    }
}

fn cleanup_sysroot_previous_build(library_dir: &Path) {
    // Cleanup for previous run
    // Clean target dir except for build scripts and incremental cache
    let _ = walk_dir(
        library_dir.join("target"),
        &mut |dir: &Path| {
            for top in &["debug", "release"] {
                let _ = fs::remove_dir_all(dir.join(top).join("build"));
                let _ = fs::remove_dir_all(dir.join(top).join("deps"));
                let _ = fs::remove_dir_all(dir.join(top).join("examples"));
                let _ = fs::remove_dir_all(dir.join(top).join("native"));

                let _ = walk_dir(
                    dir.join(top),
                    &mut |sub_dir: &Path| {
                        if sub_dir
                            .file_name()
                            .map(|filename| filename.to_str().unwrap().starts_with("libsysroot"))
                            .unwrap_or(false)
                        {
                            let _ = fs::remove_dir_all(sub_dir);
                        }
                        Ok(())
                    },
                    &mut |file: &Path| {
                        if file
                            .file_name()
                            .map(|filename| filename.to_str().unwrap().starts_with("libsysroot"))
                            .unwrap_or(false)
                        {
                            let _ = fs::remove_file(file);
                        }
                        Ok(())
                    },
                    false,
                );
            }
            Ok(())
        },
        &mut |_| Ok(()),
        false,
    );
}

pub fn build_sysroot(env: &HashMap<String, String>, config: &ConfigInfo) -> Result<(), String> {
    let start_dir = get_sysroot_dir();

    // Symlink libgccjit.so to sysroot.
    let lib_path = start_dir.join("sysroot").join("lib");
    let rustlib_target_path = lib_path
        .join("rustlib")
        .join(&config.host_triple)
        .join("codegen-backends")
        .join("lib")
        .join(&config.target_triple);
    let libgccjit_path =
        PathBuf::from(config.gcc_path.as_ref().expect("libgccjit should be set by this point"))
            .join("libgccjit.so");
    let libgccjit_in_sysroot_path = rustlib_target_path.join("libgccjit.so");
    // First remove the file to be able to create the symlink even when the file already exists.
    let _ = fs::remove_file(&libgccjit_in_sysroot_path);
    create_dir(&rustlib_target_path)?;
    symlink(libgccjit_path, &libgccjit_in_sysroot_path)
        .map_err(|error| format!("Cannot create symlink for libgccjit.so: {}", error))?;

    let library_dir = start_dir.join("sysroot_src").join("library");
    cleanup_sysroot_previous_build(&library_dir);

    // Builds libs
    let mut rustflags = env.get("RUSTFLAGS").cloned().unwrap_or_default();
    if config.sysroot_panic_abort {
        rustflags.push_str(" -Cpanic=abort -Zpanic-abort-tests");
    }
    rustflags.push_str(" -Z force-unstable-if-unmarked");
    if config.no_default_features {
        rustflags.push_str(" -Csymbol-mangling-version=v0");
    }

    let mut args: Vec<&dyn AsRef<OsStr>> = vec![&"cargo", &"build", &"--target", &config.target];
    if config.target.ends_with(".json") {
        args.push(&"-Zjson-target-spec");
    }

    for feature in &config.features {
        args.push(&"--features");
        args.push(feature);
    }

    if config.no_default_features {
        rustflags.push_str(" -Csymbol-mangling-version=v0");
        args.push(&"--no-default-features");
    }

    let channel = if config.sysroot_release_channel {
        rustflags.push_str(" -Zmir-opt-level=3");
        args.push(&"--release");
        "release"
    } else {
        "debug"
    };

    // We have a different environment variable than RUSTFLAGS to make sure those flags are only
    // sent to rustc_codegen_gcc and not the LLVM backend.
    if let Ok(cg_rustflags) = std::env::var("CG_RUSTFLAGS") {
        rustflags.push(' ');
        rustflags.push_str(&cg_rustflags);
    }

    args.push(&"--features");
    args.push(&"backtrace");

    let mut env = env.clone();
    env.insert("RUSTFLAGS".to_string(), rustflags);
    let sysroot_dir = library_dir.join("sysroot");
    run_command_with_output_and_env(&args, Some(&sysroot_dir), Some(&env))?;

    // Copy files to sysroot
    let sysroot_path = lib_path.join(format!("rustlib/{}/lib/", config.target_triple));
    // To avoid errors like "multiple candidates for `rmeta` dependency `core` found", we clean the
    // sysroot directory before copying the sysroot build artifacts.
    let _ = fs::remove_dir_all(&sysroot_path);
    create_dir(&sysroot_path)?;
    let mut copier = |dir_to_copy: &Path| {
        // FIXME: should not use shell command!
        run_command(&[&"cp", &"-r", &dir_to_copy, &sysroot_path], None).map(|_| ())
    };
    walk_dir(
        library_dir.join(format!("target/{}/{}/deps", config.target_triple, channel)),
        &mut copier.clone(),
        &mut copier,
        false,
    )?;

    // Copy the source files to the sysroot (Rust for Linux needs this).
    let sysroot_src_path = start_dir.join("sysroot/lib/rustlib/src/rust");
    create_dir(&sysroot_src_path)?;
    run_command(&[&"cp", &"-r", &library_dir, &sysroot_src_path], None)?;

    Ok(())
}

fn build_codegen(args: &mut BuildArg) -> Result<(), String> {
    let mut env = HashMap::new();

    if args.config_info.no_default_features {
        env.insert("RUSTFLAGS".to_string(), "-Csymbol-mangling-version=v0".to_string());
    }

    let mut command: Vec<&dyn AsRef<OsStr>> = vec![&"cargo", &"rustc"];
    if args.config_info.channel == Channel::Release {
        command.push(&"--release");
        env.insert("CHANNEL".to_string(), "release".to_string());
        env.insert("CARGO_INCREMENTAL".to_string(), "1".to_string());
    } else {
        env.insert("CHANNEL".to_string(), "debug".to_string());
    }
    if args.config_info.no_default_features {
        command.push(&"--no-default-features");
    }
    let flags = args.flags.iter().map(|s| s.as_str()).collect::<Vec<_>>();
    for flag in &flags {
        command.push(flag);
    }
    run_command_with_output_and_env(&command, None, Some(&env))?;

    args.config_info.setup(&mut env, false)?;

    // We voluntarily ignore the error.
    let _ = fs::remove_dir_all("target/out");
    let gccjit_target = "target/out/gccjit";
    create_dir(gccjit_target)?;
    if args.build_sysroot {
        println!("[BUILD] sysroot");
        build_sysroot(&env, &args.config_info)?;
    }
    Ok(())
}

/// Executes the build process.
pub fn run() -> Result<(), String> {
    let mut args = match BuildArg::new()? {
        Some(args) => args,
        None => return Ok(()),
    };
    args.config_info.setup_gcc_path()?;
    build_codegen(&mut args)?;
    Ok(())
}
