//! The revive LLVM `wasm32_unknown_emscripten` builder.
//!
//! Cross-compiling LLVM for Emscripten requires llvm-tblgen, clang-tblgen and llvm-config.

use std::{collections::HashSet, path::Path, process::Command};

/// The building sequence.
#[allow(clippy::too_many_arguments)]
pub fn build(
    build_type: crate::BuildType,
    targets: HashSet<crate::Platform>,
    llvm_projects: HashSet<crate::llvm_project::LLVMProject>,
    enable_rtti: bool,
    default_target: Option<crate::TargetTriple>,
    enable_tests: bool,
    enable_coverage: bool,
    extra_args: &[String],
    ccache_variant: Option<crate::ccache_variant::CcacheVariant>,
    enable_assertions: bool,
    sanitizer: Option<crate::sanitizer::Sanitizer>,
    enable_valgrind: bool,
) -> anyhow::Result<()> {
    crate::utils::check_presence("cmake")?;
    crate::utils::check_presence("ninja")?;
    crate::utils::check_presence("emsdk")?;
    crate::utils::check_presence("clang")?;
    crate::utils::check_presence("clang++")?;
    if cfg!(target_os = "linux") {
        crate::utils::check_presence("lld")?;
    }

    let llvm_module_llvm = crate::LLVMPath::llvm_module_llvm()?;
    let llvm_host_module_llvm = crate::LLVMPath::llvm_host_module_llvm()?;

    let llvm_build_host = crate::LLVMPath::llvm_build_host()?;
    let llvm_target_host = crate::LLVMPath::llvm_target_host()?;

    let llvm_build_final = crate::LLVMPath::llvm_build_final()?;
    let llvm_target_final = crate::LLVMPath::llvm_target_final()?;

    build_host(
        llvm_host_module_llvm.as_path(),
        llvm_build_host.as_path(),
        llvm_target_host.as_path(),
        ccache_variant,
    )?;

    build_target(
        build_type,
        targets,
        llvm_projects,
        enable_rtti,
        default_target,
        llvm_module_llvm.as_path(),
        llvm_build_final.as_path(),
        llvm_target_final.as_path(),
        llvm_target_host.as_path(),
        enable_tests,
        enable_coverage,
        extra_args,
        ccache_variant,
        enable_assertions,
        sanitizer,
        enable_valgrind,
    )?;

    Ok(())
}

/// The host toolchain building sequence.
fn build_host(
    source_directory: &Path,
    build_directory: &Path,
    target_directory: &Path,
    ccache_variant: Option<crate::ccache_variant::CcacheVariant>,
) -> anyhow::Result<()> {
    log::info!("building the LLVM Emscripten host utilities");

    crate::utils::command(
        Command::new("cmake")
            .args([
                "-S",
                source_directory.to_string_lossy().as_ref(),
                "-B",
                build_directory.to_string_lossy().as_ref(),
                "-G",
                "Ninja",
                "-DLINKER_SUPPORTS_COLOR_DIAGNOSTICS=0",
                &format!(
                    "-DCMAKE_INSTALL_PREFIX='{}'",
                    target_directory.to_string_lossy()
                ),
                "-DLLVM_BUILD_SHARED_LIBS='Off'",
                "-DCMAKE_BUILD_TYPE='Release'",
                &format!(
                    "-DLLVM_TARGETS_TO_BUILD='WebAssembly;{}'",
                    crate::Platform::PolkaVM
                ),
                "-DLLVM_ENABLE_PROJECTS='clang;lld'",
            ])
            .args(crate::platforms::shared::SHARED_BUILD_OPTS)
            .args(crate::platforms::shared::SHARED_BUILD_OPTS_NOT_MUSL)
            .args(crate::platforms::shared::shared_build_opts_ccache(
                ccache_variant,
            )),
        "LLVM host building cmake config",
    )?;

    crate::utils::ninja(build_directory)?;

    Ok(())
}

