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

# Includes all helper files for access to necessary functions.
macro(qt_ir_include_all_helpers)
    include(QtIRCommandLineHelpers)
    include(QtIRGitHelpers)
    include(QtIROptionsHelpers)
    include(QtIRParsingHelpers)
    include(QtIRProcessHelpers)
    include(QtTopLevelHelpers)
endmacro()

# Convenience macro to get the working directory from the arguments passed to
# cmake_parse_arguments. Saves a few lines and makes reading the code slightly
# easier.
macro(qt_ir_get_working_directory_from_arg out_var)
    if(NOT arg_WORKING_DIRECTORY)
        message(FATAL_ERROR "No working directory specified")
    endif()
    set(${out_var} "${arg_WORKING_DIRECTORY}")
endmacro()

# Convenience function to set the variable to the name of cmake_parse_arguments
# flag option if it is active.
function(qt_ir_get_cmake_flag flag_name out_var)
    if(arg_${flag_name})
        set(${out_var} "${flag_name}" PARENT_SCOPE)
    else()
        set(${out_var} "" PARENT_SCOPE)
    endif()
endfunction()

# There are some init-repository options that we do not want to allow when called from
# configure. Make sure we error out when they are set by the user.
function(qt_ir_validate_options_for_configure)
    set(disallowed_options
        # Disallow mirror options, because users should set up a proper git mirror manually,
        # not via configure.
        mirror
        oslo
        berlin
    )
    foreach(disallowed_option IN LISTS disallowed_options)
        qt_ir_get_option_value(${disallowed_option} value)
        if(value)
            set(msg
                "Initialization option '${disallowed_option}' is not supported by configure. "
                "If you think this option should be supported, please let us know at "
                "https://bugreports.qt.io/"
            )
            message(FATAL_ERROR ${msg})
        endif()
    endforeach()
endfunction()

# Handle the case when init-repository is called from the configure script.
function(qt_ir_handle_called_from_configure top_level_src_path out_var_exit_reason)
    # Nothing special to do if we're not called from configure.
    qt_ir_is_called_from_configure(is_called_from_configure)
    if(NOT is_called_from_configure)
        set(${out_var_exit_reason} FALSE PARENT_SCOPE)
        return()
    endif()

    # Check whether qtbase was cloned, if not, tell the user how to initialize
    # the repos as part of the configure script.
    qt_ir_get_option_value(init-submodules init_submodules)
    set(configure_script "${top_level_src_path}/qtbase/configure")
    if(NOT EXISTS "${configure_script}" AND NOT init_submodules)
        set(msg "Oops. It looks like you didn't initialize any submodules yet.\nCall configure "
            "with the -init-submodules option to automatically clone a default set of "
            "submodules before configuring Qt.\nYou can also pass "
            "-submodules submodule2,submodule3 to clone a particular set of submodules "
            "and their dependencies. See ./init-repository --help for more information on values "
            "accepted by --module-subset (which gets its values from -submodules).")
        message(${msg})
        set(${out_var_exit_reason} NEED_INIT_SUBMODULES PARENT_SCOPE)
        return()
    endif()

    # Don't do init-repository things when called from configure, qtbase exists and the
    # -init-submodules option is not passed. We assume the repo was already
    # initialized.
    if(NOT init_submodules)
        set(${out_var_exit_reason} ALREADY_INITIALIZED PARENT_SCOPE)
        return()
    endif()

    qt_ir_validate_options_for_configure()

    # -init_submodules implies --force
    qt_ir_set_option_value(force TRUE)

    set(${out_var_exit_reason} FALSE PARENT_SCOPE)
endfunction()

