## @file
# Install distribution package.
#
# Copyright (c) 2007, Intel Corporation. All rights reserved.<BR>
# 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 sys
import glob
import shutil
import traceback
import platform
from optparse import OptionParser

import Common.EdkLogger as EdkLogger
from Common.BuildToolError import *
from Common.Misc import *
from Common.XmlParser import *
from Common.InfClassObjectLight import Inf
from Common.DecClassObjectLight import Dec

from PackageFile import *
from IpiDb import *
from DependencyRules import *
import md5

# Version and Copyright
VersionNumber = "0.1"
__version__ = "%prog Version " + VersionNumber
__copyright__ = "Copyright (c) 2008, Intel Corporation  All rights reserved."

## Check environment variables
#
#  Check environment variables that must be set for build. Currently they are
#
#   WORKSPACE           The directory all packages/platforms start from
#   EDK_TOOLS_PATH      The directory contains all tools needed by the build
#   PATH                $(EDK_TOOLS_PATH)/Bin/<sys> must be set in PATH
#
#   If any of above environment variable is not set or has error, the build
#   will be broken.
#
def CheckEnvVariable():
    # check WORKSPACE
    if "WORKSPACE" not in os.environ:
        EdkLogger.error("InstallPkg", ATTRIBUTE_NOT_AVAILABLE, "Environment variable not found",
                        ExtraData="WORKSPACE")

    WorkspaceDir = os.path.normpath(os.environ["WORKSPACE"])
    if not os.path.exists(WorkspaceDir):
        EdkLogger.error("InstallPkg", FILE_NOT_FOUND, "WORKSPACE doesn't exist", ExtraData="%s" % WorkspaceDir)
    elif ' ' in WorkspaceDir:
        EdkLogger.error("InstallPkg", FORMAT_NOT_SUPPORTED, "No space is allowed in WORKSPACE path", 
                        ExtraData=WorkspaceDir)
    os.environ["WORKSPACE"] = WorkspaceDir

## Parse command line options
#
# Using standard Python module optparse to parse command line option of this tool.
#
#   @retval Opt   A optparse.Values object containing the parsed options
#   @retval Args  Target of build command
#
def MyOptionParser():
    UsageString = "%prog -i <distribution_package> [-t] [-f] [-q | -v] [-h]"

    Parser = OptionParser(description=__copyright__,version=__version__,prog="InstallPkg",usage=UsageString)

    Parser.add_option("-?", action="help", help="show this help message and exit")

    Parser.add_option("-i", "--distribution-package", action="store", type="string", dest="PackageFile",
            help="The distribution package to be installed")

    Parser.add_option("-t", "--install-tools", action="store_true", type=None, dest="Tools",
            help="Specify it to install tools or ignore the tools of the distribution package.")
    
    Parser.add_option("-f", "--misc-files", action="store_true", type=None, dest="MiscFiles",
            help="Specify it to install misc file or ignore the misc files of the distribution package.")

    Parser.add_option("-q", "--quiet", action="store_const", dest="LogLevel", const=EdkLogger.QUIET,
            help="Disable all messages except FATAL ERRORS.")

    Parser.add_option("-v", "--verbose", action="store_const", dest="LogLevel", const=EdkLogger.VERBOSE,
            help="Turn on verbose output")

    Parser.add_option("-d", "--debug", action="store", type="int", dest="LogLevel",
            help="Enable debug messages at specified level.")

    Parser.set_defaults(LogLevel=EdkLogger.INFO)

    (Opt, Args)=Parser.parse_args()

    return Opt

def InstallNewPackage(WorkspaceDir, Path):
    FullPath = os.path.normpath(os.path.join(WorkspaceDir, Path))
    if os.path.exists(FullPath):
        print "Directory [%s] already exists, please select another location, press [Enter] with no input to quit:" %Path
        Input = sys.stdin.readline()
        Input = Input.replace('\r', '').replace('\n', '')
        if Input == '':
            EdkLogger.error("InstallPkg", UNKNOWN_ERROR, "User interrupt")
        Input = Input.replace('\r', '').replace('\n', '')
        return InstallNewPackage(WorkspaceDir, Input)
    else:
        return Path

def InstallNewFile(WorkspaceDir, File):
    FullPath = os.path.normpath(os.path.join(WorkspaceDir, File))
    if os.path.exists(FullPath):
        print "File [%s] already exists, please select another path, press [Enter] with no input to quit:" %File
        Input = sys.stdin.readline()
        Input = Input.replace('\r', '').replace('\n', '')
        if Input == '':
            EdkLogger.error("InstallPkg", UNKNOWN_ERROR, "User interrupt")
        Input = Input.replace('\r', '').replace('\n', '')
        return InstallNewFile(WorkspaceDir, Input)
    else:
        return File

