# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

import collections
import contextlib
import io
import pathlib

from compiler import Compiler
import config
from graph import IncludeDir
from graph import Target

_HEADER = """# Copyright 2025 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# AUTOGENERATED FILE. DO NOT EDIT.
# To regenerate, see instructions at build/modules/modularize/README.md

"""
_RULE_MAP = {
    IncludeDir.Builtin: 'builtin_module',
    IncludeDir.Framework: 'framework_module',
    IncludeDir.LibCxx: 'libcxx_module',
    IncludeDir.Sysroot: 'sysroot_module',
    IncludeDir.SysrootModule: 'apple_sysroot_module',
}

_SYSROOT_MODULEMAP = """sysroot_modulemap("sysroot_modulemap") {
  source = "module.modulemap.in"
  out = "${target_gen_dir}/module.modulemap"
}

"""


def _filter_targets(targets: list[Target], compiler: Compiler) -> list[Target]:
    module_to_modulemap = compiler.modulemaps_for_modules()
    modulemap_to_modules = collections.defaultdict(list)
    for k, v in module_to_modulemap.items():
        modulemap_to_modules[v].append(k)

    remaining = [target for target in targets if config.should_compile(target)]
    compiled = set(remaining)
    targets = {target.name: target for target in targets}
    while remaining:
        target = remaining.pop()
        for dep in target.public_deps:
            dep = targets[dep]
            if dep not in compiled:
                compiled.add(dep)
                remaining.append(dep)

        # If we use -fmodule-map-file=c_standard_library.modulemap for one target,
        # all modules in c_standard_library.modulemap need to be precompiled.
        if dep.name not in module_to_modulemap:
            continue
        for module in modulemap_to_modules[module_to_modulemap[dep.name]]:
            if module in targets and targets[module] not in compiled:
                compiled.add(targets[module])
                remaining.append(targets[module])
    return sorted(compiled)


def _update_content(path: pathlib.Path, content: str):
    """Updates the content of a file if changed, thus preventing a siso reload."""
    with contextlib.suppress(FileNotFoundError):
        if path.read_text() == content:
            return
    path.write_text(content)


def render_modulemap(
    out_dir: pathlib.Path,
    replacements: dict[pathlib.Path, str],
    targets: list[Target],
):
    """Writes a modulemap to {out_dir}"""
    f = io.StringIO()
    f.write(_HEADER.replace('#', '//'))
    for target in targets:
        if target.include_dir != IncludeDir.Sysroot:
            continue
        f.write(f'\nmodule {target.name} [system] {{\n')
        for header in target.headers:
            assert header.include_dir == IncludeDir.Sysroot
            f.write(f'  module {header.submodule_name} {{\n')
            for single in sorted(header.group):
                assert single.abs is not None
                path = str(single.abs)
                for frm, to in replacements.items():
                    path = path.replace(str(frm), to)
                # After replacements, all paths should be absolute.
                assert not path.startswith('/'), path
                f.write(f'    header "{path}"\n')
            if not header.exports and header.exports is not None:
                f.write('    export *\n')
            for export in header.exports or []:
                f.write(f'    export {export.submodule_name}\n')

            f.write('  }\n')

        f.write('}\n')
    _update_content(out_dir / 'module.modulemap.in', f.getvalue())


def _render_string_list(f, indent: int, key: str, values: list[str]):
    if not values:
        return
    indent = " " * indent
    f.write(f'{indent}{key} = ')

    if len(values) == 1:
        f.write(f'[ "{values[0]}" ]\n')
    elif len(values) > 1:
        f.write(f'[\n')
        for value in values:
            f.write(f'{indent}  "{value}",\n')
        f.write(f'{indent}]\n')


def render_build_gn(
    out_dir: pathlib.Path, targets: list[Target], compiler: Compiler
):
    """Renders a BUILD.gn file for a specific platform to {out_dir}"""
    targets = _filter_targets(targets, compiler)

    f = io.StringIO()
    f.write(_HEADER)
    f.write('import("//buildtools/third_party/libc++/modules.gni")\n\n')
    all_modulemap_configs = set()
    if compiler.sysroot_dir == IncludeDir.Sysroot:
        all_modulemap_configs.add("sysroot_modulemap")
        f.write(_SYSROOT_MODULEMAP)

    module_to_modulemaps = compiler.modulemaps_for_modules()

    for target in sorted(targets):
        rule = _RULE_MAP[target.include_dir]

        modulemap_target = None
        if target.name in module_to_modulemaps:
            kind, rel = compiler.split_path(module_to_modulemaps[target.name])
            modulemap_target = (
                rel.removesuffix('/module.modulemap')
                .removesuffix('.modulemap')
                .replace('/', '_')
                + '_modulemap'
            )
            if (
                kind == IncludeDir.Framework
                and modulemap_target not in all_modulemap_configs
            ):
                f.write(f'framework_modulemap("{modulemap_target}") {{\n')
                f.write(
                    f'  name = "{modulemap_target.removesuffix("_modulemap")}"\n'
                )
                f.write('}\n')
                all_modulemap_configs.add(modulemap_target)
            elif (
                kind == IncludeDir.SysrootModule
                and modulemap_target not in all_modulemap_configs
            ):
                f.write(f'apple_sysroot_modulemap("{modulemap_target}") {{\n')
                f.write(f'  sysroot_path = "{rel}"\n')
                f.write('}\n')
                all_modulemap_configs.add(modulemap_target)

        f.write(f'{rule}("{target.name}") {{\n')
        if (
            target.include_dir
            in [IncludeDir.SysrootModule, IncludeDir.Framework]
            and modulemap_target is not None
        ):
            f.write(f'  modulemap = ":{modulemap_target}"\n')
            if kind == IncludeDir.SysrootModule:
                f.write(f'  modulemap_path = "{rel}"\n')
        _render_string_list(
            f, 2, 'public_deps', [f':{dep}' for dep in target.public_deps]
        )
        for k, v in sorted(target.kwargs.items()):
            _render_string_list(f, 2, k, sorted(v))
        f.write('}\n\n')

    f.write('group("all_modules") {\n')
    f.write('  public_deps = [\n')
    for target in sorted(targets):
        f.write(f'    ":{target.name}",\n')
    f.write('  ]\n')
    f.write('}\n\n')

    f.write('config("all_modulemap_configs") {\n')
    f.write('  configs = [\n')
    f.write('    "//buildtools/third_party/libc++:builtin_modulemap",\n')
    f.write('    "//buildtools/third_party/libc++:libcxx_modulemap",\n')
    for target in sorted(all_modulemap_configs):
        f.write(f'    ":{target}",\n')
    f.write('  ]\n')
    f.write('}\n')

    _update_content(out_dir / 'BUILD.gn', f.getvalue())
