# -*- coding: utf-8 -*-

#-------------------------------------------------------------------------
# drawElements Quality Program utilities
# --------------------------------------
#
# Copyright 2017 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
#-------------------------------------------------------------------------

# \todo [2017-04-10 pyry]
# * Use smarter asset copy in main build
#   * cmake -E copy_directory doesn't copy timestamps which will cause
#     assets to be always re-packaged
# * Consider adding an option for downloading SDK & NDK

import os
import re
import sys
import glob
import string
import shutil
import argparse
import tempfile
import xml.etree.ElementTree

# Import from <root>/scripts
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))

from ctsbuild.common import *
from ctsbuild.config import *
from ctsbuild.build import *

class SDKEnv:
    def __init__(self, path, desired_version):
        self.path = path
        self.buildToolsVersion = SDKEnv.selectBuildToolsVersion(self.path, desired_version)

    @staticmethod
    def getBuildToolsVersions (path):
        buildToolsPath = os.path.join(path, "build-tools")
        versions = {}

        if os.path.exists(buildToolsPath):
            for item in os.listdir(buildToolsPath):
                m = re.match(r'^([0-9]+)\.([0-9]+)\.([0-9]+)$', item)
                if m != None:
                    versions[int(m.group(1))] = (int(m.group(1)), int(m.group(2)), int(m.group(3)))

        return versions

    @staticmethod
    def selectBuildToolsVersion (path, preferred):
        versions = SDKEnv.getBuildToolsVersions(path)

        if len(versions) == 0:
            return (0,0,0)

        if preferred == -1:
            return max(versions.values())

        if preferred in versions:
            return versions[preferred]

        # Pick newest
        newest_version = max(versions.values())
        print("Couldn't find Android Tool version %d, %d was selected." % (preferred, newest_version[0]))
        return newest_version

    def getPlatformLibrary (self, apiVersion):
        return os.path.join(self.path, "platforms", "android-%d" % apiVersion, "android.jar")

    def getBuildToolsPath (self):
        return os.path.join(self.path, "build-tools", "%d.%d.%d" % self.buildToolsVersion)

class NDKEnv:
    def __init__(self, path):
        self.path = path
        self.version = NDKEnv.detectVersion(self.path)
        self.hostOsName = NDKEnv.detectHostOsName(self.path)

    @staticmethod
    def getKnownAbis ():
        return ["armeabi-v7a", "arm64-v8a", "x86", "x86_64"]

    @staticmethod
    def getAbiPrebuiltsName (abiName):
        prebuilts = {
            "armeabi-v7a": 'android-arm',
            "arm64-v8a": 'android-arm64',
            "x86": 'android-x86',
            "x86_64": 'android-x86_64',
        }

        if not abiName in prebuilts:
            raise Exception("Unknown ABI: " + abiName)

        return prebuilts[abiName]

    @staticmethod
    def detectVersion (path):
        propFilePath = os.path.join(path, "source.properties")
        try:
            with open(propFilePath) as propFile:
                for line in propFile:
                    keyValue = list(map(lambda x: x.strip(), line.split("=")))
                    if keyValue[0] == "Pkg.Revision":
                        versionParts = keyValue[1].split(".")
                        return tuple(map(int, versionParts[0:2]))
        except Exception as e:
            raise Exception("Failed to read source prop file '%s': %s" % (propFilePath, str(e)))
        except:
            raise Exception("Failed to read source prop file '%s': unkown error")

        raise Exception("Failed to detect NDK version (does %s/source.properties have Pkg.Revision?)" % path)

    @staticmethod
    def isHostOsSupported (hostOsName):
        os = HostInfo.getOs()
        bits = HostInfo.getArchBits()
        hostOsParts = hostOsName.split('-')

        if len(hostOsParts) > 1:
            assert(len(hostOsParts) == 2)
            assert(hostOsParts[1] == "x86_64")

            if bits != 64:
                return False

        if os == HostInfo.OS_WINDOWS:
            return hostOsParts[0] == 'windows'
        elif os == HostInfo.OS_LINUX:
            return hostOsParts[0] == 'linux'
        elif os == HostInfo.OS_OSX:
            return hostOsParts[0] == 'darwin'
        else:
            raise Exception("Unhandled HostInfo.getOs() '%d'" % os)

    @staticmethod
    def detectHostOsName (path):
        hostOsNames = [
            "windows",
            "windows-x86_64",
            "darwin-x86",
            "darwin-x86_64",
            "linux-x86",
            "linux-x86_64"
        ]

        for name in hostOsNames:
            if os.path.exists(os.path.join(path, "prebuilt", name)):
                return name

        raise Exception("Failed to determine NDK host OS")

