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

function(qt_ensure_perl)
    find_program(HOST_PERL "perl" DOC "Perl binary")
    if (NOT HOST_PERL)
        message(FATAL_ERROR "Perl needs to be available to build Qt.")
    endif()
endfunction()

function(qt_ensure_sync_qt)
    qt_ensure_perl()
    if(DEFINED QT_SYNCQT)
        return()
    endif()

    get_property(QT_SYNCQT GLOBAL PROPERTY _qt_syncqt)
    if(NOT "${QT_SYNCQT}" STREQUAL "")
        set(QT_SYNCQT "${QT_SYNCQT}" PARENT_SCOPE)
        return()
    endif()

    # When building qtbase, use the source syncqt, otherwise use the installed one.
    set(SYNCQT_FROM_SOURCE "${QtBase_SOURCE_DIR}/libexec/syncqt.pl")
    if(NOT ("${QtBase_SOURCE_DIR}" STREQUAL "") AND EXISTS "${SYNCQT_FROM_SOURCE}")
        set(syncqt_absolute_path "${SYNCQT_FROM_SOURCE}")
        message(STATUS "Using source syncqt found at: ${syncqt_absolute_path}")

        qt_path_join(syncqt_install_dir ${QT_INSTALL_DIR} ${INSTALL_LIBEXECDIR})
        qt_copy_or_install(PROGRAMS "${SYNCQT_FROM_SOURCE}"
                           DESTINATION "${syncqt_install_dir}")
    elseif(NOT "${QT_HOST_PATH}" STREQUAL "")
        get_filename_component(syncqt_absolute_path
                               "${QT_HOST_PATH}/${QT${PROJECT_VERSION_MAJOR}_HOST_INFO_LIBEXECDIR}/syncqt.pl"
                               ABSOLUTE)
        message(STATUS "Using host syncqt found at: ${syncqt_absolute_path}")
    else()
        get_filename_component(syncqt_absolute_path
                               "${QT_BUILD_INTERNALS_RELOCATABLE_INSTALL_PREFIX}/${INSTALL_LIBEXECDIR}/syncqt.pl"
                               ABSOLUTE)
        message(STATUS "Using installed syncqt found at: ${syncqt_absolute_path}")
    endif()

    set(QT_SYNCQT "${syncqt_absolute_path}" PARENT_SCOPE)
    set_property(GLOBAL PROPERTY _qt_syncqt "${syncqt_absolute_path}")
endfunction()

