include(CMakeParseArguments)
include(ExternalProject)

set(CURRENT_LIST_DIR "${CMAKE_CURRENT_LIST_DIR}")

function(_cmake_args_propagate VARIABLE)
  set(ARGS "CMAKE_GENERATOR;${CMAKE_GENERATOR}")
  if(CMAKE_GENERATOR_PLATFORM AND CMAKE_VERSION VERSION_GREATER_EQUAL "3.14.0")
    list(APPEND ARGS "CMAKE_GENERATOR_PLATFORM;${CMAKE_GENERATOR_PLATFORM}")
  endif()
  if(CMAKE_TOOLCHAIN_FILE)
    list(APPEND ARGS "CMAKE_ARGS;-DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}")
  endif()
  set(${VARIABLE} "${ARGS}" PARENT_SCOPE)
endfunction(_cmake_args_propagate)

function(_argument_default VARIABLE)
  if(args_${VARIABLE})
    set(${VARIABLE} "${args_${VARIABLE}}" PARENT_SCOPE)
  else()
    set(${VARIABLE} "${ARGN}" PARENT_SCOPE)
  endif()
endfunction(_argument_default)

#
# Builds an external cmake package, as generated by install(EXPORT ...)
# and to be used by find_package(...).
#
# bootstrap_external_package(<target> <name> CONFIG <path> ...)
function(bootstrap_external_package TARGET NAME)
  set(ARGS_ONE_VALUE GIT_TAG CONFIG)
  cmake_parse_arguments(args "" "${ARGS_ONE_VALUE}" "" ${ARGN})
  
  if(NOT args_CONFIG)
    message(FATAL_ERROR "Missing external package CONFIG for ${TARGET}")
  endif()
  _argument_default(CONFIG "")
  _argument_default(GIT_TAG "[unknown]")

  # Set source and build directory.
  set(SOURCE_DIR "${CMAKE_CURRENT_BINARY_DIR}/${TARGET}/")
  set(BUILD_DIR "${SOURCE_DIR}/build/")
  file(MAKE_DIRECTORY ${SOURCE_DIR})
  file(MAKE_DIRECTORY ${BUILD_DIR})

  # Set variable for find_package().
  set(CONFIG_PATH "${BUILD_DIR}/content-prefix/${CONFIG}")
  get_filename_component(${NAME}_ROOT ${CONFIG_PATH} DIRECTORY)
  set(${NAME}_ROOT ${${NAME}_ROOT} PARENT_SCOPE)
  
  if(NOT EXISTS "${CONFIG_PATH}")
    message(STATUS "${TARGET} tag: ${GIT_TAG}")

    # Compose arguments for ExternalProject_Add.
    _cmake_args_propagate(BASE_ARGS)
    set(ARGN_EXT "${ARGN}")
    list(FIND ARGN_EXT "CONFIG" CONFIG_INDEX)
    list(REMOVE_AT ARGN_EXT ${CONFIG_INDEX})
    list(REMOVE_AT ARGN_EXT ${CONFIG_INDEX})
    list(APPEND ARGN_EXT ${BASE_ARGS} CMAKE_ARGS
      "-DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>")

    # Generate a CMakeLists.txt.
    file(WRITE "${SOURCE_DIR}/CMakeLists.txt"
      "cmake_minimum_required(VERSION 3.0)\n"
      "include(ExternalProject)\n"
      "project(${TARGET})\n"
      "ExternalProject_Add(content \"${ARGN_EXT}\")\n")

    # Configure package.
    message(STATUS "Configuring ${TARGET}...")
    execute_process(
      COMMAND ${CMAKE_COMMAND} ${SOURCE_DIR} ${BASE_ARGS}
      WORKING_DIRECTORY ${BUILD_DIR}
      OUTPUT_QUIET
      RESULT_VARIABLE CONFIG_RESULT)
    if(NOT "${CONFIG_RESULT}" STREQUAL "0")
      message(FATAL_ERROR "Fatal error while configuring ${TARGET}.")
    endif()

    # Build package.
    message(STATUS "Compiling ${TARGET}...")
    execute_process(
      COMMAND ${CMAKE_COMMAND} --build . --parallel
      OUTPUT_QUIET
      WORKING_DIRECTORY ${BUILD_DIR})
 else()
   message(STATUS "${TARGET} tag: ${GIT_TAG} (cached)")
 endif()
endfunction(bootstrap_external_package)

#
# Adds an external project.
#
# See ExternalProject_Add(...) for usage.
#
function(add_external_project TARGET)
  set(ARGS_ONE_VALUE GIT_TAG)
  cmake_parse_arguments(args "" "${ARGS_ONE_VALUE}" "" ${ARGN})

  if(args_GIT_TAG)
    message(STATUS "${TARGET} tag: ${args_GIT_TAG}")
  endif()

  # Compose arguments for ExternalProject_Add.
  _cmake_args_propagate(BASE_ARGS)
  set(ARGN_EXT "${ARGN}")
  list(APPEND ARGN_EXT ${BASE_ARGS} CMAKE_ARGS
    "-DCMAKE_INSTALL_PREFIX:PATH=<INSTALL_DIR>"
    "-DCMAKE_C_COMPILER=${CMAKE_C_COMPILER}"
    "-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}")

  # Add external project.
  ExternalProject_Add(${TARGET} "${ARGN_EXT}")
