# @file GuidCheck.py
#
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: BSD-2-Clause-Patent
##
import logging
from edk2toolext.environment.plugintypes.ci_build_plugin import ICiBuildPlugin
from edk2toollib.uefi.edk2.guid_list import GuidList
from edk2toolext.environment.var_dict import VarDict


class GuidCheck(ICiBuildPlugin):
    """
    A CiBuildPlugin that scans the code tree and looks for duplicate guids
    from the package being tested.

    Configuration options:
    "GuidCheck": {
        "IgnoreGuidName": [], # provide in format guidname=guidvalue or just guidname
        "IgnoreGuidValue": [],
        "IgnoreFoldersAndFiles": [],
        "IgnoreDuplicates": [] # Provide in format guidname=guidname=guidname...
    }
    """

    def GetTestName(self, packagename: str, environment: VarDict) -> tuple:
        """ Provide the testcase name and classname for use in reporting

            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)
                testclassname: a descriptive string for the testcase can include whitespace
                classname: should be patterned <packagename>.<plugin>.<optionally any unique condition>
        """
        return ("Confirm GUIDs are unique in " + packagename, packagename + ".GuidCheck")

    def _FindConflictingGuidValues(self, guidlist: list) -> list:
        """ Find all duplicate guids by guid value and report them as errors
        """
        # Sort the list by guid
        guidsorted = sorted(
            guidlist, key=lambda x: x.guid.upper(), reverse=True)

        previous = None  # Store previous entry for comparison
        error = None
        errors = []
        for index in range(len(guidsorted)):
            i = guidsorted[index]
            if(previous is not None):
                if i.guid == previous.guid:  # Error
                    if(error is None):
                        # Catch errors with more than 1 conflict
                        error = ErrorEntry("guid")
                        error.entries.append(previous)
                        errors.append(error)
                    error.entries.append(i)
                else:
                    # no match.  clear error
                    error = None
            previous = i
        return errors

    def _FindConflictingGuidNames(self, guidlist: list) -> list:
        """ Find all duplicate guids by name and if they are not all
        from inf files report them as errors.  It is ok to have
        BASE_NAME duplication.

        Is this useful?  It would catch two same named guids in dec file
        that resolve to different values.
        """
        # Sort the list by guid
        namesorted = sorted(guidlist, key=lambda x: x.name.upper())

        previous = None  # Store previous entry for comparison
        error = None
        errors = []
        for index in range(len(namesorted)):
            i = namesorted[index]
            if(previous is not None):
                # If name matches
                if i.name == previous.name:
                    if(error is None):
                        # Catch errors with more than 1 conflict
                        error = ErrorEntry("name")
                        error.entries.append(previous)
                        errors.append(error)
                    error.entries.append(i)
                else:
                    # no match.  clear error
                    error = None
            previous = i

            # Loop thru and remove any errors where all files are infs as it is ok if
            # they have the same inf base name.
            for e in errors[:]:
                if len( [en for en in e.entries if not en.absfilepath.lower().endswith(".inf")]) == 0:
                    errors.remove(e)

        return errors

    ##
    # External function of plugin.  This function is used to perform the task of the MuBuild 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):
        Errors = []

        abs_pkg_path = Edk2pathObj.GetAbsolutePathOnThisSytemFromEdk2RelativePath(
            packagename)

        if abs_pkg_path is None:
            tc.SetSkipped()
            tc.LogStdError("No package {0}".format(packagename))
            return -1

        All_Ignores = ["/Build", "/Conf"]
        # Parse the config for other ignores
        if "IgnoreFoldersAndFiles" in pkgconfig:
            All_Ignores.extend(pkgconfig["IgnoreFoldersAndFiles"])

        # Parse the workspace for all GUIDs
        gs = GuidList.guidlist_from_filesystem(
            Edk2pathObj.WorkspacePath, ignore_lines=All_Ignores)

        # Remove ignored guidvalue
        if "IgnoreGuidValue" in pkgconfig:
            for a in pkgconfig["IgnoreGuidValue"]:
                try:
                    tc.LogStdOut("Ignoring Guid {0}".format(a.upper()))
                    for b in gs[:]:
                        if b.guid == a.upper():
                            gs.remove(b)
                except:
                    tc.LogStdError("GuidCheck.IgnoreGuid -> {0} not found.  Invalid ignore guid".format(a.upper()))
                    logging.info("GuidCheck.IgnoreGuid -> {0} not found.  Invalid ignore guid".format(a.upper()))

        # Remove ignored guidname
        if "IgnoreGuidName" in pkgconfig:
            for a in pkgconfig["IgnoreGuidName"]:
                entry = a.split("=")
                if(len(entry) > 2):
                    tc.LogStdError("GuidCheck.IgnoreGuidName -> {0} Invalid Format.".format(a))
                    logging.info("GuidCheck.IgnoreGuidName -> {0} Invalid Format.".format(a))
                    continue
                try:
                    tc.LogStdOut("Ignoring Guid {0}".format(a))
                    for b in gs[:]:
                        if b.name == entry[0]:
                            if(len(entry) == 1):
                                gs.remove(b)
                            elif(len(entry) == 2 and b.guid.upper() == entry[1].upper()):
                                gs.remove(b)
                            else:
                                c.LogStdError("GuidCheck.IgnoreGuidName -> {0} incomplete match.  Invalid ignore guid".format(a))

                except:
                    tc.LogStdError("GuidCheck.IgnoreGuidName -> {0} not found.  Invalid ignore name".format(a))
                    logging.info("GuidCheck.IgnoreGuidName -> {0} not found.  Invalid ignore name".format(a))

        # Find conflicting Guid Values
        Errors.extend(self._FindConflictingGuidValues(gs))

        # Check if there are expected duplicates and remove it from the error list
        if "IgnoreDuplicates" in pkgconfig:
            for a in pkgconfig["IgnoreDuplicates"]:
                names = a.split("=")
                if len(names) < 2:
                    tc.LogStdError("GuidCheck.IgnoreDuplicates -> {0} invalid format".format(a))
                    logging.info("GuidCheck.IgnoreDuplicates -> {0} invalid format".format(a))
                    continue

                for b in Errors[:]:
                    if b.type != "guid":
                        continue
                    ## Make a list of the names that are not in the names list.  If there
                    ## are any in the list then this error should not be ignored.
                    t = [x for x in b.entries if x.name not in names]
                    if(len(t) == len(b.entries)):
                        ## did not apply to any entry
                        continue
                    elif(len(t) == 0):
                        ## full match - ignore duplicate
                        tc.LogStdOut("GuidCheck.IgnoreDuplicates -> {0}".format(a))
                        Errors.remove(b)
                    elif(len(t) < len(b.entries)):
                        ## partial match
                        tc.LogStdOut("GuidCheck.IgnoreDuplicates -> {0} incomplete match".format(a))
                        logging.info("GuidCheck.IgnoreDuplicates -> {0} incomplete match".format(a))
                    else:
                        tc.LogStdOut("GuidCheck.IgnoreDuplicates -> {0} unknown error.".format(a))
                        logging.info("GuidCheck.IgnoreDuplicates -> {0} unknown error".format(a))



        # Find conflicting Guid Names
        Errors.extend(self._FindConflictingGuidNames(gs))

        # Log errors for anything within the package under test
        for er in Errors[:]:
            InMyPackage = False
            for a in er.entries:
                if abs_pkg_path in a.absfilepath:
                    InMyPackage = True
                    break
            if(not InMyPackage):
                Errors.remove(er)
            else:
                logging.error(str(er))
                tc.LogStdError(str(er))

        # add result to test case
        overall_status = len(Errors)
        if overall_status != 0:
            tc.SetFailed("GuidCheck {0} Failed.  Errors {1}".format(
                packagename, overall_status), "CHECK_FAILED")
        else:
            tc.SetSuccess()
        return overall_status


class ErrorEntry():
    """ Custom/private class for reporting errors in the GuidList
    """

    def __init__(self, errortype):
        self.type = errortype  # 'guid' or 'name' depending on error type
        self.entries = []  # GuidListEntry that are in error condition

    def __str__(self):
        a = f"Error Duplicate {self.type}: "
        if(self.type == "guid"):
            a += f" {self.entries[0].guid}"
        elif(self.type == "name"):
            a += f" {self.entries[0].name}"

        a += f" ({len(self.entries)})\n"

        for e in self.entries:
            a += "\t" + str(e) + "\n"
        return a