# Returns a list of command line arguments with the init-repository specific
# options removed, which are not recognized by configure.
# It also handles -submodules values like 'essential', 'existing' and '-qtsvg' and transforms them
# into the final list of submodules to be included and excluded, which are then translated
# to configure -submodules and -skip options.
function(qt_ir_get_args_from_optfile_configure_filtered optfile_path out_var)
    cmake_parse_arguments(arg "ALREADY_INITIALIZED" "" "" ${ARGV})

    # Get args unknown to init-repository, and pass them to configure as-is.
    qt_ir_get_unknown_args(unknown_args)

    set(filtered_args ${unknown_args})
    set(extra_configure_args "")
    set(extra_cmake_args "")

    # If the -submodules or --module-subset options were specified, transform
    # the values into something configure understands and pass them to configure.
    qt_ir_get_option_value(module-subset submodules)
    if(submodules)
        qt_ir_get_top_level_submodules(include_submodules exclude_submodules)
        if(NOT include_submodules AND arg_ALREADY_INITIALIZED)
            string(REPLACE "," ";" include_submodules "${submodules}")
        endif()

        # qtrepotools is always implicitly cloned, but it doesn't actually
        # have a CMakeLists.txt, so remove it.
        list(REMOVE_ITEM include_submodules "qtrepotools")

        # Make sure to explicitly pass -DBUILD_<module>=ON, in case they were
        # skipped before, otherwise configure might fail.
        if(include_submodules)
            set(explicit_build_submodules "${include_submodules}")
            list(TRANSFORM explicit_build_submodules PREPEND "-DBUILD_")
            list(TRANSFORM explicit_build_submodules APPEND "=ON")
            list(APPEND extra_cmake_args ${explicit_build_submodules})
        endif()

        list(JOIN include_submodules "," include_submodules)
        list(JOIN exclude_submodules "," exclude_submodules)

        # Handle case when the -skip argument is already passed.
        # In that case read the passed values, merge with new ones,
        # remove both the -skip and its values, and re-add it later.
        list(FIND filtered_args "-skip" skip_index)
        if(exclude_submodules AND skip_index GREATER -1)
            list(LENGTH filtered_args filtered_args_length)
            math(EXPR skip_args_index "${skip_index} + 1")

            if(skip_args_index LESS filtered_args_length)
                list(GET filtered_args "${skip_args_index}" skip_args)
                string(REPLACE "," ";" skip_args "${skip_args}")
                list(APPEND skip_args ${exclude_submodules})
                list(REMOVE_DUPLICATES skip_args)
                list(JOIN skip_args "," exclude_submodules)
                list(REMOVE_AT filtered_args "${skip_args_index}")
                list(REMOVE_AT filtered_args "${skip_index}")
            endif()
        endif()

        # Handle case when only '-submodules existing' is passed and the
        # subset ends up empty.
        if(include_submodules)
            list(APPEND extra_configure_args "-submodules" "${include_submodules}")
        endif()
        if(exclude_submodules)
            list(APPEND extra_configure_args "-skip" "${exclude_submodules}")
        endif()
    endif()

    # Insert the extra arguments into the proper positions before and after '--'.
    list(FIND filtered_args "--" cmake_args_index)

    # -- is not found
    if(cmake_args_index EQUAL -1)
        # Append extra configure args if present
        if(extra_configure_args)
            list(APPEND filtered_args ${extra_configure_args})
        endif()
        # Append extra cmake args if present, but make sure to add -- first at the end
        if(extra_cmake_args)
            list(APPEND filtered_args "--")
            list(APPEND filtered_args ${extra_cmake_args})
        endif()
    else()
        # -- is found, that means we probably have cmake args
        # Insert extra configure args if present, before the -- index.
        if(extra_configure_args)
            list(INSERT filtered_args "${cmake_args_index}" ${extra_configure_args})
        endif()
        # Find the -- index again, because it might have moved
        list(FIND filtered_args "--" cmake_args_index)
        # Compute the index of the argument after the --.
        math(EXPR cmake_args_index "${cmake_args_index} + 1")
        # Insert extra cmake args if present, after the -- index.
        if(extra_cmake_args)
            list(INSERT filtered_args "${cmake_args_index}" ${extra_cmake_args})
        endif()
    endif()

    # Pass --help if it was requested.
    qt_ir_is_help_requested(show_help)
    if(show_help)
        list(APPEND filtered_args "-help")
    endif()

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

# Checks whether any of the arguments passed on the command line are options
# that are marked as unsupported in the cmake port of init-repository.
function(qt_ir_check_if_unsupported_options_used out_var out_var_option_name)
    qt_ir_get_unsupported_options(unsupported_options)

    set(unsupported_options_used FALSE)
    foreach(unsupported_option IN LISTS unsupported_options)
        qt_ir_get_option_value(${unsupported_option} value)
        if(value)
            set(${out_var_option_name} "${unsupported_option}" PARENT_SCOPE)
            set(unsupported_options_used TRUE)
            break()
        endif()
    endforeach()
    set(${out_var} "${unsupported_options_used}" PARENT_SCOPE)
endfunction()

# When an unsupported option is used, show an error message and tell the user
# to run the perly script manually.
function(qt_ir_show_error_how_to_run_perl opt_file unsupported_option_name)
    qt_ir_get_raw_args_from_optfile("${opt_file}" args)
    qt_ir_prettify_command_args(args "${args}")

    set(perl_cmd "perl ./init-repository.pl ${args}")

    message(FATAL_ERROR
        "Option '${unsupported_option_name}' is not implemented in the cmake "
        "port of init-repository. Please let us know if this option is really "
        "important for you at https://bugreports.qt.io/. Meanwhile, you can "
        "still run the perl script directly. \n ${perl_cmd}")
endfunction()

# Check whether help was requested.
function(qt_ir_is_help_requested out_var)
    qt_ir_get_option_value(help value)
    set(${out_var} "${value}" PARENT_SCOPE)
endfunction()

# Check whether the verbose option was used.
function(qt_ir_is_verbose out_var)
    qt_ir_get_option_value(verbose value)
    set(${out_var} "${value}" PARENT_SCOPE)
