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

#-------------------------------------------------------------------------
# drawElements Quality Program utilities
# --------------------------------------
#
# Copyright 2016 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.
#
#-------------------------------------------------------------------------

import itertools
import os
import argparse
import tempfile
import sys

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

pythonExecutable = sys.executable or "python"

class Environment:
    def __init__ (self, srcDir, tmpDir, verbose):
        self.srcDir = srcDir
        self.tmpDir = tmpDir
        self.verbose = verbose

class BuildTestStep:
    def getName (self):
        return "<unknown>"

    def isAvailable (self, env):
        return True

    def run (self, env):
        raise Exception("Not implemented")

class RunScript(BuildTestStep):
    def __init__ (self, scriptPath, getExtraArgs = None):
        self.scriptPath = scriptPath
        self.getExtraArgs = getExtraArgs

    def getName (self):
        return self.scriptPath

    def run (self, env):
        args = [pythonExecutable, os.path.join(env.srcDir, self.scriptPath)]

        if self.getExtraArgs != None:
            args += self.getExtraArgs(env)

        execute(args)

    def __repr__(self):
        return "RunScript:%s" % (self.scriptPath)

def makeCflagsArgs (cflags):
    cflagsStr = " ".join(cflags)
    return ["-DCMAKE_C_FLAGS=%s" % cflagsStr, "-DCMAKE_CXX_FLAGS=%s" % cflagsStr]

def makeBuildArgs (target, cc, cpp, cflags):
    return ["-DDEQP_TARGET=%s" % target, "-DCMAKE_C_COMPILER=%s" % cc, "-DCMAKE_CXX_COMPILER=%s" % cpp] + makeCflagsArgs(cflags)

class BuildConfigGen:
    def isAvailable (self, env):
        return True

class UnixConfig(BuildConfigGen):
    def __init__ (self, target, buildType, cc, cpp, cflags):
        self.target = target
        self.buildType = buildType
        self.cc = cc
        self.cpp = cpp
        self.cflags = cflags

    def isAvailable (self, env):
        return which(self.cc) != None and which(self.cpp) != None

    def getBuildConfig (self, env, buildDir):
        args = makeBuildArgs(self.target, self.cc, self.cpp, self.cflags)
        return BuildConfig(buildDir, self.buildType, args, env.srcDir)

class VSConfig(BuildConfigGen):
    def __init__ (self, buildType):
        self.buildType = buildType

    def getBuildConfig (self, env, buildDir):
        args = ["-DCMAKE_C_FLAGS=/WX -DCMAKE_CXX_FLAGS=/WX"]
        return BuildConfig(buildDir, self.buildType, args, env.srcDir)

class Build(BuildTestStep):
    def __init__ (self, buildDir, configGen, generator):
        self.buildDir = buildDir
        self.configGen = configGen
        self.generator = generator

    def getName (self):
        return self.buildDir

    def isAvailable (self, env):
        return self.configGen.isAvailable(env) and self.generator != None and self.generator.isAvailable()

    def run (self, env):
        # specialize config for env
        buildDir = os.path.join(env.tmpDir, self.buildDir)
        curConfig = self.configGen.getBuildConfig(env, buildDir)

        build(curConfig, self.generator)

class CheckSrcChanges(BuildTestStep):
    def getName (self):
        return "check for changes"

    def run (self, env):
        pushWorkingDir(env.srcDir)
        execute(["git", "diff", "--exit-code"])
        popWorkingDir()

def getClangVersion ():
    knownVersions = ["4.0", "3.9", "3.8", "3.7", "3.6", "3.5"]
    for version in knownVersions:
        if which("clang-" + version) != None:
            return "-" + version
    return ""

def runSteps (steps):
    for step in steps:
        if step.isAvailable(env):
            print("Run: %s" % step.getName())
            step.run(env)
        else:
            print("Skip: %s" % step.getName())