## Tool entrance method
#
# This method mainly dispatch specific methods per the command line options.
# If no error found, return zero value so the caller of this tool can know
# if it's executed successfully or not.
#
#   @retval 0     Tool was successful
#   @retval 1     Tool failed
#
def Main():
    EdkLogger.Initialize()
    Options = None
    DistFileName = 'dist.pkg'
    ContentFileName = 'content.zip'
    DistFile, ContentZipFile, UnpackDir = None, None, None
    
    Options = MyOptionParser()
    try:
        if Options.LogLevel < EdkLogger.DEBUG_9:
            EdkLogger.SetLevel(Options.LogLevel + 1)
        else:
            EdkLogger.SetLevel(Options.LogLevel)

        CheckEnvVariable()
        WorkspaceDir = os.environ["WORKSPACE"]
        if not Options.PackageFile:
            EdkLogger.error("InstallPkg", OPTION_NOT_SUPPORTED, ExtraData="Must specify one distribution package")

        # unzip dist.pkg file
        EdkLogger.quiet("Unzipping and parsing distribution package XML file ... ")
        DistFile = PackageFile(Options.PackageFile)
        UnpackDir = os.path.normpath(os.path.join(WorkspaceDir, ".tmp"))
        DistPkgFile = DistFile.UnpackFile(DistFileName, os.path.normpath(os.path.join(UnpackDir, DistFileName)))
        if not DistPkgFile:
            EdkLogger.error("InstallPkg", FILE_NOT_FOUND, "File [%s] is broken in distribution package" %DistFileName)
        
        # Generate distpkg
        DistPkgObj = DistributionPackageXml()
        DistPkg = DistPkgObj.FromXml(DistPkgFile)

        # prepare check dependency
        Db = IpiDatabase(os.path.normpath(os.path.join(WorkspaceDir, "Conf/DistributionPackageDatabase.db")))
        Db.InitDatabase()
        Dep = DependencyRules(Db)
        
        # Check distribution package exist
        if Dep.CheckDpExists(DistPkg.Header.Guid, DistPkg.Header.Version):
            EdkLogger.error("InstallPkg", UNKNOWN_ERROR, "This distribution package has been installed", ExtraData=DistPkg.Header.Name)
        
        # unzip contents.zip file
        ContentFile = DistFile.UnpackFile(ContentFileName, os.path.normpath(os.path.join(UnpackDir, ContentFileName)))
        ContentZipFile = PackageFile(ContentFile)
        if not ContentFile:
            EdkLogger.error("InstallPkg", FILE_NOT_FOUND, "File [%s] is broken in distribution package" %ContentFileName)
        
        # verify MD5 signature
        Md5Sigature = md5.new(open(ContentFile).read())
        if DistPkg.Header.Signature != Md5Sigature.hexdigest():
            EdkLogger.error("InstallPkg", FILE_CHECKSUM_FAILURE, ExtraData=ContentFile)
        
        # Check package exist and install
        for Guid,Version,Path in DistPkg.PackageSurfaceArea:
            PackagePath = os.path.dirname(Path)
            NewPackagePath = PackagePath
            Package = DistPkg.PackageSurfaceArea[Guid,Version,Path]
            EdkLogger.info("Installing package ... %s" % Package.PackageHeader.Name)
            if Dep.CheckPackageExists(Guid, Version):
                EdkLogger.quiet("Package [%s] has been installed" %Path)
            NewPackagePath = InstallNewPackage(WorkspaceDir, PackagePath)
            Package.FileList = []
            for Item in Package.MiscFiles.Files:
                FromFile = os.path.join(PackagePath, Item.Filename)
                ToFile = os.path.normpath(os.path.join(WorkspaceDir, NewPackagePath, Item.Filename))
                ContentZipFile.UnpackFile(FromFile, ToFile)
                Package.FileList.append(ToFile)
            
            # Update package
            Package.PackageHeader.CombinePath = Package.PackageHeader.CombinePath.replace(PackagePath, NewPackagePath, 1)
            # Update modules of package
            Module = None
            for ModuleGuid, ModuleVersion, ModulePath in Package.Modules:
                Module = Package.Modules[ModuleGuid, ModuleVersion, ModulePath]
                NewModulePath = ModulePath.replace(PackagePath, NewPackagePath, 1)
                del Package.Modules[ModuleGuid, ModuleVersion, ModulePath]
                Package.Modules[ModuleGuid, ModuleVersion, NewModulePath] = Module
            del DistPkg.PackageSurfaceArea[Guid,Version,Path]
            DistPkg.PackageSurfaceArea[Guid,Version,Package.PackageHeader.CombinePath] = Package

