# Copyright 2022 The Pigweed Authors
#
# 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
#
#     https://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.
"""Manages the list of Pigweed modules.

Used by modules.gni to generate:

- a build arg for each module,
- a list of module paths (pw_modules),
- a list of module tests (pw_module_tests), and
- a list of module docs (pw_module_docs).
"""

import difflib
from pathlib import Path
import sys
from typing import Iterator, List, Sequence, Tuple


# TODO(hepler): Add tests and docs targets to all modules.
def _find_tests_and_docs(
        root: Path, modules: Sequence[str]) -> Tuple[List[str], List[str]]:
    """Lists "tests" and "docs" targets for modules that declare them."""
    tests = []
    docs = []

    for module in modules:
        build_gn_contents = root.joinpath(module, 'BUILD.gn').read_bytes()
        if b'group("tests")' in build_gn_contents:
            tests.append(f'  "$dir_{module}:tests",')

        if b'group("docs")' in build_gn_contents:
            docs.append(f'  "$dir_{module}:docs",')

    return tests, docs


_MISSING_MODULES_WARNING = '''
The PIGWEED_MODULES list is missing the following modules:
{modules}

If the listed modules are Pigweed modules, add them to PIGWEED_MODULES.

If the listed modules are not actual Pigweed modules, remove any stray pw_*
directories in the Pigweed repository (git clean -fd).
'''


def _check_modules_list(root: Path, modules: Sequence[str]) -> Iterator[str]:
    missing = _missing_modules(root, modules)

    out_of_order = any(modules[i] > modules[i + 1]
                       for i in range(len(modules) - 1))

    if not missing and not out_of_order:
        return

    yield ''
    yield 'if (current_toolchain == default_toolchain) {'
    yield 'print()'
    yield 'print("\033[41m\033[37m\033[1m!!! WARNING !!!\033[0m")'

    if missing:
        msg = _MISSING_MODULES_WARNING.format(modules=''.join(
            f'\n  {module}' for module in missing))
        for line in msg.splitlines():
            yield f'print("{line}")'

    if out_of_order:
        yield 'print()'
        yield 'print("The Pigweed modules list is not sorted!")'
        yield 'print("Apply the following diff to fix the order:")'
        yield 'print()'
        for line in difflib.unified_diff(modules,
                                         sorted(modules),
                                         lineterm='',
                                         n=1,
                                         fromfile='PIGWEED_MODULES',
                                         tofile='PIGWEED_MODULES'):
            yield f'print("{line}")'

    yield 'print()'
    yield '}'


def _generate_modules_gni(root: Path, prefix: str,
                          modules: Sequence[str]) -> Iterator[str]:
    """Generates a .gni file with variables and lists for Pigweed modules."""
    script = Path(__file__).relative_to(root.resolve()).as_posix()

    yield '# Build args and lists for all modules in Pigweed.'
    yield '#'
    yield f'# DO NOT EDIT! Generated by {prefix}{script}.'
    yield '#'
    yield f'# To add modules here, list them in {prefix}PIGWEED_MODULES.'
    yield '#'
    yield '# DO NOT IMPORT THIS FILE DIRECTLY!'
    yield '#'
    yield '# Import it through //build_overrides/pigweed.gni instead.'
    yield ''
    # Create a build arg for each module.
    yield 'declare_args() {'

    for module in modules:
        yield f'  dir_{module} = get_path_info("{prefix}{module}", "abspath")'

    yield '}'
    yield ''
    # Create a list with paths to all modules.
    yield 'pw_modules = ['

    for module in modules:
        yield f'  dir_{module},'

    yield ']'
    yield ''

    # Create a list of module test targets and a list of module docs targets.
    tests, docs = _find_tests_and_docs(root, modules)

    yield 'pw_module_tests = ['
    yield from tests
    yield ']'
    yield ''
    yield 'pw_module_docs = ['
    yield from docs
    yield ']'

    # Display a warning about missing modules when building for upstream
    # Pigweed. The prefix will only be // when building Pigweed directly.
    if prefix == '//':
        yield from _check_modules_list(root, modules)


def _missing_modules(root: Path, modules: Sequence[str]) -> Sequence[str]:
    return sorted(
        frozenset(
            str(p.relative_to(root))
            for p in root.glob('pw_*') if p.is_dir()) - frozenset(modules))


def _main(root: Path, gn_path_prefix: str, modules_file: Path,
          modules_gni_file: Path) -> int:
    modules = tuple(modules_file.read_text().splitlines())
    with modules_gni_file.open('w', encoding='utf-8') as file:
        for line in _generate_modules_gni(root, gn_path_prefix, modules):
            print(line, file=file)

    return 0


if __name__ == '__main__':
    if len(sys.argv) != 5:
        print(__doc__, file=sys.stderr)
        sys.exit(1)

    # Usage: modules.py PIGWEED_ROOT GN_PATH_PREFIX INPUT OUTPUT
    sys.exit(
        _main(Path(sys.argv[1]), sys.argv[2], Path(sys.argv[3]),
              Path(sys.argv[4])))
