# @file DebugMacroCheckBuildPlugin.py
#
# A build plugin that checks if DEBUG macros are formatted properly.
#
# In particular, that print format specifiers are defined
# with the expected number of arguments in the variable
# argument list.
#
# Copyright (c) Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: BSD-2-Clause-Patent
##

import logging
import os
import pathlib
import sys
import yaml

# Import the build plugin
plugin_file = pathlib.Path(__file__)
sys.path.append(str(plugin_file.parent.parent))

# flake8 (E402): Ignore flake8 module level import not at top of file
import DebugMacroCheck                          # noqa: E402

from edk2toolext import edk2_logging                               # noqa: E402
from edk2toolext.environment.plugintypes.uefi_build_plugin import \
    IUefiBuildPlugin                                               # noqa: E402
from edk2toolext.environment.uefi_build import UefiBuilder         # noqa: E402
from edk2toollib.uefi.edk2.path_utilities import Edk2Path          # noqa: E402
from pathlib import Path                                           # noqa: E402


class DebugMacroCheckBuildPlugin(IUefiBuildPlugin):

    def do_pre_build(self, builder: UefiBuilder) -> int:
        """Debug Macro Check pre-build functionality.

        The plugin is invoked in pre-build since it can operate independently
        of build tools and to notify the user of any errors earlier in the
        build process to reduce feedback time.

        Args:
            builder (UefiBuilder): A UEFI builder object for this build.

        Returns:
            int: The number of debug macro errors found. Zero indicates the
            check either did not run or no errors were found.
        """

        # Check if disabled in the environment
        env_disable = builder.env.GetValue("DISABLE_DEBUG_MACRO_CHECK")
        if env_disable:
            return 0

        # Only run on targets with compilation
        build_target = builder.env.GetValue("TARGET").lower()
        if "no-target" in build_target:
            return 0

        edk2 = builder.edk2path
        package = edk2.GetContainingPackage(
            builder.edk2path.GetAbsolutePathOnThisSystemFromEdk2RelativePath(
                builder.env.GetValue("ACTIVE_PLATFORM")
            )
        )
        package_path = Path(
                          edk2.GetAbsolutePathOnThisSystemFromEdk2RelativePath(
                                package))

        # Every debug macro is printed at DEBUG logging level.
        # Ensure the level is above DEBUG while executing the macro check
        # plugin to avoid flooding the log handler.
        handler_level_context = []
        for h in logging.getLogger().handlers:
            if h.level < logging.INFO:
                handler_level_context.append((h, h.level))
                h.setLevel(logging.INFO)

        edk2_logging.log_progress("Checking DEBUG Macros")

        # There are two ways to specify macro substitution data for this
        # plugin. If multiple options are present, data is appended from
        # each option.
        #
        # 1. Specify the substitution data in the package CI YAML file.
        # 2. Specify a standalone substitution data YAML file.
        ##
        sub_data = {}

        # 1. Allow substitution data to be specified in a "DebugMacroCheck" of
        # the package CI YAML file. This is used to provide a familiar per-
        # package customization flow for a package maintainer.
        package_config_file = Path(
                                os.path.join(
                                    package_path, package + ".ci.yaml"))
        if package_config_file.is_file():
            with open(package_config_file, 'r') as cf:
                package_config_file_data = yaml.safe_load(cf)
                if "DebugMacroCheck" in package_config_file_data and \
                   "StringSubstitutions" in \
                   package_config_file_data["DebugMacroCheck"]:
                    logging.info(f"Loading substitution data in "
                                 f"{str(package_config_file)}")
                    sub_data |= package_config_file_data["DebugMacroCheck"]["StringSubstitutions"] # noqa

        # 2. Allow a substitution file to be specified as an environment
        # variable. This is used to provide flexibility in how to specify a
        # substitution file. The value can be set anywhere prior to this plugin
        # getting called such as pre-existing build script.
        sub_file = builder.env.GetValue("DEBUG_MACRO_CHECK_SUB_FILE")
        if sub_file:
            logging.info(f"Loading substitution file {sub_file}")
            with open(sub_file, 'r') as sf:
                sub_data |= yaml.safe_load(sf)

        try:
            error_count = DebugMacroCheck.check_macros_in_directory(
                                            package_path,
                                            ignore_git_submodules=False,
                                            show_progress_bar=False,
                                            **sub_data)
        finally:
            for h, l in handler_level_context:
                h.setLevel(l)

        return error_count
