cmake_minimum_required (VERSION 3.19)

# CMake utils
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

set(CMAKE_INSTALL_LIBDIR "lib")
set(CMAKE_INSTALL_INCLUDEDIR "include")
set(CMAKE_INSTALL_BINDIR "bin")

# Forbids in-source builds
include(CheckNotInSources)

# Check for some headers
include(CheckIncludeFileCXX)

# use PkgConfig to find packages
find_package(PkgConfig REQUIRED)

#------------------------------------------------------
#----------------- Main configuration -----------------
#------------------------------------------------------

# custom variable allowing to define version suffixes such as -rc*, -beta*, ...
set(PUGS_VERSION "0.5.0")

# deduce PUGS_SHORT_VERSION using regex
string(REGEX MATCH "^[0-9]+\.[0-9]+\.[0-9]+" PUGS_SHORT_VERSION ${PUGS_VERSION})
if("${PUGS_SHORT_VERSION}" STREQUAL "")
  message(FATAL_ERROR "Unable to compute short version from PUGS_VERSION=${PUGS_VERSION}")
endif()

# set project version as PUGS_SHORT_VERSION
project (Pugs VERSION ${PUGS_SHORT_VERSION})

# -----------------------------------------------------
# dynamic libraries

set(CMAKE_POSITION_INDEPENDENT_CODE ON)
link_libraries("-rdynamic")
set(CMAKE_INSTALL_RPATH "${CMAKE_INSTALL_PREFIX}/lib")

#------------------------------------------------------

set(CMAKE_CONFIGURATION_TYPES "Release;Debug;Coverage" CACHE STRING INTERNAL FORCE )

#------------------------------------------------------

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

#------------------------------------------------------

set(PUGS_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}")
set(PUGS_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}")

# Add new build types
set(CMAKE_CXX_FLAGS_COVERAGE
  "-g -O0 --coverage"
  CACHE STRING "Flags used by the C++ compiler during coverage builds."
  FORCE )
set(CMAKE_C_FLAGS_COVERAGE
  "-g -O0 --coverage"
  CACHE STRING "Flags used by the C compiler during coverage builds."
  FORCE )
set(CMAKE_EXE_LINKER_FLAGS_COVERAGE
  "--coverage"
  CACHE STRING "Flags used for linking binaries during coverage builds."
  FORCE )
set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE
  "--coverage"
  CACHE STRING "Flags used by the shared libraries linker during coverage builds."
  FORCE )
mark_as_advanced(
  CMAKE_CXX_FLAGS_COVERAGE
  CMAKE_C_FLAGS_COVERAGE
  CMAKE_EXE_LINKER_FLAGS_COVERAGE
  CMAKE_SHARED_LINKER_FLAGS_COVERAGE )

if(CMAKE_BUILD_TYPE)
  string(REGEX MATCH "^(Release|Debug|Coverage)$" VALID_BUILD_TYPE "${CMAKE_BUILD_TYPE}")
  if(NOT VALID_BUILD_TYPE)
    message(FATAL_ERROR "Invalid CMAKE_BUILD_TYPE: '${CMAKE_BUILD_TYPE}'")
  endif()
endif()

# Default build type is Release
if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE "Release" CACHE STRING
      "Choose the type of build: Release Debug Coverage."
      FORCE)
endif()

#------------------------------------------------------
# default build shared libraries
if (NOT BUILD_SHARED_LIBS)
  set(BUILD_SHARED_LIBS ON CACHE STRING "" FORCE)
endif()

#------------------------------------------------------

# Checks if compiler version is compatible with Pugs sources
set(GNU_CXX_MIN_VERSION "10.0.0")
set(CLANG_CXX_MIN_VERSION "14.0.0")

#------------------------------------------------------
# Change Kokkos namespace to avoid conflicts
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DKokkos=InlineKokkos")

#------------------------------------------------------
# Pugs default compiler flags
set(PUGS_CXX_FLAGS "${PUGS_CXX_FLAGS} -Wall -Wextra -pedantic -Wshadow -Wsign-compare -Wunused")

if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
  if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "${GNU_CXX_MIN_VERSION}")
    message(FATAL_ERROR "Pugs build requires at least g++-${GNU_CXX_MIN_VERSION}")
  endif()
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
  if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "${CLANG_CXX_MIN_VERSION}")
    message(FATAL_ERROR "Pugs build requires at least llvm/clang ++-${CLANG_CXX_MIN_VERSION}")
  endif()
  set(PUGS_CXX_FLAGS "${PUGS_CXX_FLAGS}  -Wunused-member-function -Wunused-private-field")
endif()

#------------------------------------------------------

include (TestBigEndian)
TEST_BIG_ENDIAN(IS_BIG_ENDIAN)
if(IS_BIG_ENDIAN)
  set(PUGS_LITTLE_ENDIAN FALSE)
else()
  set(PUGS_LITTLE_ENDIAN TRUE)
endif()

#------------------------------------------------------
# defaults use of MPI
set(PUGS_ENABLE_MPI AUTO CACHE STRING
  "Choose one of: AUTO ON OFF")