class Environment:
    def __init__(self, sdk, ndk):
        self.sdk = sdk
        self.ndk = ndk

class Configuration:
    def __init__(self, env, buildPath, abis, nativeApi, javaApi, minApi, nativeBuildType, gtfTarget, verbose, layers, angle):
        self.env = env
        self.sourcePath = DEQP_DIR
        self.buildPath = buildPath
        self.abis = abis
        self.nativeApi = nativeApi
        self.javaApi = javaApi
        self.minApi = minApi
        self.nativeBuildType = nativeBuildType
        self.gtfTarget = gtfTarget
        self.verbose = verbose
        self.layers = layers
        self.angle = angle
        self.dCompilerName = "d8"
        self.cmakeGenerator = selectFirstAvailableGenerator([NINJA_GENERATOR, MAKEFILE_GENERATOR, NMAKE_GENERATOR])

    def check (self):
        if self.cmakeGenerator == None:
            raise Exception("Failed to find build tools for CMake")

        if not os.path.exists(self.env.ndk.path):
            raise Exception("Android NDK not found at %s" % self.env.ndk.path)

        if not NDKEnv.isHostOsSupported(self.env.ndk.hostOsName):
            raise Exception("NDK '%s' is not supported on this machine" % self.env.ndk.hostOsName)

        if self.env.ndk.version[0] < 15:
            raise Exception("Android NDK version %d is not supported; build requires NDK version >= 15" % (self.env.ndk.version[0]))

        if not (self.minApi <= self.javaApi <= self.nativeApi):
            raise Exception("Requires: min-api (%d) <= java-api (%d) <= native-api (%d)" % (self.minApi, self.javaApi, self.nativeApi))

        if self.env.sdk.buildToolsVersion == (0,0,0):
            raise Exception("No build tools directory found at %s" % os.path.join(self.env.sdk.path, "build-tools"))

        if not os.path.exists(os.path.join(self.env.sdk.path, "platforms", "android-%d" % self.javaApi)):
            raise Exception("No SDK with api version %d directory found at %s for Java Api" % (self.javaApi, os.path.join(self.env.sdk.path, "platforms")))

        # Try to find first d8 since dx was deprecated
        if which(self.dCompilerName, [self.env.sdk.getBuildToolsPath()]) == None:
            print("Couldn't find %s, will try to find dx", self.dCompilerName)
            self.dCompilerName = "dx"

        androidBuildTools = ["aapt", "zipalign", "apksigner", self.dCompilerName]
        for tool in androidBuildTools:
            if which(tool, [self.env.sdk.getBuildToolsPath()]) == None:
                raise Exception("Missing Android build tool: %s in %s" % (tool, self.env.sdk.getBuildToolsPath()))

        requiredToolsInPath = ["javac", "jar", "keytool"]
        for tool in requiredToolsInPath:
            if which(tool) == None:
                raise Exception("%s not in PATH" % tool)

def log (config, msg):
    if config.verbose:
        print(msg)

def executeAndLog (config, args):
    if config.verbose:
        print(" ".join(args))
    execute(args)

# Path components

class ResolvablePathComponent:
    def __init__ (self):
        pass

class SourceRoot (ResolvablePathComponent):
    def resolve (self, config):
        return config.sourcePath

class BuildRoot (ResolvablePathComponent):
    def resolve (self, config):
        return config.buildPath

class NativeBuildPath (ResolvablePathComponent):
    def __init__ (self, abiName):
        self.abiName = abiName

    def resolve (self, config):
        return getNativeBuildPath(config, self.abiName)

class GeneratedResSourcePath (ResolvablePathComponent):
    def __init__ (self, package):
        self.package = package

    def resolve (self, config):
        packageComps = self.package.getPackageName(config).split('.')
        packageDir = os.path.join(*packageComps)

        return os.path.join(config.buildPath, self.package.getAppDirName(), "src", packageDir, "R.java")

