##===----------------------------------------------------------------------===##
#
# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
# See https://llvm.org/LICENSE.txt for license information.
# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#
##===----------------------------------------------------------------------===##
#
# Find OpenMP Target offloading Support for various compilers.
#
##===----------------------------------------------------------------------===##

#[========================================================================[.rst:
FindOpenMPTarget
----------------

Finds OpenMP Target Offloading Support.

This module can be used to detect OpenMP target offloading support in a
compiler. If the compiler support OpenMP Offloading to a specified target, the
flags required to compile offloading code to that target are output for each
target.

This module will automatically include OpenMP support if it was not loaded
already. It does not need to be included separately to get full OpenMP support.

Variables
^^^^^^^^^

The module exposes the components ``NVPTX`` and ``AMDGPU``.  Each of these
controls the various offloading targets to search OpenMP target offloasing
support for.

Depending on the enabled components the following variables will be set:

``OpenMPTarget_FOUND``
  Variable indicating that OpenMP target offloading flags for all requested
  targets have been found.

This module will set the following variables per language in your
project, where ``<device>`` is one of NVPTX or AMDGPU

``OpenMPTarget_<device>_FOUND``
  Variable indicating if OpenMP support for the ``<device>`` was detected.
``OpenMPTarget_<device>_FLAGS``
  OpenMP compiler flags for offloading to ``<device>``, separated by spaces.

For linking with OpenMP code written in ``<device>``, the following
variables are provided:

``OpenMPTarget_<device>_LIBRARIES``
  A list of libraries needed to link with OpenMP code written in ``<lang>``.

Additionally, the module provides :prop_tgt:`IMPORTED` targets:

``OpenMPTarget::OpenMPTarget_<device>``
  Target for using OpenMP offloading to ``<device>``.

If the specific architecture of the target is needed, it can be manually
specified by setting a variable to the desired architecture. Variables can also
be used to override the standard flag searching for a given compiler.

``OpenMPTarget_<device>_ARCH``
  Sets the architecture of ``<device>`` to compile for. Such as `sm_70` for NVPTX
  or `gfx908` for AMDGPU. 

``OpenMPTarget_<device>_DEVICE``
  Sets the name of the device to offload to.

``OpenMPTarget_<device>_FLAGS``
  Sets the compiler flags for offloading to ``<device>``.

#]========================================================================]

# TODO: Support Fortran
# TODO: Support multiple offloading targets by setting the "OpenMPTarget" target
#       to include flags for all components loaded
# TODO: Configure target architecture without a variable (component NVPTX_SM_70)
# TODO: Test more compilers

cmake_policy(PUSH)
cmake_policy(VERSION 3.20.0)

find_package(OpenMP ${OpenMPTarget_FIND_VERSION} REQUIRED)

# Find the offloading flags for each compiler.
function(_OPENMP_TARGET_DEVICE_FLAG_CANDIDATES LANG DEVICE)
  if(NOT OpenMPTarget_${LANG}_FLAGS)
    unset(OpenMPTarget_FLAG_CANDIDATES)

    set(OMPTarget_FLAGS_Clang "-fopenmp-targets=${DEVICE}")
    set(OMPTarget_FLAGS_GNU "-foffload=${DEVICE}=\"-lm -latomic\"")
    set(OMPTarget_FLAGS_XL "-qoffload")
    set(OMPTarget_FLAGS_PGI "-mp=${DEVICE}")
    set(OMPTarget_FLAGS_NVHPC "-mp=${DEVICE}")

    if(DEFINED OMPTarget_FLAGS_${CMAKE_${LANG}_COMPILER_ID})
      set(OpenMPTarget_FLAG_CANDIDATES "${OMPTarget_FLAGS_${CMAKE_${LANG}_COMPILER_ID}}")
    endif()

    set(OpenMPTarget_${LANG}_FLAG_CANDIDATES "${OpenMPTarget_FLAG_CANDIDATES}" PARENT_SCOPE)
  else()
    set(OpenMPTarget_${LANG}_FLAG_CANDIDATES "${OpenMPTarget_${LANG}_FLAGS}" PARENT_SCOPE)
  endif()
endfunction()

