# Define a shiboken bindings library.
function(shiboken_add_bindings
    bindings_lib_arg # The name of the generated bindings module (as imported in Python).
)
    pretty_print_header(${CMAKE_CURRENT_FUNCTION})

    set(
        _no_arg_options

        # Set this if wrapping classes that derive from Qt classes
        WITH_QT
    )

    set(
        _one_arg_options

        # The C++ lib that is wrapped.
        WRAPPED_LIB

        # The header file with all the types and functions for which bindings
        # will be generated. Usually it simply includes other headers of the
        # library you are creating bindings for.
        BINDINGS_HEADER_FILE

        # The typesystem xml file which defines the relationships between the
        # C++ types / functions and the corresponding Python equivalents.
        BINDINGS_TYPESYSTEM_FILE

        # The bindings install folder.
        BINDINGS_INSTALL_DIR

        # The path to the shiboken_generator.
        SHIBOKEN_GENERATOR
    )

    set(
        _multi_arg_options

        # The C++ files will be generated by shiboken. This includes the module
        # wrapper and a '.cpp' file per C++ type that is exported to Python.
        # These are needed for generating the module shared library.
        BINDINGS_GENERATED_SOURCES

        # Extra include dirs for building the binding lib
        BINDINGS_INCLUDE_DIRS

        # Extra libraries to be linked with the binding lib
        BINDINGS_LINK_LIBRARIES

        # The options to be passed to shiboken_generator.
        SHIBOKEN_GENERATOR_OPTIONS

        # Extra include dirs to be passed to shiboken_generator by prepending
        # each element with "-I". The include dirs defined for the wrapped lib
        # are automatically extracted and don't have to be explicitly passed.
        SHIBOKEN_GENERATOR_INCLUDE_OTHER_PATHS
    )

    cmake_parse_arguments(
        PARSE_ARGV 1
        arg
        "${_no_arg_options}"
        "${_one_arg_options}"
        "${_multi_arg_options}"
    )

    # fail if there are unparsed arguments
    if(DEFINED arg_UNPARSED_ARGUMENTS)
        message(FATAL_ERROR "Unknown options detected: ${arg_UNPARSED_ARGUMENTS}")
    endif()

    # define required options
    set(
        _required_options
        WRAPPED_LIB
        BINDINGS_HEADER_FILE
        BINDINGS_TYPESYSTEM_FILE
        BINDINGS_GENERATED_SOURCES
    )

    # fail if any required option is missing
    foreach(_option IN LISTS _required_options)
        if("${arg_${_option}}" STREQUAL "")
            message(FATAL_ERROR "${_option} is not defined.")
        endif()
    endforeach()

    # If BINDINGS_INSTALL_DIR option is not explicitly set, set it using the
    # global BINDINGS_INSTALL_DIR variable the the name of the binding lib to:
    # ${BINDINGS_INSTALL_DIR}/${bindings_lib_arg}
    if(NOT DEFINED arg_BINDINGS_INSTALL_DIR)
        set(arg_BINDINGS_INSTALL_DIR "${BINDINGS_INSTALL_DIR}/${bindings_lib_arg}")
    endif()

    # if not explicitly set, some options will be set to the value of the
    # global variable with the same name as the option name
    set(
        _optional_options
        SHIBOKEN_GENERATOR
    )

    foreach(_option IN LISTS _optional_options)
        if(NOT DEFINED arg_${_option})
            set(arg_${_option} ${${_option}})

            # pretty_print_text("Using global value for ${_option}")
        endif()
    endforeach()

    # print the input parameters
    pretty_print_value("bindings lib" "${bindings_lib_arg}")
    pretty_print_value("WRAPPED_LIB" "${arg_WRAPPED_LIB}")
    pretty_print_value("WITH_QT" "${arg_WITH_QT}")
    pretty_print_value("BINDINGS_HEADER_FILE" "${arg_BINDINGS_HEADER_FILE}")
    pretty_print_value("BINDINGS_TYPESYSTEM_FILE" "${arg_BINDINGS_TYPESYSTEM_FILE}")
    pretty_print_value("BINDINGS_GENERATED_SOURCES" "${arg_BINDINGS_GENERATED_SOURCES}" AS_LIST)
    pretty_print_value("BINDINGS_INSTALL_DIR" "${arg_BINDINGS_INSTALL_DIR}")
    pretty_print_value("BINDINGS_INCLUDE_DIRS" "${arg_BINDINGS_INCLUDE_DIRS}" AS_LIST)
    pretty_print_value("BINDINGS_LINK_LIBRARIES" "${arg_BINDINGS_LINK_LIBRARIES}" AS_LIST)
    pretty_print_value("SHIBOKEN_GENERATOR" "${arg_SHIBOKEN_GENERATOR}")
    pretty_print_value("SHIBOKEN_GENERATOR_INCLUDE_OTHER_PATHS" "${arg_SHIBOKEN_GENERATOR_INCLUDE_OTHER_PATHS}" AS_LIST)
    pretty_print_value("SHIBOKEN_GENERATOR_OPTIONS (input)" "${arg_SHIBOKEN_GENERATOR_OPTIONS}" AS_LIST)

    # retrieve the include directories defined for the wrapped lib
    get_target_property(
        _wrapped_lib_include_dirs
        ${arg_WRAPPED_LIB}
        INCLUDE_DIRECTORIES
    )

    # assemble the include dirs that must be passed to shiboken and prepend
    # "-I" to each of them
    set(
        _shiboken_generator_include_dirs
        ${_wrapped_lib_include_dirs}
        ${arg_SHIBOKEN_GENERATOR_INCLUDE_OTHER_PATHS}
    )
    list(TRANSFORM _shiboken_generator_include_dirs PREPEND -I)

    set(_with_qt_includes "")

    if(DEFINED arg_WITH_QT)
        # Get the relevant Qt include dirs, to pass them on to shiboken.
        get_property(_qt_widgets_include_dirs TARGET Qt6::Widgets PROPERTY INTERFACE_INCLUDE_DIRECTORIES)

        foreach(_include_dir IN LISTS _qt_widgets_include_dirs)
            list(APPEND _with_qt_includes "-I${_include_dir}")
        endforeach()

        # On macOS, check if Qt is a framework build. This affects how include paths should be handled.
        get_target_property(_qt_core_is_framework Qt6::Core FRAMEWORK)

        if(_qt_core_is_framework)
            get_target_property(_qt_core_library_location Qt6::Core LOCATION)
            get_filename_component(_qt_core_library_location_dir "${_qt_core_library_location}" DIRECTORY)
            get_filename_component(_lib_dir "${_qt_core_library_location_dir}/../" ABSOLUTE)
            list(APPEND _with_qt_includes "--framework-include-paths=${_lib_dir}")
        endif()
    endif()

    # append the shiboken included dirs to the other shiboken options
    list(APPEND arg_SHIBOKEN_GENERATOR_OPTIONS ${_shiboken_generator_include_dirs} ${_with_qt_includes})

    # print the final shiboken options
    pretty_print_separator()
    pretty_print_value("SHIBOKEN_GENERATOR_OPTIONS (final)" "${arg_SHIBOKEN_GENERATOR_OPTIONS}")

    # Add custom target to run shiboken to generate the binding cpp files.
    add_custom_command(
        OUTPUT ${arg_BINDINGS_GENERATED_SOURCES}
        COMMAND
        ${arg_SHIBOKEN_GENERATOR}
        ${arg_SHIBOKEN_GENERATOR_OPTIONS}
        ${arg_BINDINGS_HEADER_FILE}
        ${arg_BINDINGS_TYPESYSTEM_FILE}
        DEPENDS ${arg_BINDINGS_HEADER_FILE} ${arg_BINDINGS_TYPESYSTEM_FILE}
        IMPLICIT_DEPENDS CXX ${arg_BINDINGS_HEADER_FILE}
        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
        COMMENT "Running generator for ${arg_BINDINGS_TYPESYSTEM_FILE}."
    )

    # Define and build the bindings library.
    add_library(${bindings_lib_arg} MODULE ${arg_BINDINGS_GENERATED_SOURCES})

    # Apply relevant include and link flags.
    target_include_directories(
        ${bindings_lib_arg} PRIVATE
        ${arg_BINDINGS_INCLUDE_DIRS}
    )

    target_link_libraries(
        ${bindings_lib_arg} PRIVATE
        ${arg_WRAPPED_LIB}
        ${arg_BINDINGS_LINK_LIBRARIES}
        Python::Module
    )

    # Adjust the name of generated module.
    set_property(TARGET ${bindings_lib_arg} PROPERTY PREFIX "")
    set_property(TARGET ${bindings_lib_arg} PROPERTY OUTPUT_NAME
        "${bindings_lib_arg}${PYTHON_EXTENSION_SUFFIX}"
    )

    if(WIN32)
        set_property(TARGET ${bindings_lib_arg} PROPERTY SUFFIX ".pyd")
    endif()

    # Make sure the linker doesn't complain about not finding Python symbols on macOS.
    if(APPLE)
        set_target_properties(${bindings_lib_arg} PROPERTIES LINK_FLAGS "-undefined dynamic_lookup")
    endif(APPLE)

    # Install the library and the bindings module in arg_BINDINGS_INSTALL_DIR.
    install(TARGETS ${bindings_lib_arg} ${arg_WRAPPED_LIB}
        LIBRARY DESTINATION ${arg_BINDINGS_INSTALL_DIR}
        RUNTIME DESTINATION ${arg_BINDINGS_INSTALL_DIR}
    )

    # Check if Qt6::Core5Compat was used and, if it was used, install it
    set(__property_names LINK_LIBRARIES)
    get_target_property(_wrapped_lib_linked_libs ${arg_WRAPPED_LIB} LINK_LIBRARIES)

    if(Qt6::Core5Compat IN_LIST _wrapped_lib_linked_libs)
        find_file(
            _qt6_core5compat_shared_lib
            Qt6Core5Compat${CMAKE_SHARED_LIBRARY_SUFFIX}
            PATHS ${QT6_INSTALL_PREFIX}
            PATH_SUFFIXES bin
            NO_DEFAULT_PATH
            NO_CMAKE_FIND_ROOT_PATH
            REQUIRED
        )
        pretty_print_value("_qt6_core5compat_shared_lib" "${_qt6_core5compat_shared_lib}")

        install(
            FILES ${_qt6_core5compat_shared_lib}
            DESTINATION ${arg_BINDINGS_INSTALL_DIR}
        )
    endif()

    pretty_print_footer()