def resolvePath (config, path):
    resolvedComps = []

    for component in path:
        if isinstance(component, ResolvablePathComponent):
            resolvedComps.append(component.resolve(config))
        else:
            resolvedComps.append(str(component))

    return os.path.join(*resolvedComps)

def resolvePaths (config, paths):
    return list(map(lambda p: resolvePath(config, p), paths))

class BuildStep:
    def __init__ (self):
        pass

    def getInputs (self):
        return []

    def getOutputs (self):
        return []

    @staticmethod
    def expandPathsToFiles (paths):
        """
        Expand mixed list of file and directory paths into a flattened list
        of files. Any non-existent input paths are preserved as is.
        """

        def getFiles (dirPath):
            for root, dirs, files in os.walk(dirPath):
                for file in files:
                    yield os.path.join(root, file)

        files = []
        for path in paths:
            if os.path.isdir(path):
                files += list(getFiles(path))
            else:
                files.append(path)

        return files

    def isUpToDate (self, config):
        inputs = resolvePaths(config, self.getInputs())
        outputs = resolvePaths(config, self.getOutputs())

        assert len(inputs) > 0 and len(outputs) > 0

        expandedInputs = BuildStep.expandPathsToFiles(inputs)
        expandedOutputs = BuildStep.expandPathsToFiles(outputs)

        existingInputs = list(filter(os.path.exists, expandedInputs))
        existingOutputs = list(filter(os.path.exists, expandedOutputs))

        if len(existingInputs) != len(expandedInputs):
            for file in expandedInputs:
                if file not in existingInputs:
                    print("ERROR: Missing input file: %s" % file)
            die("Missing input files")

        if len(existingOutputs) != len(expandedOutputs):
            return False # One or more output files are missing

        lastInputChange = max(map(os.path.getmtime, existingInputs))
        firstOutputChange = min(map(os.path.getmtime, existingOutputs))

        return lastInputChange <= firstOutputChange

    def update (config):
        die("BuildStep.update() not implemented")

def getNativeBuildPath (config, abiName):
    return os.path.join(config.buildPath, "%s-%s-%d" % (abiName, config.nativeBuildType, config.nativeApi))

def clearCMakeCacheVariables(args):
    # New value, so clear the necessary cmake variables
    args.append('-UANGLE_LIBS')
    args.append('-UGLES1_LIBRARY')
    args.append('-UGLES2_LIBRARY')
    args.append('-UEGL_LIBRARY')

def buildNativeLibrary (config, abiName):
    def makeNDKVersionString (version):
        minorVersionString = (chr(ord('a') + version[1]) if version[1] > 0 else "")
        return "r%d%s" % (version[0], minorVersionString)

    def getBuildArgs (config, abiName):
        args = ['-DDEQP_TARGET=android',
                '-DDEQP_TARGET_TOOLCHAIN=ndk-modern',
                '-DCMAKE_C_FLAGS=-Werror',
                '-DCMAKE_CXX_FLAGS=-Werror',
                '-DANDROID_NDK_PATH=%s' % config.env.ndk.path,
                '-DANDROID_ABI=%s' % abiName,
                '-DDE_ANDROID_API=%s' % config.nativeApi,
                '-DGLCTS_GTF_TARGET=%s' % config.gtfTarget]

        if config.angle is None:
            # Find any previous builds that may have embedded ANGLE libs and clear the CMake cache
            for abi in NDKEnv.getKnownAbis():
                cMakeCachePath = os.path.join(getNativeBuildPath(config, abi), "CMakeCache.txt")
                try:
                    if 'ANGLE_LIBS' in open(cMakeCachePath).read():
                        clearCMakeCacheVariables(args)
                except IOError:
                    pass
        else:
            cMakeCachePath = os.path.join(getNativeBuildPath(config, abiName), "CMakeCache.txt")
            angleLibsDir = os.path.join(config.angle, abiName)
            # Check if the user changed where the ANGLE libs are being loaded from
            try:
                if angleLibsDir not in open(cMakeCachePath).read():
                    clearCMakeCacheVariables(args)
            except IOError:
                pass
            args.append('-DANGLE_LIBS=%s' % angleLibsDir)

        return args

    nativeBuildPath = getNativeBuildPath(config, abiName)
    buildConfig = BuildConfig(nativeBuildPath, config.nativeBuildType, getBuildArgs(config, abiName))

    build(buildConfig, config.cmakeGenerator, ["deqp"])