endfunction()

function(qt_ir_is_called_from_configure out_var)
    qt_ir_get_option_value(from-configure value)
    set(${out_var} "${value}" PARENT_SCOPE)
endfunction()

# Main logic of the script.
function(qt_ir_run_after_args_parsed top_level_src_path out_var_exit_reason)
    set(${out_var_exit_reason} FALSE PARENT_SCOPE)

    qt_ir_is_called_from_configure(is_called_from_configure)

    qt_ir_is_help_requested(show_help)
    if(show_help AND NOT is_called_from_configure)
        qt_ir_show_help()
        set(${out_var_exit_reason} SHOWED_HELP PARENT_SCOPE)
        return()
    endif()

    set(working_directory "${top_level_src_path}")

    qt_ir_handle_if_already_initialized(already_initialized "${working_directory}")
    if(already_initialized)
        set(${out_var_exit_reason} ALREADY_INITIALIZED PARENT_SCOPE)
        return()
    endif()

    # This will be used by the module subset processing to determine whether we
    # should re-initialize the previously initialized (existing) subset.
    qt_ir_check_if_already_initialized_cmake_style(is_initialized
        "${working_directory}" FORCE_QUIET)
    set(previously_initialized_option "")
    if(is_initialized)
        set(previously_initialized_option PREVIOUSLY_INITIALIZED)
    endif()


    # Ge the name of the qt5 repo (tqtc- or not) and the base url for all other repos
    qt_ir_get_qt5_repo_name_and_base_url(
        OUT_VAR_QT5_REPO_NAME qt5_repo_name
        OUT_VAR_BASE_URL base_url
        WORKING_DIRECTORY "${working_directory}")

    qt_ir_get_already_initialized_submodules("${prefix}"
        already_initialized_submodules
        "${qt5_repo_name}"
        "${working_directory}")

    # Get some additional options to pass down.
    qt_ir_get_option_value(alternates alternates)
    qt_ir_get_option_as_cmake_flag_option(branch "CHECKOUT_BRANCH" checkout_branch_option)

    # The prefix for the cmake-style 'dictionary' that will be used by various functions.
    set(prefix "ir_top")

    # Initialize and clone the submodules
    qt_ir_handle_init_submodules("${prefix}"
        ALTERNATES "${alternates}"
        ALREADY_INITIALIZED_SUBMODULES "${already_initialized_submodules}"
        BASE_URL "${base_url}"
        PARENT_REPO_BASE_GIT_PATH "${qt5_repo_name}"
        PROCESS_SUBMODULES_FROM_COMMAND_LINE
        WORKING_DIRECTORY "${working_directory}"
        ${checkout_branch_option}
        ${previously_initialized_option}
    )

    # Add gerrit remotes.
    qt_ir_add_git_remotes("${qt5_repo_name}" "${working_directory}")

    # Install commit and other various hooks.
    qt_ir_install_git_hooks(
        PARENT_REPO_BASE_GIT_PATH "${qt5_repo_name}"
        TOP_LEVEL_SRC_PATH "${top_level_src_path}"
        WORKING_DIRECTORY "${working_directory}"
    )

    # Mark the repo as being initialized.
    qt_ir_set_is_initialized("${working_directory}")
endfunction()

# Entrypoint of the init-repository script.
function(qt_ir_run_main_script top_level_src_path out_var_exit_reason)
    set(${out_var_exit_reason} FALSE PARENT_SCOPE)

    # Windows passes backslash paths.
    file(TO_CMAKE_PATH "${top_level_src_path}" top_level_src_path)

    qt_ir_set_known_command_line_options()

    # If called from configure, there might be arguments that init-repository doesn't know about
    # because they are meant for configure. In that case ignore unknown arguments.
    qt_ir_get_option_value(from-configure from_configure)
    if(from_configure)
        set(ignore_unknown_args "IGNORE_UNKNOWN_ARGS")
    else()
        set(ignore_unknown_args "")
    endif()

    qt_ir_process_args_from_optfile("${OPTFILE}" "${ignore_unknown_args}")

    qt_ir_handle_called_from_configure("${top_level_src_path}" exit_reason)
    if(exit_reason)
        set(${out_var_exit_reason} "${exit_reason}" PARENT_SCOPE)
        return()
    endif()

    qt_ir_check_if_unsupported_options_used(
        unsupported_options_used option_name)
    if(unsupported_options_used)
        qt_ir_show_error_how_to_run_perl("${OPTFILE}" "${option_name}")
    endif()

    qt_ir_run_after_args_parsed("${top_level_src_path}" exit_reason)
    set(${out_var_exit_reason} "${exit_reason}" PARENT_SCOPE)

    # TODO: Consider using cmake_language(EXIT <exit-code>) when cmake 3.29 is released.
endfunction()
