use std::{io::Write, path::PathBuf};

// ## lib_name: "protobufd"
// ## dir: "..../api_circuits/target/debug/build/lib-circuits-wrapper-49025516ce40925e/out/build/_deps/protobuf_fetch-build"
// rustc-link-search=native=..../api_circuits/target/debug/build/lib-circuits-wrapper-49025516ce40925e/out/build/_deps/protobuf_fetch-build
// rustc-link-lib=protobufd
// ## lib_name: "libyosys"
// ## dir: "..../api_circuits/target/debug/build/lib-circuits-wrapper-49025516ce40925e/out/build/_deps/yosys_fetch-build"
// rustc-link-search=native=..../api_circuits/target/debug/build/lib-circuits-wrapper-49025516ce40925e/out/build/_deps/yosys_fetch-build
// rustc-link-lib=libyosys
// ## lib_name: "xxhash"
// ## dir: "..../api_circuits/target/debug/build/lib-circuits-wrapper-49025516ce40925e/out/build/_deps/xxhash-build"
// rustc-link-search=native=..../api_circuits/target/debug/build/lib-circuits-wrapper-49025516ce40925e/out/build/_deps/xxhash-build
// rustc-link-lib=xxhash
fn parse_lib_path_dir_and_name(static_lib_str: &str) -> (PathBuf, String, bool, bool, bool) {
    let static_lib_path = std::path::Path::new(static_lib_str);

    // NOTE: file_stem only split eg "libprotobufd.so.3.19.4.0" -> "libprotobufd.so.3.19.4"
    // but that is NOT what we want (ie "libprotobufd")
    // TODO use "file_prefix" https://github.com/rust-lang/rust/issues/86319
    let liblib_name = static_lib_path.my_file_prefix().unwrap();
    let liblib_name_str: String = liblib_name.to_str().unwrap().into();
    let lib_name_str = liblib_name_str
        .strip_prefix("lib")
        .unwrap_or(&liblib_name_str);

    // basically:
    // - input = /.../target/debug/build/lib-circuits-wrapper-49025516ce40925e/out/build/_deps/glog-build/libglogd.so.0.6.0
    // - get the extension: a (or "so.3.19.4" or "so" etc)
    // NOTE: extension DOES NOT work(same issue than file_stem)
    // eg ".../libglogd.so.0.6.0".extension() == "0" (ie the part after the last dot)
    // and we NEED "so" (ie the part after the FIRST dot)
    let file_with_ext = static_lib_path.file_name().unwrap();
    let full_ext = file_with_ext
        .to_str()
        .unwrap()
        .trim_start_matches(&liblib_name_str);
    let is_static = full_ext.starts_with(".a");

    let dir = static_lib_path.parent().unwrap();

    // COULD probably have a more foolproof system by using the IMPORTED property in CMake
    // and writing that to a different file(or the same one with a prefix/suffix?)
    // NOTE: be sure that the prefix does not conflict with the Dockerfile WORKDIR /usr/src/app
    let is_system = dir.starts_with("/usr/lib/");

    let is_framework = static_lib_str.ends_with(".framework");

    (
        dir.to_path_buf(),
        lib_name_str.to_string(),
        is_static,
        is_system,
        is_framework,
    )
}

// Parse the content of "cmake_generated_rust_wrapper_libs" which SHOULD have
// been generated by our CMake function.
// It is expected to contain a list of space separated libraries eg:
// "/full/path/build/liblib1.so /full/path/build/liblib2.a /usr/lib/x86_64-linux-gnu/libpng16.so.16.37.0"
// etc
fn read_cmake_generated_to_output(
    cmake_generated_rust_wrapper_libs_str: &str,
    output: &mut impl Write,
) {
    // Previous version was globing all .a and .so in the build dir but it only worked for SHARED dependencies.
    // That is b/c when linking STATIC libs order matters! So we must get a proper list from CMake.
    for static_lib_str in cmake_generated_rust_wrapper_libs_str
        .split(&[' ', '\n'][..])
        .filter(|&x| !x.is_empty())
    {
        let (dir, lib_name_str, is_static, is_system, is_framework) =
            parse_lib_path_dir_and_name(static_lib_str);
        // WARNING: we MUST add to the linker path:
        // - NON system libs (obviously) wether SHARED or STATIC
        // - system STATIC libs eg /usr/lib/x86_64-linux-gnu/libboost_filesystem.a else
        //  "error: could not find native static library `boost_filesystem`, perhaps an -L flag is missing?"
        if (!is_system && !is_framework) || is_static {
            writeln!(output, "cargo:rustc-link-search=native={}", dir.display()).unwrap();
        }

        writeln!(
            output,
            "cargo:rustc-link-lib={}={}",
            if is_framework {
                "framework"
            } else if is_static {
                "static"
            } else {
                "dylib"
            },
            lib_name_str
        )
        .unwrap();
    }
}

pub fn read_cmake_generated(cmake_generated_rust_wrapper_libs_str: &str) {
    read_cmake_generated_to_output(
        cmake_generated_rust_wrapper_libs_str,
        &mut std::io::stdout(),
    )
}

////////////////////////////////////////////////////////////////////////////////
/// TEMP
/// Implement "file_prefix"
/// copy pasted from https://github.com/rust-lang/rust/issues/86319
use std::ffi::OsStr;

trait HasMyFilePrefix {
    fn my_file_prefix(&self) -> Option<&OsStr>;
}

impl HasMyFilePrefix for std::path::Path {
    fn my_file_prefix(&self) -> Option<&OsStr> {
        self.file_name()
            .map(split_file_at_dot)
            .map(|(before, _after)| before)
    }
}