def executeSteps (config, steps):
    for step in steps:
        if not step.isUpToDate(config):
            step.update(config)

def parsePackageName (manifestPath):
    tree = xml.etree.ElementTree.parse(manifestPath)

    if not 'package' in tree.getroot().attrib:
        raise Exception("'package' attribute missing from root element in %s" % manifestPath)

    return tree.getroot().attrib['package']

class PackageDescription:
    def __init__ (self, appDirName, appName, hasResources = True):
        self.appDirName = appDirName
        self.appName = appName
        self.hasResources = hasResources

    def getAppName (self):
        return self.appName

    def getAppDirName (self):
        return self.appDirName

    def getPackageName (self, config):
        manifestPath = resolvePath(config, self.getManifestPath())

        return parsePackageName(manifestPath)

    def getManifestPath (self):
        return [SourceRoot(), "android", self.appDirName, "AndroidManifest.xml"]

    def getResPath (self):
        return [SourceRoot(), "android", self.appDirName, "res"]

    def getSourcePaths (self):
        return [
                [SourceRoot(), "android", self.appDirName, "src"]
            ]

    def getAssetsPath (self):
        return [BuildRoot(), self.appDirName, "assets"]

    def getClassesJarPath (self):
        return [BuildRoot(), self.appDirName, "bin", "classes.jar"]

    def getClassesDexDirectory (self):
        return [BuildRoot(), self.appDirName, "bin",]

    def getClassesDexPath (self):
        return [BuildRoot(), self.appDirName, "bin", "classes.dex"]

    def getAPKPath (self):
        return [BuildRoot(), self.appDirName, "bin", self.appName + ".apk"]

# Build step implementations

class BuildNativeLibrary (BuildStep):
    def __init__ (self, abi):
        self.abi = abi

    def isUpToDate (self, config):
        return False

    def update (self, config):
        log(config, "BuildNativeLibrary: %s" % self.abi)
        buildNativeLibrary(config, self.abi)

class GenResourcesSrc (BuildStep):
    def __init__ (self, package):
        self.package = package

    def getInputs (self):
        return [self.package.getResPath(), self.package.getManifestPath()]

    def getOutputs (self):
        return [[GeneratedResSourcePath(self.package)]]

    def update (self, config):
        aaptPath = which("aapt", [config.env.sdk.getBuildToolsPath()])
        dstDir = os.path.dirname(resolvePath(config, [GeneratedResSourcePath(self.package)]))

        if not os.path.exists(dstDir):
            os.makedirs(dstDir)

        executeAndLog(config, [
                aaptPath,
                "package",
                "-f",
                "-m",
                "-S", resolvePath(config, self.package.getResPath()),
                "-M", resolvePath(config, self.package.getManifestPath()),
                "-J", resolvePath(config, [BuildRoot(), self.package.getAppDirName(), "src"]),
                "-I", config.env.sdk.getPlatformLibrary(config.javaApi)
            ])

# Builds classes.jar from *.java files
class BuildJavaSource (BuildStep):
    def __init__ (self, package, libraries = []):
        self.package = package
        self.libraries = libraries

    def getSourcePaths (self):
        srcPaths = self.package.getSourcePaths()

        if self.package.hasResources:
            srcPaths.append([BuildRoot(), self.package.getAppDirName(), "src"]) # Generated sources

        return srcPaths

    def getInputs (self):
        inputs = self.getSourcePaths()

        for lib in self.libraries:
            inputs.append(lib.getClassesJarPath())

        return inputs

    def getOutputs (self):
        return [self.package.getClassesJarPath()]

    def update (self, config):
        srcPaths = resolvePaths(config, self.getSourcePaths())
        srcFiles = BuildStep.expandPathsToFiles(srcPaths)
        jarPath = resolvePath(config, self.package.getClassesJarPath())
        objPath = resolvePath(config, [BuildRoot(), self.package.getAppDirName(), "obj"])
        classPaths = [objPath] + [resolvePath(config, lib.getClassesJarPath()) for lib in self.libraries]
        pathSep = ";" if HostInfo.getOs() == HostInfo.OS_WINDOWS else ":"

        if os.path.exists(objPath):
            shutil.rmtree(objPath)

        os.makedirs(objPath)

        for srcFile in srcFiles:
            executeAndLog(config, [
                    "javac",
                    "-source", "1.7",
                    "-target", "1.7",
                    "-d", objPath,
                    "-bootclasspath", config.env.sdk.getPlatformLibrary(config.javaApi),
                    "-classpath", pathSep.join(classPaths),
                    "-sourcepath", pathSep.join(srcPaths),
                    srcFile
                ])

        if not os.path.exists(os.path.dirname(jarPath)):
            os.makedirs(os.path.dirname(jarPath))

        try:
            pushWorkingDir(objPath)
            executeAndLog(config, [
                    "jar",
                    "cf",
                    jarPath,
                    "."
                ])
        finally:
            popWorkingDir()

