# Adds the requires compiler & linker flags to enable coverage reporting
#
# @arg TARGET The target to add the flags to.
function(add_coverage_flags TARGET)
  if(CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU")
    target_compile_options(${TARGET} PRIVATE -fprofile-arcs -ftest-coverage)
    target_link_libraries(${TARGET} PRIVATE gcov)
    target_link_options(${TARGET} PRIVATE --coverage)
  elseif(CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang"
         OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang")
    target_compile_options(${TARGET} PRIVATE -fprofile-arcs -ftest-coverage)
    target_link_libraries(${TARGET} PRIVATE gcov)
    target_link_options(${TARGET} PRIVATE --coverage)
  else()
    message(WARNING "Code coverage is only supported for GNU/Clang compilers")
  endif()
endfunction()

# Add a CTest test for validating test coverage
#
# @arg NAME The name for the test
# @param ROOT The root directory to search for source files (default:
#   ${PROJECT_SOURCE_DIR}).
# @param OBJ The directory to search for object files (default:
#   ${PROJECT_BINARY_DIR}).
# @param LINE The percentage of line coverage that the test requires in order
#   to pass.
# @param BRANCH The percentage of branch coverage that the test requires in
#   order to pass.
# @param LIST The name of a test list that this CTest is dependent on, This is
#   useful for GoogleTest or Catch2 which will generate a variable container a
#   list of the autodetected tests `<TARGET>_TESTS`.
# @group ARGS Additional arguments to pass to `gcovr`.
# @group EXCLUDE A list of exclude patterns to exclude from the coverage
#   collection.
function(add_coverage_test NAME)
  set(options)
  set(single_value_args ROOT OBJ LINE BRANCH LIST)
  set(multi_value_args ARGS EXCLUDE)
  cmake_parse_arguments("" "${options}" "${single_value_args}"
                        "${multi_value_args}" ${ARGN})
  if(NOT GCOVR_FOUND)
    return()
  endif()

  set(gcovr_base_cmd "${GCOVR_EXECUTABLE}")
  if((CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU"))
    if(GCOV_FOUND)
      list(APPEND gcovr_base_cmd --gcov-executable "${GCOV_EXECUTABLE}")
    else()
      message(WARNING "GNU coverage requires the gcov executable")
      return()
    endif()
  elseif((CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang"
          OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang"))
    if(GCOV_FOUND AND LLVMCOV_FOUND)
      list(APPEND gcovr_base_cmd --gcov-executable "${LLVMCOV_EXECUTABLE} gcov")
    else()
      message(
        WARNING "Clang coverage requires the llvmcov and gcov executables")
      return()
    endif()
  else()
    message(WARNING "Code coverage is only supported for GNU/Clang compilers")
    return()
  endif()

  if(NOT _ROOT)
    set(_ROOT "${PROJECT_SOURCE_DIR}")
  endif()
  if(NOT _OBJ)
    set(_OBJ "${PROJECT_BINARY_DIR}")
  endif()
  list(APPEND gcovr_base_cmd --root "${_ROOT}")
  list(APPEND gcovr_base_cmd --object-directory "${_OBJ}")
  foreach(exclude ${_EXCLUDE})
    list(APPEND gcovr_base_cmd -e "${exclude}")
  endforeach()

  set(gcovr_xml_cmd ${gcovr_base_cmd} "--xml" "--output"
                    "${CMAKE_CURRENT_BINARY_DIR}/${NAME}-coverage.xml")
  set(gcovr_summary_cmd ${gcovr_base_cmd})
  if(_LINE)
    list(APPEND gcovr_summary_cmd --fail-under-line ${_LINE})
  endif()
  if(_BRANCH)
    list(APPEND gcovr_summary_cmd --fail-under-branch ${_BRANCH})
  endif()
  string(SHA1 hash "${NAME}")
  string(SUBSTRING "${hash}" 0 7 hash)
  set(ctest_include_file
      "${CMAKE_CURRENT_BINARY_DIR}/${NAME}_include-${hash}.cmake")

  set(summary_cmd "")
  foreach(arg ${gcovr_summary_cmd} ${_ARGS})
    set(summary_cmd "${summary_cmd} \"${arg}\"")
  endforeach()

  set(xml_cmd "")
  foreach(arg ${gcovr_xml_cmd} ${_ARGS})
    set(xml_cmd "${xml_cmd} \"${arg}\"")
  endforeach()
  file(
    WRITE "${ctest_include_file}"
    "if(${_LIST})\n"
    "  add_test(${NAME}-XML ${xml_cmd})\n"
    "  add_test(${NAME} ${summary_cmd})\n"
    "  set_tests_properties(${NAME}-XML PROPERTIES DEPENDS \${${_LIST}} \
    WORKING_DIRECTORY \"${CMAKE_CURRENT_BINARY_DIR}\")\n"
    "  set_tests_properties(${NAME} PROPERTIES DEPENDS \${${_LIST}} \
    WORKING_DIRECTORY \"${CMAKE_CURRENT_BINARY_DIR}\")\n"
    "endif()\n")
  set_property(
    DIRECTORY
    APPEND
    PROPERTY TEST_INCLUDE_FILES "${ctest_include_file}")
endfunction()

# Add a utility target for generating test coverage reports
#
# @arg NAME The name for the target.
# @flag DISABLED Explicitly disables the target generation.
# @flag ENABLED Explicitly enables the target generation.
# @param TYPE Define the reporter type. This modifies the arguments to generate
#   the correct reporter. Choices are `TEXT`, `HTML`, `XML`, `JSON`,
#   `JSONSUMMARY`, and `CSV`.
# @param OUTPUT Set the output directory for the generated reporter (default:
#   `${CMAKE_CURRENT_SOURCE_DIR}/<TYPE>/index.<EXT>`).
# @param COMMENT Set a custom comment for the target (default: `Collecting test
#   coverage report`)
# @param ROOT The root directory to search for source files (default:
#   `${PROJECT_SOURCE_DIR}`).
# @param OBJ The directory to search for object files (default:
#   `${PROJECT_BINARY_DIR}`).
# @group ARGS Additional arguments to pass to `gcovr`.
# @group EXCLUDE A list of exclude patterns to exclude from the coverage
#   collection.
# @group DEPENDS A list of CMake targets or files that the target will be
#   dependent on.
function(add_coverage_target NAME)
  set(options DISABLED ENABLED)
  set(single_value_args TYPE OUTPUT COMMENT ROOT OBJ)
  set(multi_value_args ARGS EXCLUDE DEPENDS)
  cmake_parse_arguments("" "${options}" "${single_value_args}"
                        "${multi_value_args}" ${ARGN})
  if(_DISABLED)
    return()
  endif()
  if(NOT GCOVR_FOUND)
    return()
  endif()

  if("${_TYPE}" STREQUAL "TEXT")
    list(APPEND _ARGS "--print-summary")
  elseif("${_TYPE}" STREQUAL "HTML")
    list(APPEND _ARGS "--html" "--html-details")
  elseif("${_TYPE}" STREQUAL "XML")
    list(APPEND _ARGS "--xml" "--xml-pretty")
  elseif("${_TYPE}" STREQUAL "JSON")
    list(APPEND _ARGS "--json" "--json-pretty")
  elseif("${_TYPE}" STREQUAL "JSONSUMMARY")
    list(APPEND _ARGS "--json-summary" "--json-summary-pretty")
  elseif("${_TYPE}" STREQUAL "CSV")
    list(APPEND _ARGS "--csv")
  endif()

  if(NOT _COMMENT)
    set(_COMMENT "Collecting test coverage report")
  endif()

  set(gcovr_cmd "${GCOVR_EXECUTABLE}")
  if((CMAKE_C_COMPILER_ID MATCHES "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "GNU"))
    if(GCOV_FOUND)
      list(APPEND gcovr_cmd --gcov-executable "${GCOV_EXECUTABLE}")
    else()
      message(WARNING "GNU coverage requires the gcov executable")
      return()
    endif()
  elseif((CMAKE_C_COMPILER_ID MATCHES "(Apple)?[Cc]lang"
          OR CMAKE_CXX_COMPILER_ID MATCHES "(Apple)?[Cc]lang"))
    if(GCOV_FOUND AND LLVMCOV_FOUND)
      list(APPEND gcovr_cmd --gcov-executable "${LLVMCOV_EXECUTABLE} gcov")
    else()
      message(
        WARNING "Clang coverage requires the llvmcov and gcov executables")
      return()
    endif()
  else()
    message(WARNING "Code coverage is only supported for GNU/Clang compilers")
    return()
  endif()

  if(NOT _ROOT)
    set(_ROOT "${PROJECT_SOURCE_DIR}")
  endif()
  if(NOT _OBJ)
    set(_OBJ "${PROJECT_BINARY_DIR}")
  endif()
  list(APPEND gcovr_cmd --root "${_ROOT}")
  list(APPEND gcovr_cmd --object-directory "${_OBJ}")
  foreach(exclude ${_EXCLUDE})
    list(APPEND gcovr_cmd -e "${exclude}")
  endforeach()

  if(NOT _OUTPUT)
    if("${_TYPE}" STREQUAL "HTML")
      set(_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/coverage/html/index.html")
    elseif("${_TYPE}" STREQUAL "XML")
      set(_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/coverage/xml/index.xml")
    elseif("${_TYPE}" STREQUAL "JSON")
      set(_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/coverage/json/index.json")
    elseif("${_TYPE}" STREQUAL "JSONSUMMARY")
      set(_OUTPUT
          "${CMAKE_CURRENT_BINARY_DIR}/coverage/json_summary/index.json")
    elseif("${_TYPE}" STREQUAL "CSV")
      set(_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/coverage/csv/index.csv")
    endif()
  endif()

  if(_OUTPUT)
    get_filename_component(dirname "${_OUTPUT}" DIRECTORY)
    add_custom_target(
      ${NAME}
      COMMAND ${CMAKE_COMMAND} -E make_directory "${dirname}"
      COMMAND ${gcovr_cmd} ${_ARGS} --output ${_OUTPUT}
      WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
      VERBATIM
      COMMENT "${_COMMENT}"
      DEPENDS ${_DEPENDS})
  else()
    add_custom_target(
      ${NAME}
      COMMAND ${gcovr_cmd} ${_ARGS}
      WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
      VERBATIM
      COMMENT "${_COMMENT}"
      DEPENDS ${_DEPENDS})
  endif()
endfunction()

set(_reporters "Text" "Html" "Xml" "Json" "JsonSummary" "Csv")

# Enable test coverage reporting for the provided CMake target.
#
# For `EXECUTABLE` targets, this will add the necessary compiler and linker
# flags, as well as generate targets for collecting the coverage reports. It
# can also create a CTest target for testing the code coverage.
#
# For `<TYPE>_LIBRARY` targets, this will only configure the required compiler
# and linker flags.
#
# @arg TARGET The target to configure code coverage for.
# @param NAME The name for the target.
# @param DEFAULT The default reporter (default: `TEXT`).
# @param ROOT The root directory to search for source files (default:
#   `${PROJECT_SOURCE_DIR}`).
# @param OBJ The directory to search for object files (default:
#   `${PROJECT_BINARY_DIR}`).
# @group TEST Additional configuration for generating a CTest test.
#
#   @param LINE The percentage of line coverage that the test requires in order
#     to pass.
#   @param BRANCH The percentage of branch coverage that the test requires in
#     order to pass.
#   @param LIST The name of a test list that this CTest is dependent on, This is
#     useful for GoogleTest or Catch2 which will generate a variable container a
#     list of the autodetected tests `<TARGET>_TESTS`.
#   @group ARGS Additional arguments to pass to `gcovr`.
# @group <FORMAT> Additional fields to control the generated reporter targets.
#   `<FORMAT>` can be any one of `TEXT`, `HTML`, `XML`, `JSON`, `JSONSUMMARY`,
#   or `CSV`.
#
#   @flag DISABLED Explicitly disables the target generation.
#   @flag ENABLED Explicitly enables the target generation.
#   @param OUTPUT Set the output directory for the generated reporter (default:
#     `${CMAKE_CURRENT_SOURCE_DIR}/<TYPE>/index.<EXT>`).
#   @param COMMENT Set a custom comment for the target (default: `Collecting
#     test coverage report`)
# @group EXCLUDE A list of exclude patterns to exclude from the coverage
#   collection.
# @group DEPENDS A list of CMake targets or files that the target will be
#   dependent on.
function(add_test_coverage TARGET)
  set(options)
  set(single_value_args DEFAULT NAME ROOT OBJ)
  set(multi_value_args TEST EXCLUDE DEPENDS)
  foreach(reporter ${_reporters})
    string(TOUPPER "${reporter}" upper)
    list(APPEND multi_value_args ${upper})
  endforeach()
  cmake_parse_arguments("" "${options}" "${single_value_args}"
                        "${multi_value_args}" ${ARGN})

  if(NOT TARGET "${TARGET}")
    message(FATAL_ERROR "add_test_coverage requires a valid target")
  endif()

  if(NOT _DEFAULT)
    set(_DEFAULT "TEXT")
  endif()
  if(NOT _NAME)
    set(_NAME "${TARGET}Cov")
  endif()

  get_target_property(type ${TARGET} TYPE)
  if("${type}" MATCHES "EXECUTABLE")
    add_coverage_flags(${TARGET})

    add_coverage_target(
      "${_NAME}"
      TYPE ${_DEFAULT}
      ROOT "${_ROOT}"
      OBJ "${_OBJ}" ${_${_DEFAULT}}
      EXCLUDE ${_EXCLUDE}
      DEPENDS ${_DEPENDS} ${TARGET})
    foreach(reporter ${_reporters})
      string(TOUPPER "${reporter}" upper)
      if(_${upper})
        add_coverage_target(
          "${_NAME}${reporter}"
          TYPE ${upper}
          ROOT "${_ROOT}"
          OBJ "${_OBJ}" ${_${upper}}
          EXCLUDE ${_EXCLUDE}
          DEPENDS ${_DEPENDS} ${TARGET})
      endif()
    endforeach()

    if(_TEST)
      add_coverage_test(
        "${_NAME}"
        ROOT "${_ROOT}"
        OBJ "${_OBJ}" ${_TEST}
        EXCLUDE "${_EXCLUDE}")
    endif()
  elseif("${type}" MATCHES "STATIC_LIBRARY|SHARED_LIBRARY|OBJECT_LIBRARY")
    add_coverage_flags(${TARGET})
  else()
    message(WARNING "Cannot enable coverage on target of type \"${type}\"")
  endif()
endfunction()
