#
# Contains helper functions for running Chimera in other CMake projects.
#

# Chimera requires the generation of a compilation database.
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Function to create a build target for a Chimera binding.
# The result of this operation is a CMake library target.
#
# add_chimera_binding(TARGET target
#                     [DESTINATION destination_dir] # Defaults to `target`
#                     [MODULE module]               # Defaults to `<BINARY_DIR>/target`
#                     [BINDING binding]             # Binding definition (overrides config)
#                     [CONFIGURATION config_file]
#                     [NAMESPACES namespace1 namespace2 ...])
#                     SOURCES source1_file [source2_file ...]
#                     [EXTRA_SOURCES source1_file ...]
#                     [GENERATED_SOURCES_VAR]       # Output variable containing list of generated binding source files
#                     [DEBUG] [EXCLUDE_FROM_ALL]
function(add_chimera_binding)
    include(ExternalProject)

    # Parse boolean, unary, and list arguments from input.
    # Unparsed arguments can be found in variable ARG_UNPARSED_ARGUMENTS.
    set(prefix binding)
    set(options DEBUG EXCLUDE_FROM_ALL)
    set(oneValueArgs TARGET MODULE CONFIGURATION DESTINATION BINDING GENERATED_SOURCES_VAR)
    set(multiValueArgs SOURCES NAMESPACES EXTRA_SOURCES LINK_LIBRARIES)
    cmake_parse_arguments("${prefix}" "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

    # Print errors if arguments are missing.
    if(NOT binding_TARGET)
        message(FATAL_ERROR "Chimera requires a CMake TARGET name.")
    elseif(NOT binding_SOURCES)
        message(FATAL_ERROR "Chimera requires one or more SOURCES.")
    endif()

    # Default MODULE to be the same as TARGET.
    if(NOT binding_MODULE)
        set(binding_MODULE "${binding_TARGET}")
    endif()

    # Default DESTINATION to `<BINARY_DIR>/<TARGET>`
    if(NOT binding_DESTINATION)
        set(binding_DESTINATION "${CMAKE_CURRENT_BINARY_DIR}/${binding_TARGET}")
    endif()

    # Set EXCLUDE_FROM_ALL flag if provided.
    if(binding_EXCLUDE_FROM_ALL)
        set(binding_EXCLUDE_FROM_ALL_FLAG "EXCLUDE_FROM_ALL")
    else()
        set(binding_EXCLUDE_FROM_ALL_FLAG "")
    endif()

    # If the debug flag is specified, dump all the chimera settings.
    if(binding_DEBUG)
        message(STATUS "Chimera binding: ${binding_TARGET}")
        message(STATUS "  Exclude from ALL: ${binding_EXCLUDE_FROM_ALL}")
        message(STATUS "  Module: ${binding_MODULE}")
        if(binding_BINDING)
            message(STATUS "  Binding: ${binding_BINDING}")
        endif()
        if(binding_CONFIGURATION)
            message(STATUS "  Configuration: ${binding_CONFIGURATION}")
        endif()
        if(binding_NAMESPACES)
            message(STATUS "  Namespaces:")
            foreach(namespace ${binding_NAMESPACES})
                message(STATUS "  - ${namespace}")
            endforeach()
        endif()
        if(binding_SOURCES)
            message(STATUS "  Sources:")
            foreach(source ${binding_SOURCES})
                message(STATUS "  - ${source}")
            endforeach()
        endif()
        if(binding_EXTRA_SOURCES)
            message(STATUS "  Extra Sources:")
            foreach(source ${binding_EXTRA_SOURCES})
                message(STATUS "  - ${source}")
            endforeach()
        endif()
    endif()

    # Ensure that the output destination directory is created.
    file(MAKE_DIRECTORY "${binding_DESTINATION}")
    set(binding_SOURCES_TXT "${binding_DESTINATION}/sources.txt")
    set(binding_EMPTY_CPP "${binding_DESTINATION}/empty.cpp")
    file(WRITE "${binding_EMPTY_CPP}"
      "/// ==================================================================\n"
      "/// AUTOGENERATED BY CHIMERA - DO NOT EDIT\n"
      "///\n"
      "/// This file is used as a placeholder during the Chimera CMake build.\n"
      "/// It is intentionally generated without contents.\n"
      "/// ==================================================================\n"
    )

    # Construct the list of chimera arguments.
    set(binding_ARGS)
    list(APPEND binding_ARGS -m "${binding_MODULE}")
    list(APPEND binding_ARGS -o "${binding_DESTINATION}")
    list(APPEND binding_ARGS -p "${PROJECT_BINARY_DIR}")
    if(binding_BINDING)
        list(APPEND binding_ARGS -b "${binding_BINDING}")
    endif()
    if(binding_CONFIGURATION)
        list(APPEND binding_ARGS -c "${binding_CONFIGURATION}")
    endif()
    if(binding_NAMESPACES)
        foreach(namespace ${binding_NAMESPACES})
            list(APPEND binding_ARGS -n "${namespace}")
        endforeach()
    endif()
    list(APPEND binding_ARGS ${binding_SOURCES})
    list(APPEND binding_ARGS > "${binding_SOURCES_TXT}.staging")

    # Create an external target that re-runs chimera when any of the sources have changed.
    # This will necessarily invalidate a placeholder dependency that causes CMake to
    # rerun the compilation of the library if sources are regenerated.
    add_custom_target("${binding_TARGET}_SOURCES" DEPENDS "${binding_SOURCES_TXT}")
    add_custom_command(
        OUTPUT "${binding_SOURCES_TXT}"
        COMMAND "${chimera_EXECUTABLE}" ARGS ${binding_ARGS}
        COMMAND ${CMAKE_COMMAND} ARGS -E rename "${binding_SOURCES_TXT}.staging" "${binding_SOURCES_TXT}"
        DEPENDS "${binding_CONFIGURATION}" ${binding_SOURCES}
        WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
        COMMENT "Generating bindings for ${binding_TARGET}."
        VERBATIM
    )

    # Get the current list of generated sources if already generated.
    if(EXISTS "${binding_SOURCES_TXT}")
        file(STRINGS "${binding_SOURCES_TXT}" binding_GENERATED_RELATIVE NO_HEX_CONVERSION)

        set(binding_GENERATED)
        foreach(relative_path ${binding_GENERATED_RELATIVE})
            list(APPEND binding_GENERATED "${binding_DESTINATION}/${relative_path}")
        endforeach()
    endif()

    # Placeholder target to generate compilation database.
    #
    # (We force all SOURCES to be treated as CXX so that we can generate bindings
    # from only header files, a very common use-case.)
    set_source_files_properties(${binding_SOURCES}
        PROPERTIES LANGUAGE CXX
    )
    add_library("${binding_TARGET}_placeholder" EXCLUDE_FROM_ALL
        ${binding_SOURCES}
    )
    target_link_libraries("${binding_TARGET}_placeholder"
        $<TARGET_PROPERTY:${binding_TARGET},LINK_LIBRARIES>
    )
    target_include_directories("${binding_TARGET}_placeholder" SYSTEM PUBLIC
        $<TARGET_PROPERTY:${binding_TARGET},INCLUDE_DIRECTORIES>
    )
    target_compile_options("${binding_TARGET}_placeholder" PUBLIC
        $<TARGET_PROPERTY:${binding_TARGET},COMPILE_OPTIONS>
    )
    target_compile_definitions("${binding_TARGET}_placeholder" PUBLIC
        $<TARGET_PROPERTY:${binding_TARGET},COMPILE_DEFINITIONS>
    )
    set_target_properties("${binding_TARGET}_placeholder" PROPERTIES
        LINKER_LANGUAGE CXX
    )

    # Create a library target to build the binding as a module.
    add_library("${binding_TARGET}" MODULE
        "${binding_EXCLUDE_FROM_ALL_FLAG}"
        "${binding_EMPTY_CPP}"
        ${binding_GENERATED}
        ${binding_EXTRA_SOURCES}
    )

    # Trigger the rebuild of the library target after new sources have been generated.
    #
    # For BUILD_COMMAND, '$(MAKE)' is used instead of 'make' to propagate the
    # make commands of the parent project to the child process.
    # (see: http://stackoverflow.com/a/33171336)
    ExternalProject_Add("${binding_TARGET}_REBUILD"
        DOWNLOAD_COMMAND ""
        INSTALL_COMMAND ""
        BUILD_COMMAND $(MAKE) "${binding_TARGET}_SOURCES"
        DEPENDS "${binding_TARGET}_SOURCES"
        SOURCE_DIR "${PROJECT_SOURCE_DIR}"
        BINARY_DIR "${PROJECT_BINARY_DIR}"
    )
    set_target_properties("${binding_TARGET}_REBUILD" PROPERTIES EXCLUDE_FROM_ALL TRUE)
    add_dependencies("${binding_TARGET}" "${binding_TARGET}_REBUILD")

    # Set ${binding_GENERATED_SOURCES_VAR} with the list of generated bindings
    if(binding_GENERATED_SOURCES_VAR)
        set(${binding_GENERATED_SOURCES_VAR} ${binding_GENERATED} PARENT_SCOPE)
    endif()

endfunction(add_chimera_binding)