class BuildDex (BuildStep):
    def __init__ (self, package, libraries):
        self.package = package
        self.libraries = libraries

    def getInputs (self):
        return [self.package.getClassesJarPath()] + [lib.getClassesJarPath() for lib in self.libraries]

    def getOutputs (self):
        return [self.package.getClassesDexPath()]

    def update (self, config):
        dxPath = which(config.dCompilerName, [config.env.sdk.getBuildToolsPath()])
        dexPath = resolvePath(config, self.package.getClassesDexDirectory())
        jarPaths = [resolvePath(config, self.package.getClassesJarPath())]

        for lib in self.libraries:
            jarPaths.append(resolvePath(config, lib.getClassesJarPath()))

        args = [ dxPath ]
        if config.dCompilerName == "d8":
            args.append("--lib")
            args.append(config.env.sdk.getPlatformLibrary(config.javaApi))
        else:
            args.append("--dex")
        args.append("--output")
        args.append(dexPath)

        executeAndLog(config, args + jarPaths)

class CreateKeystore (BuildStep):
    def __init__ (self):
        self.keystorePath = [BuildRoot(), "debug.keystore"]

    def getOutputs (self):
        return [self.keystorePath]

    def isUpToDate (self, config):
        return os.path.exists(resolvePath(config, self.keystorePath))

    def update (self, config):
        executeAndLog(config, [
                "keytool",
                "-genkeypair",
                "-keystore", resolvePath(config, self.keystorePath),
                "-storepass", "android",
                "-alias", "androiddebugkey",
                "-keypass", "android",
                "-keyalg", "RSA",
                "-keysize", "2048",
                "-validity", "10000",
                "-dname", "CN=, OU=, O=, L=, S=, C=",
            ])

# Builds APK without code
class BuildBaseAPK (BuildStep):
    def __init__ (self, package, libraries = []):
        self.package = package
        self.libraries = libraries
        self.dstPath = [BuildRoot(), self.package.getAppDirName(), "tmp", "base.apk"]

    def getResPaths (self):
        paths = []
        for pkg in [self.package] + self.libraries:
            if pkg.hasResources:
                paths.append(pkg.getResPath())
        return paths

    def getInputs (self):
        return [self.package.getManifestPath()] + self.getResPaths()

    def getOutputs (self):
        return [self.dstPath]

    def update (self, config):
        aaptPath = which("aapt", [config.env.sdk.getBuildToolsPath()])
        dstPath = resolvePath(config, self.dstPath)

        if not os.path.exists(os.path.dirname(dstPath)):
            os.makedirs(os.path.dirname(dstPath))

        args = [
            aaptPath,
            "package",
            "-f",
            "--min-sdk-version", str(config.minApi),
            "--target-sdk-version", str(config.javaApi),
            "-M", resolvePath(config, self.package.getManifestPath()),
            "-I", config.env.sdk.getPlatformLibrary(config.javaApi),
            "-F", dstPath,
            "-0", "arsc" # arsc files need to be uncompressed for SDK version 30 and up
        ]

        for resPath in self.getResPaths():
            args += ["-S", resolvePath(config, resPath)]

        if config.verbose:
            args.append("-v")

        executeAndLog(config, args)

def addFilesToAPK (config, apkPath, baseDir, relFilePaths):
    aaptPath = which("aapt", [config.env.sdk.getBuildToolsPath()])
    maxBatchSize = 25

    pushWorkingDir(baseDir)
    try:
        workQueue = list(relFilePaths)
        # Workaround for Windows.
        if os.path.sep == "\\":
            workQueue = [i.replace("\\", "/") for i in workQueue]

        while len(workQueue) > 0:
            batchSize = min(len(workQueue), maxBatchSize)
            items = workQueue[0:batchSize]

            executeAndLog(config, [
                    aaptPath,
                    "add",
                    "-f", apkPath,
                ] + items)

            del workQueue[0:batchSize]
    finally:
        popWorkingDir()