endfunction(add_external_project)

#
# Adds an external library, depending on an external project.
#
# add_external_library(<target> (STATIC|SHARED|INTERFACE)
#     DEPENDS <external_project>
#     LIBRARY_DEBUG "Debug.dll"
#     LIBRARY_RELEASE "Release.dll"
#     LIBRARY_RELWITHDEBINFO "ReleaseWithDebInfo.dll"
#     LIBRARY_MINSIZEREL "MinSizeRel.dll"
#     IMPORT_LIBRARY_DEBUG "Debug.lib"
#     IMPORT_LIBRARY_RELEASE "Release.lib"
#     IMPORT_LIBRARY_RELWITHDEBINFO "ReleaseWithDebInfo.lib"
#     IMPORT_LIBRARY_MINSIZEREL "MinSizeRel.lib"
#     INTERFACE_LIBRARIES <external_library>*")
#
function(add_external_library TARGET LINKAGE)
  set(ARGS_ONE_VALUE DEPENDS COMPILE_DEFINITIONS INCLUDE_DIR LIBRARY LIBRARY_DEBUG LIBRARY_RELEASE LIBRARY_RELWITHDEBINFO LIBRARY_MINSIZEREL IMPORT_LIBRARY IMPORT_LIBRARY_DEBUG IMPORT_LIBRARY_RELEASE IMPORT_LIBRARY_RELWITHDEBINFO IMPORT_LIBRARY_MINSIZEREL INTERFACE_LIBRARIES)
  cmake_parse_arguments(args "" "${ARGS_ONE_VALUE}" "" ${ARGN})

   if(NOT args_DEPENDS)
     message(FATAL_ERROR "Missing external project for ${TARGET} to depend on")
   endif()
  _argument_default(DEPENDS "")

  # Guess library properties, unless set.
  ExternalProject_Get_Property(${DEPENDS} INSTALL_DIR)
  _argument_default(COMPILE_DEFINITIONS "")
  _argument_default(INCLUDE_DIR include)
  _argument_default(LIBRARY "NOTFOUND")
  _argument_default(LIBRARY_DEBUG "${LIBRARY}")
  _argument_default(LIBRARY_RELEASE "${LIBRARY}")
  _argument_default(LIBRARY_RELWITHDEBINFO "${LIBRARY_RELEASE}")
  _argument_default(LIBRARY_MINSIZEREL "${LIBRARY_RELEASE}")
  _argument_default(IMPORT_LIBRARY "NOTFOUND")
  _argument_default(IMPORT_LIBRARY_DEBUG "${IMPORT_LIBRARY}")
  _argument_default(IMPORT_LIBRARY_RELEASE "${IMPORT_LIBRARY}")
  _argument_default(IMPORT_LIBRARY_RELWITHDEBINFO "${IMPORT_LIBRARY_RELEASE}")
  _argument_default(IMPORT_LIBRARY_MINSIZEREL "${IMPORT_LIBRARY_RELEASE}")

  # Create include directory as required by INTERFACE_INCLUDE_DIRECTORIES.
  file(MAKE_DIRECTORY "${INSTALL_DIR}/${INCLUDE_DIR}")

  # Add an imported library.
  add_library(${TARGET} ${LINKAGE} IMPORTED GLOBAL)
  add_dependencies(${TARGET} ${DEPENDS})
  set_target_properties(${TARGET} PROPERTIES
    INTERFACE_COMPILE_DEFINITIONS "${COMPILE_DEFINITIONS}"
    INTERFACE_INCLUDE_DIRECTORIES "${INSTALL_DIR}/${INCLUDE_DIR}"
    INTERFACE_LINK_LIBRARIES "${INTERFACE_LIBRARIES}")
  if(LINKAGE STREQUAL "STATIC" OR LINKAGE STREQUAL "SHARED")
    set_target_properties(${TARGET} PROPERTIES
      IMPORTED_CONFIGURATIONS "Debug;Release;MinSizeRel;ReleaseWithDebInfo"
      IMPORTED_LOCATION_DEBUG "${INSTALL_DIR}/${LIBRARY_DEBUG}"
      IMPORTED_LOCATION_RELEASE "${INSTALL_DIR}/${LIBRARY_RELEASE}"
      IMPORTED_LOCATION_RELWITHDEBINFO "${INSTALL_DIR}/${LIBRARY_RELWITHDEBINFO}"
      IMPORTED_LOCATION_MINSIZEREL "${INSTALL_DIR}/${LIBRARY_MINSIZEREL}")
  endif()
  if(LINKAGE STREQUAL "SHARED")
    set_target_properties(${TARGET} PROPERTIES
      IMPORTED_IMPLIB_DEBUG "${INSTALL_DIR}/${IMPORT_LIBRARY_DEBUG}"
      IMPORTED_IMPLIB_RELEASE "${INSTALL_DIR}/${IMPORT_LIBRARY_RELEASE}"
      IMPORTED_IMPLIB_RELWITHDEBINFO "${INSTALL_DIR}/${IMPORT_LIBRARY_RELWITHDEBINFO}"
      IMPORTED_IMPLIB_MINSIZEREL "${INSTALL_DIR}/${IMPORT_LIBRARY_MINSIZEREL}")
  endif()

  #message(STATUS
  #  "${DEPENDS} / ${TARGET} settings:\n"
  #  "  Definitions:\t${COMPILE_DEFINITIONS}\n"
  #  "  Include:\t\t${INSTALL_DIR}/${INCLUDE_DIR}\n"
  #  "  Library (debug):\t${INSTALL_DIR}/${LIBRARY_DEBUG}\n"
  #  "  Library (release):\t${INSTALL_DIR}/${LIBRARY_RELEASE}\n"
  #  "  Import (debug):\t${INSTALL_DIR}/${IMPORT_LIBRARY_DEBUG}\n"
  #  "  Import (release):\t${INSTALL_DIR}/${IMPORT_LIBRARY_RELEASE}\n"
  #  "  Interface:\t${INTERFACE_LIBRARIES}")
