#!/usr/bin/env python
# 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.
"""Generates a patch file for sources from Fuchsia's fit and stdcompat.

Run this script to update third_party/fuchsia/pigweed_adaptations.patch.
"""

from pathlib import Path
import re
import subprocess
import tempfile
from typing import Iterable, TextIO
from datetime import datetime

HEADER = f'''# Copyright {datetime.today().year} 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.

# DO NOT EDIT! This file is generated by {Path(__file__).name}.
#
# To make changes, update and run ./{Path(__file__).name}.

# Patch the fit::function implementation for use in Pigweed:
#
#   - Use PW_ASSERT instead of __builtin_abort.
#   - Temporarily disable sanitizers when invoking a function for b/241567321.
#
'''.encode()


def _read_files_list(file: TextIO) -> Iterable[str]:
    """Reads the files list from the copy.bara.sky file."""
    found_list = False

    for line in file:
        if found_list:
            yield line
            if line == ']\n':
                break
        else:
            if line == 'fuchsia_repo_files = [\n':
                found_list = True
                yield '['


def _clone_fuchsia(temp_path: Path) -> Path:
    subprocess.run(
        [
            'git',
            '-C',
            temp_path,
            'clone',
            '--depth',
            '1',
            'https://fuchsia.googlesource.com/fuchsia',
        ],
        check=True,
    )

    return temp_path / 'fuchsia'


def _read_files(script: Path) -> list[Path]:
    with script.open() as file:
        paths_list: list[str] = eval(  # pylint: disable=eval-used
            ''.join(_read_files_list(file))
        )
        return list(Path(p) for p in paths_list if not 'lib/stdcompat/' in p)


def _add_include_before_namespace(text: str, include: str) -> str:
    return text.replace(
        '\nnamespace ', f'\n#include "{include}"\n\nnamespace ', 1
    )


_ASSERT = re.compile(r'\bassert\(')


def _patch_assert(text: str) -> str:
    replaced = text.replace('__builtin_abort()', 'PW_ASSERT(false)')
    replaced = _ASSERT.sub('PW_ASSERT(', replaced)

    if replaced == text:
        return replaced

    return _add_include_before_namespace(replaced, 'pw_assert/assert.h')


_CONSTINIT = re.compile(r'\b__CONSTINIT\b')


def _patch_constinit(text: str) -> str:
    replaced = _CONSTINIT.sub('PW_CONSTINIT', text)

    if replaced == text:
        return text

    replaced = replaced.replace('#include <zircon/compiler.h>\n', '')
    return _add_include_before_namespace(
        replaced, "pw_polyfill/language_feature_macros.h"
    )


_INVOKE_PATCH = (
    '\n'
    '  // TODO: b/241567321 - Remove "no sanitize" after pw_protobuf is fixed.\n'
    '  Result invoke(Args... args) const PW_NO_SANITIZE("function") {'
)


def _patch_invoke(file: Path, text: str) -> str:
    # Update internal/function.h only.
    if file.name != 'function.h' or file.parent.name != 'internal':
        return text

    text = _add_include_before_namespace(text, 'pw_preprocessor/compiler.h')
    return text.replace(
        '\n  Result invoke(Args... args) const {', _INVOKE_PATCH
    )


def _patch(file: Path) -> str | None:
    text = file.read_text()
    updated = _patch_assert(text)
    updated = _patch_constinit(updated)
    updated = _patch_invoke(file, updated)
    return None if text == updated else updated


def _main() -> None:
    output_path = Path(__file__).parent

    # Clone Fuchsia to a temp directory
    with tempfile.TemporaryDirectory() as directory:
        repo = _clone_fuchsia(Path(directory))

        # Read the files list from copy.bara.sky and patch those files.
        paths = _read_files(output_path / 'copy.bara.sky')
        for file in (repo / path for path in paths):
            if (text := _patch(file)) is not None:
                print('Patching', file)
                file.write_text(text)
                subprocess.run(['clang-format', '-i', file], check=True)

        # Create a diff for the changes.
        diff = subprocess.run(
            ['git', '-C', repo, 'diff'], stdout=subprocess.PIPE, check=True
        ).stdout
        for path in paths:
            diff = diff.replace(
                path.as_posix().encode(),
                Path('third_party/fuchsia/repo', path).as_posix().encode(),
            )

    # Write the diff to function.patch.
    with output_path.joinpath('pigweed_adaptations.patch').open('wb') as output:
        output.write(HEADER)

        for line in diff.splitlines(keepends=True):
            if line == b' \n':
                output.write(b'\n')
            elif not line.startswith(b'index '):  # drop line with Git hashes
                output.write(line)


if __name__ == '__main__':
    _main()