def addFileToAPK (config, apkPath, baseDir, relFilePath):
    addFilesToAPK(config, apkPath, baseDir, [relFilePath])

class AddJavaToAPK (BuildStep):
    def __init__ (self, package):
        self.package = package
        self.srcPath = BuildBaseAPK(self.package).getOutputs()[0]
        self.dstPath = [BuildRoot(), self.package.getAppDirName(), "tmp", "with-java.apk"]

    def getInputs (self):
        return [
                self.srcPath,
                self.package.getClassesDexPath(),
            ]

    def getOutputs (self):
        return [self.dstPath]

    def update (self, config):
        srcPath = resolvePath(config, self.srcPath)
        dstPath = resolvePath(config, self.getOutputs()[0])
        dexPath = resolvePath(config, self.package.getClassesDexPath())

        shutil.copyfile(srcPath, dstPath)
        addFileToAPK(config, dstPath, os.path.dirname(dexPath), os.path.basename(dexPath))

class AddAssetsToAPK (BuildStep):
    def __init__ (self, package, abi):
        self.package = package
        self.buildPath = [NativeBuildPath(abi)]
        self.srcPath = AddJavaToAPK(self.package).getOutputs()[0]
        self.dstPath = [BuildRoot(), self.package.getAppDirName(), "tmp", "with-assets.apk"]

    def getInputs (self):
        return [
                self.srcPath,
                self.buildPath + ["assets"]
            ]

    def getOutputs (self):
        return [self.dstPath]

    @staticmethod
    def getAssetFiles (buildPath):
        allFiles = BuildStep.expandPathsToFiles([os.path.join(buildPath, "assets")])
        return [os.path.relpath(p, buildPath) for p in allFiles]

    def update (self, config):
        srcPath = resolvePath(config, self.srcPath)
        dstPath = resolvePath(config, self.getOutputs()[0])
        buildPath = resolvePath(config, self.buildPath)
        assetFiles = AddAssetsToAPK.getAssetFiles(buildPath)

        shutil.copyfile(srcPath, dstPath)

        addFilesToAPK(config, dstPath, buildPath, assetFiles)

class AddNativeLibsToAPK (BuildStep):
    def __init__ (self, package, abis):
        self.package = package
        self.abis = abis
        self.srcPath = AddAssetsToAPK(self.package, "").getOutputs()[0]
        self.dstPath = [BuildRoot(), self.package.getAppDirName(), "tmp", "with-native-libs.apk"]

    def getInputs (self):
        paths = [self.srcPath]
        for abi in self.abis:
            paths.append([NativeBuildPath(abi), "libdeqp.so"])
        return paths

    def getOutputs (self):
        return [self.dstPath]

    def update (self, config):
        srcPath = resolvePath(config, self.srcPath)
        dstPath = resolvePath(config, self.getOutputs()[0])
        pkgPath = resolvePath(config, [BuildRoot(), self.package.getAppDirName()])
        libFiles = []

        # Create right directory structure first
        for abi in self.abis:
            libSrcPath = resolvePath(config, [NativeBuildPath(abi), "libdeqp.so"])
            libRelPath = os.path.join("lib", abi, "libdeqp.so")
            libAbsPath = os.path.join(pkgPath, libRelPath)

            if not os.path.exists(os.path.dirname(libAbsPath)):
                os.makedirs(os.path.dirname(libAbsPath))

            shutil.copyfile(libSrcPath, libAbsPath)
            libFiles.append(libRelPath)

            if config.layers:
                # Need to copy everything in the layer folder
                layersGlob = os.path.join(config.layers, abi, "*")
                libVkLayers = glob.glob(layersGlob)
                for layer in libVkLayers:
                    layerFilename = os.path.basename(layer)
                    layerRelPath = os.path.join("lib", abi, layerFilename)
                    layerAbsPath = os.path.join(pkgPath, layerRelPath)
                    shutil.copyfile(layer, layerAbsPath)
                    libFiles.append(layerRelPath)
                    print("Adding layer binary: %s" % (layer,))

            if config.angle:
                angleGlob = os.path.join(config.angle, abi, "lib*_angle.so")
                libAngle = glob.glob(angleGlob)
                for lib in libAngle:
                    libFilename = os.path.basename(lib)
                    libRelPath = os.path.join("lib", abi, libFilename)
                    libAbsPath = os.path.join(pkgPath, libRelPath)
                    shutil.copyfile(lib, libAbsPath)
                    libFiles.append(libRelPath)
                    print("Adding ANGLE binary: %s" % (lib,))

        shutil.copyfile(srcPath, dstPath)
        addFilesToAPK(config, dstPath, pkgPath, libFiles)

