# @file LibraryClassCheck.py
#
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: BSD-2-Clause-Patent
##
import logging
import os
from edk2toolext.environment.plugintypes.ci_build_plugin import ICiBuildPlugin
from edk2toollib.uefi.edk2.parsers.dec_parser import DecParser
from edk2toollib.uefi.edk2.parsers.inf_parser import InfParser
from edk2toolext.environment.var_dict import VarDict


class LibraryClassCheck(ICiBuildPlugin):
    """
    A CiBuildPlugin that scans the code tree and library classes for undeclared
    files

    Configuration options:
    "LibraryClassCheck": {
        IgnoreHeaderFile: [],  # Ignore a file found on disk
        IgnoreLibraryClass: [] # Ignore a declaration found in dec file
    }
    """

    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)
        """
        return ("Check library class declarations in " + packagename, packagename + ".LibraryClassCheck")

    def __GetPkgDec(self, rootpath):
        try:
            allEntries = os.listdir(rootpath)
            for entry in allEntries:
                if entry.lower().endswith(".dec"):
                    return(os.path.join(rootpath, entry))
        except Exception:
            logging.error("Unable to find DEC for package:{0}".format(rootpath))

        return None

    ##
    # 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):
        overall_status = 0
        LibraryClassIgnore = []

        abs_pkg_path = Edk2pathObj.GetAbsolutePathOnThisSytemFromEdk2RelativePath(packagename)
        abs_dec_path = self.__GetPkgDec(abs_pkg_path)
        wsr_dec_path = Edk2pathObj.GetEdk2RelativePathFromAbsolutePath(abs_dec_path)

        if abs_dec_path is None or wsr_dec_path is "" or not os.path.isfile(abs_dec_path):
            tc.SetSkipped()
            tc.LogStdError("No DEC file {0} in package {1}".format(abs_dec_path, abs_pkg_path))
            return -1

        # Get all include folders
        dec = DecParser()
        dec.SetBaseAbsPath(Edk2pathObj.WorkspacePath).SetPackagePaths(Edk2pathObj.PackagePathList)
        dec.ParseFile(wsr_dec_path)

        AllHeaderFiles = []

        for includepath in dec.IncludePaths:
            ## Get all header files in the library folder
            AbsLibraryIncludePath = os.path.join(abs_pkg_path, includepath, "Library")
            if(not os.path.isdir(AbsLibraryIncludePath)):
                continue

            hfiles = self.WalkDirectoryForExtension([".h"], AbsLibraryIncludePath)
            hfiles = [os.path.relpath(x,abs_pkg_path) for x in hfiles]  # make package root relative path
            hfiles = [x.replace("\\", "/") for x in hfiles]  # make package relative path

            AllHeaderFiles.extend(hfiles)

        if len(AllHeaderFiles) == 0:
            tc.SetSkipped()
            tc.LogStdError(f"No Library include folder in any Include path")
            return -1

        # Remove ignored paths
        if "IgnoreHeaderFile" in pkgconfig:
            for a in pkgconfig["IgnoreHeaderFile"]:
                try:
                    tc.LogStdOut("Ignoring Library Header File {0}".format(a))
                    AllHeaderFiles.remove(a)
                except:
                    tc.LogStdError("LibraryClassCheck.IgnoreHeaderFile -> {0} not found.  Invalid Header File".format(a))
                    logging.info("LibraryClassCheck.IgnoreHeaderFile -> {0} not found.  Invalid Header File".format(a))

        if "IgnoreLibraryClass" in pkgconfig:
            LibraryClassIgnore = pkgconfig["IgnoreLibraryClass"]


        ## Attempt to find library classes
        for lcd in dec.LibraryClasses:
            ## Check for correct file path separator
            if "\\" in lcd.path:
                tc.LogStdError("LibraryClassCheck.DecFilePathSeparator -> {0} invalid.".format(lcd.path))
                logging.error("LibraryClassCheck.DecFilePathSeparator -> {0} invalid.".format(lcd.path))
                overall_status += 1
                continue

            if lcd.name in LibraryClassIgnore:
                tc.LogStdOut("Ignoring Library Class Name {0}".format(lcd.name))
                LibraryClassIgnore.remove(lcd.name)
                continue

            logging.debug(f"Looking for Library Class {lcd.path}")
            try:
                AllHeaderFiles.remove(lcd.path)

            except ValueError:
                tc.LogStdError(f"Library {lcd.name} with path {lcd.path} not found in package filesystem")
                logging.error(f"Library {lcd.name} with path {lcd.path} not found in package filesystem")
                overall_status += 1

        ## any remaining AllHeaderFiles are not described in DEC
        for h in AllHeaderFiles:
            tc.LogStdError(f"Library Header File {h} not declared in package DEC {wsr_dec_path}")
            logging.error(f"Library Header File {h} not declared in package DEC {wsr_dec_path}")
            overall_status += 1

        ## Warn about any invalid library class names in the ignore list
        for r in LibraryClassIgnore:
            tc.LogStdError("LibraryClassCheck.IgnoreLibraryClass -> {0} not found.  Library Class not found".format(r))
            logging.info("LibraryClassCheck.IgnoreLibraryClass -> {0} not found.  Library Class not found".format(r))


        # If XML object exists, add result
        if overall_status is not 0:
            tc.SetFailed("LibraryClassCheck {0} Failed.  Errors {1}".format(wsr_dec_path, overall_status), "CHECK_FAILED")
        else:
            tc.SetSuccess()
        return overall_status
