# Basic CMake integration for featomic.
cmake_minimum_required(VERSION 3.22)

# Is featomic the main project configured by the user? Or is this being used
# as a submodule/subdirectory?
if (${CMAKE_CURRENT_SOURCE_DIR} STREQUAL ${CMAKE_SOURCE_DIR})
    set(FEATOMIC_MAIN_PROJECT ON)
else()
    set(FEATOMIC_MAIN_PROJECT OFF)
endif()

if(${FEATOMIC_MAIN_PROJECT} AND NOT "${CACHED_LAST_CMAKE_VERSION}" VERSION_EQUAL ${CMAKE_VERSION})
    # We use CACHED_LAST_CMAKE_VERSION to only print the cmake version
    # once in the configuration log
    set(CACHED_LAST_CMAKE_VERSION ${CMAKE_VERSION} CACHE INTERNAL "Last version of cmake used to configure")
    message(STATUS "Running CMake version ${CMAKE_VERSION}")
endif()

if (POLICY CMP0135)
    cmake_policy(SET CMP0135 NEW) # Timestamp for FetchContent
endif()

if (POLICY CMP0077)
    cmake_policy(SET CMP0077 NEW) # use variables to set OPTIONS
endif()

file(STRINGS "Cargo.toml" CARGO_TOML_CONTENT)
foreach(line ${CARGO_TOML_CONTENT})
    string(REGEX REPLACE "version = \"([0-9]+\\.[0-9]+\\.[0-9]+.*)\"" "\\1" FEATOMIC_VERSION ${line})
    if (NOT ${CMAKE_MATCH_COUNT} EQUAL 0)
        # stop on the first regex match, this should be featomic version
        break()
    endif()
endforeach()

include(cmake/dev-versions.cmake)
create_development_version("${FEATOMIC_VERSION}" FEATOMIC_FULL_VERSION "featomic-v")
message(STATUS "Building featomic v${FEATOMIC_FULL_VERSION}")

# strip any -dev/-rc suffix on the version since project(VERSION) does not support it
string(REGEX REPLACE "([0-9]*)\\.([0-9]*)\\.([0-9]*).*" "\\1.\\2.\\3" FEATOMIC_VERSION ${FEATOMIC_FULL_VERSION})
project(featomic
    VERSION ${FEATOMIC_VERSION}
    LANGUAGES C CXX # we need to declare a language to access CMAKE_SIZEOF_VOID_P later
)
set(PROJECT_VERSION ${FEATOMIC_FULL_VERSION})

# We follow the standard CMake convention of using BUILD_SHARED_LIBS to provide
# either a shared or static library as a default target. But since cargo always
# builds both versions by default, we also install both versions by default.
# `FEATOMIC_INSTALL_BOTH_STATIC_SHARED=OFF` allow to disable this behavior, and
# only install the file corresponding to `BUILD_SHARED_LIBS=ON/OFF`.
#
# BUILD_SHARED_LIBS controls the `featomic` cmake target, making it an alias of
# either `featomic::static` or `featomic::shared`. This is mainly relevant
# when using featomic from another cmake project, either as a submodule or from
# an installed library (see cmake/featomic-config.cmake)
option(BUILD_SHARED_LIBS "Use a shared library by default instead of a static one" ON)
option(FEATOMIC_INSTALL_BOTH_STATIC_SHARED "Install both shared and static libraries" ON)
option(FEATOMIC_USE_STATIC_METATENSOR "Link featomic with the static metatensor library instead of the shared one" OFF)

set(BIN_INSTALL_DIR "bin" CACHE PATH "Path relative to CMAKE_INSTALL_PREFIX where to install binaries/DLL")
set(LIB_INSTALL_DIR "lib" CACHE PATH "Path relative to CMAKE_INSTALL_PREFIX where to install libraries")
set(INCLUDE_INSTALL_DIR "include" CACHE PATH "Path relative to CMAKE_INSTALL_PREFIX where to install headers")

set(RUST_BUILD_TARGET "" CACHE STRING "Cross-compilation target for rust code. Leave empty to build for the host")
set(EXTRA_RUST_FLAGS "" CACHE STRING "Flags used to build rust code")
mark_as_advanced(RUST_BUILD_TARGET EXTRA_RUST_FLAGS)

option(FEATOMIC_FETCH_METATENSOR "Download and build the metatensor C API before building featomic" OFF)

set(CMAKE_MACOSX_RPATH ON)
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/${LIB_INSTALL_DIR}")