fn split_file_at_dot(file: &OsStr) -> (&OsStr, Option<&OsStr>) {
    let slice = os_str_as_u8_slice(file);
    if slice == b".." {
        return (file, None);
    }

    // The unsafety here stems from converting between &OsStr and &[u8]
    // and back. This is safe to do because (1) we only look at ASCII
    // contents of the encoding and (2) new &OsStr values are produced
    // only from ASCII-bounded slices of existing &OsStr values.
    let i = match slice[1..].iter().position(|b| *b == b'.') {
        Some(i) => i + 1,
        None => return (file, None),
    };
    let before = &slice[..i];
    let after = &slice[i + 1..];
    unsafe { (u8_slice_as_os_str(before), Some(u8_slice_as_os_str(after))) }
}

fn os_str_as_u8_slice(s: &OsStr) -> &[u8] {
    unsafe { &*(s as *const OsStr as *const [u8]) }
}
unsafe fn u8_slice_as_os_str(s: &[u8]) -> &OsStr {
    // SAFETY: see the comment of `os_str_as_u8_slice`
    {
        &*(s as *const [u8] as *const OsStr)
    }
}

////////////////////////////////////////////////////////////////////////////////

#[cfg(test)]
mod tests {
    use crate::{parse_lib_path_dir_and_name, read_cmake_generated_to_output};

    #[test]
    fn parse_local_lib_static_ok() {
        let (dir, lib_name_str, is_static, is_system, is_framework) =
            parse_lib_path_dir_and_name("/some/path/liblibstatic.a");
        assert_eq!(dir.as_os_str(), "/some/path");
        assert_eq!(lib_name_str, "libstatic");
        assert!(is_static);
        assert!(!is_system);
        assert!(!is_framework);
    }

    #[test]
    fn parse_local_lib_shared_ok() {
        let (dir, lib_name_str, is_static, is_system, is_framework) =
            parse_lib_path_dir_and_name("/some/path/liblibshared.so");
        assert_eq!(dir.as_os_str(), "/some/path");
        assert_eq!(lib_name_str, "libshared");
        assert!(!is_static);
        assert!(!is_system);
        assert!(!is_framework);
    }

    #[test]
    fn parse_local_lib_shared_with_soversion_ok() {
        let (dir, lib_name_str, is_static, is_system, is_framework) =
            parse_lib_path_dir_and_name("/some/path/liblibshared.so.1.2.3");
        assert_eq!(dir.as_os_str(), "/some/path");
        assert_eq!(lib_name_str, "libshared");
        assert!(!is_static);
        assert!(!is_system);
        assert!(!is_framework);
    }

    #[test]
    fn parse_system_lib_static_ok() {
        let (dir, lib_name_str, is_static, is_system, is_framework) =
            parse_lib_path_dir_and_name("/usr/lib/libsystem1.a");
        assert_eq!(dir.as_os_str(), "/usr/lib");
        assert_eq!(lib_name_str, "system1");
        assert!(is_static);
        assert!(is_system);
        assert!(!is_framework);
    }

    #[test]
    fn parse_system_lib_shared_ok() {
        let (dir, lib_name_str, is_static, is_system, is_framework) =
            parse_lib_path_dir_and_name("/usr/lib/libsystem2.so");
        assert_eq!(dir.as_os_str(), "/usr/lib");
        assert_eq!(lib_name_str, "system2");
        assert!(!is_static);
        assert!(is_system);
        assert!(!is_framework);
    }

    #[test]
    fn parse_framework_ok() {
        let (dir, lib_name_str, is_static, is_system, is_framework) =
            parse_lib_path_dir_and_name("/AAA/BBB.framework");
        assert_eq!(dir.as_os_str(), "/AAA");
        assert_eq!(lib_name_str, "BBB");
        assert!(!is_static);
        assert!(!is_system);
        assert!(is_framework);
    }

    #[test]
    fn test_read_cmake_generated_to_output() {
        let input = "/some/libA.a /some/libB.so";
        let mut stdout = Vec::new();
        read_cmake_generated_to_output(input, &mut stdout);

        assert_eq!(
            std::str::from_utf8(&stdout).unwrap(),
            "cargo:rustc-link-search=native=/some\n\
        cargo:rustc-link-lib=static=A\n\
        cargo:rustc-link-search=native=/some\n\
        cargo:rustc-link-lib=dylib=B\n"
        );
    }

    // no need to touch "rustc-link-search" to link with eg "/usr/lib/x86_64-linux-gnu/libpng16.so.16.37.0"
    // simply "cargo:rustc-link-lib=dylib=png16.so" is OK
    #[test]
    fn test_read_cmake_generated_to_output_system_shared_no_rustc_link_search() {
        let input = "/usr/lib/x86_64-linux-gnu/libpng16.so.16.37.0";
        let mut stdout = Vec::new();
        read_cmake_generated_to_output(input, &mut stdout);

        assert_eq!(
            std::str::from_utf8(&stdout).unwrap(),
            "cargo:rustc-link-lib=dylib=png16\n"
        );
    }

    // BUT system STATIC libs require "rustc-link-search"??
    #[test]
    fn test_read_cmake_generated_to_output_system_static_rustc_link_search() {
        let input = "/usr/lib/x86_64-linux-gnu/libpng16.a";
        let mut stdout = Vec::new();
        read_cmake_generated_to_output(input, &mut stdout);

        assert_eq!(
            std::str::from_utf8(&stdout).unwrap(),
            "cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu\n\
            cargo:rustc-link-lib=static=png16\n"
        );
    }
}
