#=============================================================================
# Adds the following DIY library targets:
# 1. diy:          The main diy interface library and the only target for
#                  header-only mode.
# 2. diympi:       Generated when `build_diy_mpi_lib` and `mpi` are turned on.
#                  Isolates the MPI dependent part of diy into a library.
# 3. diympi_nompi: Generated when `build_diy_mpi_lib` is on and either `mpi`
#                  is off or `build_diy_nompi_lib` is on.
#
# Both mpi and non-mpi libraries can be generated by turning on `build_diy_mpi_lib`
# and `build_diy_nompi_lib`. In this case, one of these targets must be explicitly
# specified when linking againts diy.
#=============================================================================

project                     (DIY)
cmake_minimum_required      (VERSION 3.15)

list (APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)

include(CMakeDependentOption)

# Provides an option if it is not already defined.
# This can be replaced when CMake 3.13 is our cmake_minimum_required
macro (diy_option variable)
  if (NOT DEFINED "${variable}")
    option("${variable}" ${ARGN})
  endif ()
endmacro ()

macro (diy_dependent_option variable)
  if (NOT DEFINED "${variable}")
    cmake_dependent_option("${variable}" ${ARGN})
  endif ()
endmacro ()

set                         (compiler_supports_sanitizers OFF)
if                          (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR
                             CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang" OR
                             CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    set                     (compiler_supports_sanitizers ON)
endif                       ()

diy_option                  (threads             "Build DIY with threading"                                  ON)
diy_option                  (log                 "Build DIY with logging"                                    OFF)
diy_option                  (profile             "Build DIY with profiling"                                  OFF)
diy_option                  (caliper             "Build DIY with caliper"                                    OFF)
diy_option                  (mpi                 "Build DIY with mpi"                                        ON)
diy_option                  (wrapped_mpi         "MPI compiler wrapper requires no further MPI libraries"    OFF)
diy_option                  (build_diy_mpi_lib   "Build diy::mpi as a library"                               OFF)
diy_dependent_option        (BUILD_SHARED_LIBS   "Create shared libraries if on"                             ON  "build_diy_mpi_lib" OFF)
diy_dependent_option        (build_diy_nompi_lib "Also build the nompi version of diy::mpi"                  OFF "mpi;build_diy_mpi_lib" OFF)
diy_option                  (build_examples      "Build DIY examples"                                        ON)
diy_option                  (build_tests         "Build DIY tests"                                           ON)
diy_option                  (python              "Build Python bindings"                                     OFF)
cmake_dependent_option      (enable_sanitizers   "Build DIY with sanitizer support"                          OFF "compiler_supports_sanitizers" OFF)

# Default to Release
if                          (NOT CMAKE_BUILD_TYPE)
    set                     (CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build, options are: Debug Release RelWithDebInfo MinSizeRel." FORCE)
    set_property            (CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS "Debug" "Release" "MinSizeRel" "RelWithDebInfo")
endif                       (NOT CMAKE_BUILD_TYPE)

set (diy_definitions "")
set (diy_include_directories "")
set (diy_include_thirdparty_directories "")
set (diy_libraries "")

# Debugging
if                          (${CMAKE_BUILD_TYPE} STREQUAL "Debug" OR
                             ${CMAKE_BUILD_TYPE} STREQUAL "RelWithDebInfo")
    list (APPEND diy_definitions "-DVISKORESDIY_DEBUG")
endif                       ()

# Logging
if                          (log)
    list (APPEND diy_definitions "-DVTMDIY_USE_SPDLOG")
    find_package            (spdlog REQUIRED)
endif()

# Profiling
if                          (profile)
    list (APPEND diy_definitions "-DVISKORESDIY_PROFILE")
endif()

if                          (caliper)
    list (APPEND diy_definitions "-DVISKORESDIY_USE_CALIPER")

    find_package            (caliper)
    list (APPEND diy_include_thirdparty_directories $<BUILD_INTERFACE:${caliper_INCLUDE_DIR}>)
    list (APPEND diy_libraries caliper caliper-mpi)
endif()

# Threads
if                          (NOT threads)
    list (APPEND diy_definitions "-DVISKORESDIY_NO_THREADS")
else                        (NOT threads)
    find_package            (Threads)
    list (APPEND diy_libraries ${CMAKE_THREAD_LIBS_INIT})
endif                       (NOT threads)

# MPI
if (mpi AND NOT wrapped_mpi)
    find_package(MPI REQUIRED)
endif()

# configuration variables for diy build and install
# if diy is a sub-project, the following variables allow the parent project to
# easily customize the library
if (NOT DEFINED diy_prefix)
    set(diy_prefix "diy")
endif()
if (NOT DEFINED diy_install_include_dir)
    set(diy_install_include_dir "include")
endif()
if (NOT DEFINED diy_install_bin_dir)
    set(diy_install_bin_dir "bin")
endif()
if (NOT DEFINED diy_install_lib_dir)
    set(diy_install_lib_dir "lib")
endif()
if (NOT DEFINED diy_export_name)
    set(diy_export_name "diy_targets")
endif()
if (NOT DEFINED diy_development_component)
    set(diy_development_component "development")
endif()
if (NOT DEFINED diy_runtime_component)
    set(diy_runtime_component "runtime")
endif()

if  (NOT DEFINED CMAKE_ARCHIVE_OUTPUT_DIRECTORY)
    set (CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib")
endif()
if  (NOT DEFINED CMAKE_LIBRARY_OUTPUT_DIRECTORY)
    set (CMAKE_LIBRARY_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/lib")
endif()

# for diy_developer_flags
include(DIYCompilerFlags)

function(add_diy_mpi_library use_mpi)
    set (sources
        "include/${diy_prefix}/mpi/collectives.cpp"
        "include/${diy_prefix}/mpi/communicator.cpp"
        "include/${diy_prefix}/mpi/datatypes.cpp"
        "include/${diy_prefix}/mpi/environment.cpp"
        "include/${diy_prefix}/mpi/io.cpp"
        "include/${diy_prefix}/mpi/operations.cpp"
        "include/${diy_prefix}/mpi/point-to-point.cpp"
        "include/${diy_prefix}/mpi/request.cpp"
        "include/${diy_prefix}/mpi/status.cpp"
        "include/${diy_prefix}/mpi/window.cpp")

    if (use_mpi)
        set (lib_name ${diy_prefix}mpi)
        set (has_mpi_val 1)
    else()
        set (lib_name ${diy_prefix}mpi_nompi)
        set (has_mpi_val 0)
    endif()

    add_library(${lib_name} ${sources})
    set_target_properties(${lib_name} PROPERTIES POSITION_INDEPENDENT_CODE ON)
    target_compile_features(${lib_name} PRIVATE cxx_std_14)
    target_compile_definitions(${lib_name}
        PRIVATE -DVISKORESDIY_HAS_MPI=${has_mpi_val}
        PRIVATE -Ddiy=${diy_prefix}         # mangle diy namespace
        PRIVATE ${diy_definitions})
    target_include_directories(${lib_name} SYSTEM PRIVATE ${PROJECT_SOURCE_DIR}/include) # for types.hpp
    target_include_directories(${lib_name} SYSTEM PRIVATE ${diy_include_directories}) # for mpitypes.hpp
    target_include_directories(${lib_name} SYSTEM PRIVATE ${diy_include_thirdparty_directories})
    target_link_libraries(${lib_name} PRIVATE diy_developer_flags)
    if (log)
        target_link_libraries(${lib_name} PUBLIC spdlog::spdlog_header_only)
    endif ()
    if (use_mpi AND TARGET MPI::MPI_CXX)
        target_link_libraries(${lib_name} PRIVATE MPI::MPI_CXX)
    endif()
endfunction()

# create the targets
set (diy_targets)

if (build_diy_mpi_lib)
    include(DIYConfigureMPI)

    # To be interchangeable, these libraries should only have PRIVATE properties.
    # Properties that should be public should also be part of the core diy target.
    list(APPEND diy_definitions -DVISKORESDIY_MPI_AS_LIB)
    list(APPEND diy_include_directories
        "$<BUILD_INTERFACE:${PROJECT_BINARY_DIR}/include/${diy_prefix}/mpi>"
        "$<INSTALL_INTERFACE:${diy_install_include_dir}/${diy_prefix}/mpi>")

    # macro required for proper export macros for static vs shared builds
    if (NOT BUILD_SHARED_LIBS)
        list(APPEND diy_definitions -DVISKORESDIY_MPI_STATIC_BUILD)
    endif()

    if (mpi)
        add_diy_mpi_library(ON)
        list(APPEND diy_targets ${diy_prefix}mpi)
    endif()
    if ((NOT mpi) OR build_diy_nompi_lib)
        add_diy_mpi_library(OFF)
        list(APPEND diy_targets ${diy_prefix}mpi_nompi)
    endif()
endif() # build_diy_mpi_lib

add_library(${diy_prefix} INTERFACE)
target_compile_features(${diy_prefix} INTERFACE cxx_std_14)
target_compile_definitions(${diy_prefix} INTERFACE ${diy_definitions})
target_include_directories(${diy_prefix} SYSTEM INTERFACE
    "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>"
    "$<INSTALL_INTERFACE:${diy_install_include_dir}>")
target_include_directories(${diy_prefix} SYSTEM INTERFACE ${diy_include_thirdparty_directories})
if (diy_include_directories)
    target_include_directories(${diy_prefix} SYSTEM INTERFACE ${diy_include_directories})
endif()
if (log)
    target_link_libraries(${diy_prefix} INTERFACE spdlog::spdlog_header_only)
endif ()
target_link_libraries(${diy_prefix} INTERFACE ${diy_libraries})
if (NOT build_diy_mpi_lib)
    if (mpi)
        target_compile_definitions(${diy_prefix} INTERFACE -DVISKORESDIY_HAS_MPI=1)
        if (TARGET MPI::MPI_CXX)
            target_link_libraries(${diy_prefix} INTERFACE MPI::MPI_CXX)
        endif()
    else()
        target_compile_definitions(${diy_prefix} INTERFACE -DVISKORESDIY_HAS_MPI=0)
    endif()
elseif (NOT build_diy_nompi_lib)
    if (mpi)
        target_link_libraries(${diy_prefix} INTERFACE ${diy_prefix}mpi)
    else()
        target_link_libraries(${diy_prefix} INTERFACE ${diy_prefix}mpi_nompi)
    endif()
endif()

list(APPEND diy_targets ${diy_prefix} diy_developer_flags)

# libraries used by examples and tests
set(libraries ${diy_prefix})
if (${diy_prefix}mpi IN_LIST diy_targets)
    list(APPEND libraries ${diy_prefix}mpi)
elseif (${diy_prefix}mpi_nompi IN_LIST diy_targets)
    list(APPEND libraries ${diy_prefix}mpi_nompi)
endif()
list(APPEND libraries diy_developer_flags)

# Sanitizers
if                          (enable_sanitizers)
    set(sanitizer           "address" CACHE STRING "The sanitizer to use")

    string                  (APPEND CMAKE_CXX_FLAGS " -fsanitize=${sanitizer}")
    string                  (APPEND CMAKE_C_FLAGS " -fsanitize=${sanitizer}")
    string                  (APPEND CMAKE_EXE_LINKER_FLAGS " -fsanitize=${sanitizer}")
    string                  (APPEND CMAKE_SHARED_LINKER_FLAGS " -fsanitize=${sanitizer}")
endif                       ()

# enable testing and CDash dashboard submission
enable_testing              ()
include                     (CTest)

if                          (build_examples)
    add_subdirectory        (examples)
endif                       (build_examples)

if                          (build_tests)
    add_subdirectory        (tests)
endif                       (build_tests)

# configure find_package script
include(CMakePackageConfigHelpers)
configure_package_config_file(
    "${PROJECT_SOURCE_DIR}/cmake/diy-config.cmake.in"
    "${PROJECT_BINARY_DIR}/diy-config.cmake"
    INSTALL_DESTINATION ".")

# install targets
if (NOT DEFINED diy_install_only_libraries) # defined by parent project if building for binary distribution
    install(DIRECTORY ${PROJECT_SOURCE_DIR}/include/${diy_prefix}
        DESTINATION ${diy_install_include_dir}
        COMPONENT ${diy_development_component})

    if (build_diy_mpi_lib)
        install(FILES ${PROJECT_BINARY_DIR}/include/${diy_prefix}/mpi/mpitypes.hpp
            DESTINATION ${diy_install_include_dir}/${diy_prefix}/mpi
            COMPONENT ${diy_development_component})
    endif()
endif()

install(TARGETS ${diy_targets} EXPORT ${diy_export_name}
    ARCHIVE DESTINATION ${diy_install_lib_dir} COMPONENT ${diy_development_component}
    LIBRARY DESTINATION ${diy_install_lib_dir} COMPONENT ${diy_runtime_component}
    RUNTIME DESTINATION ${diy_install_bin_dir} COMPONENT ${diy_runtime_component})

if (CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR) # Only generate these files when diy is the main project
  export(EXPORT ${diy_export_name} NAMESPACE DIY:: FILE "${PROJECT_BINARY_DIR}/diy-targets.cmake")
  install(EXPORT ${diy_export_name} NAMESPACE DIY:: DESTINATION "." FILE diy-targets.cmake COMPONENT ${diy_development_component})
  install(FILES "${PROJECT_BINARY_DIR}/diy-config.cmake" DESTINATION "." COMPONENT ${diy_development_component})
endif()

if      (python)
    add_subdirectory(bindings/python)
endif   (python)
