#!/usr/bin/env vpython3
# Copyright 2023 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
#
# Generates a single BUILD.gn file with build targets generated using the
# manifest files in the SDK.

# TODO(b/40935282): Likely this file should belong to the
# //third_party/fuchsia-gn-sdk/ instead of //build/fuchsia/.

import json
import logging
import os
import sys

sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__),
                                             'test')))

from common import DIR_SRC_ROOT, SDK_ROOT, GN_SDK_ROOT, get_host_os

assert GN_SDK_ROOT.startswith(DIR_SRC_ROOT)
assert GN_SDK_ROOT[-1] != '/'
GN_SDK_GN_ROOT = GN_SDK_ROOT[len(DIR_SRC_ROOT):]
assert GN_SDK_GN_ROOT.startswith('/')

# Inserted at the top of the generated BUILD.gn file.
_GENERATED_PREAMBLE = f"""# DO NOT EDIT! This file was generated by
# //build/fuchsia/gen_build_def.py.
# Any changes made to this file will be discarded.

import("/{GN_SDK_GN_ROOT}/fidl_library.gni")
import("/{GN_SDK_GN_ROOT}/fuchsia_sdk_package.gni")
import("/{GN_SDK_GN_ROOT}/fuchsia_sdk_pkg.gni")

"""


def ReformatTargetName(dep_name):
  """"Substitutes characters in |dep_name| which are not valid in GN target
  names (e.g. dots become hyphens)."""
  return dep_name


def FormatGNTarget(fields):
  """Returns a GN target definition as a string.

  |fields|: The GN fields to include in the target body.
            'target_name' and 'type' are mandatory."""

  output = '%s("%s") {\n' % (fields['type'], fields['target_name'])
  del fields['target_name']
  del fields['type']

  # Ensure that fields with no ordering requirement are sorted.
  for field in ['sources', 'public_deps']:
    if field in fields:
      fields[field].sort()

  for key, val in fields.items():
    if isinstance(val, str):
      val_serialized = '\"%s\"' % val
    elif isinstance(val, list):
      # Serialize a list of strings in the prettiest possible manner.
      if len(val) == 0:
        val_serialized = '[]'
      elif len(val) == 1:
        val_serialized = '[ \"%s\" ]' % val[0]
      else:
        val_serialized = '[\n    ' + ',\n    '.join(['\"%s\"' % x
                                                     for x in val]) + '\n  ]'
    else:
      raise Exception('Could not serialize %r' % val)

    output += '  %s = %s\n' % (key, val_serialized)
  output += '}'

  return output


def MetaRootRelativePaths(sdk_relative_paths, meta_root):
  return [os.path.relpath(path, meta_root) for path in sdk_relative_paths]


def ConvertCommonFields(json):
  """Extracts fields from JSON manifest data which are used across all
  target types. Note that FIDL packages do their own processing."""

  meta_root = json['root']

  converted = {'target_name': ReformatTargetName(json['name'])}

  if 'deps' in json:
    converted['public_deps'] = MetaRootRelativePaths(json['deps'],
                                                     os.path.dirname(meta_root))

  # FIDL bindings dependencies are relative to the "fidl" sub-directory.
  if 'fidl_binding_deps' in json:
    for entry in json['fidl_binding_deps']:
      converted['public_deps'] += MetaRootRelativePaths([
          'fidl/' + dep + ':' + os.path.basename(dep) + '_' +
          entry['binding_type'] for dep in entry['deps']
      ], meta_root)

  return converted


def ConvertFidlLibrary(json):
  """Converts a fidl_library manifest entry to a GN target.

  Arguments:
    json: The parsed manifest JSON.
  Returns:
    The GN target definition, represented as a string."""

  meta_root = json['root']

  converted = ConvertCommonFields(json)
  converted['type'] = 'fidl_library'
  converted['sources'] = MetaRootRelativePaths(json['sources'], meta_root)
  converted['library_name'] = json['name']

  return converted


