cmake_minimum_required (VERSION 3.10)

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

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

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

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

# checks for MPI
if (PUGS_ENABLE_MPI MATCHES "^(AUTO|ON)$")
  find_package(MPI)
endif()


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

if(${MPI_FOUND})
  find_package(ParMETIS)
  if (NOT PARMETIS_LIBRARIES)
    if(PUGS_ENABLE_MPI MATCHES "^AUTO$")
      message(WARNING "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()
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(WARNING "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(WARNING "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
  PugsUtils
  PugsLanguage
  PugsLanguageAST
  PugsLanguageModules
  PugsLanguageAlgorithms
  PugsMesh
  PugsUtils
  PugsLanguageUtils
  kokkos
  ${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()