if (NOT PUGS_ENABLE_MPI MATCHES "^(AUTO|ON|OFF)$")
  message(FATAL_ERROR "PUGS_ENABLE_MPI='${PUGS_ENABLE_MPI}'. Must be set to one of AUTO, ON or OFF")
endif()

# Check for MPI
if (PUGS_ENABLE_MPI MATCHES "^(AUTO|ON)$")
  set(MPI_DETERMINE_LIBRARY_VERSION TRUE)
  find_package(MPI)
endif()

#------------------------------------------------------
# Search for ParMETIS

find_package(ParMETIS)
if(PARMETIS_LIBRARIES AND MPI_FOUND)
  add_library(PkgConfig::ParMETIS STATIC IMPORTED)
  set_property(TARGET PkgConfig::ParMETIS PROPERTY
    IMPORTED_LOCATION "${PARMETIS_LIBRARIES}")

  set(PUGS_HAS_PARMETIS TRUE)
  set(PARMETIS_TARGET PkgConfig::ParMETIS)
else()
  unset(PUGS_HAS_PARMETIS)
endif()

#------------------------------------------------------
# Search for PTScotch

find_package(PTScotch)

if (PTScotch_FOUND AND MPI_FOUND)
  add_library(PkgConfig::PTScotch STATIC IMPORTED)
  set_property(TARGET PkgConfig::PTScotch PROPERTY
    IMPORTED_LOCATION "${PTSCOTCH_LIBRARY}")
  set_property(TARGET PkgConfig::PTScotch PROPERTY
    INTERFACE_INCLUDE_DIRECTORIES "${PTSCOTCH_INCLUDE_DIR}")

  set(PTSCOTCH_TARGET PkgConfig::PTScotch)
  include_directories(SYSTEM "${PTSCOTCH_INCLUDE_DIR}")
  set(PUGS_HAS_PTSCOTCH TRUE)
else()
  set(PTSCOTCH_LIBRARY "")
  unset(PUGS_HAS_PTSCOTCH)
endif()

#------------------------------------------------------

if(${MPI_FOUND})
  if ((NOT PARMETIS_LIBRARIES) AND (NOT PTSCOTCH_LIBRARIES))
    if(PUGS_ENABLE_MPI MATCHES "^AUTO$")
      message(STATUS "MPI support deactivated: ParMETIS cannot be found!")
      unset(MPI_FOUND)
      unset(MPI_CXX_LINK_FLAGS)
      unset(MPI_CXX_LIBRARIES)
    else()
      message(FATAL_ERROR "MPI support requires ParMETIS which cannot be found!")
    endif()
  endif()
else()
  if(PUGS_ENABLE_MPI MATCHES "^ON$")
    message(FATAL_ERROR "Cannot find MPI!")
  endif()
endif()

#------------------------------------------------------
# Check for PETSc
# defaults use PETSc
set(PUGS_ENABLE_PETSC AUTO CACHE STRING
  "Choose one of: AUTO ON OFF")

if (PUGS_ENABLE_PETSC MATCHES "^(AUTO|ON)$")
  if (MPI_FOUND)
    # PETSc support is deactivated if MPI is not found
    pkg_check_modules(PETSC IMPORTED_TARGET GLOBAL PETSc)

    if (${PETSC_FOUND})
      set_property(TARGET PkgConfig::PETSC PROPERTY
	IMPORTED_LOCATION "${PETSC_LIBRARIES}"
      )
      set_property(TARGET PkgConfig::PETSC PROPERTY
	INTERFACE_INCLUDE_DIRECTORIES "${PETSC_INCLUDE_DIRS}"
      )

      set(PETSC_TARGET PkgConfig::PETSC)
    endif()
  else()
    message(STATUS "PETSc support is deactivated since pugs will not be build with MPI support")
    set(PETSC_FOUND FALSE)
    unset(PUGS_HAS_PETSC)
  endif()
  set(PUGS_HAS_PETSC ${PETSC_FOUND})
else()
  unset(PUGS_HAS_PETSC)
endif()

if (${PETSC_FOUND})
  include_directories(SYSTEM "${PETSC_INCLUDE_DIRS}")
else()
  if (PUGS_ENABLE_PETSC MATCHES "^ON$")
    message(FATAL_ERROR "Could not find PETSc!")
  endif()
endif()

#------------------------------------------------------
# Check for SLEPc
# defaults use SLEPc
set(PUGS_ENABLE_SLEPC AUTO CACHE STRING
  "Choose one of: AUTO ON OFF")

if (PUGS_ENABLE_SLEPC MATCHES "^(AUTO|ON)$")
  if (PETSC_FOUND)
    # SLEPc support is deactivated if PETSc is not found
    pkg_check_modules(SLEPC IMPORTED_TARGET GLOBAL SLEPc)

    if (${SLEPC_FOUND})
      set_property(TARGET PkgConfig::SLEPC PROPERTY
	IMPORTED_LOCATION "${SLEPC_LIBRARIES}"
      )
      set_property(TARGET PkgConfig::SLEPC PROPERTY
	INTERFACE_INCLUDE_DIRECTORIES "${SLEPC_INCLUDE_DIRS}"
      )

      set(SLEPC_TARGET PkgConfig::SLEPC)
    endif()
  else()
    message(STATUS "SLEPc support is deactivated since pugs will not be build with PETSc support")
    set(SLEPC_FOUND FALSE)
    unset(PUGS_HAS_SLEPC)
  endif()
  set(PUGS_HAS_SLEPC ${SLEPC_FOUND})
