# @file HostUnitTestCompilerPlugin.py
##
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: BSD-2-Clause-Patent
##

import logging
import os
import re
from edk2toollib.uefi.edk2.parsers.dsc_parser import DscParser
from edk2toolext.environment.plugintypes.ci_build_plugin import ICiBuildPlugin
from edk2toolext.environment.uefi_build import UefiBuilder
from edk2toolext import edk2_logging
from edk2toolext.environment.var_dict import VarDict
from edk2toollib.utility_functions import GetHostInfo


class HostUnitTestCompilerPlugin(ICiBuildPlugin):
    """
    A CiBuildPlugin that compiles the dsc for host based unit test apps.
    An IUefiBuildPlugin may be attached to this plugin that will run the
    unit tests and collect the results after successful compilation.

    Configuration options:
    "HostUnitTestCompilerPlugin": {
        "DscPath": "<path to dsc from root of pkg>"
    }
    """

    def GetTestName(self, packagename: str, environment: VarDict) -> tuple:
        """ Provide the testcase name and classname for use in reporting
            testclassname: a descriptive string for the testcase can include whitespace
            classname: should be patterned <packagename>.<plugin>.<optionally any unique condition>

            Args:
              packagename: string containing name of package to build
              environment: The VarDict for the test to run in
            Returns:
                a tuple containing the testcase name and the classname
                (testcasename, classname)
        """
        num,types = self.__GetHostUnitTestArch(environment)
        types = types.replace(" ", "_")

        return ("Compile and Run Host-Based UnitTests for " + packagename + " on arch " + types,
                packagename + ".HostUnitTestCompiler." + types)

    def RunsOnTargetList(self):
        return ["NOOPT"]

    #
    # Find the intersection of application types that can run on this host
    # and the TARGET_ARCH being build in this request.
    #
    # return tuple with (number of UEFI arch types, space separated string)
    def __GetHostUnitTestArch(self, environment):
        requested = environment.GetValue("TARGET_ARCH").split(' ')
        host = []
        if GetHostInfo().arch == 'x86':
            #assume 64bit can handle 64 and 32
            #assume 32bit can only handle 32
            ## change once IA32 issues resolved host.append("IA32")
            if GetHostInfo().bit == '64':
                host.append("X64")
        elif GetHostInfo().arch == 'ARM':
            if GetHostInfo().bit == '64':
                host.append("AARCH64")
            elif GetHostInfo().bit == '32':
                host.append("ARM")

        willrun = set(requested) & set(host)
        return (len(willrun), " ".join(willrun))


    ##
    # External function of plugin.  This function is used to perform the task of the ICiBuildPlugin Plugin
    #
    #   - package is the edk2 path to package.  This means workspace/packagepath relative.
    #   - edk2path object configured with workspace and packages path
    #   - PkgConfig Object (dict) for the pkg
    #   - EnvConfig Object
    #   - Plugin Manager Instance
    #   - Plugin Helper Obj Instance
    #   - Junit Logger
    #   - output_stream the StringIO output stream from this plugin via logging
    def RunBuildPlugin(self, packagename, Edk2pathObj, pkgconfig, environment, PLM, PLMHelper, tc, output_stream=None):
        self._env = environment
        environment.SetValue("CI_BUILD_TYPE", "host_unit_test", "Set in HostUnitTestCompilerPlugin")

        # Parse the config for required DscPath element
        if "DscPath" not in pkgconfig:
            tc.SetSkipped()
            tc.LogStdError("DscPath not found in config file.  Nothing to compile for HostBasedUnitTests.")
            return -1

        AP = Edk2pathObj.GetAbsolutePathOnThisSystemFromEdk2RelativePath(packagename)

        APDSC = os.path.join(AP, pkgconfig["DscPath"].strip())
        AP_Path = Edk2pathObj.GetEdk2RelativePathFromAbsolutePath(APDSC)
        if AP is None or AP_Path is None or not os.path.isfile(APDSC):
            tc.SetSkipped()
            tc.LogStdError("Package HostBasedUnitTest Dsc not found.")
            return -1

        logging.info("Building {0}".format(AP_Path))
        self._env.SetValue("ACTIVE_PLATFORM", AP_Path, "Set in Compiler Plugin")
        num, RUNNABLE_ARCHITECTURES = self.__GetHostUnitTestArch(environment)
        if(num == 0):
            tc.SetSkipped()
            tc.LogStdError("No host architecture compatibility")
            return -1

        if not environment.SetValue("TARGET_ARCH",
                                    RUNNABLE_ARCHITECTURES,
                                    "Update Target Arch based on Host Support"):
            #use AllowOverride function since this is a controlled attempt to change
            environment.AllowOverride("TARGET_ARCH")
            if not environment.SetValue("TARGET_ARCH",
                                        RUNNABLE_ARCHITECTURES,
                                        "Update Target Arch based on Host Support"):
                raise RuntimeError("Can't Change TARGET_ARCH as required")

        # Parse DSC to check for SUPPORTED_ARCHITECTURES
        dp = DscParser()
        dp.SetBaseAbsPath(Edk2pathObj.WorkspacePath)
        dp.SetPackagePaths(Edk2pathObj.PackagePathList)
        dp.ParseFile(AP_Path)
        if "SUPPORTED_ARCHITECTURES" in dp.LocalVars:
            SUPPORTED_ARCHITECTURES = dp.LocalVars["SUPPORTED_ARCHITECTURES"].split('|')
            TARGET_ARCHITECTURES = environment.GetValue("TARGET_ARCH").split(' ')

            # Skip if there is no intersection between SUPPORTED_ARCHITECTURES and TARGET_ARCHITECTURES
            if len(set(SUPPORTED_ARCHITECTURES) & set(TARGET_ARCHITECTURES)) == 0:
                tc.SetSkipped()
                tc.LogStdError("No supported architecutres to build for host unit tests")
                return -1

        uefiBuilder = UefiBuilder()
        # do all the steps
        # WorkSpace, PackagesPath, PInHelper, PInManager
        ret = uefiBuilder.Go(Edk2pathObj.WorkspacePath, os.pathsep.join(Edk2pathObj.PackagePathList), PLMHelper, PLM)
        if ret != 0:  # failure:
            tc.SetFailed("Compile failed for {0}".format(packagename), "Compile_FAILED")
            tc.LogStdError("{0} Compile failed with error code {1} ".format(AP_Path, ret))
            return 1

        else:
            tc.SetSuccess()
            return 0
