// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0 OR ISC

use crate::cc_builder::CcBuilder;
use crate::OutputLib::{Crypto, RustWrapper, Ssl};
use crate::{
    allow_prebuilt_nasm, cargo_env, effective_target, emit_warning, execute_command,
    get_crate_cflags, is_crt_static, is_no_asm, is_no_pregenerated_src, optional_env,
    optional_env_optional_crate_target, set_env, set_env_for_target, target_arch, target_env,
    target_os, test_nasm_command, use_prebuilt_nasm, OutputLibType,
};
use std::env;
use std::ffi::OsString;
use std::path::PathBuf;

pub(crate) struct CmakeBuilder {
    manifest_dir: PathBuf,
    out_dir: PathBuf,
    build_prefix: Option<String>,
    output_lib_type: OutputLibType,
}

fn test_clang_cl_command() -> bool {
    execute_command("clang-cl".as_ref(), &["--version".as_ref()]).status
}

fn find_cmake_command() -> Option<OsString> {
    if let Some(cmake) = optional_env_optional_crate_target("CMAKE") {
        emit_warning(&format!(
            "CMAKE environment variable set: {}",
            cmake.clone()
        ));
        if execute_command(cmake.as_ref(), &["--version".as_ref()]).status {
            Some(cmake.into())
        } else {
            None
        }
    } else if execute_command("cmake3".as_ref(), &["--version".as_ref()]).status {
        Some("cmake3".into())
    } else if execute_command("cmake".as_ref(), &["--version".as_ref()]).status {
        Some("cmake".into())
    } else {
        None
    }
}

impl CmakeBuilder {
    pub(crate) fn new(
        manifest_dir: PathBuf,
        out_dir: PathBuf,
        build_prefix: Option<String>,
        output_lib_type: OutputLibType,
    ) -> Self {
        Self {
            manifest_dir,
            out_dir,
            build_prefix,
            output_lib_type,
        }
    }

    fn artifact_output_dir(&self) -> PathBuf {
        self.out_dir.join("build").join("artifacts")
    }

    fn get_cmake_config(&self) -> cmake::Config {
        cmake::Config::new(&self.manifest_dir)
    }

