# Copyright (C) 2024 The Qt Company Ltd.
# SPDX-License-Identifier: BSD-3-Clause

# Walks a target's direct dependencies and assembles a list of relationships between the packages
# of the target dependencies.
# Currently handles various Qt targets and system libraries.
# For SPDX documents, it collects the relationships in OUT_SPDX_RELATIONSHIPS.
# For CYDX documents, it collects the dependencies (not relationship info) in OUT_CYDX_DEPENDENCIES.
# If a CYDX dependency is in an external docuemnt, the dependency is added to
# OUT_EXTERNAL_TARGET_DEPENDENCIES instead.
function(_qt_internal_sbom_handle_target_dependencies target)
    set(opt_args "")
    set(single_args
        SPDX_ID
        OUT_CYDX_DEPENDENCIES
        OUT_SPDX_RELATIONSHIPS
        OUT_EXTERNAL_TARGET_DEPENDENCIES
    )
    set(multi_args
        LIBRARIES
        PUBLIC_LIBRARIES
    )
    cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}")
    _qt_internal_validate_all_args_are_parsed(arg)

    if(NOT arg_SPDX_ID)
        message(FATAL_ERROR "SPDX_ID must be set")
    endif()
    set(package_spdx_id "${arg_SPDX_ID}")


    set(libraries "")
    if(arg_LIBRARIES)
        list(APPEND libraries "${arg_LIBRARIES}")
    endif()

    get_target_property(extend_libraries "${target}" _qt_extend_target_libraries)
    if(extend_libraries)
        list(APPEND libraries ${extend_libraries})
    endif()

    get_target_property(target_type ${target} TYPE)
    set(valid_target_types
        EXECUTABLE
        SHARED_LIBRARY
        MODULE_LIBRARY
        STATIC_LIBRARY
        OBJECT_LIBRARY
    )
    if(target_type IN_LIST valid_target_types)
        get_target_property(link_libraries "${target}" LINK_LIBRARIES)
        if(link_libraries)
            list(APPEND libraries ${link_libraries})
        endif()
    endif()

    set(public_libraries "")
    if(arg_PUBLIC_LIBRARIES)
        list(APPEND public_libraries "${arg_PUBLIC_LIBRARIES}")
    endif()

    get_target_property(extend_public_libraries "${target}" _qt_extend_target_public_libraries)
    if(extend_public_libraries)
        list(APPEND public_libraries ${extend_public_libraries})
    endif()

    set(sbom_dependencies "")
    if(arg_SBOM_DEPENDENCIES)
        list(APPEND sbom_dependencies "${arg_SBOM_DEPENDENCIES}")
    endif()

    get_target_property(extend_sbom_dependencies "${target}" _qt_extend_target_sbom_dependencies)
    if(extend_sbom_dependencies)
        list(APPEND sbom_dependencies ${extend_sbom_dependencies})
    endif()

    list(REMOVE_DUPLICATES libraries)
    list(REMOVE_DUPLICATES public_libraries)
    list(REMOVE_DUPLICATES sbom_dependencies)

    set(all_direct_libraries ${libraries} ${public_libraries} ${sbom_dependencies})
    list(REMOVE_DUPLICATES all_direct_libraries)

    set(regular_cydx_dependencies "")
    set(regular_spdx_dependencies "")

    set(external_cydx_dependencies "")
    set(external_spdx_dependencies "")
    set(external_target_dependencies "")

    # Go through each direct linked lib.
    foreach(direct_lib IN LISTS all_direct_libraries)
        if(NOT TARGET "${direct_lib}")
            continue()
        endif()

        # Check for Qt-specific system library targets. These are marked via qt_find_package calls.
        get_target_property(is_system_library "${direct_lib}" _qt_internal_sbom_is_system_library)
        if(is_system_library)

            # We need to check if the dependency is a FindWrap dependency that points either to a
            # system library or a vendored / bundled library. We need to depend on whichever one
            # the FindWrap script points to.
            __qt_internal_walk_libs(
                "${direct_lib}"
                lib_walked_targets
                _discarded_out_var
                "sbom_targets"
                "collect_targets")

            # Detect if we are dealing with a vendored / bundled lib.
            set(bundled_targets_found FALSE)
            if(lib_walked_targets)
                foreach(lib_walked_target IN LISTS lib_walked_targets)
                    get_target_property(is_3rdparty_bundled_lib
                        "${lib_walked_target}" _qt_module_is_3rdparty_library)
                    _qt_internal_sbom_get_spdx_id_for_target("${lib_walked_target}" lib_spdx_id)

                    # Add a dependency on the vendored lib instead of the Wrap target.
                    if(is_3rdparty_bundled_lib AND lib_spdx_id)
                        list(APPEND regular_cydx_dependencies "${lib_spdx_id}")
                        list(APPEND regular_spdx_dependencies "${lib_spdx_id}")
                        set(bundled_targets_found TRUE)
                    endif()
                endforeach()

                if(bundled_targets_found)
                    # If we handled a bundled target, we can move on to process the next direct_lib.
                    continue()
                endif()
            endif()

            if(NOT bundled_targets_found)
                # If we haven't found a bundled target, then it's a regular system library
                # dependency. Make sure to mark the system library as consumed, so that we later
                # generate an sbom for it.
                # Also fall through to the code that actually adds the dependency on the target.
                _qt_internal_append_to_cmake_property_without_duplicates(
                    _qt_internal_sbom_consumed_system_library_targets
                    "${direct_lib}"
                )
            endif()
        endif()

        # Get the spdx id of the dependency.
        _qt_internal_sbom_get_spdx_id_for_target("${direct_lib}" lib_spdx_id)
        if(NOT lib_spdx_id)
            message(DEBUG "Could not add target dependency on target ${direct_lib} "
                "because no spdx id for it could be found.")
            continue()
        endif()

        # Check if the target sbom is defined in an external document.

        _qt_internal_sbom_is_external_target_dependency("${direct_lib}"
            OUT_VAR is_dependency_in_external_document
        )

        if(NOT is_dependency_in_external_document)
            # If the target is not in the external document, it must be one built as part of the
            # current project.
            list(APPEND regular_cydx_dependencies "${lib_spdx_id}")
            list(APPEND regular_spdx_dependencies "${lib_spdx_id}")
        else()
            # Refer to the package in the external document. This can be the case
            # in a top-level build, where a system library is reused across repos, or for any
            # regular dependency that was built as part of a different project.
            _qt_internal_sbom_add_external_target_dependency("${direct_lib}"
                OUT_CYDX_DEPENDENCIES extra_cydx_dependencies
                OUT_SPDX_DEPENDENCIES extra_spdx_dependencies
                OUT_TARGET_DEPENDENCIES extra_target_dependencies
            )
            if(extra_spdx_dependencies)
                list(APPEND external_cydx_dependencies ${extra_cydx_dependencies})
                list(APPEND external_spdx_dependencies ${extra_spdx_dependencies})
                list(APPEND external_target_dependencies ${extra_target_dependencies})
            endif()
        endif()
    endforeach()

    set(spdx_relationships "")

    # Keep the external dependencies first, so they are neatly ordered.
    foreach(dep_spdx_id IN LISTS external_spdx_dependencies regular_spdx_dependencies)
        set(relationship "${package_spdx_id} DEPENDS_ON ${dep_spdx_id}")
        list(APPEND spdx_relationships "${relationship}")
    endforeach()

    set(all_cydx_dependencies ${external_cydx_dependencies} ${regular_cydx_dependencies})

    set(${arg_OUT_CYDX_DEPENDENCIES} "${all_cydx_dependencies}" PARENT_SCOPE)
    set(${arg_OUT_SPDX_RELATIONSHIPS} "${spdx_relationships}" PARENT_SCOPE)
    set(${arg_OUT_EXTERNAL_TARGET_DEPENDENCIES} "${external_target_dependencies}" PARENT_SCOPE)