class SignAPK (BuildStep):
    def __init__ (self, package):
        self.package = package
        self.srcPath = AlignAPK(self.package).getOutputs()[0]
        self.dstPath = [BuildRoot(), getBuildRootRelativeAPKPath(self.package)]
        self.keystorePath = CreateKeystore().getOutputs()[0]

    def getInputs (self):
        return [self.srcPath, self.keystorePath]

    def getOutputs (self):
        return [self.dstPath]

    def update (self, config):
        apksigner = which("apksigner", [config.env.sdk.getBuildToolsPath()])
        srcPath = resolvePath(config, self.srcPath)
        dstPath = resolvePath(config, self.dstPath)

        executeAndLog(config, [
                apksigner,
                "sign",
                "--ks", resolvePath(config, self.keystorePath),
                "--ks-key-alias", "androiddebugkey",
                "--ks-pass", "pass:android",
                "--key-pass", "pass:android",
                "--min-sdk-version", str(config.minApi),
                "--max-sdk-version", str(config.javaApi),
                "--out", dstPath,
                srcPath
            ])

def getBuildRootRelativeAPKPath (package):
    return os.path.join(package.getAppDirName(), package.getAppName() + ".apk")

class AlignAPK (BuildStep):
    def __init__ (self, package):
        self.package = package
        self.srcPath = AddNativeLibsToAPK(self.package, []).getOutputs()[0]
        self.dstPath = [BuildRoot(), self.package.getAppDirName(), "tmp", "aligned.apk"]
        self.keystorePath = CreateKeystore().getOutputs()[0]

    def getInputs (self):
        return [self.srcPath]

    def getOutputs (self):
        return [self.dstPath]

    def update (self, config):
        srcPath = resolvePath(config, self.srcPath)
        dstPath = resolvePath(config, self.dstPath)
        zipalignPath = os.path.join(config.env.sdk.getBuildToolsPath(), "zipalign")

        executeAndLog(config, [
                zipalignPath,
                "-f", "4",
                srcPath,
                dstPath
            ])

def getBuildStepsForPackage (abis, package, libraries = []):
    steps = []

    assert len(abis) > 0

    # Build native code first
    for abi in abis:
        steps += [BuildNativeLibrary(abi)]

    # Build library packages
    for library in libraries:
        if library.hasResources:
            steps.append(GenResourcesSrc(library))
        steps.append(BuildJavaSource(library))

    # Build main package .java sources
    if package.hasResources:
        steps.append(GenResourcesSrc(package))
    steps.append(BuildJavaSource(package, libraries))
    steps.append(BuildDex(package, libraries))

    # Build base APK
    steps.append(BuildBaseAPK(package, libraries))
    steps.append(AddJavaToAPK(package))

    # Add assets from first ABI
    steps.append(AddAssetsToAPK(package, abis[0]))

    # Add native libs to APK
    steps.append(AddNativeLibsToAPK(package, abis))

    # Finalize APK
    steps.append(CreateKeystore())
    steps.append(AlignAPK(package))
    steps.append(SignAPK(package))

    return steps

def getPackageAndLibrariesForTarget (target):
    deqpPackage = PackageDescription("package", "dEQP")
    ctsPackage = PackageDescription("openglcts", "Khronos-CTS", hasResources = False)

    if target == 'deqp':
        return (deqpPackage, [])
    elif target == 'openglcts':
        return (ctsPackage, [deqpPackage])
    else:
        raise Exception("Uknown target '%s'" % target)

def findNDK ():
    ndkBuildPath = which('ndk-build')
    if ndkBuildPath != None:
        return os.path.dirname(ndkBuildPath)
    else:
        return None

def findSDK ():
    sdkBuildPath = which('android')
    if sdkBuildPath != None:
        return os.path.dirname(os.path.dirname(sdkBuildPath))
    else:
        return None

