## @file
# Collects the Guid Information in current workspace.
#
# Copyright (c) 2007, Intel Corporation
# All rights reserved. This program and the accompanying materials
# are licensed and made available under the terms and conditions of the BSD License
# which accompanies this distribution.  The full text of the license may be found at
# http://opensource.org/licenses/bsd-license.php
#
# THE PROGRAM IS DISTRIBUTED UNDER THE BSD LICENSE ON AN "AS IS" BASIS,
# WITHOUT WARRANTIES OR REPRESENTATIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED.
#

##
# Import Modules
#
import os
import fnmatch
from Common.EdkIIWorkspace import EdkIIWorkspace
from Common.MigrationUtilities import *

## A class for EdkII work space to resolve Guids.
#
# This class inherits from EdkIIWorkspace and collects the Guids information
# in current workspace. The Guids information is important to translate the
# package Guids and recommended library instances Guids to relative file path
# (to workspace directory) in MSA files.
#
class EdkIIWorkspaceGuidsInfo(EdkIIWorkspace):

    ## The classconstructor.
    #
    # The constructor initialize workspace directory. It does not collect
    # pakage and module Guids info at initialization; instead, it collects them
    # on the fly.
    #
    # @param  self           The object pointer.
    #
    def __init__(self):
        # Initialize parent class.
        EdkIIWorkspace.__init__(self)
        # The internal map from Guid to FilePath.
        self.__GuidToFilePath = {}
        # The internal package directory list.
        self.__PackageDirList = []
        # The internal flag to indicate whether package Guids info has been
        # to avoid re-collection collected.
        self.__PackageGuidInitialized = False
        # The internal flag to indicate whether module Guids info has been
        # to avoid re-collection collected.
        self.__ModuleGuidInitialized = False

    ## Add Guid, Version and FilePath to Guids database.
    #
    # Add Guid, Version and FilePath to Guids database. It constructs a map
    # table from Guid, Version to FilePath internally. If also detects possible
    # Guid collision. For now, the version information is simply ignored and
    # Guid value itself acts as master key.
    #
    # @param  self           The object pointer.
    # @param  Guid           The Guid Value.
    # @param  Version        The version information
    #
    # @retval True           The Guid value is successfully added to map table.
    # @retval False          The Guid is an empty string or the map table
    #                        already contains a same Guid.
    #
    def __AddGuidToFilePath(self, Guid, Version, FilePath):
        if Guid == "":
            EdkLogger.info("Cannot find Guid in file %s" % FilePath)
            return False
        #Add the Guid value to map table to ensure case insensitive comparison.
        OldFilePath = self.__GuidToFilePath.setdefault(Guid.lower(), FilePath)
        if OldFilePath == FilePath:
            EdkLogger.verbose("File %s has new Guid '%s'" % (FilePath, Guid))
            return True
        else:
            EdkLogger.info("File %s has duplicate Guid with & %s" % (FilePath, OldFilePath))
            return False
        

    ## Gets file information from a module description file.
    #
    # Extracts Module Name, File Guid and Version number from INF, MSA and NMSA
    # file. It supports to exact such information from text based INF file or
    # XML based (N)MSA file.
    #
    # @param  self           The object pointer.
    # @param  FileName       The input module file name.
    #
    # @retval True           This module file represents a new module discovered
    #                        in current workspace.
    # @retval False          This module file is not regarded as a valid module.
    #                        The File Guid cannot be extracted or the another
    #                        file with the same Guid already exists
    #
    def __GetModuleFileInfo(self, FileName):
        if fnmatch.fnmatch(FileName, "*.inf"):
            TagTuple = ("BASE_NAME", "FILE_GUID", "VERSION_STRING")
            (Name, Guid, Version) = GetTextFileInfo(FileName, TagTuple)
        else :
            XmlTag1 = "ModuleSurfaceArea/MsaHeader/ModuleName"
            XmlTag2 = "ModuleSurfaceArea/MsaHeader/GuidValue"
            XmlTag3 = "ModuleSurfaceArea/MsaHeader/Version"
            TagTuple = (XmlTag1, XmlTag2, XmlTag3)
            (Name, Guid, Version) = GetXmlFileInfo(FileName, TagTuple)

        return self.__AddGuidToFilePath(Guid, Version, FileName)
    
    
    ## Gets file information from a package description file.
    #
    # Extracts Package Name, File Guid and Version number from INF, SPD and NSPD
    # file. It supports to exact such information from text based DEC file or
    # XML based (N)SPD file. EDK Compatibility Package is hardcoded to be
    # ignored since no EDKII INF file depends on that package.
    #
    # @param  self           The object pointer.
    # @param  FileName       The input package file name.
    #
    # @retval True           This package file represents a new package
    #                        discovered in current workspace.
    # @retval False          This package is not regarded as a valid package.
    #                        The File Guid cannot be extracted or the another
    #                        file with the same Guid already exists
    #
    def __GetPackageFileInfo(self, FileName):
        if fnmatch.fnmatch(FileName, "*.dec"):
            TagTuple = ("PACKAGE_NAME", "PACKAGE_GUID", "PACKAGE_VERSION")
            (Name, Guid, Version) = GetTextFileInfo(FileName, TagTuple)
        else:
            XmlTag1 = "PackageSurfaceArea/SpdHeader/PackageName"
            XmlTag2 = "PackageSurfaceArea/SpdHeader/GuidValue"
            XmlTag3 = "PackageSurfaceArea/SpdHeader/Version"
            TagTuple = (XmlTag1, XmlTag2, XmlTag3)
            (Name, Guid, Version) = GetXmlFileInfo(FileName, TagTuple)
                
        if Name == "EdkCompatibilityPkg":
            # Do not scan EDK compatibitilty package to avoid Guid collision
            # with those in EDK Glue Library.
            EdkLogger.verbose("Bypass EDK Compatibility Pkg")
            return False
        
        return self.__AddGuidToFilePath(Guid, Version, FileName)

    ## Iterate on all package files listed in framework database file.
    #
    # Yields all package description files listed in framework database files.
    # The framework database file describes the packages current workspace
    # includes.
    #
    # @param  self           The object pointer.
    #
    def __FrameworkDatabasePackageFiles(self):
        XmlFrameworkDb = XmlParseFile(self.WorkspaceFile)
        XmlTag = "FrameworkDatabase/PackageList/Filename"
        for PackageFile in XmlElementList(XmlFrameworkDb, XmlTag):
            yield os.path.join(self.WorkspaceDir, PackageFile)
    
    
    ## Iterate on all package files in current workspace directory.
    #
    # Yields all package description files listed in current workspace
    # directory. This happens when no framework database file exists.
    #
    # @param  self           The object pointer.
    #
    def __TraverseAllPackageFiles(self):
        for Path, Dirs, Files in os.walk(self.WorkspaceDir):
            # Ignore svn version control directory.
            if ".svn" in Dirs:
                Dirs.remove(".svn")
            if "Build" in Dirs:
                Dirs.remove("Build")
            # Assume priority from high to low: DEC, NSPD, SPD.
            PackageFiles = fnmatch.filter(Files, "*.dec")
            if len(PackageFiles) == 0:
                PackageFiles = fnmatch.filter(Files, "*.nspd")
                if len(PackageFiles) == 0:
                    PackageFiles = fnmatch.filter(Files, "*.spd")

            for File in PackageFiles:
                # Assume no more package decription file in sub-directory.
                del Dirs[:]
                yield os.path.join(Path, File)

    ## Iterate on all module files in current package directory.
    #
    # Yields all module description files listed in current package
    # directory.
    #
    # @param  self           The object pointer.
    #
    def __TraverseAllModuleFiles(self):
        for PackageDir in self.__PackageDirList:
            for Path, Dirs, Files in os.walk(PackageDir):
                # Ignore svn version control directory.
                if ".svn" in Dirs:
                    Dirs.remove(".svn")
                # Assume priority from high to low: INF, NMSA, MSA.
                ModuleFiles = fnmatch.filter(Files, "*.inf")
                if len(ModuleFiles) == 0:
                    ModuleFiles = fnmatch.filter(Files, "*.nmsa")
                    if len(ModuleFiles) == 0:
                        ModuleFiles = fnmatch.filter(Files, "*.msa")

                for File in ModuleFiles:
                    yield os.path.join(Path, File)

    ## Initialize package Guids info mapping table.
    #
    # Collects all package guids map to package decription file path. This
    # function is invokes on demand to avoid unnecessary directory scan.
    #
    # @param  self           The object pointer.
    #
    def __InitializePackageGuidInfo(self):
        if self.__PackageGuidInitialized:
            return

        EdkLogger.verbose("Start to collect Package Guids Info.")
   
        WorkspaceFile = os.path.join("Conf", "FrameworkDatabase.db")
        self.WorkspaceFile = os.path.join(self.WorkspaceDir, WorkspaceFile)
        
        # Try to find the frameworkdatabase file to discover package lists
        if os.path.exists(self.WorkspaceFile):
            TraversePackage = self.__FrameworkDatabasePackageFiles
            EdkLogger.verbose("Package list bases on: %s" % self.WorkspaceFile)
        else:
            TraversePackage = self.__TraverseAllPackageFiles
            EdkLogger.verbose("Package list in: %s" % self.WorkspaceDir)

        for FileName in TraversePackage():
            if self.__GetPackageFileInfo(FileName):
                PackageDir = os.path.dirname(FileName)
                EdkLogger.verbose("Find new package directory %s" % PackageDir)
                self.__PackageDirList.append(PackageDir)
                
        self.__PackageGuidInitialized = True

    ## Initialize module Guids info mapping table.
    #
    # Collects all module guids map to module decription file path. This
    # function is invokes on demand to avoid unnecessary directory scan.
    #
    # @param  self           The object pointer.
    #
    def __InitializeModuleGuidInfo(self):
        if self.__ModuleGuidInitialized:
            return
        EdkLogger.verbose("Start to collect Module Guids Info")
        
        self.__InitializePackageGuidInfo()
        for FileName in self.__TraverseAllModuleFiles():
            if self.__GetModuleFileInfo(FileName):
                EdkLogger.verbose("Find new module %s" % FileName)
                
        self.__ModuleGuidInitialized = True

    ## Get Package file path by Package guid and Version.
    #
    # Translates the Package Guid and Version to a file path relative
    # to workspace directory. If no package in current workspace match the
    # input Guid, an empty file path is returned. For now, the version
    # value is simply ignored.
    #
    # @param  self           The object pointer.
    # @param  Guid           The Package Guid value to look for.
    # @param  Version        The Package Version value to look for.
    #
    def ResolvePackageFilePath(self, Guid, Version = ""):
        self.__InitializePackageGuidInfo()
        
        EdkLogger.verbose("Resolve Package Guid '%s'" % Guid)
        FileName = self.__GuidToFilePath.get(Guid.lower(), "")
        if FileName == "":
            EdkLogger.info("Cannot resolve Package Guid '%s'" % Guid)
        else:
            FileName = self.WorkspaceRelativePath(FileName)
            FileName = os.path.splitext(FileName)[0] + ".dec"
            FileName = FileName.replace("\\", "/")
        return FileName

    ## Get Module file path by Package guid and Version.
    #
    # Translates the Module Guid and Version to a file path relative
    # to workspace directory. If no module in current workspace match the
    # input Guid, an empty file path is returned. For now, the version
    # value is simply ignored.
    #
    # @param  self           The object pointer.
    # @param  Guid           The Module Guid value to look for.
    # @param  Version        The Module Version value to look for.
    #
    def ResolveModuleFilePath(self, Guid, Version = ""):
        self.__InitializeModuleGuidInfo()
        
        EdkLogger.verbose("Resolve Module Guid '%s'" % Guid)
        FileName = self.__GuidToFilePath.get(Guid.lower(), "")
        if FileName == "":
            EdkLogger.info("Cannot resolve Module Guid '%s'" % Guid)
        else:
            FileName = self.WorkspaceRelativePath(FileName)
            FileName = os.path.splitext(FileName)[0] + ".inf"
            FileName = FileName.replace("\\", "/")
        return FileName

# A global class object of EdkIIWorkspaceGuidsInfo for external reference.
gEdkIIWorkspaceGuidsInfo = EdkIIWorkspaceGuidsInfo()

# This acts like the main() function for the script, unless it is 'import'ed
# into another script.
if __name__ == '__main__':
    # Test the translation of package Guid.
    MdePkgGuid = "1E73767F-8F52-4603-AEB4-F29B510B6766"
    OldMdePkgGuid = "5e0e9358-46b6-4ae2-8218-4ab8b9bbdcec"
    print gEdkIIWorkspaceGuidsInfo.ResolveModuleFilePath(MdePkgGuid)
    print gEdkIIWorkspaceGuidsInfo.ResolveModuleFilePath(OldMdePkgGuid)
    
    # Test the translation of module Guid.
    UefiLibGuid = "3a004ba5-efe0-4a61-9f1a-267a46ae5ba9"
    UefiDriverModelLibGuid = "52af22ae-9901-4484-8cdc-622dd5838b09"
    print gEdkIIWorkspaceGuidsInfo.ResolveModuleFilePath(UefiLibGuid)
    print gEdkIIWorkspaceGuidsInfo.ResolveModuleFilePath(UefiDriverModelLibGuid)