COMMON_CFLAGS = ["-Werror", "-Wno-error=unused-function"]
COMMON_GCC_CFLAGS = COMMON_CFLAGS + ["-Wno-error=array-bounds", "-Wno-error=address", "-Wno-error=nonnull"]
COMMON_CLANG_CFLAGS = COMMON_CFLAGS + ["-Wno-error=unused-command-line-argument"]
GCC_32BIT_CFLAGS = COMMON_GCC_CFLAGS + ["-m32"]
CLANG_32BIT_CFLAGS = COMMON_CLANG_CFLAGS + ["-m32"]
GCC_64BIT_CFLAGS = COMMON_GCC_CFLAGS + ["-m64"]
CLANG_64BIT_CFLAGS = COMMON_CLANG_CFLAGS + ["-m64"]
CLANG_VERSION = getClangVersion()

# Always ran before any receipe
PREREQUISITES = [
    RunScript(os.path.join("external", "fetch_sources.py"), lambda env: ["--force"] + (["--verbose"] if env.verbose else []))
]

# Always ran after any receipe
POST_CHECKS = [
    CheckSrcChanges()
]

# Optional step to clean up external resources after finishing receipe
POST_CLEANUP = [
    RunScript(os.path.join("external", "fetch_sources.py"), lambda env: ["--clean"])
]

BUILD_TARGETS = [
    Build("clang-64-debug",
          UnixConfig("null",
                     "Debug",
                     "clang" + CLANG_VERSION,
                     "clang++" + CLANG_VERSION,
                     CLANG_64BIT_CFLAGS),
          ANY_UNIX_GENERATOR),
    Build("gcc-32-debug",
          UnixConfig("null",
                     "Debug",
                     "gcc",
                     "g++",
                     GCC_32BIT_CFLAGS),
          ANY_UNIX_GENERATOR),
    Build("gcc-64-release",
          UnixConfig("null",
                     "Release",
                     "gcc",
                     "g++",
                     GCC_64BIT_CFLAGS),
          ANY_UNIX_GENERATOR),
    Build("vs-64-debug",
          VSConfig("Debug"),
          ANY_VS_X64_GENERATOR),
]

EARLY_SPECIAL_RECIPES = [
    ('gen-inl-files', [
            RunScript(os.path.join("scripts", "gen_egl.py")),
            RunScript(os.path.join("scripts", "opengl", "gen_all.py")),
            RunScript(os.path.join("external", "vulkancts", "scripts", "gen_framework.py"), lambda env: (["--verbose"] if env.verbose else []) ),
            RunScript(os.path.join("external", "vulkancts", "scripts", "gen_framework_c.py"), lambda env: (["--verbose"] if env.verbose else []) ),
            RunScript(os.path.join("external", "vulkancts", "scripts", "gen_framework.py"), lambda env: ["--api", "SC"] ),
            RunScript(os.path.join("external", "vulkancts", "scripts", "gen_framework_c.py"), lambda env: ["--api", "SC"] ),
            RunScript(os.path.join("scripts", "gen_android_bp.py")),
            RunScript(os.path.join("scripts", "gen_khronos_cts_bp.py"))
        ]),
]

LATE_SPECIAL_RECIPES = [
    ('android-mustpass', [
            RunScript(os.path.join("scripts", "build_android_mustpass.py"),
                      lambda env: ["--build-dir", os.path.join(env.tmpDir, "android-mustpass")] + (["--verbose"] if env.verbose else [])),
        ]),
    ('vulkan-mustpass', [
            RunScript(os.path.join("external", "vulkancts", "scripts", "build_mustpass.py"),
                      lambda env: ["--build-dir", os.path.join(env.tmpDir, "vulkan-mustpass")] + (["--verbose"] if env.verbose else [])),
        ]),
    ('spirv-binaries', [
            RunScript(os.path.join("external", "vulkancts", "scripts", "build_spirv_binaries.py"),
                      lambda env: ["--build-type", "Release",
                                    "--build-dir", os.path.join(env.tmpDir, "spirv-binaries"),
                                    "--dst-path", os.path.join(env.tmpDir, "spirv-binaries")]),
        ]),
    ('amber-verify', [
            RunScript(os.path.join("external", "vulkancts", "scripts", "amber_verify.py"),
                      lambda env: ["--build-type", "Release",
                                    "--build-dir", os.path.join(env.tmpDir, "amber-verify"),
                                    "--dst-path", os.path.join(env.tmpDir, "amber-verify")]),
        ]),
    ('check-all', [
            RunScript(os.path.join("scripts", "src_util", "check_all.py")),
        ])
]

