//! LevelDB library build module.

use super::{C_STANDARD, CXX_STANDARD};
use crate::linker;
use regex::Regex;
use std::{
    env,
    path::{Path, PathBuf},
};

fn replace_in_file(file_path: &Path, pattern: &str, replacement: String, file_name: &str) -> bool {
    let Ok(content) = std::fs::read_to_string(file_path) else {
        println!("cargo:warning=Failed to read {}", file_name);
        return false;
    };
    println!("Try to set: {}", &replacement);
    let re = Regex::new(pattern).unwrap();
    let modified = re.replace(&content, replacement);
    if let Err(e) = std::fs::write(file_path, modified.as_ref()) {
        println!("cargo:warning=Failed to set in {}: {}", file_name, e);
        return false;
    }
    true
}

struct FileConfig {
    file_path: &'static str,
    file_name: &'static str,
    pattern_prefix: &'static str,
    pattern_suffix: &'static str,
    replacement_prefix: &'static str,
    replacement_suffix: &'static str,
    replacement_end: &'static str,
    warning_prefix: &'static str,
}

const DB_FORMAT_CONFIG: FileConfig = FileConfig {
    file_path: "deps/leveldb/db/dbformat.h",
    file_name: "dbformat.h",
    pattern_prefix: r"static const int ",
    pattern_suffix: r" = [^;]+;",
    replacement_prefix: "static const int ",
    replacement_suffix: " = ",
    replacement_end: ";",
    warning_prefix: "Set LevelDB format constant",
};

const OPTIONS_CONFIG: FileConfig = FileConfig {
    file_path: "deps/leveldb/include/leveldb/options.h",
    file_name: "options.h",
    pattern_prefix: r"int ",
    pattern_suffix: r" = [^;]+;",
    replacement_prefix: "int ",
    replacement_suffix: " = ",
    replacement_end: ";",
    warning_prefix: "Set LevelDB options default",
};

const VERSION_SET_CONFIG: FileConfig = FileConfig {
    file_path: "deps/leveldb/db/version_set.cc",
    file_name: "version_set.cc",
    pattern_prefix: r"static_assert\(config::",
    pattern_suffix: r" == [^,]+,",
    replacement_prefix: "static_assert(config::",
    replacement_suffix: " == ",
    replacement_end: ",",
    warning_prefix: "Set LevelDB version_set assert for",
};

fn configure_variable(variable_name: &str, env_var: &str, config: &FileConfig) {
    println!("cargo:rerun-if-env-changed={}", env_var);

    let Ok(value) = env::var(env_var) else {
        return;
    };

    let pattern = format!(
        "{}{}{}",
        config.pattern_prefix, variable_name, config.pattern_suffix
    );
    let replacement = format!(
        "{}{}{}{}{}",
        config.replacement_prefix,
        variable_name,
        config.replacement_suffix,
        value,
        config.replacement_end
    );

    if replace_in_file(
        std::path::Path::new(config.file_path),
        &pattern,
        replacement,
        config.file_name,
    ) {
        println!(
            "cargo:warning={} {} to {}",
            config.warning_prefix, variable_name, value
        );
    }
}

fn pre_configure() {
    // DB Format Const Value
    configure_variable(
        "kNumLevels",
        "LEVELDB_RS_BINDING_FORMAT_NUM_LEVELS",
        &DB_FORMAT_CONFIG,
    );
    configure_variable(
        "kNumLevels",
        "LEVELDB_RS_BINDING_FORMAT_NUM_LEVELS",
        &VERSION_SET_CONFIG,
    );

    configure_variable(
        "kL0_CompactionTrigger",
        "LEVELDB_RS_BINDING_FORMAT_L0_COMPACTION_TRIGGER",
        &DB_FORMAT_CONFIG,
    );
    configure_variable(
        "kL0_SlowdownWritesTrigger",
        "LEVELDB_RS_BINDING_FORMAT_L0_SLOWDOWN_WRITES_TRIGGER",
        &DB_FORMAT_CONFIG,
    );
    configure_variable(
        "kL0_StopWritesTrigger",
        "LEVELDB_RS_BINDING_FORMAT_L0_STOP_WRITES_TRIGGER",
        &DB_FORMAT_CONFIG,
    );

    configure_variable(
        "kMaxMemCompactLevel",
        "LEVELDB_RS_BINDING_FORMAT_MAX_MEM_COMPACT_LEVEL",
        &DB_FORMAT_CONFIG,
    );
    configure_variable(
        "kReadBytesPeriod",
        "LEVELDB_RS_BINDING_FORMAT_READ_BYTES_PERIOD",
        &DB_FORMAT_CONFIG,
    );

    // Options Default Value
    configure_variable(
        "zstd_compression_level",
        "LEVELDB_RS_BINDING_OPTIONS_ZSTD_DEFAULT_COMPRESSION_LEVEL",
        &OPTIONS_CONFIG,
    );
}

fn clear_code_changes() {
    if env::var("LEVELDB_RS_BINDING_CLEAR_CODE_CHANGES").is_ok() {
        match std::process::Command::new("git")
            .args(["-C", "deps/leveldb", "reset", "--hard"])
            .output()
        {
            Ok(output) if output.status.success() => {
                println!("cargo:warning=Reset LevelDB source to original state");
            }
            Ok(output) => {
                println!(
                    "cargo:warning=Failed to reset LevelDB: {}",
                    String::from_utf8_lossy(&output.stderr)
                );
            }
            Err(e) => {
                println!("cargo:warning=Failed to execute git reset: {}", e);
            }
        }
    }
}

pub fn build(
    install_dir: &Path,
    snappy_prefix: Option<PathBuf>,
    zstd_prefix: Option<PathBuf>,
) -> PathBuf {
    println!("[leveldb] Building");
    pre_configure();
    let mut config = cmake::Config::new(std::path::Path::new("deps").join("leveldb"));
    config
        .define("CMAKE_CXX_STANDARD", CXX_STANDARD)
        .define("CMAKE_C_STANDARD", C_STANDARD)
        .define("LEVELDB_BUILD_TESTS", "OFF")
        .define("LEVELDB_BUILD_BENCHMARKS", "OFF")
        .define("CMAKE_INSTALL_LIBDIR", install_dir);
    linker::apply_cmake_flags(&mut config);

    let mut lib_paths = Vec::new();
    if let Some(snappy_prefix) = snappy_prefix {
        lib_paths.push(
            snappy_prefix
                .join(install_dir.file_name().unwrap())
                .display()
                .to_string(),
        );
        config
            .define("HAVE_SNAPPY", "ON")
            .cflag(format!("-I{}", snappy_prefix.join("include").display()))
            .cxxflag(format!("-I{}", snappy_prefix.join("include").display()));
    } else {
        config.define("HAVE_SNAPPY", "OFF");
    }
    if let Some(zstd_prefix) = zstd_prefix {
        lib_paths.push(
            zstd_prefix
                .join(install_dir.file_name().unwrap())
                .display()
                .to_string(),
        );
        config
            .define("HAVE_ZSTD", "ON")
            .cflag(format!("-I{}", zstd_prefix.join("include").display()))
            .cxxflag(format!("-I{}", zstd_prefix.join("include").display()));
    } else {
        config.define("HAVE_ZSTD", "OFF");
    }
    linker::set_ldflags(&lib_paths);
    let dest_prefix = config.build();

    assert_eq!(
        dest_prefix.join(install_dir.file_name().unwrap()),
        install_dir,
        "CMake should build LevelDB in provided install_dir"
    );

    clear_code_changes();
    dest_prefix
}
