# @file CodeQlBuildPlugin.py
#
# A build plugin that produces CodeQL results for the present build.
#
# Copyright (c) Microsoft Corporation. All rights reserved.
# SPDX-License-Identifier: BSD-2-Clause-Patent
##

import glob
import logging
import os
import stat
from common import codeql_plugin
from pathlib import Path

from edk2toolext import edk2_logging
from edk2toolext.environment.plugintypes.uefi_build_plugin import \
    IUefiBuildPlugin
from edk2toolext.environment.uefi_build import UefiBuilder
from edk2toollib.uefi.edk2.path_utilities import Edk2Path
from edk2toollib.utility_functions import GetHostInfo, RemoveTree


class CodeQlBuildPlugin(IUefiBuildPlugin):

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

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

        Returns:
            int: The plugin return code. Zero indicates the plugin ran
            successfully. A non-zero value indicates an unexpected error
            occurred during plugin execution.
        """

        if not builder.SkipBuild:
            self.builder = builder
            self.package = builder.edk2path.GetContainingPackage(
                builder.edk2path.GetAbsolutePathOnThisSystemFromEdk2RelativePath(
                    builder.env.GetValue("ACTIVE_PLATFORM")
                )
            )

            self.target = builder.env.GetValue("TARGET")

            self.build_output_dir = builder.env.GetValue("BUILD_OUTPUT_BASE")

            self.codeql_db_path = codeql_plugin.get_codeql_db_path(
                                    builder.ws, self.package, self.target)

            edk2_logging.log_progress(f"{self.package} will be built for CodeQL")
            edk2_logging.log_progress(f"  CodeQL database will be written to "
                                    f"{self.codeql_db_path}")

            self.codeql_path = codeql_plugin.get_codeql_cli_path()
            if not self.codeql_path:
                logging.critical("CodeQL build enabled but CodeQL CLI application "
                                "not found.")
                return -1

            # CodeQL can only generate a database on clean build
            #
            # Note: builder.CleanTree() cannot be used here as some platforms
            #       have build steps that run before this plugin that store
            #       files in the build output directory.
            #
            #       CodeQL does not care about with those files or many others such
            #       as the FV directory, build logs, etc. so instead focus on
            #       removing only the directories with compilation/linker output
            #       for the architectures being built (that need clean runs for
            #       CodeQL to work).
            targets = self.builder.env.GetValue("TARGET_ARCH").split(" ")
            for target in targets:
                directory_to_delete = Path(self.build_output_dir, target)

                if directory_to_delete.is_dir():
                    logging.debug(f"Removing {str(directory_to_delete)} to have a "
                                f"clean build for CodeQL.")
                    RemoveTree(str(directory_to_delete))

            # CodeQL CLI does not handle spaces passed in CLI commands well
            # (perhaps at all) as discussed here:
            #   1. https://github.com/github/codeql-cli-binaries/issues/73
            #   2. https://github.com/github/codeql/issues/4910
            #
            # Since it's unclear how quotes are handled and may change in the
            # future, this code is going to use the workaround to place the
            # command in an executable file that is instead passed to CodeQL.
            self.codeql_cmd_path = Path(self.build_output_dir, "codeql_build_command")

            build_params = self._get_build_params()

            codeql_build_cmd = ""
            if GetHostInfo().os == "Windows":
                self.codeql_cmd_path = self.codeql_cmd_path.parent / (
                    self.codeql_cmd_path.name + '.bat')
            elif GetHostInfo().os == "Linux":
                self.codeql_cmd_path = self.codeql_cmd_path.parent / (
                    self.codeql_cmd_path.name + '.sh')
                codeql_build_cmd += f"#!/bin/bash{os.linesep * 2}"
            codeql_build_cmd += "build " + build_params

            self.codeql_cmd_path.parent.mkdir(exist_ok=True, parents=True)
            self.codeql_cmd_path.write_text(encoding='utf8', data=codeql_build_cmd)

            if GetHostInfo().os == "Linux":
                os.chmod(self.codeql_cmd_path,
                        os.stat(self.codeql_cmd_path).st_mode | stat.S_IEXEC)
                for f in glob.glob(os.path.join(
                    os.path.dirname(self.codeql_path), '**/*'), recursive=True):
                        os.chmod(f, os.stat(f).st_mode | stat.S_IEXEC)

            codeql_params = (f'database create {self.codeql_db_path} '
                            f'--language=cpp '
                            f'--source-root={builder.ws} '
                            f'--command={self.codeql_cmd_path}')

            # Set environment variables so the CodeQL build command is picked up
            # as the active build command.
            #
            # Note: Requires recent changes in edk2-pytool-extensions (0.20.0)
            #       to support reading these variables.
            builder.env.SetValue(
                "EDK_BUILD_CMD", self.codeql_path, "Set in CodeQL Build Plugin")
            builder.env.SetValue(
                "EDK_BUILD_PARAMS", codeql_params, "Set in CodeQL Build Plugin")

        return 0

    def _get_build_params(self) -> str:
        """Returns the build command parameters for this build.

        Based on the well-defined `build` command-line parameters.

        Returns:
            str: A string representing the parameters for the build command.
        """
        build_params = f"-p {self.builder.env.GetValue('ACTIVE_PLATFORM')}"
        build_params += f" -b {self.target}"
        build_params += f" -t {self.builder.env.GetValue('TOOL_CHAIN_TAG')}"

        max_threads = self.builder.env.GetValue('MAX_CONCURRENT_THREAD_NUMBER')
        if max_threads is not None:
            build_params += f" -n {max_threads}"

        rt = self.builder.env.GetValue("TARGET_ARCH").split(" ")
        for t in rt:
            build_params += " -a " + t

        if (self.builder.env.GetValue("BUILDREPORTING") == "TRUE"):
            build_params += (" -y " +
                             self.builder.env.GetValue("BUILDREPORT_FILE"))
            rt = self.builder.env.GetValue("BUILDREPORT_TYPES").split(" ")
            for t in rt:
                build_params += " -Y " + t

        # add special processing to handle building a single module
        mod = self.builder.env.GetValue("BUILDMODULE")
        if (mod is not None and len(mod.strip()) > 0):
            build_params += " -m " + mod
            edk2_logging.log_progress("Single Module Build: " + mod)

        build_vars = self.builder.env.GetAllBuildKeyValues(self.target)
        for key, value in build_vars.items():
            build_params += " -D " + key + "=" + value

        return build_params