def getBuildRecipes ():
    return [(b.getName(), [b]) for b in BUILD_TARGETS]

def getAllRecipe (recipes):
    allSteps = {}
    for name, steps in recipes:
        allSteps[name] = steps
    return allSteps

def getRecipes ():
    recipes = EARLY_SPECIAL_RECIPES + getBuildRecipes() + LATE_SPECIAL_RECIPES
    return recipes

def getRecipesByName (recipes, recipeNames):
    selectedRecipes = {}
    for recipeName in recipeNames:
        for curName, steps in recipes:
            logging.debug("Evaluating %s against %s" % (recipeName, curName))
            if curName == recipeName:
                selectedRecipes[curName] = steps
    return selectedRecipes

RECIPES = getRecipes()

def parseArgs ():
    parser = argparse.ArgumentParser(description = "Build and test source",
                                     formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument("-s",
                        "--src-dir",
                        dest="srcDir",
                        default=DEQP_DIR,
                        help="Source directory")
    parser.add_argument("-t",
                        "--tmp-dir",
                        dest="tmpDir",
                        default=os.path.join(tempfile.gettempdir(), "deqp-build-test"),
                        help="Temporary directory")
    parser.add_argument("-r",
                        "--recipe",
                        dest="recipes",
                        nargs='+',
                        choices=[n for n, s in RECIPES] + ["all"],
                        default="all",
                        help="Build / test recipe")
    parser.add_argument("-d",
                        "--dump-recipes",
                        dest="dumpRecipes",
                        action="store_true",
                        help="Print out recipes that have any available actions")
    parser.add_argument("--skip-prerequisites",
                        dest="skipPrerequisites",
                        action="store_true",
                        help="Skip external dependency fetch")
    parser.add_argument("--skip-post-checks",
                        dest="skipPostCheck",
                        action="store_true",
                        help="Skip post recipe checks")
    parser.add_argument("--apply-post-external-cleanup",
                        dest="applyPostExternalDependencyCleanup",
                        action="store_true",
                        help="skip external dependency clean up")
    parser.add_argument("-v", "--verbose",
                        dest="verbose",
                        action="store_true",
                        help="Enable verbose logging")

    return parser.parse_args()

if __name__ == "__main__":
    args = parseArgs()
    env = Environment(args.srcDir, args.tmpDir, args.verbose)
    initializeLogger(args.verbose)

    if args.dumpRecipes:
        for name, steps in RECIPES:
            for step in steps:
                if step.isAvailable(env):
                    print(name)
                    break
    else:
        selectedRecipes = getAllRecipe(RECIPES) if args.recipes == "all" \
                        else getRecipesByName(RECIPES, args.recipes)

        print("Running %s" % ','.join(selectedRecipes.keys()))
        selectedSteps = list(itertools.chain.from_iterable(selectedRecipes.values()))
        allSteps = (PREREQUISITES if (args.skipPrerequisites == False) else []) + selectedSteps + (POST_CHECKS if (args.skipPostCheck == False) else []) + (POST_CLEANUP if (args.applyPostExternalDependencyCleanup == True) else [])
        runSteps(allSteps)

        print("All steps completed successfully")
