# 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
#
"""Bazel's implementation of cc_proto_library"""

load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
load("@rules_cc//cc:find_cc_toolchain.bzl", "use_cc_toolchain")
load("@rules_cc//cc/common:cc_info.bzl", "CcInfo")
load("//bazel/common:proto_common.bzl", "proto_common")
load("//bazel/common:proto_info.bzl", "ProtoInfo")
load("//bazel/private:cc_proto_support.bzl", "cc_proto_compile_and_link")
load("//bazel/private:toolchain_helpers.bzl", "toolchains")

_CC_PROTO_TOOLCHAIN = Label("//bazel/private:cc_toolchain_type")

_ProtoCcFilesInfo = provider(fields = ["files"], doc = "Provide cc proto files.")
_ProtoCcHeaderInfo = provider(fields = ["headers"], doc = "Provide cc proto headers.")

def _get_output_files(actions, proto_info, suffixes):
    result = []
    for suffix in suffixes:
        result.extend(proto_common.declare_generated_files(
            actions = actions,
            proto_info = proto_info,
            extension = suffix,
        ))
    return result

def _get_strip_include_prefix(ctx, proto_info):
    proto_root = proto_info.proto_source_root
    if proto_root == "." or proto_root == ctx.label.workspace_root:
        return ""
    strip_include_prefix = ""
    if proto_root.startswith(ctx.bin_dir.path):
        proto_root = proto_root[len(ctx.bin_dir.path) + 1:]
    elif proto_root.startswith(ctx.genfiles_dir.path):
        proto_root = proto_root[len(ctx.genfiles_dir.path) + 1:]

    if proto_root.startswith(ctx.label.workspace_root):
        proto_root = proto_root[len(ctx.label.workspace_root):]

    strip_include_prefix = "//" + proto_root
    return strip_include_prefix

def _aspect_impl(target, ctx):
    proto_info = target[ProtoInfo]

    sources = []
    headers = []
    textual_hdrs = []

    proto_toolchain = toolchains.find_toolchain(ctx, "proto_toolchain_for_cc", _CC_PROTO_TOOLCHAIN)
    should_generate_code = proto_common.experimental_should_generate_code(proto_info, proto_toolchain, "cc_proto_library", target.label)

    if should_generate_code:
        if len(proto_info.direct_sources) != 0:
            source_suffixes = ctx.attr._cc_proto_library_source_suffixes[BuildSettingInfo].value
            header_suffixes = ctx.attr._cc_proto_library_header_suffixes[BuildSettingInfo].value
            sources = _get_output_files(ctx.actions, proto_info, source_suffixes)
            headers = _get_output_files(ctx.actions, proto_info, header_suffixes)
            header_provider = _ProtoCcHeaderInfo(headers = depset(headers))
        else:
            # If this proto_library doesn't have sources, it provides the combined headers of all its
            # direct dependencies. Thus, if a direct dependency does have sources, the generated files
            # are also provided by this library. If a direct dependency does not have sources, it will
            # do the same thing, so that effectively this library looks through all source-less
            # proto_libraries and provides all generated headers of the proto_libraries with sources
            # that it depends on.
            transitive_headers = []
            for dep in getattr(ctx.rule.attr, "deps", []):
                if _ProtoCcHeaderInfo in dep:
                    textual_hdrs.extend(dep[_ProtoCcHeaderInfo].headers.to_list())
                    transitive_headers.append(dep[_ProtoCcHeaderInfo].headers)
            header_provider = _ProtoCcHeaderInfo(headers = depset(transitive = transitive_headers))

    else:  # shouldn't generate code
        header_provider = _ProtoCcHeaderInfo(headers = depset())

    proto_common.compile(
        actions = ctx.actions,
        proto_info = proto_info,
        proto_lang_toolchain_info = proto_toolchain,
        generated_files = sources + headers,
        experimental_output_files = "multiple",
    )

    deps = []
    if proto_toolchain.runtime:
        deps = [proto_toolchain.runtime]
    deps.extend(getattr(ctx.rule.attr, "deps", []))

    cc_info, libraries, temps = cc_proto_compile_and_link(
        ctx = ctx,
        deps = deps,
        sources = sources,
        headers = headers,
        textual_hdrs = textual_hdrs,
        strip_include_prefix = _get_strip_include_prefix(ctx, proto_info),
    )

    return [
        cc_info,
        _ProtoCcFilesInfo(files = depset(sources + headers + libraries)),
        OutputGroupInfo(temp_files_INTERNAL_ = temps),
        header_provider,
    ]

cc_proto_aspect = aspect(
    implementation = _aspect_impl,
    attr_aspects = ["deps"],
    fragments = ["cpp", "proto"],
    required_providers = [ProtoInfo],
    provides = [CcInfo],
    attrs = {
                "_cc_proto_library_header_suffixes": attr.label(
                    default = "//bazel/flags/cc:cc_proto_library_header_suffixes",
                ),
                "_cc_proto_library_source_suffixes": attr.label(
                    default = "//bazel/flags/cc:cc_proto_library_source_suffixes",
                ),
            } |
            toolchains.if_legacy_toolchain({
                "_proto_toolchain_for_cc": attr.label(
                    default = Label("//bazel/flags/cc:proto_toolchain_for_cc"),
                ),
            }),
    toolchains = use_cc_toolchain() + toolchains.use_toolchain(_CC_PROTO_TOOLCHAIN),
)

def _cc_proto_library_impl(ctx):
    if len(ctx.attr.deps) != 1:
        fail(
            "'deps' attribute must contain exactly one label " +
            "(we didn't name it 'dep' for consistency). " +
            "The main use-case for multiple deps is to create a rule that contains several " +
            "other targets. This makes dependency bloat more likely. It also makes it harder" +
            "to remove unused deps.",
            attr = "deps",
        )
    dep = ctx.attr.deps[0]

    proto_toolchain = toolchains.find_toolchain(ctx, "proto_toolchain_for_cc", _CC_PROTO_TOOLCHAIN)
    proto_common.check_collocated(ctx.label, dep[ProtoInfo], proto_toolchain)

    return [DefaultInfo(files = dep[_ProtoCcFilesInfo].files), dep[CcInfo], dep[OutputGroupInfo]]

cc_proto_library = rule(
    implementation = _cc_proto_library_impl,
    doc = """
<p>
<code>cc_proto_library</code> generates C++ code from <code>.proto</code> files.
</p>

<p>
<code>deps</code> must point to <a href="protocol-buffer.html#proto_library"><code>proto_library
</code></a> rules.
</p>

<p>
Example:
</p>

<pre>
<code class="lang-starlark">
cc_library(
    name = "lib",
    deps = [":foo_cc_proto"],
)

cc_proto_library(
    name = "foo_cc_proto",
    deps = [":foo_proto"],
)

proto_library(
    name = "foo_proto",
)
</code>
</pre>
""",
    attrs = {
        "deps": attr.label_list(
            aspects = [cc_proto_aspect],
            providers = [ProtoInfo],
            allow_files = False,
            doc = """
The list of <a href="protocol-buffer.html#proto_library"><code>proto_library</code></a>
rules to generate C++ code for.""",
        ),
    } | toolchains.if_legacy_toolchain({
        "_proto_toolchain_for_cc": attr.label(
            default = "//bazel/flags/cc:proto_toolchain_for_cc",
        ),
    }),
    provides = [CcInfo],
    toolchains = toolchains.use_toolchain(_CC_PROTO_TOOLCHAIN),
)