endfunction()

# Checks whether the current target will have its sbom generated into the current repo sbom
# document, or whether it is present in an external sbom document.
function(_qt_internal_sbom_is_external_target_dependency target)
    set(opt_args
        SYSTEM_LIBRARY
    )
    set(single_args
        OUT_VAR
    )
    set(multi_args "")
    cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}")
    _qt_internal_validate_all_args_are_parsed(arg)

    get_target_property(is_imported "${target}" IMPORTED)
    get_target_property(is_custom_sbom_target "${target}" _qt_sbom_is_custom_sbom_target)

    _qt_internal_sbom_get_root_project_name_lower_case(current_repo_project_name)
    get_property(target_repo_project_name TARGET ${target}
        PROPERTY _qt_sbom_spdx_repo_project_name_lowercase)

    if(NOT "${target_repo_project_name}" STREQUAL ""
            AND NOT "${target_repo_project_name}" STREQUAL "${current_repo_project_name}")
        set(part_of_other_repo TRUE)
    else()
        set(part_of_other_repo FALSE)
    endif()

    set(${arg_OUT_VAR} "${part_of_other_repo}" PARENT_SCOPE)
endfunction()

# Handles generating an external document reference SDPX element for each target package that is
# located in a different spdx document, and collects the reference in OUT_SPDX_DEPENDENCIES.
# In case of CycloneDX, we just collect the dependency spdx id (bom-ref) and the target name,
# which are added to OUT_CYDX_DEPENDENCIES and OUT_TARGET_DEPENDENCIES respectively.
function(_qt_internal_sbom_add_external_target_dependency target)
    set(opt_args "")
    set(single_args
        OUT_CYDX_DEPENDENCIES
        OUT_SPDX_DEPENDENCIES
        OUT_TARGET_DEPENDENCIES
    )
    set(multi_args "")
    cmake_parse_arguments(PARSE_ARGV 1 arg "${opt_args}" "${single_args}" "${multi_args}")
    _qt_internal_validate_all_args_are_parsed(arg)

    _qt_internal_sbom_get_spdx_id_for_target("${target}" dep_spdx_id)

    if(NOT dep_spdx_id)
        message(DEBUG "Could not add external target dependency on ${target} "
            "because no spdx id could be found")
        set(${arg_OUT_CYDX_DEPENDENCIES} "" PARENT_SCOPE)
        set(${arg_OUT_SPDX_DEPENDENCIES} "" PARENT_SCOPE)
        set(${arg_OUT_TARGET_DEPENDENCIES} "" PARENT_SCOPE)
        return()
    endif()

    set(cydx_dependencies "")
    set(spdx_dependencies "")
    set(target_depdendencies "")

    # Get the external document path and the repo it belongs to for the given target.
    get_property(relative_installed_repo_document_path TARGET ${target}
        PROPERTY _qt_sbom_spdx_relative_installed_repo_document_path)

    get_property(project_name_lowercase TARGET ${target}
        PROPERTY _qt_sbom_spdx_repo_project_name_lowercase)

    if(relative_installed_repo_document_path AND project_name_lowercase)
        _qt_internal_sbom_get_external_document_ref_spdx_id(
            "${project_name_lowercase}" external_document_ref)

        get_cmake_property(known_external_document
            _qt_known_external_documents_${external_document_ref})

        set(spdx_dependency "${external_document_ref}:${dep_spdx_id}")

        list(APPEND cydx_dependencies "${dep_spdx_id}")
        list(APPEND spdx_dependencies "${spdx_dependency}")
        list(APPEND target_depdendencies "${target}")

        # Only add a reference to the external document package, if we haven't done so already.
        if(NOT known_external_document)
            set(install_prefixes "")

            get_cmake_property(install_prefix _qt_internal_sbom_install_prefix)
            list(APPEND install_prefixes "${install_prefix}")

            set(external_document "${relative_installed_repo_document_path}")

            _qt_internal_sbom_generate_add_external_reference(
                EXTERNAL_DOCUMENT_FILE_PATH "${external_document}"
                EXTERNAL_DOCUMENT_INSTALL_PREFIXES ${install_prefixes}
                EXTERNAL_DOCUMENT_SPDX_ID "${external_document_ref}"
            )

            set_property(GLOBAL PROPERTY
                _qt_known_external_documents_${external_document_ref} TRUE)
            set_property(GLOBAL APPEND PROPERTY
                _qt_known_external_documents "${external_document_ref}")
        endif()
    else()
        message(AUTHOR_WARNING
            "Missing spdx document path for external target dependency: ${target}")
    endif()

    set(${arg_OUT_CYDX_DEPENDENCIES} "${cydx_dependencies}" PARENT_SCOPE)
    set(${arg_OUT_SPDX_DEPENDENCIES} "${spdx_dependencies}" PARENT_SCOPE)
    set(${arg_OUT_TARGET_DEPENDENCIES} "${target_depdendencies}" PARENT_SCOPE)
endfunction()