else()
  unset(PUGS_HAS_SLEPC)
endif()

if (${SLEPC_FOUND})
  include_directories(SYSTEM "${SLEPC_INCLUDE_DIRS}")
else()
  if (PUGS_ENABLE_SLEPC MATCHES "^ON$")
    message(FATAL_ERROR "Could not find SLEPc!")
  endif()
endif()

# -----------------------------------------------------
# Check for Eigen3
# defaults use Eigen3
set(PUGS_ENABLE_EIGEN3 AUTO CACHE STRING
  "Choose one of: AUTO ON OFF")

if (PUGS_ENABLE_EIGEN3 MATCHES "^(AUTO|ON)$")
  find_package (Eigen3 NO_MODULE)

  if (TARGET Eigen3::Eigen)
    set(EIGEN3_TARGET Eigen3::Eigen)
    set(EIGEN3_FOUND TRUE)
  else()
    set(EIGEN3_FOUND FALSE)
  endif (TARGET Eigen3::Eigen)
  set(PUGS_HAS_EIGEN3 ${EIGEN3_FOUND})
else()
  unset(PUGS_HAS_EIGEN3)
endif()

# -----------------------------------------------------

if (${MPI_FOUND})
  set(PUGS_CXX_FLAGS "${PUGS_CXX_FLAGS} ${MPI_CXX_COMPILER_FLAGS}")
  include_directories(SYSTEM "${MPI_CXX_INCLUDE_DIRS}")
elseif(PUGS_ENABLE_MPI STREQUAL "ON")
  message(FATAL_ERROR "Could not find MPI library while requested")
endif()

set(PUGS_HAS_MPI ${MPI_FOUND})

#------------------------------------------------------
# search for HDF5

set(PUGS_ENABLE_HDF5 AUTO CACHE STRING
  "Choose one of: AUTO ON OFF")

if (PUGS_ENABLE_HDF5 MATCHES "^(AUTO|ON)$")
  if (MPI_FOUND)
    # May be risky. (make to show pugs build options)
    set(HDF5_PREFER_PARALLEL TRUE)
    find_package(HDF5)
    if (HDF5_FOUND)
      # HighFive
      set(HIGHFIVE_USE_BOOST  OFF)   # no Boost
      set(HIGHFIVE_BUILD_DOCS OFF)   # no doc
      set(HIGHFIVE_UNIT_TESTS OFF)   # no unit tests
      set(HIGHFIVE_UNIT_TESTS OFF)   # no unit tests
      set(HIGHFIVE_EXAMPLES OFF)     # no examples
      add_subdirectory(${PUGS_SOURCE_DIR}/packages/HighFive/)
      set(HIGHFIVE_TARGET HighFive::HighFive)

      add_library(PkgConfig::HDF5 STATIC IMPORTED)
      set_property(TARGET PkgConfig::HDF5 PROPERTY
	IMPORTED_LOCATION "${HDF5_LIBRARIES}")

      set_property(TARGET PkgConfig::HDF5 PROPERTY
	INTERFACE_INCLUDE_DIRECTORIES "${HDF5_INCLUDE_DIRS}")

      set(HDF5_TARGET PkgConfig::HDF5)
    endif()
  else()
    message(STATUS "HDF5 support is deactivated since pugs will not be build with MPI support")
    set(HDF5_FOUND FALSE)
  endif()
  set(PUGS_HAS_HDF5 ${HDF5_FOUND})
else()
  unset(HIGHFIVE_TARGET)
  unset(HDF5_TARGET)
  unset(PUGS_HAS_HDF5)
endif()

#------------------------------------------------------
# search for libslurm

find_package(Slurm)

set(PUGS_HAS_SLURM ${SLURM_FOUND})

if (SLURM_FOUND)
  add_library(PkgConfig::Slurm STATIC IMPORTED)
  set_property(TARGET PkgConfig::Slurm PROPERTY
    IMPORTED_LOCATION "${SLURM_LIBRARY}")
  set_property(TARGET PkgConfig::Slurm PROPERTY
    INTERFACE_INCLUDE_DIRECTORIES "${SLURM_INCLUDE_DIR}")

  set(SLURM_TARGET PkgConfig::Slurm)
  include_directories(SYSTEM "${SLURM_INCLUDE_DIR}")
else()
  set(SLURM_LIBRARY "")
endif()

#------------------------------------------------------
# search for clang-format

find_program(CLANG_FORMAT clang-format)
if (CLANG_FORMAT)
  add_custom_target(clang-format
    COMMAND ${CMAKE_COMMAND}
    -DPUGS_SOURCE_DIR="${PUGS_SOURCE_DIR}"
    -DCLANG_FORMAT="${CLANG_FORMAT}"
    -P ${PUGS_SOURCE_DIR}/cmake/ClangFormatProcess.cmake
    COMMENT "running ${CLANG_FORMAT} ...")