function(qt_install_injections target build_dir install_dir)
    set(injections ${ARGN})
    qt_internal_module_info(module ${target})
    get_target_property(target_type ${target} TYPE)
    if (target_type STREQUAL "INTERFACE_LIBRARY")
        set(is_framework FALSE)
    else()
        get_target_property(is_framework ${target} FRAMEWORK)
    endif()
    # examples:
    #  SYNCQT.INJECTIONS = src/corelib/global/qconfig.h:qconfig.h:QtConfig src/corelib/global/qconfig_p.h:5.12.0/QtCore/private/qconfig_p.h
    #  SYNCQT.INJECTIONS = src/gui/vulkan/qvulkanfunctions.h:^qvulkanfunctions.h:QVulkanFunctions:QVulkanDeviceFunctions src/gui/vulkan/qvulkanfunctions_p.h:^5.12.0/QtGui/private/qvulkanfunctions_p.h
    # The are 3 parts to the assignment, divded by colons ':'.
    # The first part contains a path to a generated file in a build folder.
    # The second part contains the file name that the forwarding header should have, which points
    # to the file in the first part.
    # The third part contains multiple UpperCaseFileNames that should be forwarding headers to the
    # header specified in the second part.
    separate_arguments(injections UNIX_COMMAND "${injections}")
    foreach(injection ${injections})
        string(REPLACE ":" ";" injection ${injection})
        # Part 1.
        list(GET injection 0 file)
        # Part 2.
        list(GET injection 1 destination)
        string(REGEX REPLACE "^\\^" "" destination "${destination}")
        list(REMOVE_AT injection 0 1)
        # Part 3.
        set(fwd_hdrs ${injection})
        get_filename_component(destinationdir ${destination} DIRECTORY)
        get_filename_component(destinationname ${destination} NAME)
        get_filename_component(original_file_name ${file} NAME)

        # This describes a concrete example for easier comprehension:
        # A file 'qtqml-config.h' is generated by qt_internal_feature_write_file into
        # ${qtdeclarative_build_dir}/src/{module_include_name}/qtqml-config.h (part 1).
        #
        # Generate a lower case forwarding header (part 2) 'qtqml-config.h' at the following
        # location:
        # ${some_prefix}/include/${module_include_name}/qtqml-config.h.
        #
        # Inside this file, we #include the originally generated file,
        # ${qtdeclarative_build_dir}/src/{module_include_name}/qtqml-config.h.
        #
        # ${some_prefix}'s value depends on the build type.
        # If doing a prefix build, it should point to
        # ${current_repo_build_dir} which is ${qtdeclarative_build_dir}.
        # If doing a non-prefix build, it should point to
        # ${qtbase_build_dir}.
        #
        # In the code below, ${some_prefix} == ${build_dir}.
        set(lower_case_forwarding_header_path "${build_dir}/include/${module_include_name}")
        if(destinationdir)
            string(APPEND lower_case_forwarding_header_path "/${destinationdir}")
        endif()
        set(current_repo_build_dir "${PROJECT_BINARY_DIR}")

        file(RELATIVE_PATH relpath
                           "${lower_case_forwarding_header_path}"
                           "${current_repo_build_dir}/${file}")
        set(main_contents "#include \"${relpath}\"")

        qt_configure_file(OUTPUT "${lower_case_forwarding_header_path}/${original_file_name}"
             CONTENT "${main_contents}")

        if(is_framework)
            if(file MATCHES "_p\\.h$")
                set(header_type PRIVATE)
            else()
                set(header_type PUBLIC)
            endif()
            qt_copy_framework_headers(${target} ${header_type}
                ${current_repo_build_dir}/${file})
        else()
            # Copy the actual injected (generated) header file (not the just created forwarding one)
            # to its install location when doing a prefix build. In an non-prefix build, the qt_install
            # will be a no-op.
            qt_path_join(install_destination
                        ${install_dir} ${INSTALL_INCLUDEDIR}
                        ${module_include_name} ${destinationdir})
            qt_install(FILES ${current_repo_build_dir}/${file}
                    DESTINATION ${install_destination}
                    RENAME ${destinationname} OPTIONAL)
        endif()

        # Generate UpperCaseNamed forwarding headers (part 3).
        foreach(fwd_hdr ${fwd_hdrs})
            set(upper_case_forwarding_header_path "include/${module_include_name}")
            if(destinationdir)
                string(APPEND upper_case_forwarding_header_path "/${destinationdir}")
            endif()

            # Generate upper case forwarding header like QVulkanFunctions or QtConfig.
            qt_configure_file(OUTPUT "${build_dir}/${upper_case_forwarding_header_path}/${fwd_hdr}"
                 CONTENT "#include \"${destinationname}\"\n")

            if(is_framework)
                # Copy the forwarding header to the framework's Headers directory.
                qt_copy_framework_headers(${target} PUBLIC
                    "${build_dir}/${upper_case_forwarding_header_path}/${fwd_hdr}")
            else()
                # Install the forwarding header.
                qt_path_join(install_destination "${install_dir}" "${INSTALL_INCLUDEDIR}"
                    ${module_include_name})
                qt_install(FILES "${build_dir}/${upper_case_forwarding_header_path}/${fwd_hdr}"
                        DESTINATION ${install_destination} OPTIONAL)
            endif()
        endforeach()
    endforeach()