    fn apply_universal_build_options<'a>(
        &self,
        cmake_cfg: &'a mut cmake::Config,
    ) -> &'a cmake::Config {
        // Use the compiler options identified by CcBuilder
        let cc_builder = CcBuilder::new(
            self.manifest_dir.clone(),
            self.out_dir.clone(),
            self.build_prefix.clone(),
            self.output_lib_type,
        );
        let cc_build = cc::Build::new();
        let (is_like_msvc, build_options) = cc_builder.collect_universal_build_options(&cc_build);
        for option in &build_options {
            option.apply_cmake(cmake_cfg, is_like_msvc);
        }
        cmake_cfg
    }

    #[allow(clippy::too_many_lines)]
    fn prepare_cmake_build(&self) -> cmake::Config {
        let mut cmake_cfg = self.get_cmake_config();
        if let Some(generator) = optional_env_optional_crate_target("CMAKE_GENERATOR") {
            set_env("CMAKE_GENERATOR", generator);
        }

        if OutputLibType::default() == OutputLibType::Dynamic {
            cmake_cfg.define("BUILD_SHARED_LIBS", "1");
        } else {
            cmake_cfg.define("BUILD_SHARED_LIBS", "0");
        }

        if let Some(prefix) = &self.build_prefix {
            cmake_cfg.define("BORINGSSL_PREFIX", format!("{prefix}_"));
            let include_path = self.manifest_dir.join("generated-include");
            cmake_cfg.define(
                "BORINGSSL_PREFIX_HEADERS",
                include_path.display().to_string(),
            );
        }

        // Build flags that minimize our crate size.
        cmake_cfg.define("BUILD_TESTING", "OFF");
        cmake_cfg.define("BUILD_TOOL", "OFF");
        if cfg!(feature = "ssl") {
            cmake_cfg.define("BUILD_LIBSSL", "ON");
        } else {
            cmake_cfg.define("BUILD_LIBSSL", "OFF");
        }
        if is_no_pregenerated_src() {
            // Go and Perl will be required.
            cmake_cfg.define("DISABLE_PERL", "OFF");
            cmake_cfg.define("DISABLE_GO", "OFF");
        } else {
            // Build flags that minimize our dependencies.
            cmake_cfg.define("DISABLE_PERL", "ON");
            cmake_cfg.define("DISABLE_GO", "ON");
        }

        if is_no_asm() {
            let opt_level = cargo_env("OPT_LEVEL");
            if opt_level == "0" {
                cmake_cfg.define("OPENSSL_NO_ASM", "1");
            } else {
                panic!("AWS_LC_SYS_NO_ASM only allowed for debug builds!")
            }
        }

        if cfg!(feature = "asan") {
            set_env_for_target("CC", "clang");
            set_env_for_target("CXX", "clang++");

            cmake_cfg.define("ASAN", "1");
        }

        let cflags = get_crate_cflags();
        if !cflags.is_empty() {
            set_env_for_target("CFLAGS", cflags);
        }

        if target_env() == "ohos" {
            Self::configure_open_harmony(&mut cmake_cfg);
            return cmake_cfg;
        }

        // cmake-rs has logic that strips Optimization/Debug options that are passed via CFLAGS:
        // https://github.com/rust-lang/cmake-rs/issues/240
        // This breaks build configurations that generate warnings when optimizations
        // are disabled.
        Self::preserve_cflag_optimization_flags(&mut cmake_cfg);

        // Allow environment to specify CMake toolchain.
        if let Some(toolchain) = optional_env_optional_crate_target("CMAKE_TOOLCHAIN_FILE") {
            set_env_for_target("CMAKE_TOOLCHAIN_FILE", toolchain);
            return cmake_cfg;
        }
        // We only consider compiler CFLAGS when no cmake toolchain is set
        self.apply_universal_build_options(&mut cmake_cfg);

        // See issue: https://github.com/aws/aws-lc-rs/issues/453
        if target_os() == "windows" {
            self.configure_windows(&mut cmake_cfg);
        }

        // If the build environment vendor is Apple
        #[cfg(target_vendor = "apple")]
        {
            if target_arch() == "aarch64" {
                cmake_cfg.define("CMAKE_OSX_ARCHITECTURES", "arm64");
                cmake_cfg.define("CMAKE_SYSTEM_PROCESSOR", "arm64");
            }
            if target_arch() == "x86_64" {
                cmake_cfg.define("CMAKE_OSX_ARCHITECTURES", "x86_64");
                cmake_cfg.define("CMAKE_SYSTEM_PROCESSOR", "x86_64");
            }
            if target_os().trim() == "ios" {
                cmake_cfg.define("CMAKE_SYSTEM_NAME", "iOS");
                if effective_target().ends_with("-ios-sim") || target_arch() == "x86_64" {
                    cmake_cfg.define("CMAKE_OSX_SYSROOT", "iphonesimulator");
                } else {
                    cmake_cfg.define("CMAKE_OSX_SYSROOT", "iphoneos");
                }
                cmake_cfg.define("CMAKE_THREAD_LIBS_INIT", "-lpthread");
            }
            if target_os().trim() == "macos" {
                cmake_cfg.define("CMAKE_SYSTEM_NAME", "Darwin");
                cmake_cfg.define("CMAKE_OSX_SYSROOT", "macosx");
            }
        }

        if target_os() == "android" {
            self.configure_android(&mut cmake_cfg);
        }

        cmake_cfg
    }

    fn preserve_cflag_optimization_flags(cmake_cfg: &mut cmake::Config) {
        if let Ok(cflags) = env::var("CFLAGS") {
            let split = cflags.split_whitespace();
            for arg in split {
                if arg.starts_with("-O") || arg.starts_with("/O") {
                    emit_warning(&format!("Preserving optimization flag: {arg}"));
                    cmake_cfg.cflag(arg);
                }
            }
        }
    }

    #[allow(clippy::unused_self)]
    fn configure_android(&self, _cmake_cfg: &mut cmake::Config) {
        // If we leave CMAKE_SYSTEM_PROCESSOR unset, then cmake-rs should handle properly setting
        // CMAKE_SYSTEM_NAME and CMAKE_SYSTEM_PROCESSOR:
        // https://github.com/rust-lang/cmake-rs/blob/b689783b5448966e810d515c798465f2e0ab56fd/src/lib.rs#L450-L499

        // Log relevant environment variables.
        if let Some(value) = optional_env_optional_crate_target("ANDROID_NDK_ROOT") {
            set_env("ANDROID_NDK_ROOT", value);
        } else {
            emit_warning("ANDROID_NDK_ROOT not set.");
        }
        if let Some(value) = optional_env_optional_crate_target("ANDROID_NDK") {
            set_env("ANDROID_NDK", value);
        } else {
            emit_warning("ANDROID_NDK not set.");
        }
        if let Some(value) = optional_env_optional_crate_target("ANDROID_STANDALONE_TOOLCHAIN") {
            set_env("ANDROID_STANDALONE_TOOLCHAIN", value);
        } else {
            emit_warning("ANDROID_STANDALONE_TOOLCHAIN not set.");
        }
    }

    fn configure_windows(&self, cmake_cfg: &mut cmake::Config) {
        match (target_env().as_str(), target_arch().as_str()) {
            ("msvc", "aarch64") => {
                // If CMAKE_GENERATOR is either not set or not set to "Ninja"
                let cmake_generator = optional_env("CMAKE_GENERATOR");
                if cmake_generator.is_none() || cmake_generator.unwrap().to_lowercase() != "ninja" {
                    // The following is not supported by the Ninja generator
                    cmake_cfg.generator_toolset(format!(
                        "ClangCL{}",
                        if cfg!(target_arch = "x86_64") {
                            ",host=x64"
                        } else {
                            ""
                        }
                    ));
                    cmake_cfg.define("CMAKE_GENERATOR_PLATFORM", "ARM64");
                }
                cmake_cfg.static_crt(is_crt_static());
                cmake_cfg.define("CMAKE_SYSTEM_NAME", "Windows");
                cmake_cfg.define("CMAKE_SYSTEM_PROCESSOR", "ARM64");
            }
            ("msvc", "x86") => {
                cmake_cfg.static_crt(is_crt_static());
                cmake_cfg.define("CMAKE_SYSTEM_NAME", "");
                cmake_cfg.define("CMAKE_SYSTEM_PROCESSOR", "");
            }
            ("msvc", _) => {
                cmake_cfg.static_crt(is_crt_static());
            }
            ("gnu", "x86") => {
                cmake_cfg.define("CMAKE_SYSTEM_NAME", "Windows");
                cmake_cfg.define("CMAKE_SYSTEM_PROCESSOR", "x86");
            }
            ("gnullvm", arch) => {
                cmake_cfg.define("CMAKE_SYSTEM_NAME", "Windows");
                cmake_cfg.define("CMAKE_SYSTEM_PROCESSOR", arch);
            }
            _ => {}
        }
        if use_prebuilt_nasm() {
            emit_warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
            emit_warning("!!!   Using pre-built NASM binaries   !!!");
            emit_warning("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");

            let script_name = if cfg!(target_os = "windows") {
                "prebuilt-nasm.bat"
            } else {
                "prebuilt-nasm.sh"
            };

            let script_path = self
                .manifest_dir
                .join("builder")
                .join(script_name)
                .display()
                .to_string();
            let script_path = script_path.replace('\\', "/");

            cmake_cfg.define("CMAKE_ASM_NASM_COMPILER", script_path.as_str());
            // Without the following definition, the build fails with a message similar to the one
            // reported here: https://gitlab.kitware.com/cmake/cmake/-/issues/19453
            // The variables below were found in the associated fix:
            // https://gitlab.kitware.com/cmake/cmake/-/merge_requests/4257/diffs
            cmake_cfg.define(
                "CMAKE_ASM_NASM_COMPILE_OPTIONS_MSVC_RUNTIME_LIBRARY_MultiThreaded",
                "",
            );
            cmake_cfg.define(
                "CMAKE_ASM_NASM_COMPILE_OPTIONS_MSVC_RUNTIME_LIBRARY_MultiThreadedDLL",
                "",
            );
            cmake_cfg.define(
                "CMAKE_ASM_NASM_COMPILE_OPTIONS_MSVC_RUNTIME_LIBRARY_MultiThreadedDebug",
                "",
            );
            cmake_cfg.define(
                "CMAKE_ASM_NASM_COMPILE_OPTIONS_MSVC_RUNTIME_LIBRARY_MultiThreadedDebugDLL",
                "",
            );
            cmake_cfg.define(
                "CMAKE_ASM_NASM_COMPILE_OPTIONS_MSVC_DEBUG_INFORMATION_FORMAT_ProgramDatabase",
                "",
            );
        }
    }

    fn configure_open_harmony(cmake_cfg: &mut cmake::Config) {
        let mut cflags = vec!["-Wno-unused-command-line-argument"];
        let mut asmflags = vec![];

        // If a toolchain is not specified by the environment
        if optional_env_optional_crate_target("CMAKE_TOOLCHAIN_FILE").is_none() {
            if let Ok(ndk) = env::var("OHOS_NDK_HOME") {
                set_env_for_target(
                    "CMAKE_TOOLCHAIN_FILE",
                    format!("{ndk}/native/build/cmake/ohos.toolchain.cmake"),
                );
            } else if let Ok(sdk) = env::var("OHOS_SDK_NATIVE") {
                set_env_for_target(
                    "CMAKE_TOOLCHAIN_FILE",
                    format!("{sdk}/build/cmake/ohos.toolchain.cmake"),
                );
            } else {
                emit_warning(
                    "Neither OHOS_NDK_HOME nor OHOS_SDK_NATIVE are set! No toolchain found.",
                );
            }
        }

        match effective_target().as_str() {
            "aarch64-unknown-linux-ohos" => {
                cmake_cfg.define("OHOS_ARCH", "arm64-v8a");
            }
            "armv7-unknown-linux-ohos" => {
                const ARM7_FLAGS: [&str; 6] = [
                    "-march=armv7-a",
                    "-mfloat-abi=softfp",
                    "-mtune=generic-armv7-a",
                    "-mthumb",
                    "-mfpu=neon",
                    "-DHAVE_NEON",
                ];
                cflags.extend(ARM7_FLAGS);
                asmflags.extend(ARM7_FLAGS);
                cmake_cfg.define("OHOS_ARCH", "armeabi-v7a");
            }
            "x86_64-unknown-linux-ohos" => {
                const X86_64_FLAGS: [&str; 3] = ["-msse4.1", "-DHAVE_NEON_X86", "-DHAVE_NEON"];
                cflags.extend(X86_64_FLAGS);
                asmflags.extend(X86_64_FLAGS);
                cmake_cfg.define("OHOS_ARCH", "x86_64");
            }
            ohos_target => {
                emit_warning(format!("Target: {ohos_target} is not support yet!").as_str());
            }
        }
        cmake_cfg
            .cflag(cflags.join(" ").as_str())
            .cxxflag(cflags.join(" ").as_str())
            .asmflag(asmflags.join(" ").as_str());
    }

    fn build_rust_wrapper(&self) -> PathBuf {
        self.prepare_cmake_build()
            .configure_arg("--no-warn-unused-cli")
            .build()
    }
}