# Get the coded name of the device for each compiler.
function(_OPENMP_TARGET_DEVICE_CANDIDATES LANG DEVICE)
  if (NOT OpenMPTarget_${DEVICE}_DEVICE)
    unset(OpenMPTarget_DEVICE_CANDIDATES)

    # Check each supported device.
    if("${DEVICE}" STREQUAL "NVPTX")
      if ("${CMAKE_SIZEOF_VOID_P}" STREQUAL "4")
        set(OMPTarget_DEVICE_Clang "nvptx32-nvidia-cuda")
      else()
        set(OMPTarget_DEVICE_Clang "nvptx64-nvidia-cuda")
      endif()
      set(OMPTarget_DEVICE_GNU "nvptx-none")
      set(OMPTarget_DEVICE_XL "")
      set(OMPTarget_DEVICE_PGI "gpu")
      set(OMPTarget_DEVICE_NVHPC "gpu")

      if(DEFINED OMPTarget_DEVICE_${CMAKE_${LANG}_COMPILER_ID})
        set(OpenMPTarget_DEVICE_CANDIDATES "${OMPTarget_DEVICE_${CMAKE_${LANG}_COMPILER_ID}}")
      endif()
    elseif("${DEVICE}" STREQUAL "AMDGPU")
      set(OMPTarget_DEVICE_Clang "amdgcn-amd-amdhsa")
      set(OMPTarget_DEVICE_GNU "hsa")

      if(DEFINED OMPTarget_DEVICE_${CMAKE_${LANG}_COMPILER_ID})
        set(OpenMPTarget_DEVICE_CANDIDATES "${OMPTarget_DEVICE_${CMAKE_${LANG}_COMPILER_ID}}")
      endif()
    endif()
    set(OpenMPTarget_${LANG}_DEVICE_CANDIDATES "${OpenMPTarget_DEVICE_CANDIDATES}" PARENT_SCOPE)
  else()
    set(OpenMPTarget_${LANG}_DEVICE_CANDIDATES "${OpenMPTarget_${LANG}_DEVICE}" PARENT_SCOPE)
  endif()
endfunction()

# Get flags for setting the device's architecture for each compiler.
function(_OPENMP_TARGET_DEVICE_ARCH_CANDIDATES LANG DEVICE DEVICE_FLAG)
  if(OpenMPTarget_${DEVICE}_ARCH)
    # Only Clang supports selecting the architecture for now.
    set(OMPTarget_ARCH_Clang "-Xopenmp-target=${DEVICE_FLAG} -march=${OpenMPTarget_${DEVICE}_ARCH}")

    if(DEFINED OMPTarget_ARCH_${CMAKE_${LANG}_COMPILER_ID})
      set(OpenMPTarget_DEVICE_ARCH_CANDIDATES "${OMPTarget_ARCH_${CMAKE_${LANG}_COMPILER_ID}}")
    endif()
    set(OpenMPTarget_${LANG}_DEVICE_ARCH_CANDIDATES "${OpenMPTarget_DEVICE_ARCH_CANDIDATES}" PARENT_SCOPE)
  else()
    set(OpenMPTarget_${LANG}_DEVICE_ARCH_CANDIDATES "" PARENT_SCOPE)
  endif()
endfunction()

