# This file provides utilities for building and working with thrust
# configuration targets.
#
# THRUST_TARGETS
#  - Built by the calling the `thrust_build_target_list()` function.
#  - Each item is the name of a thrust interface target that is configured for a
#    certain combination of host/device/dialect.
#
# thrust_build_target_list()
# - Creates the THRUST_TARGETS list.
#
# The following functions can be used to test/set metadata on a thrust target:
#
# thrust_get_target_property(<prop_var> <target_name> <prop>)
#   - Checks the ${prop} target property on thrust target ${target_name}
#     and sets the ${prop_var} variable in the caller's scope.
#   - <prop_var> is any valid cmake identifier.
#   - <target_name> is the name of a thrust target.
#   - <prop> is one of the following:
#     - HOST: The host system. Valid values: CPP, OMP, TBB.
#     - DEVICE: The device system. Valid values: CUDA, CPP, OMP, TBB.
#     - DIALECT: The C++ dialect. Valid values: 11, 14, 17.
#     - PREFIX: A unique prefix that should be used to name all
#       targets/tests/examples that use this configuration.
#
# thrust_get_target_properties(<target_name>)
#   - Defines ${target_name}_${prop} in the caller's scope, for `prop` in:
#     HOST, DEVICE, DIALECT, PREFIX. See above for details.
#
# thrust_clone_target_properties(<dst_target> <src_target>)
#   - Set the HOST, DEVICE, DIALECT, PREFIX metadata on ${dst_target} to match
#     ${src_target}. See above for details.
#   - This *MUST* be called on any targets that link to another thrust target
#     to ensure that dialect information is updated correctly, e.g.
#     `thrust_clone_target_properties(${my_thrust_test} ${some_thrust_target})`

define_property(TARGET PROPERTY _THRUST_HOST
  BRIEF_DOCS "A target's host system: CPP, TBB, or OMP."
  FULL_DOCS "A target's host system: CPP, TBB, or OMP."
)
define_property(TARGET PROPERTY _THRUST_DEVICE
  BRIEF_DOCS "A target's device system: CUDA, CPP, TBB, or OMP."
  FULL_DOCS "A target's device system: CUDA, CPP, TBB, or OMP."
)
define_property(TARGET PROPERTY _THRUST_DIALECT
  BRIEF_DOCS "A target's C++ dialect: 11, 14, or 17."
  FULL_DOCS "A target's C++ dialect: 11, 14, or 17."
)
define_property(TARGET PROPERTY _THRUST_PREFIX
  BRIEF_DOCS "A prefix describing the config, eg. 'thrust.cpp.cuda.cpp14'."
  FULL_DOCS "A prefix describing the config, eg. 'thrust.cpp.cuda.cpp14'."
)

function(thrust_set_target_properties target_name host device dialect prefix)
  set_target_properties(${target_name}
    PROPERTIES
      _THRUST_HOST ${host}
      _THRUST_DEVICE ${device}
      _THRUST_DIALECT ${dialect}
      _THRUST_PREFIX ${prefix}
  )

  get_target_property(type ${target_name} TYPE)
  if (NOT ${type} STREQUAL "INTERFACE_LIBRARY")
    set_target_properties(${target_name}
      PROPERTIES
        CXX_STANDARD ${dialect}
        CUDA_STANDARD ${dialect}
        # Must manually request that the standards above are actually respected
        # or else CMake will silently fail to configure the targets correctly...
        # Note that this doesn't actually work as of CMake 3.16:
        # https://gitlab.kitware.com/cmake/cmake/-/issues/20953
        # We'll leave these properties enabled in hopes that they will someday
        # work.
        CXX_STANDARD_REQUIRED ON
        CUDA_STANDARD_REQUIRED ON
        ARCHIVE_OUTPUT_DIRECTORY "${THRUST_LIBRARY_OUTPUT_DIR}"
        LIBRARY_OUTPUT_DIRECTORY "${THRUST_LIBRARY_OUTPUT_DIR}"
        RUNTIME_OUTPUT_DIRECTORY "${THRUST_EXECUTABLE_OUTPUT_DIR}"
    )

    # CMake still emits errors about empty CUDA_ARCHITECTURES when CMP0104
    # is set to OLD. This suppresses the errors for good.
    if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.18)
      set_target_properties(${target_name}
        PROPERTIES
          CUDA_ARCHITECTURES OFF
      )
    endif()

    if ("CUDA" STREQUAL "${device}" AND
        "Feta" STREQUAL "${CMAKE_CUDA_COMPILER_ID}")
      set_target_properties(${target_name} PROPERTIES
        CUDA_RESOLVE_DEVICE_SYMBOLS OFF
      )
    endif()
  endif()
