# Protocol Buffers - Google's data interchange format
# Copyright 2024 Google Inc.  All rights reserved.
#
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd
#
"""Supporting C++ compilation of generated code"""

load("@proto_bazel_features//:features.bzl", "bazel_features")
load("@rules_cc//cc:find_cc_toolchain.bzl", "find_cc_toolchain")
load("@rules_cc//cc/common:cc_common.bzl", "cc_common")
load("@rules_cc//cc/common:cc_info.bzl", "CcInfo")

def get_feature_configuration(ctx, has_sources, extra_requested_features = []):
    """Returns C++ feature configuration for compiling and linking generated C++ files.

    Args:
        ctx: (RuleCtx) rule context.
        has_sources: (bool) Has the proto_library sources.
        extra_requested_features: (list[str]) Additionally requested features.
    Returns:
      (FeatureConfiguration) C++ feature configuration
    """
    cc_toolchain = find_cc_toolchain(ctx)
    requested_features = ctx.features + extra_requested_features

    # TODO: Remove LAYERING_CHECK once we have verified that there are direct
    # dependencies for all generated #includes.
    unsupported_features = ctx.disabled_features + ["parse_headers", "layering_check"]
    if has_sources:
        requested_features.append("header_modules")
    else:
        unsupported_features.append("header_modules")
    return cc_common.configure_features(
        ctx = ctx,
        cc_toolchain = cc_toolchain,
        requested_features = requested_features,
        unsupported_features = unsupported_features,
    )

def _get_libraries_from_linking_outputs(linking_outputs, feature_configuration):
    library_to_link = linking_outputs.library_to_link
    if not library_to_link:
        return []
    outputs = []
    if library_to_link.static_library:
        outputs.append(library_to_link.static_library)
    if library_to_link.pic_static_library:
        outputs.append(library_to_link.pic_static_library)

    # On Windows, dynamic library is not built by default, so don't add them to files_to_build.
    if not cc_common.is_enabled(feature_configuration = feature_configuration, feature_name = "targets_windows"):
        if library_to_link.resolved_symlink_dynamic_library:
            outputs.append(library_to_link.resolved_symlink_dynamic_library)
        elif library_to_link.dynamic_library:
            outputs.append(library_to_link.dynamic_library)
        if library_to_link.resolved_symlink_interface_library:
            outputs.append(library_to_link.resolved_symlink_interface_library)
        elif library_to_link.interface_library:
            outputs.append(library_to_link.interface_library)
    return outputs

def cc_proto_compile_and_link(ctx, deps, sources, headers, disallow_dynamic_library = None, feature_configuration = None, alwayslink = False, **kwargs):
    """Creates C++ compilation and linking actions for C++ proto sources.

    Args:
        ctx: rule context
        deps: (list[CcInfo]) List of libraries to be added as dependencies to compilation and linking
            actions.
        sources:(list[File]) List of C++ sources files.
        headers: list(File] List of C++ headers files.
        disallow_dynamic_library: (bool) Are dynamic libraries disallowed.
        feature_configuration: (FeatureConfiguration) feature configuration to use.
        alwayslink: (bool) Should the library be always linked.
        **kwargs: Additional arguments passed to the compilation. See cc_common.compile.

    Returns:
        (CcInfo, list[File], list[File])
        - CcInfo provider with compilation context and linking context
        - A list of linked libraries related to this proto
        - A list of temporary files generated durind compilation
    """
    cc_toolchain = find_cc_toolchain(ctx)
    feature_configuration = feature_configuration or get_feature_configuration(ctx, bool(sources))
    if disallow_dynamic_library == None:
        # TODO: Configure output artifact with action_config
        # once proto compile action is configurable from the crosstool.
        disallow_dynamic_library = not cc_common.is_enabled(
            feature_name = "supports_dynamic_linker",
            feature_configuration = feature_configuration,
        )

    (compilation_context, compilation_outputs) = cc_common.compile(
        actions = ctx.actions,
        feature_configuration = feature_configuration,
        cc_toolchain = cc_toolchain,
        srcs = sources,
        public_hdrs = headers,
        compilation_contexts = [dep[CcInfo].compilation_context for dep in deps if CcInfo in dep],
        name = ctx.label.name,
        # Don't instrument the generated C++ files even when --collect_code_coverage is set.
        # If we actually start generating coverage instrumentation for .proto files based on coverage
        # data from the generated C++ files, this will have to be removed. Currently, the work done
        # to instrument those files and execute the instrumentation is all for nothing, and it can
        # be quite a bit of extra computation even when that's not made worse by performance bugs,
        # as in b/64963386.
        # code_coverage_enabled = False (cc_common.compile disables code_coverage by default)
        **kwargs
    )

    if sources:
        linking_context, linking_outputs = cc_common.create_linking_context_from_compilation_outputs(
            actions = ctx.actions,
            feature_configuration = feature_configuration,
            cc_toolchain = cc_toolchain,
            compilation_outputs = compilation_outputs,
            linking_contexts = [dep[CcInfo].linking_context for dep in deps if CcInfo in dep],
            name = ctx.label.name,
            disallow_dynamic_library = disallow_dynamic_library,
            alwayslink = alwayslink,
        )
        libraries = _get_libraries_from_linking_outputs(linking_outputs, feature_configuration)
    else:
        linking_context = cc_common.merge_linking_contexts(
            linking_contexts = [dep[CcInfo].linking_context for dep in deps if CcInfo in dep],
        )
        libraries = []

    debug_context = None
    temps = []
    if bazel_features.cc.protobuf_on_allowlist:
        debug_context = cc_common.merge_debug_context(
            [cc_common.create_debug_context(compilation_outputs)] +
            [
                dep[CcInfo]._debug_context if hasattr(dep[CcInfo], "_debug_context") else dep[CcInfo].debug_context()
                for dep in deps
                if CcInfo in dep
            ],
        )
        temps = compilation_outputs.temps()

    return CcInfo(
        compilation_context = compilation_context,
        linking_context = linking_context,
        debug_context = debug_context,
    ), libraries, temps