set(OpenMPTarget_C_CXX_TEST_SOURCE
"#include <omp.h>
int main(void) {
  int isHost;
#pragma omp target map(from: isHost)
  { isHost = omp_is_initial_device(); }
  return isHost;
}")

function(_OPENMP_TARGET_WRITE_SOURCE_FILE LANG SRC_FILE_CONTENT_VAR SRC_FILE_NAME SRC_FILE_FULLPATH)
  set(WORK_DIR ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/FindOpenMPTarget)
  if("${LANG}" STREQUAL "C")
    set(SRC_FILE "${WORK_DIR}/${SRC_FILE_NAME}.c")
    file(WRITE "${SRC_FILE}" "${OpenMPTarget_C_CXX_${SRC_FILE_CONTENT_VAR}}")
  elseif("${LANG}" STREQUAL "CXX")
    set(SRC_FILE "${WORK_DIR}/${SRC_FILE_NAME}.cpp")
    file(WRITE "${SRC_FILE}" "${OpenMPTarget_C_CXX_${SRC_FILE_CONTENT_VAR}}")
  endif()
  set(${SRC_FILE_FULLPATH} "${SRC_FILE}" PARENT_SCOPE)
endfunction()

# Get the candidate flags and try to compile a test application. If it compiles
# and all the flags are found, we assume the compiler supports offloading.
function(_OPENMP_TARGET_DEVICE_GET_FLAGS LANG DEVICE OPENMP_FLAG_VAR OPENMP_LIB_VAR OPENMP_DEVICE_VAR OPENMP_ARCH_VAR)
  _OPENMP_TARGET_DEVICE_CANDIDATES(${LANG} ${DEVICE})
  _OPENMP_TARGET_DEVICE_FLAG_CANDIDATES(${LANG} "${OpenMPTarget_${LANG}_DEVICE_CANDIDATES}")
  _OPENMP_TARGET_DEVICE_ARCH_CANDIDATES(${LANG} ${DEVICE} "${OpenMPTarget_${LANG}_DEVICE_CANDIDATES}")
  _OPENMP_TARGET_WRITE_SOURCE_FILE("${LANG}" "TEST_SOURCE" OpenMPTargetTryFlag _OPENMP_TEST_SRC)

  # Try to compile a test application with the found flags.
  try_compile(OpenMPTarget_COMPILE_RESULT ${CMAKE_BINARY_DIR} ${_OPENMP_TEST_SRC}
    CMAKE_FLAGS "-DCOMPILE_DEFINITIONS:STRING=${OpenMP_${LANG}_FLAGS} ${OpenMPTarget_${LANG}_FLAG_CANDIDATES} ${OpenMPTarget_${LANG}_DEVICE_ARCH_CANDIDATES}"
    "-DINCLUDE_DIRECTORIES:STRING=${OpenMP_${LANG}_INCLUDE_DIR}"
    LINK_LIBRARIES ${CMAKE_${LANG}_VERBOSE_FLAG}
    OUTPUT_VARIABLE OpenMP_TRY_COMPILE_OUTPUT
  )

  file(APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeOutput.log
    "Detecting OpenMP ${CMAKE_${LANG}_COMPILER_ID} ${DEVICE} target support with the following Flags:
    ${OpenMP_${LANG}_FLAGS} ${OpenMPTarget_${LANG}_FLAG_CANDIDATES} ${OpenMPTarget_${LANG}_DEVICE_ARCH_CANDIDATES}
    With the following output:\n ${OpenMP_TRY_COMPILE_OUTPUT}\n")

  # If compilation was successful and the device was found set the return variables.
  if (OpenMPTarget_COMPILE_RESULT AND DEFINED OpenMPTarget_${LANG}_DEVICE_CANDIDATES)
    file(APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeOutput.log
      "Compilation successful, adding flags for ${DEVICE}.\n\n")

    # Clang has a separate library for target offloading.
    if(CMAKE_${LANG}_COMPILER_ID STREQUAL "Clang")
      find_library(OpenMPTarget_libomptarget_LIBRARY
        NAMES omptarget
        HINTS ${CMAKE_${LANG}_IMPLICIT_LINK_DIRECTORIES}
      )
      mark_as_advanced(OpenMPTarget_libomptarget_LIBRARY)
      set("${OPENMP_LIB_VAR}" "${OpenMPTarget_libomptarget_LIBRARY}" PARENT_SCOPE)
    else()
      unset("${OPENMP_LIB_VAR}" PARENT_SCOPE)
    endif()
    set("${OPENMP_DEVICE_VAR}" "${OpenMPTarget_${LANG}_DEVICE_CANDIDATES}" PARENT_SCOPE)
    set("${OPENMP_FLAG_VAR}" "${OpenMPTarget_${LANG}_FLAG_CANDIDATES}" PARENT_SCOPE)
    set("${OPENMP_ARCH_VAR}" "${OpenMPTarget_${LANG}_DEVICE_ARCH_CANDIDATES}" PARENT_SCOPE)
  else()
    unset("${OPENMP_DEVICE_VAR}" PARENT_SCOPE)
    unset("${OPENMP_FLAG_VAR}" PARENT_SCOPE)
    unset("${OPENMP_ARCH_VAR}" PARENT_SCOPE)
  endif()
endfunction()

# Load the compiler support for each device.
foreach(LANG IN ITEMS C CXX)
  # Cache the version in case CMake doesn't load the OpenMP package this time
  set(OpenMP_${LANG}_VERSION ${OpenMP_${LANG}_VERSION}
    CACHE STRING "OpenMP Version" FORCE)
  mark_as_advanced(OpenMP_${LANG}_VERSION)
  foreach(DEVICE IN ITEMS NVPTX AMDGPU)
    if(CMAKE_${LANG}_COMPILER_LOADED)
      if(NOT DEFINED OpenMPTarget_${LANG}_FLAGS OR NOT DEFINED OpenMPTarget_${LANG}_DEVICE)
        _OPENMP_TARGET_DEVICE_GET_FLAGS(${LANG} ${DEVICE}
          OpenMPTarget_${DEVICE}_FLAGS_WORK
          OpenMPTarget_${DEVICE}_LIBS_WORK
          OpenMPTarget_${DEVICE}_DEVICE_WORK
          OpenMPTarget_${DEVICE}_ARCHS_WORK)

        separate_arguments(_OpenMPTarget_${DEVICE}_FLAGS NATIVE_COMMAND "${OpenMPTarget_${DEVICE}_FLAGS_WORK}")
        separate_arguments(_OpenMPTarget_${DEVICE}_ARCHS NATIVE_COMMAND "${OpenMPTarget_${DEVICE}_ARCHS_WORK}")
        set(OpenMPTarget_${DEVICE}_FLAGS ${_OpenMPTarget_${DEVICE}_FLAGS}
            CACHE STRING "${DEVICE} target compile flags for OpenMP target offloading" FORCE)
        set(OpenMPTarget_${DEVICE}_ARCH ${_OpenMPTarget_${DEVICE}_ARCHS}
            CACHE STRING "${DEVICE} target architecture flags for OpenMP target offloading" FORCE)
        set(OpenMPTarget_${DEVICE}_LIBRARIES ${OpenMPTarget_${DEVICE}_LIBS_WORK}
            CACHE STRING "${DEVICE} target libraries for OpenMP target offloading" FORCE)
        mark_as_advanced(OpenMPTarget_${DEVICE}_FLAGS OpenMPTarget_${DEVICE}_ARCH OpenMPTarget_${DEVICE}_LIBRARIES)
      endif()
    endif()
  endforeach()
endforeach()

if(OpenMPTarget_FIND_COMPONENTS)
  set(OpenMPTarget_FINDLIST ${OpenMPTarget_FIND_COMPONENTS})
else()
  set(OpenMPTarget_FINDLIST NVPTX)
endif()

unset(_OpenMPTarget_MIN_VERSION)

# Attempt to find each requested device.
foreach(LANG IN ITEMS C CXX)
  foreach(DEVICE IN LISTS OpenMPTarget_FINDLIST)
    if(CMAKE_${LANG}_COMPILER_LOADED)
      set(OpenMPTarget_${DEVICE}_VERSION "${OpenMP_${LANG}_VERSION}")
      set(OpenMPTarget_${DEVICE}_VERSION_MAJOR "${OpenMP_${LANG}_VERSION}_MAJOR")
      set(OpenMPTarget_${DEVICE}_VERSION_MINOR "${OpenMP_${LANG}_VERSION}_MINOR")
      set(OpenMPTarget_${DEVICE}_FIND_QUIETLY ${OpenMPTarget_FIND_QUIETLY})
      set(OpenMPTarget_${DEVICE}_FIND_REQUIRED ${OpenMPTarget_FIND_REQUIRED})
      set(OpenMPTarget_${DEVICE}_FIND_VERSION ${OpenMPTarget_FIND_VERSION})
      set(OpenMPTarget_${DEVICE}_FIND_VERSION_EXACT ${OpenMPTarget_FIND_VERSION_EXACT})

      # OpenMP target offloading is only supported in OpenMP 4.0 an newer.
      if(OpenMPTarget_${DEVICE}_VERSION AND ("${OpenMPTarget_${DEVICE}_VERSION}" VERSION_LESS "4.0"))
        message(SEND_ERROR "FindOpenMPTarget requires at least OpenMP 4.0")
      endif()

      set(FPHSA_NAME_MISMATCHED TRUE)
      find_package_handle_standard_args(OpenMPTarget_${DEVICE}
        REQUIRED_VARS OpenMPTarget_${DEVICE}_FLAGS
        VERSION_VAR OpenMPTarget_${DEVICE}_VERSION)

      if(OpenMPTarget_${DEVICE}_FOUND)
        if(DEFINED OpenMPTarget_${DEVICE}_VERSION)
          if(NOT _OpenMPTarget_MIN_VERSION OR _OpenMPTarget_MIN_VERSION VERSION_GREATER OpenMPTarget_${LANG}_VERSION)
            set(_OpenMPTarget_MIN_VERSION OpenMPTarget_${DEVICE}_VERSION)
          endif()
        endif()
        # Create a new target.
        if(NOT TARGET OpenMPTarget::OpenMPTarget_${DEVICE})
          add_library(OpenMPTarget::OpenMPTarget_${DEVICE} INTERFACE IMPORTED)
        endif()
        # Get compiler flags for offloading to the device and architecture and
        # set the target features. Include the normal OpenMP flags as well.
        set_property(TARGET OpenMPTarget::OpenMPTarget_${DEVICE} PROPERTY
          INTERFACE_COMPILE_OPTIONS 
          "$<$<COMPILE_LANGUAGE:${LANG}>:${OpenMPTarget_${DEVICE}_FLAGS}>"
          "$<$<COMPILE_LANGUAGE:${LANG}>:${OpenMPTarget_${DEVICE}_ARCH}>"
          "$<$<COMPILE_LANGUAGE:${LANG}>:${OpenMP_${LANG}_FLAGS}>")
        set_property(TARGET OpenMPTarget::OpenMPTarget_${DEVICE} PROPERTY
          INTERFACE_INCLUDE_DIRECTORIES "$<BUILD_INTERFACE:${OpenMP_${LANG}_INCLUDE_DIRS}>")
        set_property(TARGET OpenMPTarget::OpenMPTarget_${DEVICE} PROPERTY
          INTERFACE_LINK_LIBRARIES 
          "${OpenMPTarget_${DEVICE}_LIBRARIES}"
          "${OpenMP_${LANG}_LIBRARIES}")
        # The offloading flags must also be passed during the linking phase so
        # the compiler can pass the binary to the correct toolchain.
        set_property(TARGET OpenMPTarget::OpenMPTarget_${DEVICE} PROPERTY
          INTERFACE_LINK_OPTIONS 
          "$<$<COMPILE_LANGUAGE:${LANG}>:${OpenMPTarget_${DEVICE}_FLAGS}>"
          "$<$<COMPILE_LANGUAGE:${LANG}>:${OpenMPTarget_${DEVICE}_ARCH}>"
          "$<$<COMPILE_LANGUAGE:${LANG}>:${OpenMP_${LANG}_FLAGS}>")
        # Combine all the flags if not using the target for convenience.
        set(OpenMPTarget_${DEVICE}_FLAGS ${OpenMP_${LANG}_FLAGS}
          ${OpenMPTarget_${DEVICE}_FLAGS} 
          ${OpenMPTarget_${DEVICE}_ARCH}
          CACHE STRING "${DEVICE} target compile flags for OpenMP target offloading" FORCE)
      endif()
    endif()
  endforeach()
endforeach()

unset(_OpenMPTarget_REQ_VARS)
foreach(DEVICE IN LISTS OpenMPTarget_FINDLIST)
  list(APPEND _OpenMPTarget_REQ_VARS "OpenMPTarget_${DEVICE}_FOUND")
endforeach()

find_package_handle_standard_args(OpenMPTarget
    REQUIRED_VARS ${_OpenMPTarget_REQ_VARS}
    VERSION_VAR ${_OpenMPTarget_MIN_VERSION}
    HANDLE_COMPONENTS)

if(NOT (CMAKE_C_COMPILER_LOADED OR CMAKE_CXX_COMPILER_LOADED) OR CMAKE_Fortran_COMPILER_LOADED)
  message(SEND_ERROR "FindOpenMPTarget requires the C or CXX languages to be enabled")
endif()

unset(OpenMPTarget_C_CXX_TEST_SOURCE)
cmake_policy(POP)
