#!/usr/bin/env python3
# Copyright (C) 2023 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import json
import os
import subprocess
import sys
from typing import Any, Dict, List
import yaml

ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

GRPC_GN_HEADER = '''
#
# DO NOT EDIT. AUTOGENERATED file
#
# This file is generated with the command:
# tools/gen_grpc_build_gn.py > buildtools/grpc/BUILD.gn
#

import("../../gn/perfetto.gni")

# Prevent the gRPC from being depended upon without explicitly being opted in.
assert(enable_perfetto_grpc)

# BoringSSL has assembly code which is tied to platform-specific. For now, we
# only care about Linux x64 so assert this as the case.
assert(is_linux && current_cpu == "x64")
'''

TARGET_TEMPLATE = """
{target_type}("{name}") {{
  sources = {srcs}
  public_deps = {deps}
  public_configs = ["..:{config_name}"]
  configs -= [ "//gn/standalone:extra_warnings" ]
  check_includes = {check_includes}
}}"""

LIBRARY_IGNORE_LIST = set([
    'grpcpp_channelz',
    'grpc++_reflection',
    'benchmark_helpers',
    'boringssl_test_util',
    'grpcpp_otel_plugin',
    'otel_plugin_test',
    # Use the shared utf8_range from buildtools/BUILD.gn
    'utf8_range_lib',
    # Use the shared re2 from buildtools/BUILD.gn
    're2',
])

TARGET_ALLOW_LIST = set([
    'grpc_cpp_plugin',
])

STATIC_LIBRARY_TARGETS = set([
    'upb',
    're2',
    'boringssl',
    'grpc++',
    'upb_json_lib',
    'upb_textformat_lib',
])

DEP_DENYLIST = set([
    'cares',
])

# Sources to exclude from specific libraries to avoid duplicate symbols.
# These files are already included in grpc/grpc_unsecure directly, so they
# should not be in their dependencies as well.
# Note: paths here should match what's in the YAML (before 'src/' prefix is added).
SOURCE_EXCLUSIONS = {
    'upb_reflection_lib':
        set([
            'src/core/ext/upb-gen/google/protobuf/descriptor.upb_minitable.c',
        ]),
}


def grpc_relpath(*segments: str) -> str:
  '''From path segments to GRPC root, returns the absolute path.'''
  return os.path.join(ROOT_DIR, 'buildtools', 'grpc', 'src', *segments)


GRPC_BUILD_YAML = grpc_relpath('build_autogenerated.yaml')
BSSL_GEN_BUILD_YAML = grpc_relpath('src', 'boringssl', 'gen_build_yaml.py')


def gen_grpc_dep_yaml(gen_path: str) -> Dict[str, Any]:
  '''Invokes a gen_build_yaml.py file for creating YAML for gRPC deps.'''
  return yaml.safe_load(subprocess.check_output(['python3', gen_path]))


def bazel_label_to_gn_target(dep: str) -> str:
  '''Converts a Bazel label name into a gn target name.'''
  if dep == 'libssl':
    return 'boringssl'
  return dep.replace('/', '_').replace(':', '_')


def bazel_label_to_gn_dep(dep: str) -> str:
  if dep == 'protobuf':
    return '..:protobuf_full'
  if dep == 'protoc':
    return '..:protoc_lib'
  if dep == 'z':
    return '..:zlib'
  # Use the shared abseil library from buildtools/BUILD.gn
  if dep.startswith('absl/'):
    return '..:abseil_cpp'
  # Use the shared re2 library from buildtools/BUILD.gn
  if dep == 're2':
    return '..:re2'
  # Use the shared utf8_range from buildtools/BUILD.gn
  if dep == 'utf8_range_lib':
    return '..:utf8_range'
  return ':' + bazel_label_to_gn_target(dep)


def get_library_target_type(target: str) -> str:
  if target in STATIC_LIBRARY_TARGETS:
    return 'static_library'
  return 'source_set'


def yaml_to_gn_targets(desc: Dict[str, Any], build_types: list[str],
                       config_name: str) -> List[str]:
  '''Given a gRPC YAML description of the build graph, generates GN targets.'''
  out = []
  for lib in desc['libs']:
    if lib['build'] not in build_types:
      continue
    if lib['name'] in LIBRARY_IGNORE_LIST:
      continue
    exclusions = SOURCE_EXCLUSIONS.get(lib['name'], set())
    srcs = [
        f'src/{file}' for file in lib['src'] + lib['headers']
        if file not in exclusions
    ]
    if 'asm_src' in lib:
      srcs += [f'src/{file}' for file in lib['asm_src']['crypto_asm']]
    # Use dict.fromkeys to deduplicate while preserving order
    deps = list(
        dict.fromkeys([
            bazel_label_to_gn_dep(dep)
            for dep in lib.get('deps', [])
            if dep not in DEP_DENYLIST
        ]))
    library_target = TARGET_TEMPLATE.format(
        name=bazel_label_to_gn_target(lib['name']),
        config_name=config_name,
        srcs=json.dumps(srcs),
        deps=json.dumps(deps),
        target_type=get_library_target_type(lib['name']),
        # Disable check_includes for targets that include headers from grpc
        # without depending on it (circular dependency in upstream grpc).
        check_includes='false' if lib['name'] in ('upb_json_lib',
                                                  'upb_textformat_lib',
                                                  'upb_reflection_lib',
                                                  'gpr') else 'true')
    out.append(library_target)

  for bin in desc.get('targets', []):
    if bin['build'] not in build_types:
      continue
    if bin['name'] not in TARGET_ALLOW_LIST:
      continue
    srcs = json.dumps([f'src/{file}' for file in bin['src'] + bin['headers']])
    # Use dict.fromkeys to deduplicate while preserving order
    deps = list(
        dict.fromkeys([
            bazel_label_to_gn_dep(dep)
            for dep in bin.get('deps', [])
            if dep not in DEP_DENYLIST
        ]))
    binary_target = TARGET_TEMPLATE.format(
        name=bazel_label_to_gn_target(bin['name']),
        config_name=config_name,
        srcs=srcs,
        deps=json.dumps(deps),
        target_type='executable',
        check_includes='true')
    out.append(binary_target)
  return out


def main():
  out: List[str] = []

  # Note: abseil rules are no longer generated here. gRPC now uses the shared
  # abseil library from buildtools/BUILD.gn (the :abseil_cpp target).

  # Generate boringssl rules
  boringssl_yaml = gen_grpc_dep_yaml(BSSL_GEN_BUILD_YAML)
  out.extend(
      yaml_to_gn_targets(boringssl_yaml, ['private'], 'grpc_boringssl_config'))

  # Generate grpc rules
  with open(GRPC_BUILD_YAML, 'r', encoding='utf-8') as f:
    grpc_yaml = yaml.safe_load(f.read())
  out.extend(
      yaml_to_gn_targets(grpc_yaml, ['all', 'protoc'], 'grpc_internal_config'))

  print(GRPC_GN_HEADER)
  print('\n'.join(out))
  return 0


if __name__ == '__main__':
  sys.exit(main())
