#!/usr/bin/env python3

# Copyright 2024 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.
"""Applies clang-tidy suggested fixes.

This uses the clang-apply-replacements tool to apply code changes described in
a YAML file generated by clang-tidy.
"""

import argparse
import logging
import re
from pathlib import Path
from pw_cli.tool_runner import BasicSubprocessRunner

_LOG: logging.Logger = logging.getLogger(__name__)
_REMOVE_CHANGE_FILES_FLAG = "--remove-change-desc-files"
_IGNORE_INSERT_CONFLICT_FLAG = "--ignore-insert-conflict"
_CLANG_TIDY_SUGGESTED_FIX_FILE_REGEX = "*.o.yaml"
_PIGWEED_INCLUDE_FIX_REGEX = re.compile(r'ReplacementText: "#include <(pw_.*)>')
_PIGWEED_INCLUDE_REPLACEMENT_REGEX = r'ReplacementText: "#include \"\1\"'


def apply_replacements(
    root: Path, remove_change_desc_files: bool, raise_insert_conflict: bool
) -> int:
    """Runs the clang-apply-replacements tool to apply clang-tidy fixes.

    Args:
        root: The directory under which clang-apply-replacements will search
            for YAML fix files generated by clang-tidy.
        remove_change_desc_files: Whether or not to remove the change
            description files regardless of successful.
        raise_insert_conflict: Whether or not to ignore insert conflicts.

    Returns:
        The return code of the clang-apply-replacements tool.
    """
    # Search under root for YAML files generated by clang-tidy that contain
    # suggested fixes
    for filepath in root.rglob(_CLANG_TIDY_SUGGESTED_FIX_FILE_REGEX):
        clang_tidy_fixes = filepath.read_text()

        # Change suggested pigweed includes from <pw_.*> to "pw_.*"
        if clang_tidy_fixes:
            new_clang_tidy_fixes = _PIGWEED_INCLUDE_FIX_REGEX.sub(
                _PIGWEED_INCLUDE_REPLACEMENT_REGEX, clang_tidy_fixes
            )
            filepath.write_text(new_clang_tidy_fixes)

    # Add flags for the clang-apply-replacements tool
    flags = []
    if remove_change_desc_files:
        flags.append(_REMOVE_CHANGE_FILES_FLAG)
    if raise_insert_conflict:
        flags.append(_IGNORE_INSERT_CONFLICT_FLAG)

    # Use the clang-apply-replacements tool via a subprocess
    _LOG.info('Applying clang-tidy fixes')
    run_tool = BasicSubprocessRunner()
    process_result = run_tool(
        'clang-apply-replacements',
        [str(root)] + flags,
        check=True,
    )
    return process_result.returncode


def arguments() -> argparse.ArgumentParser:
    """Creates an argument parser for clang-apply-replacements tool."""

    parser = argparse.ArgumentParser(description=__doc__)

    def existing_path(arg: str) -> Path:
        path = Path(arg)
        if not path.is_dir():
            raise argparse.ArgumentTypeError(
                f'{arg} is not a path to a directory'
            )

        return path

    parser.add_argument(
        'root',
        type=existing_path,
        help=(
            'Root directory that clang-apply-replacements will recursively '
            'search under for the YAML fix files generated by clang-tidy.'
        ),
    )

    parser.add_argument(
        '--remove-change-desc-files',
        action=argparse.BooleanOptionalAction,
        default=True,
        help=(
            'Remove the change description files regardless of successful '
            'merging/replacing.'
        ),
    )

    parser.add_argument(
        '--raise-insert-conflict',
        action=argparse.BooleanOptionalAction,
        default=True,
        help='Do not ignore insert conflicts.',
    )

    return parser


def main() -> int:
    """Check and fix formatting for source files."""
    return apply_replacements(**vars(arguments().parse_args()))