if (${FEATOMIC_MAIN_PROJECT})
    if("${CMAKE_BUILD_TYPE}" STREQUAL "" AND "${CMAKE_CONFIGURATION_TYPES}" STREQUAL "")
        message(STATUS "Setting build type to 'release' as none was specified.")
        set(CMAKE_BUILD_TYPE "release"
            CACHE STRING
            "Choose the type of build, options are: debug or release"
        FORCE)
        set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS release debug)
    endif()
endif()

if(${FEATOMIC_MAIN_PROJECT} AND NOT "${CACHED_LAST_CMAKE_BUILD_TYPE}" STREQUAL ${CMAKE_BUILD_TYPE})
    set(CACHED_LAST_CMAKE_BUILD_TYPE ${CMAKE_BUILD_TYPE} CACHE INTERNAL "Last build type used in configuration")
    message(STATUS "Building featomic in ${CMAKE_BUILD_TYPE} mode")
endif()

find_program(CARGO_EXE "cargo" DOC "path to cargo (Rust build system)")
if (NOT CARGO_EXE)
    message(FATAL_ERROR
        "could not find cargo, please make sure the Rust compiler is installed \
        (see https://www.rust-lang.org/tools/install) or set CARGO_EXE"
    )
endif()

execute_process(
    COMMAND ${CARGO_EXE} "--version" "--verbose"
    RESULT_VARIABLE CARGO_STATUS
    OUTPUT_VARIABLE CARGO_VERSION_RAW
)

if(CARGO_STATUS AND NOT CARGO_STATUS EQUAL 0)
    message(FATAL_ERROR
        "could not run cargo, please make sure the Rust compiler is installed \
        (see https://www.rust-lang.org/tools/install)"
    )
endif()

if (CARGO_VERSION_RAW MATCHES "cargo ([0-9]+\\.[0-9]+\\.[0-9]+).*")
    set(CARGO_VERSION "${CMAKE_MATCH_1}")
else()
    message(FATAL_ERROR "failed to determine cargo version, output was: ${CARGO_VERSION_RAW}")
endif()
if(NOT "${CACHED_LAST_CARGO_VERSION}" STREQUAL ${CARGO_VERSION})
    set(CACHED_LAST_CARGO_VERSION ${CARGO_VERSION} CACHE INTERNAL "Last version of cargo used in configuration")
    message(STATUS "Using cargo version ${CARGO_VERSION} at ${CARGO_EXE}")
    set(CARGO_VERSION_CHANGED TRUE)
endif()

# ============================================================================ #
# determine Cargo flags

set(CARGO_BUILD_ARG "")

if (EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/Cargo.lock)
    set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--locked")
endif()

# TODO: support multiple configuration generators (MSVC, ...)
string(TOLOWER ${CMAKE_BUILD_TYPE} BUILD_TYPE)
if ("${BUILD_TYPE}" STREQUAL "debug")
    set(CARGO_BUILD_TYPE "debug")
elseif("${BUILD_TYPE}" STREQUAL "release")
    set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--release")
    set(CARGO_BUILD_TYPE "release")
elseif("${BUILD_TYPE}" STREQUAL "relwithdebinfo")
    set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--release")
    set(CARGO_BUILD_TYPE "release")
else()
    message(FATAL_ERROR "unsuported build type: ${CMAKE_BUILD_TYPE}")
endif()

set(CARGO_TARGET_DIR ${CMAKE_CURRENT_BINARY_DIR}/target)
set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--target-dir=${CARGO_TARGET_DIR}")
set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--features=c-api")

# Handle cross compilation with RUST_BUILD_TARGET
if ("${RUST_BUILD_TARGET}" STREQUAL "")
        if (CARGO_VERSION_RAW MATCHES "host: ([a-zA-Z0-9_\\-]*)\n")
        set(RUST_HOST_TARGET "${CMAKE_MATCH_1}")
    else()
        message(FATAL_ERROR "failed to determine host target, output was: ${CARGO_VERSION_RAW}")
    endif()

    if (${FEATOMIC_MAIN_PROJECT})
        message(STATUS "Compiling to host (${RUST_HOST_TARGET})")
    endif()

    set(CARGO_OUTPUT_DIR "${CARGO_TARGET_DIR}/${CARGO_BUILD_TYPE}")
    set(RUST_BUILD_TARGET ${RUST_HOST_TARGET})
else()
    if (${FEATOMIC_MAIN_PROJECT})
        message(STATUS "Cross-compiling to ${RUST_BUILD_TARGET}")
    endif()

    set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--target=${RUST_BUILD_TARGET}")
    set(CARGO_OUTPUT_DIR "${CARGO_TARGET_DIR}/${RUST_BUILD_TARGET}/${CARGO_BUILD_TYPE}")