endfunction(add_external_library)

#
# Installs external targets.
#
# install_external(TARGET libA BLib C)
#
# This is a workaround for limitations of install(TARGETS ...) [1][2].
# [1] https://gitlab.kitware.com/cmake/cmake/issues/14311
# [2] https://gitlab.kitware.com/cmake/cmake/issues/14444
#
function(install_external)
  set(ARGS_MULTI_VALUE TARGETS)
  cmake_parse_arguments(args "" "" "${ARGS_MULTI_VALUE}" ${ARGN})

  foreach(target ${args_TARGETS})
    get_target_property(IMPORTED_IMPLIB_DEBUG ${target} IMPORTED_IMPLIB_DEBUG)
    get_target_property(IMPORTED_IMPLIB_RELEASE ${target} IMPORTED_IMPLIB_RELEASE)
    get_target_property(IMPORTED_IMPLIB_RELWITHDEBINFO ${target} IMPORTED_IMPLIB_RELWITHDEBINFO)
    get_target_property(IMPORTED_IMPLIB_MINSIZEREL ${target} IMPORTED_IMPLIB_MINSIZEREL)
    get_target_property(IMPORTED_LOCATION_DEBUG ${target} IMPORTED_LOCATION_DEBUG)
    get_target_property(IMPORTED_LOCATION_RELEASE ${target} IMPORTED_LOCATION_RELEASE)
    get_target_property(IMPORTED_LOCATION_RELWITHDEBINFO ${target} IMPORTED_LOCATION_RELWITHDEBINFO)
    get_target_property(IMPORTED_LOCATION_MINSIZEREL ${target} IMPORTED_LOCATION_MINSIZEREL)
    get_target_property(INTERFACE_INCLUDE_DIRECTORIES ${target} INTERFACE_INCLUDE_DIRECTORIES)

    if(NOT IMPORTED_IMPLIB_RELWITHDEBINFO)
      set(IMPORTED_IMPLIB_RELWITHDEBINFO ${IMPORTED_IMPLIB_RELEASE})
    endif()
    if(NOT IMPORTED_IMPLIB_MINSIZEREL)
      set(IMPORTED_IMPLIB_MINSIZEREL ${IMPORTED_IMPLIB_RELEASE})
    endif()

    install(DIRECTORY ${INTERFACE_INCLUDE_DIRECTORIES}/ DESTINATION ${CMAKE_INSTALL_PREFIX}/include/${target})
    if(WIN32)
      install(FILES ${IMPORTED_IMPLIB_DEBUG} DESTINATION "lib" OPTIONAL)
      install(FILES ${IMPORTED_IMPLIB_RELEASE} DESTINATION "lib" OPTIONAL)
      install(FILES ${IMPORTED_IMPLIB_RELWITHDEBINFO} DESTINATION "lib" OPTIONAL)
      install(FILES ${IMPORTED_IMPLIB_MINSIZEREL} DESTINATION "lib" OPTIONAL)
      set(TARGET_DESTINATION "bin")
    else()
      set(TARGET_DESTINATION "lib")
    endif()

    # Wildcard-based install to catch PDB files and symlinks.
    install(CODE "\
      file(GLOB DEBUG_FILES \"${IMPORTED_LOCATION_DEBUG}*\")\n \
      file(GLOB RELEASE_FILES \"${IMPORTED_LOCATION_RELEASE}*\")\n \
      file(GLOB RELWITHDEBINFO_FILES \"${IMPORTED_LOCATION_RELWITHDEBINFO}*\")\n \
      file(GLOB MINSIZEREL_FILES \"${IMPORTED_LOCATION_MINSIZEREL}*\")\n \
      file(INSTALL \${DEBUG_FILES} \${RELEASE_FILES} \${RELWITHDEBINFO_FILES} \${MINSIZEREL_FILES} DESTINATION \"${CMAKE_INSTALL_PREFIX}/${TARGET_DESTINATION}\")")
  endforeach()
endfunction(install_external)