endfunction()

# Get a thrust property from a target and store it in var_name
# thrust_get_target_property(<var_name> <target_name> [HOST|DEVICE|DIALECT|PREFIX]
macro(thrust_get_target_property prop_var target_name prop)
  get_property(${prop_var} TARGET ${target_name} PROPERTY _THRUST_${prop})
endmacro()

# Defines the following string variables in the caller's scope:
# - ${target_name}_HOST
# - ${target_name}_DEVICE
# - ${target_name}_DIALECT
# - ${target_name}_PREFIX
macro(thrust_get_target_properties target_name)
  thrust_get_target_property(${target_name}_HOST ${target_name} HOST)
  thrust_get_target_property(${target_name}_DEVICE ${target_name} DEVICE)
  thrust_get_target_property(${target_name}_DIALECT ${target_name} DIALECT)
  thrust_get_target_property(${target_name}_PREFIX ${target_name} PREFIX)
endmacro()

# Set one target's THRUST_* properties to match another target
function(thrust_clone_target_properties dst_target src_target)
  thrust_get_target_properties(${src_target})
  thrust_set_target_properties(${dst_target}
    ${${src_target}_HOST}
    ${${src_target}_DEVICE}
    ${${src_target}_DIALECT}
    ${${src_target}_PREFIX}
  )
endfunction()

# Set ${var_name} to TRUE or FALSE in the caller's scope
function(_thrust_is_config_valid var_name host device dialect)
  if (THRUST_MULTICONFIG_ENABLE_SYSTEM_${host} AND
      THRUST_MULTICONFIG_ENABLE_SYSTEM_${device} AND
      THRUST_MULTICONFIG_ENABLE_DIALECT_CPP${dialect} AND
      "${host}_${device}" IN_LIST THRUST_MULTICONFIG_WORKLOAD_${THRUST_MULTICONFIG_WORKLOAD}_CONFIGS)
    set(${var_name} TRUE PARENT_SCOPE)
  else()
    set(${var_name} FALSE PARENT_SCOPE)
  endif()
endfunction()

function(_thrust_init_target_list)
  set(THRUST_TARGETS "" CACHE INTERNAL "" FORCE)
endfunction()

function(_thrust_add_target_to_target_list target_name host device dialect prefix)
  thrust_set_target_properties(${target_name} ${host} ${device} ${dialect} ${prefix})

  target_link_libraries(${target_name} INTERFACE
    thrust.compiler_interface
  )

  # Workaround Github issue #1174. cudafe promote TBB header warnings to
  # errors, even when they're -isystem includes.
  if ((NOT host STREQUAL "TBB") OR (NOT device STREQUAL "CUDA"))
    target_link_libraries(${target_name} INTERFACE
      thrust.promote_cudafe_warnings
    )
  endif()

  set(THRUST_TARGETS ${THRUST_TARGETS} ${target_name} CACHE INTERNAL "" FORCE)

  set(label "${host}.${device}.cpp${dialect}")
  string(TOLOWER "${label}" label)
  message(STATUS "Enabling Thrust configuration: ${label}")
endfunction()