def ConvertCcPrebuiltLibrary(json):
  """Converts a cc_prebuilt_library manifest entry to a GN target.

  Arguments:
    json: The parsed manifest JSON.
  Returns:
    The GN target definition, represented as a string."""

  meta_root = json['root']

  converted = ConvertCommonFields(json)
  converted['type'] = 'fuchsia_sdk_pkg'

  converted['sources'] = MetaRootRelativePaths(json['headers'], meta_root)

  converted['include_dirs'] = MetaRootRelativePaths([json['include_dir']],
                                                    meta_root)

  if json['format'] == 'shared':
    converted['shared_libs'] = [json['name']]
  else:
    converted['static_libs'] = [json['name']]

  return converted


def ConvertCcSourceLibrary(json):
  """Converts a cc_source_library manifest entry to a GN target.

  Arguments:
    json: The parsed manifest JSON.
  Returns:
    The GN target definition, represented as a string."""

  meta_root = json['root']

  converted = ConvertCommonFields(json)
  converted['type'] = 'fuchsia_sdk_pkg'

  # Headers and source file paths can be scattered across "sources", "headers",
  # and "files". Merge them together into one source list.
  converted['sources'] = MetaRootRelativePaths(json['sources'], meta_root)
  if 'headers' in json:
    converted['sources'] += MetaRootRelativePaths(json['headers'], meta_root)
  if 'files' in json:
    converted['sources'] += MetaRootRelativePaths(json['files'], meta_root)
  converted['sources'] = list(set(converted['sources']))

  converted['include_dirs'] = MetaRootRelativePaths([json['include_dir']],
                                                    meta_root)

  return converted


def ConvertLoadableModule(json):
  """Converts a loadable module manifest entry to GN targets.

  Arguments:
    json: The parsed manifest JSON.
  Returns:
    A list of GN target definitions."""

  name = json['name']
  if name != 'vulkan_layers':
    raise RuntimeError('Unsupported loadable_module: %s' % name)

  # Copy resources and binaries
  resources = json['resources']

  binaries = json['binaries']

  def _filename_no_ext(name):
    return os.path.splitext(os.path.basename(name))[0]

  # Pair each json resource with its corresponding binary. Each such pair
  # is a "layer". We only need to check one arch because each arch has the
  # same list of binaries.
  arch = next(iter(binaries))
  binary_names = binaries[arch]
  local_pkg = json['root']
  vulkan_targets = []

  for res in resources:
    layer_name = _filename_no_ext(res)

    # Filter binaries for a matching name.
    filtered = [n for n in binary_names if _filename_no_ext(n) == layer_name]

    if not filtered:
      # If the binary could not be found then do not generate a
      # target for this layer. The missing targets will cause a
      # mismatch with the "golden" outputs.
      continue

    # Replace hardcoded arch in the found binary filename.
    binary = filtered[0].replace('/' + arch + '/', "/${target_cpu}/")

    target = {}
    target['name'] = layer_name
    target['config'] = os.path.relpath(res, start=local_pkg)
    target['binary'] = os.path.relpath(binary, start=local_pkg)

    vulkan_targets.append(target)

  converted = []
  all_target = {}
  all_target['target_name'] = 'all'
  all_target['type'] = 'group'
  all_target['data_deps'] = []
  for target in vulkan_targets:
    config_target = {}
    config_target['target_name'] = target['name'] + '_config'
    config_target['type'] = 'copy'
    config_target['sources'] = [target['config']]
    config_target['outputs'] = ['${root_gen_dir}/' + target['config']]
    converted.append(config_target)
    lib_target = {}
    lib_target['target_name'] = target['name'] + '_lib'
    lib_target['type'] = 'copy'
    lib_target['sources'] = [target['binary']]
    lib_target['outputs'] = ['${root_out_dir}/lib/{{source_file_part}}']
    converted.append(lib_target)
    group_target = {}
    group_target['target_name'] = target['name']
    group_target['type'] = 'group'
    group_target['data_deps'] = [
        ':' + target['name'] + '_config', ':' + target['name'] + '_lib'
    ]
    converted.append(group_target)
    all_target['data_deps'].append(':' + target['name'])
  converted.append(all_target)
  return converted