endfunction()

function(qt_read_headers_pri module_include_dir resultVarPrefix)
    file(STRINGS "${module_include_dir}/headers.pri" headers_pri_contents)
    foreach(line ${headers_pri_contents})
        if("${line}" MATCHES "SYNCQT.HEADER_FILES = (.*)")
            set(public_module_headers "${CMAKE_MATCH_1}")
            separate_arguments(public_module_headers UNIX_COMMAND "${public_module_headers}")
        elseif("${line}" MATCHES "SYNCQT.PRIVATE_HEADER_FILES = (.*)")
            set(private_module_headers "${CMAKE_MATCH_1}")
            separate_arguments(private_module_headers UNIX_COMMAND "${private_module_headers}")
        elseif("${line}" MATCHES "SYNCQT.GENERATED_HEADER_FILES = (.*)")
            set(generated_module_headers "${CMAKE_MATCH_1}")
            separate_arguments(generated_module_headers UNIX_COMMAND "${generated_module_headers}")
            foreach(generated_header ${generated_module_headers})
                list(APPEND public_module_headers "${module_include_dir}/${generated_header}")
            endforeach()
            list(TRANSFORM generated_module_headers PREPEND "${module_include_dir}/")
        elseif("${line}" MATCHES "SYNCQT.INJECTIONS = (.*)")
            set(injections "${CMAKE_MATCH_1}")
        elseif("${line}" MATCHES "SYNCQT.([A-Z_]+)_HEADER_FILES = (.+)")
            set(prefix "${CMAKE_MATCH_1}")
            string(TOLOWER "${prefix}" prefix)
            set(entries "${CMAKE_MATCH_2}")
            separate_arguments(entries UNIX_COMMAND "${entries}")
            set("${resultVarPrefix}_${prefix}" "${entries}" PARENT_SCOPE)
        endif()
    endforeach()
    set(${resultVarPrefix}_generated "${generated_module_headers}" PARENT_SCOPE)
    set(${resultVarPrefix}_public "${public_module_headers}" PARENT_SCOPE)
    set(${resultVarPrefix}_private "${private_module_headers}" PARENT_SCOPE)
    set(${resultVarPrefix}_injections "${injections}" PARENT_SCOPE)
endfunction()

function(qt_compute_injection_forwarding_header target)
    qt_parse_all_arguments(arg "qt_compute_injection_forwarding_header"
                           "PRIVATE" "SOURCE;OUT_VAR" "" ${ARGN})
    qt_internal_module_info(module "${target}")
    get_filename_component(file_name "${arg_SOURCE}" NAME)

    set(source_absolute_path "${CMAKE_CURRENT_BINARY_DIR}/${arg_SOURCE}")
    file(RELATIVE_PATH relpath "${PROJECT_BINARY_DIR}" "${source_absolute_path}")

    if (arg_PRIVATE)
        set(fwd "${PROJECT_VERSION}/${module_include_name}/private/${file_name}")
    else()
        set(fwd "${file_name}")
    endif()

    string(APPEND ${arg_OUT_VAR} " ${relpath}:${fwd}")
    set(${arg_OUT_VAR} ${${arg_OUT_VAR}} PARENT_SCOPE)
endfunction()