/// The target toolchain building sequence.
#[allow(clippy::too_many_arguments)]
fn build_target(
    build_type: crate::BuildType,
    targets: HashSet<crate::Platform>,
    llvm_projects: HashSet<crate::llvm_project::LLVMProject>,
    enable_rtti: bool,
    default_target: Option<crate::TargetTriple>,
    source_directory: &Path,
    build_directory: &Path,
    target_directory: &Path,
    host_target_directory: &Path,
    enable_tests: bool,
    enable_coverage: bool,
    extra_args: &[String],
    ccache_variant: Option<crate::ccache_variant::CcacheVariant>,
    enable_assertions: bool,
    sanitizer: Option<crate::sanitizer::Sanitizer>,
    enable_valgrind: bool,
) -> anyhow::Result<()> {
    let mut llvm_tblgen_path = host_target_directory.to_path_buf();
    llvm_tblgen_path.push("bin/llvm-tblgen");

    let mut clang_tblgen_path = host_target_directory.to_path_buf();
    clang_tblgen_path.push("bin/clang-tblgen");

    crate::utils::command(
        Command::new("emcmake")
            .env("EMCC_DEBUG", "2")
            .env("CXXFLAGS", "-Dwait4=__syscall_wait4")
            .env("LDFLAGS", "-lnodefs.js -s NO_INVOKE_RUN=1 -s EXIT_RUNTIME=1 -s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_RUNTIME_METHODS=FS,callMain,NODEFS -s MODULARIZE=1 -s WASM_BIGINT=1 -s ALLOW_TABLE_GROWTH=1 -s NODEJS_CATCH_EXIT=0 -sDYNAMIC_EXECUTION=0")
            .arg("cmake")
            .args([
                "-S",
                source_directory.to_string_lossy().as_ref(),
                "-B",
                build_directory.to_string_lossy().as_ref(),
                "-G",
                "Ninja",
                "-DLINKER_SUPPORTS_COLOR_DIAGNOSTICS=0",
                "-DCMAKE_BUILD_WITH_INSTALL_RPATH=1",
                // Enable thin LTO but emscripten has various issues with it.
                // FIXME: https://github.com/paritytech/revive/issues/148
                //"-DLLVM_ENABLE_LTO='Thin'",
                //"-DCMAKE_EXE_LINKER_FLAGS='-Wl,-u,htons -Wl,-u,htonl -Wl,-u,fileno -Wl,-u,ntohs'",
                &format!(
                    "-DCMAKE_INSTALL_PREFIX='{}'",
                    target_directory.to_string_lossy()
                ),
                &format!("-DCMAKE_BUILD_TYPE='{build_type}'"),
                &format!(
                    "-DLLVM_TARGETS_TO_BUILD='{}'",
                    targets
                        .into_iter()
                        .map(|platform| platform.to_string())
                        .collect::<Vec<String>>()
                        .join(";")
                ),
                &format!(
                    "-DLLVM_ENABLE_PROJECTS='{}'",
                    llvm_projects
                        .into_iter()
                        .map(|project| project.to_string())
                        .collect::<Vec<String>>()
                        .join(";")
                ),
                "-DLLVM_BUILD_SHARED_LIBS='Off'",
                "-DLLVM_ENABLE_DUMP='Off'",
                "-DLLVM_ENABLE_EXPENSIVE_CHECKS='Off'",
                "-DLLVM_ENABLE_BACKTRACES='Off'",
                "-DLLVM_ENABLE_BACKTRACES='Off'",
                "-DLLVM_ENABLE_THREADS='Off'",
                "-DLLVM_BUILD_TOOLS='Off'",
                &format!("-DLLVM_TABLEGEN='{}'", llvm_tblgen_path.to_string_lossy()),
                &format!("-DCLANG_TABLEGEN='{}'", clang_tblgen_path.to_string_lossy()),
            ])
            .args(crate::platforms::shared::shared_build_opts_default_target(
                default_target,
            ))
            .args(crate::platforms::shared::SHARED_BUILD_OPTS)
            .args(crate::platforms::shared::SHARED_BUILD_OPTS_NOT_MUSL)
            .args(crate::platforms::shared::shared_build_opts_werror(crate::target_env::TargetEnv::Emscripten))
            .args(crate::platforms::shared::shared_build_opts_tests(
                enable_tests,
            ))
            .args(crate::platforms::shared::shared_build_opts_coverage(
                enable_coverage,
            ))
            .args(extra_args)
            .args(crate::platforms::shared::shared_build_opts_ccache(
                ccache_variant,
            ))
            .args(crate::platforms::shared::shared_build_opts_assertions(
                enable_assertions,
            ))
            .args(crate::platforms::shared::shared_build_opts_rtti(
                enable_rtti,
            ))
            .args(crate::platforms::shared::shared_build_opts_sanitizers(
                sanitizer,
            ))
            .args(crate::platforms::shared::shared_build_opts_valgrind(
                enable_valgrind,
            )),
        "LLVM target building cmake",
    )?;

    crate::utils::ninja(build_directory)?;

    Ok(())
}