def ConvertPackage(json):
  """Converts a package manifest entry to a GN target.

  Arguments:
    json: The parsed manifest JSON.
  Returns:
    The GN target definition."""

  converted = {
      'target_name': ReformatTargetName(json['name']),
      'type': 'fuchsia_sdk_package',
  }

  # Extrapolate the manifest_file's path from the first variant, assuming that
  # they all follow the same format.
  variant = json['variants'][0]
  replace_pattern = '/%s-api-%d/' % (variant['arch'], variant['api_level'])
  segments = variant['manifest_file'].split(replace_pattern)
  if len(segments) != 2:
    raise RuntimeError('Unsupported pattern: %s' % variant['manifest_file'])
  converted['manifest_file'] = \
      '/${target_cpu}-api-${fuchsia_target_api_level}/'.join(segments)

  return converted


def ConvertNoOp(json):
  """Null implementation of a conversion function. No output is generated."""

  return None


"""Maps manifest types to conversion functions."""
_CONVERSION_FUNCTION_MAP = {
    'fidl_library': ConvertFidlLibrary,
    'cc_source_library': ConvertCcSourceLibrary,
    'cc_prebuilt_library': ConvertCcPrebuiltLibrary,
    'loadable_module': ConvertLoadableModule,
    'package': ConvertPackage,

    # No need to build targets for these types yet.
    'bind_library': ConvertNoOp,
    'companion_host_tool': ConvertNoOp,
    'component_manifest': ConvertNoOp,
    'config': ConvertNoOp,
    'dart_library': ConvertNoOp,
    'data': ConvertNoOp,
    'device_profile': ConvertNoOp,
    'documentation': ConvertNoOp,
    'ffx_tool': ConvertNoOp,
    'host_tool': ConvertNoOp,
    'image': ConvertNoOp,
    'sysroot': ConvertNoOp,
}


def ConvertMeta(meta_path):
  parsed = json.load(open(meta_path))
  if 'type' not in parsed:
    return

  convert_function = _CONVERSION_FUNCTION_MAP.get(parsed['type'])
  if convert_function is None:
    logging.warning('Unexpected SDK artifact type %s in %s.' %
                    (parsed['type'], meta_path))
    return

  converted = convert_function(parsed)
  if not converted:
    return
  output_path = os.path.join(os.path.dirname(meta_path), 'BUILD.gn')
  if os.path.exists(output_path):
    os.unlink(output_path)
  with open(output_path, 'w') as buildfile:
    buildfile.write(_GENERATED_PREAMBLE)

    # Loadable modules have multiple targets
    if convert_function != ConvertLoadableModule:
      buildfile.write(FormatGNTarget(converted) + '\n\n')
    else:
      for target in converted:
        buildfile.write(FormatGNTarget(target) + '\n\n')


def ProcessSdkManifest():
  toplevel_meta = json.load(
      open(os.path.join(SDK_ROOT, 'meta', 'manifest.json')))

  for part in toplevel_meta['parts']:
    meta_path = os.path.join(SDK_ROOT, part['meta'])
    ConvertMeta(meta_path)


def main():

  # Exit if there's no Fuchsia support for this platform.
  try:
    get_host_os()
  except:
    logging.warning('Fuchsia SDK is not supported on this platform.')
    return 0

  # TODO(crbug/1432399): Remove this when links to these files inside the sdk
  # directory have been redirected.
  build_path = os.path.join(SDK_ROOT, 'build')
  os.makedirs(build_path, exist_ok=True)
  for gn_file in ['component.gni', 'package.gni']:
    open(os.path.join(build_path, gn_file),
         "w").write("""# DO NOT EDIT! This file was generated by
# //build/fuchsia/gen_build_def.py.
# Any changes made to this file will be discarded.

import("/%s/%s")
      """ % (GN_SDK_GN_ROOT, gn_file))

  ProcessSdkManifest()


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