# The function generates the Qt module header structure in build directory and creates install
# rules. Apart the lists of header files the function takes into account
# QT_REPO_PUBLIC_NAMESPACE_REGEX cache variable, that can be set by repository in .cmake.conf file.
# The variable tells the syncqt program, what namespaces are treated as public. Symbols in public
# namespaces are considered when generating CaMeL case header files.
function(qt_internal_target_sync_headers target module_headers module_headers_generated)
    if(NOT TARGET ${QT_CMAKE_EXPORT_NAMESPACE}::syncqt)
        message(FATAL_ERROR "${QT_CMAKE_EXPORT_NAMESPACE}::syncqt is not a target.")
    endif()
    get_target_property(has_headers ${target} _qt_module_has_headers)
    if(NOT has_headers)
        return()
    endif()

    qt_internal_module_info(module "${target}")

    get_target_property(sync_source_directory ${target} _qt_sync_source_directory)
    set(syncqt_timestamp "${CMAKE_CURRENT_BINARY_DIR}/${target}_syncqt_timestamp")
    set(syncqt_outputs "${syncqt_timestamp}")

    set(is_interface_lib FALSE)
    get_target_property(type ${target} TYPE)
    if(type STREQUAL "INTERFACE_LIBRARY")
        set(is_interface_lib TRUE)
    endif()

    set(version_script_private_content_file "")
    if(NOT is_interface_lib)
        list(APPEND syncqt_outputs
            "${module_build_interface_include_dir}/${module}Version"
            "${module_build_interface_include_dir}/qt${module_lower}version.h")
        if(TEST_ld_version_script)
            set(version_script_private_content_file
                "${CMAKE_CURRENT_BINARY_DIR}/${target}.version.private_content")
            set(version_script_args
                "-versionScript" "${version_script_private_content_file}")
            list(APPEND syncqt_outputs "${version_script_private_content_file}")
            qt_internal_add_linker_version_script(${target}
                PRIVATE_CONTENT_FILE "${version_script_private_content_file}")
        endif()
    endif()

    # Check for _qt_module_is_3rdparty_header_library flag to detect non-Qt modules and
    # indicate this to syncqt.
    get_target_property(is_3rd_party_library ${target} _qt_module_is_3rdparty_header_library)
    set(non_qt_module_argument "")
    if(is_3rd_party_library)
        set(non_qt_module_argument "-nonQt")
    else()
        list(APPEND syncqt_outputs "${module_build_interface_include_dir}/${module}")
        get_target_property(no_headersclean_check ${target} _qt_no_headersclean_check)
        if(NOT no_headersclean_check)
            list(APPEND syncqt_outputs
                "${CMAKE_CURRENT_BINARY_DIR}/${module}_header_check_exceptions")
        endif()
    endif()

    set(is_framework FALSE)
    if(NOT is_interface_lib)
        get_target_property(is_framework ${target} FRAMEWORK)
        if(is_framework)
            qt_internal_get_framework_info(fw ${target})
            get_target_property(fw_output_base_dir ${target} LIBRARY_OUTPUT_DIRECTORY)
            set(framework_args "-framework"
                "-frameworkIncludeDir" "${fw_output_base_dir}/${fw_versioned_header_dir}"
            )
        endif()
    endif()

    qt_internal_get_qt_all_known_modules(known_modules)

    get_target_property(is_internal_module ${target} _qt_is_internal_module)
    set(internal_module_argument "")
    if(is_internal_module)
        set(internal_module_argument "-internal")
    endif()

    get_target_property(qpa_filter_regex ${target} _qt_module_qpa_headers_filter_regex)
    get_target_property(private_filter_regex ${target} _qt_module_private_headers_filter_regex)

    # We need to use the real paths since otherwise it may lead to the invalid work of the
    # std::filesystem API
    get_filename_component(source_dir_real "${sync_source_directory}" REALPATH)
    get_filename_component(binary_dir_real "${CMAKE_CURRENT_BINARY_DIR}" REALPATH)

    if(QT_REPO_PUBLIC_NAMESPACE_REGEX)
        set(public_namespaces_filter -publicNamespaceFilter "${QT_REPO_PUBLIC_NAMESPACE_REGEX}")
    endif()

    if(qpa_filter_regex)
        set(qpa_filter_argument
            -qpaHeadersFilter "${qpa_filter_regex}"
        )
    endif()

    set(common_syncqt_arguments
        -module "${module}"
        -sourceDir "${source_dir_real}"
        -binaryDir "${binary_dir_real}"
        -privateHeadersFilter "${private_filter_regex}"
        -includeDir "${module_build_interface_include_dir}"
        -privateIncludeDir "${module_build_interface_private_include_dir}"
        -qpaIncludeDir "${module_build_interface_qpa_include_dir}"
        ${qpa_filter_argument}
        ${public_namespaces_filter}
        ${non_qt_module_argument}
        ${internal_module_argument}
    )

    if(QT_INTERNAL_ENABLE_SYNCQT_DEBUG_OUTPUT)
        list(APPEND common_syncqt_arguments -debug)
    endif()

    set(build_time_syncqt_arguments "")
    if(WARNINGS_ARE_ERRORS)
        if(is_interface_lib)
            set(warnings_are_errors_enabled_genex 1)
        else()
            set(warnings_are_errors_enabled_genex
                "$<NOT:$<BOOL:$<TARGET_PROPERTY:${target},QT_SKIP_WARNINGS_ARE_ERRORS>>>")
        endif()
        list(APPEND build_time_syncqt_arguments
            "$<${warnings_are_errors_enabled_genex}:-warningsAreErrors>")
    endif()

    if(is_framework)
        list(REMOVE_ITEM module_headers "${CMAKE_CURRENT_BINARY_DIR}/${target}_fake_header.h")
    endif()

    # Filter the generated ui_ header files and header files located in the 'doc/' subdirectory.
    list(FILTER module_headers EXCLUDE REGEX
        "(.+/(ui_)[^/]+\\.h|${CMAKE_CURRENT_SOURCE_DIR}(/.+)?/doc/+\\.h)")

    set(module_headers_rsp "${binary_dir_real}/module_headers")
    list(JOIN module_headers "\n" module_headers_string)
    qt_configure_file_v2(OUTPUT "${module_headers_rsp}" CONTENT "${module_headers_string}")

    set(module_headers_generated_rsp "${binary_dir_real}/module_headers_generated")
    list(JOIN module_headers_generated "\n" module_headers_generated_string)
    qt_configure_file_v2(OUTPUT "${module_headers_generated_rsp}" CONTENT
        "${module_headers_generated_string}")

    set(syncqt_staging_dir "${module_build_interface_include_dir}/.syncqt_staging")
    add_custom_command(
        OUTPUT
            ${syncqt_outputs}
        COMMAND
            ${QT_CMAKE_EXPORT_NAMESPACE}::syncqt
            ${common_syncqt_arguments}
            ${build_time_syncqt_arguments}
            -headers "@${module_headers_rsp}"
            -generatedHeaders "@${module_headers_generated_rsp}"
            -stagingDir "${syncqt_staging_dir}"
            -knownModules ${known_modules}
            ${framework_args}
            ${version_script_args}
        COMMAND
            ${CMAKE_COMMAND} -E touch "${syncqt_timestamp}"
        DEPENDS
            ${module_headers_rsp}
            ${module_headers_generated_rsp}
            ${module_headers}
            ${QT_CMAKE_EXPORT_NAMESPACE}::syncqt
        COMMENT
            "Running syncqt.cpp for module: ${module}"
        VERBATIM
    )
    add_custom_target(${target}_sync_headers
        DEPENDS
            ${syncqt_outputs}
    )
    add_dependencies(sync_headers ${target}_sync_headers)

    # This target is required when building docs, to make all header files and their aliases
    # available for qdoc.
    # ${target}_sync_headers is added as dependency to make sure that
    # ${target}_sync_all_public_headers is running after ${target}_sync_headers, when building docs.
    add_custom_target(${target}_sync_all_public_headers
        COMMAND
            ${QT_CMAKE_EXPORT_NAMESPACE}::syncqt
            ${common_syncqt_arguments}
            -all
        DEPENDS
            ${module_headers}
            ${QT_CMAKE_EXPORT_NAMESPACE}::syncqt
            ${target}_sync_headers
        VERBATIM
    )

    if(NOT TARGET sync_all_public_headers)
        add_custom_target(sync_all_public_headers)
    endif()
    add_dependencies(sync_all_public_headers ${target}_sync_all_public_headers)

    if(NOT is_3rd_party_library AND NOT is_framework)
        # Install all the CaMeL style aliases of header files from the staging directory in one rule
        qt_install(DIRECTORY "${syncqt_staging_dir}/"
            DESTINATION "${module_install_interface_include_dir}"
        )
    endif()

    if(NOT is_interface_lib)
        set_property(TARGET ${target}
            APPEND PROPERTY AUTOGEN_TARGET_DEPENDS "${target}_sync_headers")
    endif()
    add_dependencies(${target} "${target}_sync_headers")


    get_target_property(private_module_target ${target} _qt_private_module_target_name)
    if(private_module_target)
        add_dependencies(${private_module_target} "${target}_sync_headers")
    endif()

    # Run sync Qt first time at configure step to make all header files available for the code model
    # of IDEs.
    get_property(synced_modules GLOBAL PROPERTY _qt_synced_modules)
    if(NOT "${module}" IN_LIST synced_modules)
        message(STATUS "Running syncqt.cpp for module: ${module}")
        get_target_property(syncqt_location ${QT_CMAKE_EXPORT_NAMESPACE}::syncqt LOCATION)
        execute_process(
            COMMAND
                ${syncqt_location}
                ${common_syncqt_arguments}
                -headers "@${module_headers_rsp}"
                -generatedHeaders "@${module_headers_generated_rsp}"
                -stagingDir "${syncqt_staging_dir}"
                -knownModules ${known_modules}
                ${framework_args}
            RESULT_VARIABLE syncqt_result
            OUTPUT_VARIABLE syncqt_output
            ERROR_VARIABLE syncqt_output
        )
        if(NOT syncqt_result EQUAL 0)
            message(FATAL_ERROR
                "syncqt.cpp failed for module ${module}:\n${syncqt_output}")
        endif()
        if(syncqt_output)
            message(WARNING "${syncqt_output}")
        endif()
        set_property(GLOBAL APPEND PROPERTY _qt_synced_modules ${module})
    endif()
