cmake_minimum_required (VERSION 3.10)

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

# Forbids in-source builds
include(CheckNotInSources)

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

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

# 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})

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

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

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

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

# Change RelWithDebInfo to compile assertions
SET("CMAKE_CXX_FLAGS_RELWITHDEBINFO"
   "-g -O2"
  CACHE STRING "Flags used by the compiler during release builds with debug info and assertions"
  FORCE )
SET("CMAKE_C_FLAGS_RELWITHDEBINFO"
   "-g -O2"
  CACHE STRING "Flags used by the compiler during release builds with debug info and assertions"
  FORCE )

# 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 "(Debug|Release|RelWithDebInfo|MinSizeRel|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 RelWIthDebInfo
if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING
      "Choose the type of build: Debug Release RelWithDebInfo MinSizeRel 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 "8.0.0")
set(CLANG_CXX_MIN_VERSION "8.0.0")

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

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} -Wsign-compare -Wunused -Wunused-member-function -Wunused-private-field")
endif()

if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
  if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS "9.0.0")
    set(PUGS_STD_LINK_FLAGS "-lstdc++fs")
  endif()
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

if(${MPI_FOUND})
  find_package(ParMETIS)
  if (NOT PARMETIS_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 (${CMAKE_VERSION} VERSION_GREATER "3.12")
    cmake_policy(SET CMP0075 OLD)
  endif()
  find_package(PETSc)
  set(PUGS_HAS_PETSC ${PETSC_FOUND})
else()
  unset(PUGS_HAS_PETSC)
endif()

if (${PETSC_FOUND})
  include_directories(SYSTEM ${PETSC_INCLUDES})
else()
  if (PUGS_ENABLE_PETSC MATCHES "^ON$")
    message(FATAL_ERROR "Could not find PETSc!")
  endif()
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 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++ 17 flags
set(CMAKE_CXX_STANDARD "17")

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

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

# 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_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}")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} ${PUGS_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")
  set(CMAKE_CXX_FLAGS_RELWITHDEBINFO "${CMAKE_CXX_FLAGS_RELWITHDEBINFO} -D_GLIBCXX_DEBUG -D_LIBCPP_DEBUG=1")
endif()

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

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

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

# 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")
include("${CATCH_MODULE_PATH}/contrib/ParseAndAddCatchTests.cmake")
add_subdirectory("${CATCH_MODULE_PATH}")

add_subdirectory(tests)
enable_testing()

add_custom_target(run_unit_tests
  COMMAND ${CMAKE_CTEST_COMMAND}
  DEPENDS unit_tests mpi_unit_tests
  COMMENT "Executing unit tests."
  )

# unit tests coverage

if("${CMAKE_BUILD_TYPE}" STREQUAL "Coverage")
  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)

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

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

      # zero all counters
      COMMAND ${FASTCOV} -q -z

      # Run tests
      COMMAND ${CMAKE_CTEST_COMMAND}

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

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

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

  else()

    add_custom_target(coverage
      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
      # Run tests
      COMMAND ${CMAKE_CTEST_COMMAND}
      # 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 unit_tests mpi_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()

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

link_libraries("-rdynamic")

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

# Libraries
target_link_libraries(
  pugs
  PugsMesh
  PugsAlgebra
  PugsUtils
  PugsLanguage
  PugsLanguageAST
  PugsLanguageModules
  PugsLanguageAlgorithms
  PugsMesh
  PugsAlgebra
  PugsUtils
  PugsLanguageUtils
  kokkos
  ${PETSC_LIBRARIES}
  ${PARMETIS_LIBRARIES}
  ${MPI_CXX_LINK_FLAGS} ${MPI_CXX_LIBRARIES}
  ${KOKKOS_CXX_FLAGS}
  ${OPENMP_LINK_FLAGS}
  ${PUGS_STD_LINK_FLAGS}
  stdc++fs
  )

# ---------------------- Doxygen ----------------------
include(PugsDoxygen)

# ------------------- Installation --------------------
# temporary version workaround
if(${CMAKE_VERSION} VERSION_LESS "3.13.0")
  install(TARGETS pugs
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib)
else()
  install(TARGETS pugs PugsMesh PugsUtils PugsLanguage
    RUNTIME DESTINATION bin
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib)
endif()

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(NOT PARMETIS_LIBRARIES)
    message(" MPI: deactivated: ParMETIS cannot be found!")
  else()
    if (PUGS_ENABLE_MPI MATCHES "^(AUTO|ON)$")
      message(" MPI: not found!")
    else()
      message(" MPI: explicitly deactivated!")
    endif()
  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(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: no found!")
endif()

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

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