endif()

# Get the list of libraries linked by default by cargo/rustc to add when linking
# to featomic::static
if (CARGO_VERSION_CHANGED)
    include(cmake/tempdir.cmake)
    get_tempdir(TMPDIR)

    # Adapted from https://github.com/corrosion-rs/corrosion/blob/dc1e4e5/cmake/FindRust.cmake
    execute_process(
        COMMAND "${CARGO_EXE}" new --lib _cargo_required_libs
        WORKING_DIRECTORY "${TMPDIR}"
        RESULT_VARIABLE cargo_new_result
        OUTPUT_VARIABLE cargo_new_stdout
        ERROR_VARIABLE cargo_new_stderr
    )

    if (cargo_new_result)
        message(FATAL_ERROR
            "could not create empty project to find cargo's default static libs (error ${cargo_new_result})\n"
            "stdout:\n${cargo_new_stdout}"
            "stderr:\n${cargo_new_stderr}"
        )
    endif()

    file(APPEND "${TMPDIR}/_cargo_required_libs/Cargo.toml" "[lib]\ncrate-type=[\"staticlib\"]")

    execute_process(
        COMMAND ${CARGO_EXE} rustc --color never --target=${RUST_BUILD_TARGET} -- --print=native-static-libs
        WORKING_DIRECTORY "${TMPDIR}/_cargo_required_libs"
        RESULT_VARIABLE cargo_rustc_result
        OUTPUT_VARIABLE cargo_rustc_stdout
        ERROR_VARIABLE  cargo_rustc_stderr
    )

    # clean up the files
    file(REMOVE_RECURSE "${TMPDIR}")

    if(cargo_rustc_result)
        message(FATAL_ERROR
            "could not extract cargo's default static libs (error ${cargo_rustc_result})\n"
            "stdout:\n${cargo_rustc_stdout}"
            "stderr:\n${cargo_rustc_stderr}"
        )
    endif()

    # The pattern starts with `native-static-libs:` and goes to the end of the line.
    if(cargo_rustc_stderr MATCHES "native-static-libs: ([^\r\n]+)\r?\n")
        string(REPLACE " " ";" "libs_list" "${CMAKE_MATCH_1}")
        set(stripped_lib_list "")
        foreach(lib ${libs_list})
            # Strip leading `-l` (unix) and potential .lib suffix (windows)
            string(REGEX REPLACE "^-l" "" "stripped_lib" "${lib}")
            string(REGEX REPLACE "\.lib$" "" "stripped_lib" "${stripped_lib}")
            list(APPEND stripped_lib_list "${stripped_lib}")
        endforeach()
        # Special case `msvcrt` to link with the debug version in Debug mode.
        list(TRANSFORM stripped_lib_list REPLACE "^msvcrt$" "\$<\$<CONFIG:Debug>:msvcrtd>")
        list(REMOVE_DUPLICATES stripped_lib_list)

        set(CARGO_DEFAULT_LIBRARIES "${stripped_lib_list}" CACHE INTERNAL "list of implicitly linked libraries")
        if (${FEATOMIC_MAIN_PROJECT})
            message(STATUS "Cargo default link libraries are: ${CARGO_DEFAULT_LIBRARIES}")
        endif()
    else()
        message(FATAL_ERROR "could not find cargo's default static libs: `native-static-libs` not found in: `${cargo_rustc_stderr}`")
    endif()
endif()

# ============================================================================ #
# Setup metatensor

# METATENSOR_FETCH_VERSION is the exact version we will fetch from github if
# FEATOMIC_FETCH_METATENSOR=ON, and METATENSOR_REQUIRED_VERSION is the minimal
# version we require when using `find_package` to find the library.
#
# When updating METATENSOR_FETCH_VERSION, you will also have to update the
# SHA256 sum of the file in `FetchContent_Declare`.
set(METATENSOR_FETCH_VERSION "0.1.17")
set(METATENSOR_REQUIRED_VERSION "0.1")
if (FEATOMIC_FETCH_METATENSOR)
    message(STATUS "Fetching metatensor-core from github")

    include(FetchContent)
    set(URL_ROOT "https://github.com/metatensor/metatensor/releases/download")
    FetchContent_Declare(
        metatensor
        URL      ${URL_ROOT}/metatensor-core-v${METATENSOR_FETCH_VERSION}/metatensor-core-cxx-${METATENSOR_FETCH_VERSION}.tar.gz
        URL_HASH SHA256=42119e11908239915ccc187d7ca65449b461f1d4b5af4d6df1fb613d687da76a
    )

    FetchContent_MakeAvailable(metatensor)

    # metatensor will be installed in the same place as featomic, so set
    # the RPATH to ${ORIGIN} to load the file from there
    set(METATENSOR_RPATH "$$\\{ORIGIN\\}")