#            SaveFileOnChange(os.path.join(Options.InstallDir, ModulePath, Module.Header.Name, ".inf"), Inf.ModuleToInf(Module), False)
#            EdkLogger.info("Installing package ... %s" % Package.Header.Name)
#            shutil.copytree(os.path.join(ContentFileDir, Path), Options.InstallDir)
#            SaveFileOnChange(os.path.join(Options.InstallDir, Path, Package.Header.Name, ".dec"), Dec.PackageToDec(Package), False)

        # Check module exist and install
        Module = None
        for Guid,Version,Path in DistPkg.ModuleSurfaceArea:
            ModulePath = os.path.dirname(Path)
            NewModulePath = ModulePath
            Module = DistPkg.ModuleSurfaceArea[Guid,Version,Path]
            EdkLogger.info("Installing module ... %s" % Module.ModuleHeader.Name)
            if Dep.CheckModuleExists(Guid, Version):
                EdkLogger.quiet("Module [%s] has been installed" %Path)
            NewModulePath = InstallNewPackage(WorkspaceDir, ModulePath)
            Module.FileList = []
            for Item in Module.MiscFiles.Files:
                ModulePath = ModulePath[os.path.normpath(ModulePath).rfind(os.path.normpath('/'))+1:]
                FromFile = os.path.join(ModulePath, Item.Filename)
                ToFile = os.path.normpath(os.path.join(WorkspaceDir, NewModulePath, Item.Filename))
                ContentZipFile.UnpackFile(FromFile, ToFile)
                Module.FileList.append(ToFile)
            
#            EdkLogger.info("Installing module ... %s" % Module.Header.Name)
#            shutil.copytree(os.path.join(ContentFileDir, Path), Options.InstallDir)
#            SaveFileOnChange(os.path.join(Options.InstallDir, Path, Module.Header.Name, ".inf"), Inf.ModuleToInf(Module), False)
            
            # Update module
            Module.ModuleHeader.CombinePath = Module.ModuleHeader.CombinePath.replace(os.path.dirname(Path), NewModulePath, 1)
            del DistPkg.ModuleSurfaceArea[Guid,Version,Path]
            DistPkg.ModuleSurfaceArea[Guid,Version,Module.ModuleHeader.CombinePath] = Module
#            
#        
#        for Guid,Version,Path in DistPkg.PackageSurfaceArea:
#            print Guid,Version,Path
#            for item in DistPkg.PackageSurfaceArea[Guid,Version,Path].FileList:
#                print item
#        for Guid,Version,Path in DistPkg.ModuleSurfaceArea:
#            print Guid,Version,Path
#            for item in DistPkg.ModuleSurfaceArea[Guid,Version,Path].FileList:
#                print item

        if Options.Tools:
            EdkLogger.info("Installing tools ... ")
            for File in DistPkg.Tools.Files:
                FromFile = File.Filename
                ToFile = InstallNewFile(WorkspaceDir, FromFile)
                ContentZipFile.UnpackFile(FromFile, ToFile)
        if Options.MiscFiles:
            EdkLogger.info("Installing misc files ... ")
            for File in DistPkg.MiscellaneousFiles.Files:
                FromFile = File.Filename
                ToFile = InstallNewFile(WorkspaceDir, FromFile)
                ContentZipFile.UnpackFile(FromFile, ToFile)

        # update database
        EdkLogger.quiet("Update Distribution Package Database ...")
        Db.AddDPObject(DistPkg)

    except FatalError, X:
        if Options and Options.LogLevel < EdkLogger.DEBUG_9:
            EdkLogger.quiet("(Python %s on %s) " % (platform.python_version(), sys.platform) + traceback.format_exc())
        ReturnCode = X.args[0]
    except KeyboardInterrupt:
        ReturnCode = ABORT_ERROR
        if Options and Options.LogLevel < EdkLogger.DEBUG_9:
            EdkLogger.quiet("(Python %s on %s) " % (platform.python_version(), sys.platform) + traceback.format_exc())
    except:
        EdkLogger.error(
                    "\nInstallPkg",
                    CODE_ERROR,
                    "Unknown fatal error when installing [%s]" % Options.PackageFile,
                    ExtraData="\n(Please send email to edk2-buildtools-devel@lists.sourceforge.net for help, attaching following call stack trace!)\n",
                    RaiseError=False
                    )
        EdkLogger.quiet("(Python %s on %s) " % (platform.python_version(), sys.platform) + traceback.format_exc())
        ReturnCode = CODE_ERROR
    finally:
        EdkLogger.quiet("Removing temp files ... ")
        if DistFile:
            DistFile.Close()
        if ContentZipFile:
            ContentZipFile.Close()
        if UnpackDir:
            shutil.rmtree(UnpackDir)
        
        EdkLogger.quiet("DONE")
        Progressor.Abort()

if __name__ == '__main__':
    sys.exit(Main())