endfunction()

function(qt_internal_collect_sync_header_dependencies out_var skip_non_existing)
    if(NOT QT_USE_SYNCQT_CPP)
        set(${out_var} "" PARENT_SCOPE)
        return()
    endif()

    list(LENGTH ARGN sync_headers_target_count)
    if(sync_headers_target_count EQUAL 0)
        message(FATAL_ERROR "Invalid use of qt_internal_collect_sync_header_dependencies,"
            " dependencies are not specified")
    endif()

    set(${out_var} "")
    foreach(sync_headers_target IN LISTS ARGN)
        set(sync_headers_target "${sync_headers_target}_sync_headers")
        if(NOT skip_non_existing OR TARGET ${sync_headers_target})
            list(APPEND ${out_var} ${sync_headers_target})
        endif()
    endforeach()
    list(REMOVE_DUPLICATES ${out_var})

    set(${out_var} "${${out_var}}" PARENT_SCOPE)
endfunction()

function(qt_internal_add_sync_header_dependencies target)
    qt_internal_collect_sync_header_dependencies(sync_headers_targets FALSE ${ARGN})
    if(sync_headers_targets)
        add_dependencies(${target} ${sync_headers_targets})
    endif()
endfunction()

function(qt_internal_add_autogen_sync_header_dependencies target)
    qt_internal_collect_sync_header_dependencies(sync_headers_targets TRUE ${ARGN})
    foreach(sync_headers_target IN LISTS sync_headers_targets)
        set_property(TARGET ${target} APPEND PROPERTY AUTOGEN_TARGET_DEPENDS
            "${sync_headers_target}")
    endforeach()
endfunction()