else ()
  message(STATUS "clang-format no found!")
endif()

#------------------------------------------------------
# search for clazy-standalone

find_program(CLAZY_STANDALONE clazy-standalone)
if (CLAZY_STANDALONE)
  add_custom_target(clazy-standalone
    COMMAND ${CMAKE_COMMAND}
    -DPUGS_SOURCE_DIR="${PUGS_SOURCE_DIR}"
    -DPUGS_BINARY_DIR="${PUGS_BINARY_DIR}"
    -DCLAZY_STANDALONE="${CLAZY_STANDALONE}"
    -P ${PUGS_SOURCE_DIR}/cmake/ClazyStandaloneProcess.cmake
    COMMENT "running ${CLAZY_STANDALONE} ..."
    )
else ()
  message(STATUS "clazy-standalone no found!")
endif()

#------------------------------------------------------
# C++ 20 flags
set(CMAKE_CXX_STANDARD "20")

#------------------------------------------------------
# Kokkos configuration

set(KOKKOS_SOURCE_DIR "${PUGS_SOURCE_DIR}/packages/kokkos")
set(KOKKOS_BINARY_DIR "${PUGS_BINARY_DIR}/packages/kokkos")

# test since mdspan is not supported by g++-10 and clang-11 (used in CI)
check_include_file_cxx("mdspan" CXX_MDSPAN_SUPPORT)
if (NOT DEFINED ${CXX_MDSPAN_SUPPORT})
  message(STATUS "Kokkos_ENABLE_IMPL_MDSPAN is deactivated since standard library does not support mdspan")
  set(Kokkos_ENABLE_IMPL_MDSPAN OFF CACHE BOOL "")
endif()

# disable Kokkos deprecation warnings by default
set(Kokkos_ENABLE_DEPRECATION_WARNINGS OFF CACHE BOOL "")

# setting Kokkos defaults to OpenMP when available
find_package(OpenMP)
if(OpenMP_CXX_FOUND)
  set(Kokkos_ENABLE_OPENMP ON CACHE BOOL "")
endif()

if("${Kokkos_ENABLE_OPENMP}" STREQUAL "ON")
  set(PUGS_CXX_FLAGS "${PUGS_CXX_FLAGS} ${OpenMP_CXX_FLAGS}")
  set(OPENMP_LINK_FLAGS OpenMP::OpenMP_CXX)
endif()

add_subdirectory("${PUGS_SOURCE_DIR}/packages/kokkos")

# set as SYSTEM for static analysis
include_directories(SYSTEM "${KOKKOS_SOURCE_DIR}/core/src")
include_directories(SYSTEM "${KOKKOS_SOURCE_DIR}/containers/src")
include_directories(SYSTEM "${KOKKOS_SOURCE_DIR}/tpls/desul/include")
include_directories(SYSTEM "${KOKKOS_BINARY_DIR}/core/src")
include_directories(SYSTEM "${KOKKOS_BINARY_DIR}")

set(PUGS_BUILD_KOKKOS_DEVICES "")
if(${Kokkos_ENABLE_PTHREAD})
  list(APPEND PUGS_BUILD_KOKKOS_DEVICES "Threads")
endif()

if(${Kokkos_ENABLE_CUDA})
  list(APPEND PUGS_BUILD_KOKKOS_DEVICES "CUDA")
endif()

if(${Kokkos_ENABLE_QTHREADS})
  list(APPEND PUGS_BUILD_KOKKOS_DEVICES "QThreads")
endif()

if(${Kokkos_ENABLE_HPX})
  list(APPEND PUGS_BUILD_KOKKOS_DEVICES "HPX")
endif()

if(${Kokkos_ENABLE_OPENMP})
  list(APPEND PUGS_BUILD_KOKKOS_DEVICES "OpenMP")
endif()

if(${Kokkos_ENABLE_OPENMPTARGET})
  list(APPEND PUGS_BUILD_KOKKOS_DEVICES "OpenMPTarget")
endif()

if(${Kokkos_ENABLE_HWLOC})
  list(APPEND PUGS_BUILD_KOKKOS_DEVICES "HWLoc")
endif()

if(${Kokkos_ENABLE_MPI})
  list(APPEND PUGS_BUILD_KOKKOS_DEVICES "Kokkos/MPI")
endif()

if(${Kokkos_ENABLE_CUDA_UVM})
  list(APPEND PUGS_BUILD_KOKKOS_DEVICES "CUDA UVM")
endif()

#------------------------------------------------------
# Compiler flags
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${PUGS_CXX_FLAGS}")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} ${PUGS_CXX_FLAGS}")


message(STATUS "CMAKE_CXX_FLAGS = ${CMAKE_CXX_FLAGS}")


# Add debug mode for Standard C++ library (not for AppleClang since it is broken)
if (NOT "${CMAKE_CXX_COMPILER_ID}" STREQUAL "AppleClang")
  set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -D_GLIBCXX_DEBUG -D_LIBCPP_DEBUG=1")
endif()

#------------------------------------------------------