impl crate::Builder for CmakeBuilder {
    fn check_dependencies(&self) -> Result<(), String> {
        let mut missing_dependency = false;
        if target_os() == "windows" && target_arch() == "x86_64" {
            if is_no_asm() && Some(true) == allow_prebuilt_nasm() {
                eprintln!(
                    "Build environment has both `AWS_LC_SYS_PREBUILT_NASM` and `AWS_LC_SYS_NO_ASM` set.\
                Please remove one of these environment variables.
                See User Guide: https://aws.github.io/aws-lc-rs/index.html"
                );
            }
            if !is_no_asm() && !test_nasm_command() && !use_prebuilt_nasm() {
                eprintln!(
                    "Consider installing NASM or setting `AWS_LC_SYS_PREBUILT_NASM` in the build environment.\
                See User Guide: https://aws.github.io/aws-lc-rs/index.html"
                );
                eprintln!("Missing dependency: nasm");
                missing_dependency = true;
            }
            if target_arch() == "aarch64" && target_env() == "msvc" && !test_clang_cl_command() {
                eprintln!("Missing dependency: clang-cl");
                missing_dependency = true;
            }
        }
        if let Some(cmake_cmd) = find_cmake_command() {
            env::set_var("CMAKE", cmake_cmd);
        } else {
            eprintln!("Missing dependency: cmake");
            missing_dependency = true;
        }

        if missing_dependency {
            return Err("Required build dependency is missing. Halting build.".to_owned());
        }

        Ok(())
    }
    fn build(&self) -> Result<(), String> {
        self.build_rust_wrapper();

        println!(
            "cargo:rustc-link-search=native={}",
            self.artifact_output_dir().display()
        );

        println!(
            "cargo:rustc-link-lib={}={}",
            self.output_lib_type.rust_lib_type(),
            Crypto.libname(&self.build_prefix)
        );

        if cfg!(feature = "ssl") {
            println!(
                "cargo:rustc-link-lib={}={}",
                self.output_lib_type.rust_lib_type(),
                Ssl.libname(&self.build_prefix)
            );
        }

        println!(
            "cargo:rustc-link-lib={}={}",
            self.output_lib_type.rust_lib_type(),
            RustWrapper.libname(&self.build_prefix)
        );

        Ok(())
    }

    fn name(&self) -> &'static str {
        "CMake"
    }
}