else()
    find_package(metatensor ${METATENSOR_REQUIRED_VERSION} REQUIRED CONFIG)
    # in case featomic gets installed in a different place than metatensor,
    # set the RPATH to the directory where we found metatensor
    get_target_property(METATENSOR_LOCATION metatensor::shared IMPORTED_LOCATION)
    get_filename_component(METATENSOR_LOCATION ${METATENSOR_LOCATION} DIRECTORY)
    set(METATENSOR_RPATH "${METATENSOR_LOCATION}")

    message(STATUS "Using local metatensor from ${METATENSOR_LOCATION}")
endif()

# ============================================================================ #
# Setup featomic libraries

file(GLOB_RECURSE ALL_RUST_SOURCES
    ${PROJECT_SOURCE_DIR}/../Cargo.toml
    ${PROJECT_SOURCE_DIR}/../featomic/Cargo.toml
    ${PROJECT_SOURCE_DIR}/../featomic/src/**.rs

    ${PROJECT_SOURCE_DIR}/Cargo.toml
    ${PROJECT_SOURCE_DIR}/build.rs
    ${PROJECT_SOURCE_DIR}/src/**.rs
)

add_library(featomic::shared SHARED IMPORTED GLOBAL)
set(FEATOMIC_SHARED_LOCATION "${CARGO_OUTPUT_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}featomic${CMAKE_SHARED_LIBRARY_SUFFIX}")
set(FEATOMIC_IMPLIB_LOCATION "${FEATOMIC_SHARED_LOCATION}.lib")

add_library(featomic::static STATIC IMPORTED GLOBAL)
set(FEATOMIC_STATIC_LOCATION "${CARGO_OUTPUT_DIR}/${CMAKE_STATIC_LIBRARY_PREFIX}featomic${CMAKE_STATIC_LIBRARY_SUFFIX}")

get_filename_component(FEATOMIC_SHARED_LIB_NAME ${FEATOMIC_SHARED_LOCATION} NAME)
get_filename_component(FEATOMIC_IMPLIB_NAME     ${FEATOMIC_IMPLIB_LOCATION} NAME)
get_filename_component(FEATOMIC_STATIC_LIB_NAME ${FEATOMIC_STATIC_LOCATION} NAME)

# We need to add some metadata to the shared library to enable linking to it
# without using an absolute path.
if (UNIX)
    if (APPLE)
        # set the install name to `@rpath/libfeatomic.dylib`
        set(CARGO_RUSTC_ARGS "-Clink-arg=-Wl,-install_name,@rpath/${FEATOMIC_SHARED_LIB_NAME}")
    else() # LINUX
        # set the SONAME to libfeatomic.so, and point the RPATH to metatensor
        set(CARGO_RUSTC_ARGS "-Clink-arg=-Wl,-soname,${FEATOMIC_SHARED_LIB_NAME},-rpath=${METATENSOR_RPATH}")
    endif()
else()
    set(CARGO_RUSTC_ARGS "")
endif()

if (NOT "${EXTRA_RUST_FLAGS}" STREQUAL "")
    set(CARGO_RUSTC_ARGS "${CARGO_RUSTC_ARGS};${EXTRA_RUST_FLAGS}")
endif()

# Set environement variables for cargo build
set(CARGO_ENV "")
if (NOT "${CMAKE_OSX_DEPLOYMENT_TARGET}" STREQUAL "")
    list(APPEND CARGO_ENV "MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif()
if (NOT "$ENV{RUSTC_WRAPPER}" STREQUAL "")
    list(APPEND CARGO_ENV "RUSTC_WRAPPER=$ENV{RUSTC_WRAPPER}")
endif()

if (FEATOMIC_INSTALL_BOTH_STATIC_SHARED)
    set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--crate-type=cdylib;--crate-type=staticlib")
    set(FILES_CREATED_BY_CARGO "${FEATOMIC_SHARED_LIB_NAME} and ${FEATOMIC_STATIC_LIB_NAME}")
else()
    if (BUILD_SHARED_LIBS)
        set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--crate-type=cdylib")
        set(FILES_CREATED_BY_CARGO "${FEATOMIC_SHARED_LIB_NAME}")
    else()
        set(CARGO_BUILD_ARG "${CARGO_BUILD_ARG};--crate-type=staticlib")
        set(FILES_CREATED_BY_CARGO "${FEATOMIC_STATIC_LIB_NAME}")
    endif()
endif()

add_custom_target(cargo-build-featomic ALL
    COMMAND
        ${CMAKE_COMMAND} -E env ${CARGO_ENV}
        cargo rustc ${CARGO_BUILD_ARG} -- ${CARGO_RUSTC_ARGS}
    WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
    DEPENDS ${ALL_RUST_SOURCES}
    COMMENT "Building ${FILES_CREATED_BY_CARGO} with cargo"
    BYPRODUCTS ${FEATOMIC_STATIC_LOCATION} ${FEATOMIC_SHARED_LOCATION} ${FEATOMIC_IMPLIB_LOCATION}
)

add_dependencies(featomic::shared cargo-build-featomic)
add_dependencies(featomic::static cargo-build-featomic)

set(FEATOMIC_HEADERS
    "${PROJECT_SOURCE_DIR}/include/featomic.h"
    "${PROJECT_SOURCE_DIR}/include/featomic.hpp"
)
set(FEATOMIC_INCLUDE_DIR ${PROJECT_SOURCE_DIR}/include/)

set_target_properties(featomic::shared PROPERTIES
    IMPORTED_LOCATION ${FEATOMIC_SHARED_LOCATION}
    INTERFACE_INCLUDE_DIRECTORIES ${FEATOMIC_INCLUDE_DIR}
    BUILD_VERSION "${FEATOMIC_FULL_VERSION}"
)
target_compile_features(featomic::shared INTERFACE cxx_std_17)

if (WIN32)
    set_target_properties(featomic::shared PROPERTIES
        IMPORTED_IMPLIB ${FEATOMIC_IMPLIB_LOCATION}
    )
endif()

set_target_properties(featomic::static PROPERTIES
    IMPORTED_LOCATION ${FEATOMIC_STATIC_LOCATION}
    INTERFACE_INCLUDE_DIRECTORIES ${FEATOMIC_INCLUDE_DIR}
    INTERFACE_LINK_LIBRARIES "${CARGO_DEFAULT_LIBRARIES}"
    BUILD_VERSION "${FEATOMIC_FULL_VERSION}"
)

if (BUILD_SHARED_LIBS)
    add_library(featomic ALIAS featomic::shared)
else()
    add_library(featomic ALIAS featomic::static)
endif()

add_dependencies(cargo-build-featomic metatensor)

if (FEATOMIC_USE_STATIC_METATENSOR)
    target_link_libraries(featomic::shared INTERFACE metatensor::static)
    target_link_libraries(featomic::static INTERFACE metatensor::static)
else()
    target_link_libraries(featomic::shared INTERFACE metatensor::shared)
    target_link_libraries(featomic::static INTERFACE metatensor::shared)
endif()

#------------------------------------------------------------------------------#
# Installation configuration
#------------------------------------------------------------------------------#

include(CMakePackageConfigHelpers)
configure_package_config_file(
    "${PROJECT_SOURCE_DIR}/cmake/featomic-config.in.cmake"
    "${PROJECT_BINARY_DIR}/featomic-config.cmake"
    INSTALL_DESTINATION ${LIB_INSTALL_DIR}/cmake/featomic
)

configure_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/cmake/featomic-config-version.in.cmake"
    "${CMAKE_CURRENT_BINARY_DIR}/featomic-config-version.cmake"
    @ONLY
)

install(FILES ${FEATOMIC_HEADERS} DESTINATION ${INCLUDE_INSTALL_DIR})

if (FEATOMIC_INSTALL_BOTH_STATIC_SHARED OR BUILD_SHARED_LIBS)
    if (WIN32)
        # DLL files should go in <prefix>/bin
        install(FILES ${FEATOMIC_SHARED_LOCATION} DESTINATION ${BIN_INSTALL_DIR})
        # .lib files should go in <prefix>/lib
        install(FILES ${FEATOMIC_IMPLIB_LOCATION} DESTINATION ${LIB_INSTALL_DIR})
    else()
        install(FILES ${FEATOMIC_SHARED_LOCATION} DESTINATION ${LIB_INSTALL_DIR})
    endif()
endif()

if (FEATOMIC_INSTALL_BOTH_STATIC_SHARED OR NOT BUILD_SHARED_LIBS)
    install(FILES ${FEATOMIC_STATIC_LOCATION} DESTINATION ${LIB_INSTALL_DIR})
endif()

install(FILES
    ${PROJECT_BINARY_DIR}/featomic-config-version.cmake
    ${PROJECT_BINARY_DIR}/featomic-config.cmake
    DESTINATION ${LIB_INSTALL_DIR}/cmake/featomic
)