# Rang (colors? Useless thus necessary!)
add_subdirectory(${PUGS_SOURCE_DIR}/packages/rang/)
include_directories(SYSTEM "${PUGS_SOURCE_DIR}/packages/rang/include")

# CLI11
include_directories(SYSTEM "${PUGS_SOURCE_DIR}/packages/CLI11/include")

# PEGTL
add_subdirectory(${PUGS_SOURCE_DIR}/packages/PEGTL/)
include_directories(SYSTEM "${PUGS_SOURCE_DIR}/packages/PEGTL/include/tao")

# Pugs src
add_subdirectory("${PUGS_SOURCE_DIR}/src")
include_directories("${PUGS_SOURCE_DIR}/src")
include_directories("${PUGS_BINARY_DIR}/src")

# Pugs tests
set(CATCH_MODULE_PATH "${PUGS_SOURCE_DIR}/packages/Catch2")
add_subdirectory("${CATCH_MODULE_PATH}")
target_compile_options(Catch2 PRIVATE "-w")
target_compile_options(Catch2WithMain PRIVATE "-w")

add_subdirectory(tests)

if(${PUGS_HAS_MPI})
  set(MPIEXEC_OPTION_FLAGS --oversubscribe)
  if (NOT "$ENV{GITLAB_CI}" STREQUAL "")
    set(MPIEXEC_OPTION_FLAGS ${MPIEXEC_OPTION_FLAGS} --allow-run-as-root)
  endif()
  set(MPIEXEC_NUMPROC 3)
  set(MPIEXEC_PATH_FLAG --path)
  set(MPI_LAUNCHER ${MPIEXEC} ${MPIEXEC_NUMPROC_FLAG} ${MPIEXEC_NUMPROC} ${MPIEXEC_OPTION_FLAGS})
endif()

add_custom_target(all_unit_tests
  DEPENDS unit_tests mpi_unit_tests
)

add_custom_target(check
  DEPENDS test
  )

add_custom_target(test
  DEPENDS run_all_unit_tests
  )

add_custom_target(run_all_unit_tests
  DEPENDS run_mpi_unit_tests
  )

if(PUGS_HAS_MPI)
  set(RUN_MPI_UNIT_TESTS_COMMENT "Running mpi_unit_tests [using ${MPIEXEC_NUMPROC} procs]")
else()
  set(RUN_MPI_UNIT_TESTS_COMMENT "Running mpi_unit_tests [sequentially]")
endif()

add_custom_target(run_mpi_unit_tests
  COMMAND ${MPI_LAUNCHER} "${PUGS_BINARY_DIR}/mpi_unit_tests"
  WORKING_DIRECTORY ${PUGS_BINARY_DIR}
  DEPENDS run_unit_tests
  COMMENT ${RUN_MPI_UNIT_TESTS_COMMENT}
  )

add_custom_target(run_unit_tests
  COMMAND "${PUGS_BINARY_DIR}/unit_tests"
  DEPENDS all_unit_tests
  COMMENT "Running unit_tests"
  )

# unit tests coverage