def getDefaultBuildRoot ():
    return os.path.join(tempfile.gettempdir(), "deqp-android-build")

def parseArgs ():
    nativeBuildTypes = ['Release', 'Debug', 'MinSizeRel', 'RelWithAsserts', 'RelWithDebInfo']
    defaultNDKPath = findNDK()
    defaultSDKPath = findSDK()
    defaultBuildRoot = getDefaultBuildRoot()

    parser = argparse.ArgumentParser(os.path.basename(__file__),
        formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument('--native-build-type',
        dest='nativeBuildType',
        default="RelWithAsserts",
        choices=nativeBuildTypes,
        help="Native code build type")
    parser.add_argument('--build-root',
        dest='buildRoot',
        default=defaultBuildRoot,
        help="Root build directory")
    parser.add_argument('--abis',
        dest='abis',
        default=",".join(NDKEnv.getKnownAbis()),
        help="ABIs to build")
    parser.add_argument('--native-api',
        type=int,
        dest='nativeApi',
        default=28,
        help="Android API level to target in native code")
    parser.add_argument('--java-api',
        type=int,
        dest='javaApi',
        default=28,
        help="Android API level to target in Java code")
    parser.add_argument('--tool-api',
        type=int,
        dest='toolApi',
        default=-1,
        help="Android Tools level to target (-1 being maximum present)")
    parser.add_argument('--min-api',
        type=int,
        dest='minApi',
        default=22,
        help="Minimum Android API level for which the APK can be installed")
    parser.add_argument('--sdk',
        dest='sdkPath',
        default=defaultSDKPath,
        help="Android SDK path",
        required=(True if defaultSDKPath == None else False))
    parser.add_argument('--ndk',
        dest='ndkPath',
        default=defaultNDKPath,
        help="Android NDK path",
        required=(True if defaultNDKPath == None else False))
    parser.add_argument('-v', '--verbose',
        dest='verbose',
        help="Verbose output",
        default=False,
        action='store_true')
    parser.add_argument('--target',
        dest='target',
        help='Build target',
        choices=['deqp', 'openglcts'],
        default='deqp')
    parser.add_argument('--kc-cts-target',
        dest='gtfTarget',
        default='gles32',
        choices=['gles32', 'gles31', 'gles3', 'gles2', 'gl'],
        help="KC-CTS (GTF) target API (only used in openglcts target)")
    parser.add_argument('--layers-path',
        dest='layers',
        default=None,
        required=False)
    parser.add_argument('--angle-path',
        dest='angle',
        default=None,
        required=False)

    args = parser.parse_args()

    def parseAbis (abisStr):
        knownAbis = set(NDKEnv.getKnownAbis())
        abis = []

        for abi in abisStr.split(','):
            abi = abi.strip()
            if not abi in knownAbis:
                raise Exception("Unknown ABI: %s" % abi)
            abis.append(abi)

        return abis

    # Custom parsing & checks
    try:
        args.abis = parseAbis(args.abis)
        if len(args.abis) == 0:
            raise Exception("--abis can't be empty")
    except Exception as e:
        print("ERROR: %s" % str(e))
        parser.print_help()
        sys.exit(-1)

    return args

if __name__ == "__main__":
    args = parseArgs()

    ndk = NDKEnv(os.path.realpath(args.ndkPath))
    sdk = SDKEnv(os.path.realpath(args.sdkPath), args.toolApi)
    buildPath = os.path.realpath(args.buildRoot)
    env = Environment(sdk, ndk)
    config = Configuration(env, buildPath, abis=args.abis, nativeApi=args.nativeApi, javaApi=args.javaApi, minApi=args.minApi, nativeBuildType=args.nativeBuildType, gtfTarget=args.gtfTarget,
                         verbose=args.verbose, layers=args.layers, angle=args.angle)

    try:
        config.check()
    except Exception as e:
        print("ERROR: %s" % str(e))
        print("")
        print("Please check your configuration:")
        print("  --sdk=%s" % args.sdkPath)
        print("  --ndk=%s" % args.ndkPath)
        sys.exit(-1)

    pkg, libs = getPackageAndLibrariesForTarget(args.target)
    steps = getBuildStepsForPackage(config.abis, pkg, libs)

    executeSteps(config, steps)

    print("")
    print("Built %s" % os.path.join(buildPath, getBuildRootRelativeAPKPath(pkg)))