endfunction()

# Tries to find shiboken and sets various variables if shiboken is found.
# It uses `pyside_config` for actually retrieving this information.
macro(find_shiboken)
    set(_find_shiboken_DEFAULT_PREFIX "SHIBOKEN") # prefix used if PREFIX option is not set

    cmake_parse_arguments(
        find_shiboken_arg
        ""
        "PREFIX"
        ""
        ${ARGN}
    )

    if(DEFINED find_shiboken_arg_UNPARSED_ARGUMENTS)
        message(FATAL_ERROR "Unknown options detected: ${find_shiboken_arg_UNPARSED_ARGUMENTS}")
    endif()

    if("${find_shiboken_arg_PREFIX}" STREQUAL "")
        set(find_shiboken_arg_PREFIX "${_find_shiboken_DEFAULT_PREFIX}")
    endif()

    # message(STATUS "find_shiboken_arg_PREFIX: ${find_shiboken_arg_PREFIX}")
    pyside_config("--shiboken-generator-path" PREFIX "${find_shiboken_arg_PREFIX}" OUTPUT _find_shiboken_SHIBOKEN_GENERATOR_PATH)
    set(${find_shiboken_arg_PREFIX}_GENERATOR "${_find_shiboken_SHIBOKEN_GENERATOR_PATH}/shiboken6${CMAKE_EXECUTABLE_SUFFIX}")

    # If the shiboken_generator path can't be find, abort early.
    if(NOT EXISTS ${${find_shiboken_arg_PREFIX}_GENERATOR})
        message(FATAL_ERROR "Shiboken executable not found at path: ${${find_shiboken_arg_PREFIX}_GENERATOR}")
    endif()

    set(${find_shiboken_arg_PREFIX}_FOUND TRUE)
    pyside_config(--pyside-include-path PREFIX "${find_shiboken_arg_PREFIX}")
    pyside_config(--pyside-path PREFIX "${find_shiboken_arg_PREFIX}")
    pyside_config(--pyside-shared-libraries-cmake PREFIX "${find_shiboken_arg_PREFIX}")
    pyside_config(--python-include-path PREFIX "${find_shiboken_arg_PREFIX}")
    pyside_config(--python-link-flags-cmake PREFIX "${find_shiboken_arg_PREFIX}")
    pyside_config(--shiboken-generator-include-path PREFIX "${find_shiboken_arg_PREFIX}" AS_LIST)
    pyside_config(--shiboken-module-path PREFIX "${find_shiboken_arg_PREFIX}")
    pyside_config(--shiboken-module-shared-libraries-cmake PREFIX "${find_shiboken_arg_PREFIX}")
endmacro()

# Set some variables common for all shiboken projects
macro(shiboken_standard_project_setup)
    # ---------------------
    # Force release build
    # ---------------------
    set(CMAKE_BUILD_TYPE "Release" CACHE STRING "" FORCE)

    # ---------------------
    # RPATH configuration
    # ---------------------

    # Enable rpaths so that the built shared libraries find their dependencies.
    set(CMAKE_SKIP_BUILD_RPATH FALSE)
    set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
    set(CMAKE_INSTALL_RPATH_USE_LINK_PATH TRUE)

    # The CMAKE_INSTALL_RPATH should be set in the main file, as required by
    # the actual project. At the least, add the SHIBOKEN_MODULE_PATH to it.
endmacro()