if("${CMAKE_BUILD_TYPE}" STREQUAL "Coverage")
  file(GLOB_RECURSE GCNO_FILE_LIST RELATIVE "${PUGS_BINARY_DIR}"  "*.gcno")
  list(FILTER GCNO_FILE_LIST EXCLUDE REGEX "^\.\./.*" )
  file(GLOB_RECURSE GCDA_FILE_LIST RELATIVE "${PUGS_BINARY_DIR}"  "*.gcda")
  list(FILTER GCDA_FILE_LIST EXCLUDE REGEX "^\.\./.*" )
  foreach(COV_INFO_FILE IN LISTS GCNO_FILE_LIST GCDA_FILE_LIST)
    string(REGEX REPLACE "/CMakeFiles/.*\.dir/" "/" COV_SRC_FILE "${PUGS_SOURCE_DIR}/${COV_INFO_FILE}")
    string(REGEX REPLACE "\.gcda$" "" COV_SRC_FILE "${COV_SRC_FILE}")
    string(REGEX REPLACE "\.gcno$" "" COV_SRC_FILE "${COV_SRC_FILE}")
    if (NOT EXISTS "${COV_SRC_FILE}")
      file(REMOVE "${PUGS_BINARY_DIR}/${COV_INFO_FILE}")
      message(STATUS "removed file ${COV_INFO_FILE}: no longer needed")
    endif()
  endforeach()

  find_program(LCOV lcov)
  if(NOT LCOV)
    message(FATAL_ERROR "lcov not found, cannot perform coverage.")
  endif()

  if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
    string(REGEX MATCH "^[0-9]+" GCC_VERSION
      "${CMAKE_CXX_COMPILER_VERSION}")
    find_program(GCOV_BIN NAMES gcov-${GCC_VERSION} gcov
      HINTS ${COMPILER_PATH})
  elseif("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
    string(REGEX MATCH "^[0-9]+" LLVM_VERSION
      "${CMAKE_CXX_COMPILER_VERSION}")

    if(LLVM_VERSION VERSION_GREATER 7)
      find_program(LLVM_COV_BIN NAMES "llvm-cov-${LLVM_VERSION}"
        "llvm-cov" HINTS ${COMPILER_PATH})
      mark_as_advanced(LLVM_COV_BIN)

      if (LLVM_COV_BIN)
        file(MAKE_DIRECTORY "${PUGS_BINARY_DIR}/tools/tmp")

        file(WRITE "${PUGS_BINARY_DIR}/tools/tmp/llvm-cov.sh" "#! /bin/sh\n")
        file(APPEND "${PUGS_BINARY_DIR}/tools/tmp/llvm-cov.sh" "'${LLVM_COV_BIN}' gcov \"\$@\"\n")

        file(COPY "${PUGS_BINARY_DIR}/tools/tmp/llvm-cov.sh"
             DESTINATION "${PUGS_BINARY_DIR}/tools"
             FILE_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE)

        set(GCOV_BIN "${PUGS_BINARY_DIR}/tools/llvm-cov.sh")
      endif()
    endif()
  endif()

  if(NOT GCOV_BIN)
     message(FATAL_ERROR "Cannot find a proper gcov tool, cannot perform coverage.")
  endif()

  find_program(FASTCOV fastcov fastcov.py)

  add_custom_target(coverage_unit_tests
    ALL # in coverage mode we do coverage!

    COMMAND "${PUGS_BINARY_DIR}/unit_tests"

    COMMENT "Running unit_tests"
    DEPENDS coverage_zero_counters
    )

  add_custom_target(coverage_mpi_unit_tests
    ALL # in coverage mode we do coverage!

    COMMAND ${MPI_LAUNCHER} "mpi_unit_tests"

    COMMENT "Running mpi_unit_tests"
    DEPENDS coverage_unit_tests
    )

  add_custom_target(coverage_run_all_unit_tests
    ALL # in coverage mode we do coverage!

    DEPENDS coverage_mpi_unit_tests
    )

  if (FASTCOV AND (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") AND (CMAKE_CXX_COMPILER_VERSION VERSION_GREATER "9"))

    add_custom_target(coverage_zero_counters
      ALL # in coverage mode we do coverage!

      # zero all counters
      COMMAND ${FASTCOV} -q -z --gcov "${GCOV_BIN}"
      DEPENDS all_unit_tests
      )

    add_custom_target(coverage
      ALL # in coverage mode we do coverage!

      COMMAND ${FASTCOV} --gcov "${GCOV_BIN}"
      --include "${PUGS_SOURCE_DIR}/src"
      --exclude "${PUGS_SOURCE_DIR}/src/main.cpp" "${PUGS_SOURCE_DIR}/src/utils/BacktraceManager.*" "${PUGS_SOURCE_DIR}/src/utils/FPEManager.*" "${PUGS_SOURCE_DIR}/src/utils/SignalManager.*" "${PUGS_SOURCE_DIR}/src/utils/checkpointing/pugs_checkpoint_main.cpp"
      --lcov -o coverage.info -n

      COMMAND ${LCOV} --gcov "${GCOV_BIN}" --list coverage.info

      DEPENDS coverage_run_all_unit_tests
      COMMENT "Running test coverage."
      WORKING_DIRECTORY "${PUGS_BINARY_DIR}"
      )

  else()

    add_custom_target(coverage_zero_counters
      ALL # in coverage mode we do coverage!
      # Cleanup previously generated profiling data
      COMMAND ${LCOV} -q --gcov "${GCOV_BIN}" --base-directory "${PUGS_BINARY_DIR}/src" --directory "${PUGS_BINARY_DIR}" --zerocounters
      # Initialize profiling data with zero coverage for every instrumented line of the project
      # This way the percentage of total lines covered will always be correct, even when not all source code files were loaded during the test(s)
      COMMAND ${LCOV} -q --gcov "${GCOV_BIN}" --base-directory "${PUGS_BINARY_DIR}/src" --directory "${PUGS_BINARY_DIR}" --capture --initial --output-file coverage_base.info
      DEPENDS all_unit_tests
      )


    add_custom_target(coverage
      # Collect data from executions
      COMMAND ${LCOV} --gcov "${GCOV_BIN}" --base-directory "${PUGS_BINARY_DIR}/src" --directory "${PUGS_BINARY_DIR}" --capture --output-file coverage_ctest.info
      # Combine base and ctest results
      COMMAND ${LCOV} --gcov "${GCOV_BIN}" -q --add-tracefile coverage_base.info --add-tracefile coverage_ctest.info --output-file coverage_full.info
      # Extract only project data (--no-capture or --remove options may be used to select collected data)
      COMMAND ${LCOV} --gcov "${GCOV_BIN}" -q --extract coverage_full.info "'${PUGS_SOURCE_DIR}/src/*'" --output-file coverage_extract.info

      # Remove unwanted stuff
      COMMAND ${LCOV} --gcov "${GCOV_BIN}" --remove coverage_extract.info  --output-file coverage.info
      '${PUGS_SOURCE_DIR}/src/main.cpp'
      '${PUGS_SOURCE_DIR}/src/utils/BacktraceManager.*'

      COMMAND ${LCOV} --gcov "${GCOV_BIN}" --list coverage.info

      DEPENDS coverage_run_all_unit_tests
      COMMENT "Running test coverage."
      WORKING_DIRECTORY "${PUGS_BINARY_DIR}"
      )
  endif()

  find_program(GENHTML genhtml)
  if(NOT GENHTML)
    message(WARNING "genhtml not found, cannot perform report-coverage.")
  else()
    add_custom_target(coverage-report
      ALL
      COMMAND ${CMAKE_COMMAND} -E remove_directory "${PUGS_BINARY_DIR}/coverage"
      COMMAND ${CMAKE_COMMAND} -E make_directory "${PUGS_BINARY_DIR}/coverage"
      COMMAND ${GENHTML} --demangle-cpp -q -o coverage -t "${CMAKE_PROJECT_NAME} test coverage" --ignore-errors source --legend --num-spaces 2 coverage.info
      DEPENDS coverage
      COMMENT "Building coverage html report."
      WORKING_DIRECTORY "${PUGS_BINARY_DIR}"
      )
  endif()
endif()

# ------------------- Source files --------------------
# Pugs binary
add_executable(
  pugs
  src/main.cpp)

# Libraries
target_link_libraries(
  pugs
  PugsMesh
  PugsAlgebra
  PugsAnalysis
  PugsCheckpointing
  PugsDev
  PugsUtils
  PugsLanguage
  PugsLanguageAST
  PugsLanguageModules
  PugsLanguageAlgorithms
  PugsMesh
  PugsAlgebra
  PugsScheme
  PugsUtils
  PugsOutput
  PugsLanguageUtils
  PugsCheckpointing
  Kokkos::kokkos
  ${PETSC_TARGET}
  ${SLEPC_TARGET}
  ${PARMETIS_TARGET}
  ${MPI_CXX_LINK_FLAGS} ${MPI_CXX_LIBRARIES}
  ${KOKKOS_CXX_FLAGS}
  ${OPENMP_LINK_FLAGS}
  ${PUGS_STD_LINK_FLAGS}
  ${HIGHFIVE_TARGET}
  ${SLURM_TARGET}
  stdc++fs
)

target_include_directories(pugs PUBLIC ${PETSC_INCLUDE_DIRS})
target_include_directories(pugs PUBLIC ${HDF5_INCLUDE_DIRS})

# Checkpoint management tool
add_executable(
  pugs_checkpoint
  src/utils/checkpointing/pugs_checkpoint_main.cpp
  )

target_link_libraries(
  pugs_checkpoint
  PugsCheckpointing
  PugsUtils
  PugsMesh
  PugsOutput
  PugsLanguage
  PugsLanguageAST
  PugsLanguageModules
  PugsLanguageUtils
  PugsScheme
  PugsDev
  PugsAnalysis
  PugsAlgebra
  PugsMesh
  PugsOutput
  Kokkos::kokkos
  ${PETSC_TARGET}
  ${SLEPC_TARGET}
  ${PARMETIS_TARGET}
  ${MPI_CXX_LINK_FLAGS} ${MPI_CXX_LIBRARIES}
  ${KOKKOS_CXX_FLAGS}
  ${OPENMP_LINK_FLAGS}
  ${PUGS_STD_LINK_FLAGS}
  ${HIGHFIVE_TARGET}
  ${SLURM_TARGET}
  stdc++fs
  )

# -------------------- Documentation --------------------

include(PugsDoc)

# -------------------- Doxygen --------------------------

include(PugsDoxygen)

# -------------------- Installation ---------------------
install(TARGETS
  pugs
  pugs_checkpoint
  PugsAlgebra
  PugsAnalysis
  PugsCheckpointing
  PugsDev
  PugsUtils
  PugsLanguage
  PugsLanguageAST
  PugsLanguageModules
  PugsLanguageUtils
  PugsMesh
  PugsOutput
  PugsScheme
  kokkos
  Catch2

  EXPORT "${PROJECT_NAME}Targets"

  RUNTIME DESTINATION bin
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
)

include(CMakePackageConfigHelpers)

write_basic_package_version_file(
  PugsConfigVersion.cmake
  VERSION ${PACKAGE_VERSION}
  COMPATIBILITY AnyNewerVersion
)

install(EXPORT PugsTargets
  FILE PugsTargets.cmake
  NAMESPACE Pugs::
  DESTINATION lib/cmake/pugs
)

# ------------------- Build options -------------------
message("")
message("====== pugs build options ======")
message(" version: ${PUGS_VERSION}")
message(" build type: ${CMAKE_BUILD_TYPE}")
message(" compiler: ${CMAKE_CXX_COMPILER_ID} ${CMAKE_CXX_COMPILER_VERSION}")
message(" kokkos devices: ${PUGS_BUILD_KOKKOS_DEVICES}")

if (MPI_FOUND)
  message(" MPI: ${MPI_CXX_LIBRARY_VERSION_STRING}")
else()
  if (PUGS_ENABLE_MPI MATCHES "^OFF$")
    message(" MPI: explicitly deactivated!")
  elseif((NOT DEFINED PUGS_HAS_PARMETIS) AND (NOT DEFINED PUGS_HAS_PTSCOTCH))
    message(" MPI: deactivated: ParMETIS and PTScotch cannot be found!")
  else()
    if (PUGS_ENABLE_MPI MATCHES "^(AUTO|ON)$")
      message(" MPI: not found!")
    else()
      message(" MPI: explicitly deactivated!")
    endif()
  endif()
endif()

if (PUGS_HAS_PARMETIS)
  message(" ParMETIS: ${PARMETIS_LIBRARIES}")
else()
  if (PUGS_HAS_MPI)
  message(" ParMETIS: not found!")
  else()
  message(" ParMETIS: deactivated (MPI not found)")
  endif()
endif()

if (PUGS_HAS_PTSCOTCH)
  message(" PTScotch: ${PTSCOTCH_LIBRARIES}")
else()
  if (PUGS_HAS_MPI)
  message(" PTScotch: not found!")
  else()
  message(" PTScotch: deactivated (MPI not found)")
  endif()
endif()

if (PETSC_FOUND)
  message(" PETSc: ${PETSC_VERSION}")
else()
  if (PUGS_ENABLE_PETSC MATCHES "^(AUTO|ON)$")
    message(" PETSc: not found!")
  else()
      message(" PETSc: explicitly deactivated!")
  endif()
endif()

if (SLEPC_FOUND)
  message(" SLEPc: ${SLEPC_VERSION}")
else()
  if (PUGS_ENABLE_SLEPC MATCHES "^(AUTO|ON)$")
    message(" SLEPc: not found!")
  else()
      message(" SLEPc: explicitly deactivated!")
  endif()
endif()

if (EIGEN3_FOUND)
  message(" Eigen3: ${Eigen3_VERSION}")
else()
  if (PUGS_ENABLE_EIGEN3 MATCHES "^(AUTO|ON)$")
    message(" Eigen3: not found!")
  else()
      message(" Eigen3: explicitly deactivated!")
  endif()
endif()

if (HDF5_FOUND)
  message(" HDF5: ${HDF5_VERSION} parallel: ${HDF5_IS_PARALLEL}")
else()
  if (PUGS_ENABLE_HDF5 MATCHES "^(AUTO|ON)$")
    message(" HDF5: not found!")
  else()
      message(" HDF5: explicitly deactivated!")
  endif()
endif()

if (SLURM_FOUND)
  message(" SLURM library: ${SLURM_LIBRARY}")
else()
  message(" SLURM library: not found!")
endif()

message("----------- utilities ----------")

if(CLANG_FORMAT)
  message(" clang-format: ${CLANG_FORMAT}")
else()
  message(" clang-format: not found!")
endif()

if(CLAZY_STANDALONE)
  message(" clazy-standalone: ${CLAZY_STANDALONE}")
else()
  message(" clazy-standalone: not found!")
endif()

if (DOXYGEN_FOUND)
  message(" doxygen: ${DOXYGEN_EXECUTABLE}")
else()
  message(" doxygen: not found!")
endif()

if (EMACS)
  message(" emacs: ${EMACS}")
else()
  message(" emacs: not found!")
endif()

if (GNUPLOT_FOUND)
  message(" gnuplot: ${GNUPLOT}")
else()
  message(" gnuplot: not found!")
endif()

if (GMSH)
  message(" gmsh: ${GMSH}")
else()
  message(" gmsh: not found!")
endif()

if (PYGMENTIZE)
  message(" pygmentize: ${PYGMENTIZE}")
else()
  message(" pygmentize: not found!")
endif()

if (LATEX_PDFLATEX_FOUND)
  message(" pdflatex: ${PDFLATEX_COMPILER}")
else()
  message(" pdflatex: not found!")
endif()

if (NOT EMACS OR NOT GNUPLOT_FOUND OR NOT GMSH)
  message(" ** Cannot build documentation: missing ")
elseif(NOT LATEX_PDFLATEX_FOUND OR NOT PYGMENTIZE)
  message(" ** Cannot build pdf documentation: missing")
endif()
if (NOT EMACS)
  message("    - emacs")
endif()
if (NOT GNUPLOT_FOUND)
  message("    - gnuplot")
endif()
if (NOT GMSH)
  message("    - gmsh")
endif()
if (NOT LATEX_PDFLATEX_FOUND)
  message("    - pdflatex")
endif()
if (NOT PYGMENTIZE)
  message("    - pygmentize")
endif()

message("================================")
message("")

configure_file(
  ${PUGS_SOURCE_DIR}/cmake/PugsCompileFlags.cmake.in
  ${PUGS_BINARY_DIR}/cmake/PugsCompileFlags.cmake
  @ONLY
)

install(
  FILES ${PUGS_BINARY_DIR}/cmake/PugsCompileFlags.cmake
  DESTINATION lib/cmake/pugs
)

# Ugly patch to install user headers for Catch2 (for plugins)
install(
  DIRECTORY
  "${PUGS_BINARY_DIR}/packages/Catch2/generated-includes/catch2" # Also install the generated header
  DESTINATION
  "include"
  FILES_MATCHING
  PATTERN "*.hpp"
  )