function(_thrust_build_target_list_multiconfig)
  # Find thrust and all of the required systems:
  set(req_systems)
  if (THRUST_MULTICONFIG_ENABLE_SYSTEM_CUDA)
    list(APPEND req_systems CUDA)
  endif()
  if (THRUST_MULTICONFIG_ENABLE_SYSTEM_CPP)
    list(APPEND req_systems CPP)
  endif()
  if (THRUST_MULTICONFIG_ENABLE_SYSTEM_TBB)
    list(APPEND req_systems TBB)
  endif()
  if (THRUST_MULTICONFIG_ENABLE_SYSTEM_OMP)
    list(APPEND req_systems OMP)
  endif()

  find_package(Thrust REQUIRED CONFIG
    NO_DEFAULT_PATH # Only check the explicit path in HINTS:
    HINTS "${Thrust_SOURCE_DIR}"
    COMPONENTS ${req_systems}
  )

  # This must be called after backends are loaded but
  # before _thrust_add_target_to_target_list.
  thrust_build_compiler_targets()

  # Build THRUST_TARGETS
  foreach(host IN LISTS THRUST_HOST_SYSTEM_OPTIONS)
    foreach(device IN LISTS THRUST_DEVICE_SYSTEM_OPTIONS)
      foreach(dialect IN LISTS THRUST_CPP_DIALECT_OPTIONS)
        _thrust_is_config_valid(config_valid ${host} ${device} ${dialect})
        if (config_valid)
          set(prefix "thrust.${host}.${device}.cpp${dialect}")
          string(TOLOWER "${prefix}" prefix)

          # Configure a thrust interface target for this host/device
          set(target_name "${prefix}")
          thrust_create_target(${target_name}
            HOST ${host}
            DEVICE ${device}
            ${THRUST_TARGET_FLAGS}
          )

          # Set configuration metadata for this thrust interface target:
          _thrust_add_target_to_target_list(${target_name}
            ${host} ${device} ${dialect} ${prefix}
          )

          # Create a meta target for all targets in this configuration:
          add_custom_target(${prefix}.all)
          add_dependencies(thrust.all ${prefix}.all)
        endif()
      endforeach() # dialects
    endforeach() # devices
  endforeach() # hosts

  list(LENGTH THRUST_TARGETS count)
  message(STATUS "${count} unique thrust.host.device.dialect configurations generated")
endfunction()

function(_thrust_build_target_list_singleconfig)
  find_package(Thrust REQUIRED CONFIG
    NO_DEFAULT_PATH # Only check the explicit path in HINTS:
    HINTS "${Thrust_SOURCE_DIR}"
  )
  thrust_create_target(thrust FROM_OPTIONS ${THRUST_TARGET_FLAGS})
  thrust_debug_target(thrust "${THRUST_VERSION}")

  set(host ${THRUST_HOST_SYSTEM})
  set(device ${THRUST_DEVICE_SYSTEM})
  set(dialect ${THRUST_CPP_DIALECT})
  set(prefix "thrust") # single config

  # This depends on the backends loaded by thrust_create_target, and must
  # be called before _thrust_add_target_to_target_list.
  thrust_build_compiler_targets()

  _thrust_add_target_to_target_list(thrust ${host} ${device} ${dialect} ${prefix})
endfunction()

# Build a ${THRUST_TARGETS} list containing target names for all
# requested configurations
function(thrust_build_target_list)
  # Clear the list of targets:
  _thrust_init_target_list()

  # Generic config flags:
  set(THRUST_TARGET_FLAGS)
  macro(add_flag_option flag docstring default)
    set(opt "THRUST_${flag}")
    option(${opt} "${docstring}" "${default}")
    mark_as_advanced(${opt})
    if (${${opt}})
      list(APPEND THRUST_TARGET_FLAGS ${flag})
    endif()
  endmacro()
  add_flag_option(IGNORE_DEPRECATED_CPP_DIALECT "Don't warn about any deprecated C++ standards and compilers." OFF)
  add_flag_option(IGNORE_DEPRECATED_CPP_11 "Don't warn about deprecated C++11." OFF)
  add_flag_option(IGNORE_DEPRECATED_COMPILER "Don't warn about deprecated compilers." OFF)
  add_flag_option(IGNORE_CUB_VERSION_CHECK "Don't warn about mismatched CUB versions." OFF)

  # Top level meta-target. Makes it easier to just build thrust targets when
  # building both CUB and Thrust. Add all project files here so IDEs will be
  # aware of them. This will not generate build rules.
  file(GLOB_RECURSE all_sources
    RELATIVE "${CMAKE_CURRENT_LIST_DIR}"
    "${Thrust_SOURCE_DIR}/thrust/*.h"
    "${Thrust_SOURCE_DIR}/thrust/*.inl"
  )
  add_custom_target(thrust.all SOURCES ${all_sources})

  if (THRUST_ENABLE_MULTICONFIG)
    _thrust_build_target_list_multiconfig()
  else()
    _thrust_build_target_list_singleconfig()
  endif()
endfunction()