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

#-------------------------------------------------------------------------
# drawElements Quality Program utilities
# --------------------------------------
#
# Copyright 2015 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 os
import re
import sys
import copy
import zlib
import time
import shlex
import shutil
import fnmatch
import tarfile
import argparse
import platform
import datetime
import tempfile
import posixpath
import subprocess

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

def die (msg):
	print msg
	sys.exit(-1)

def removeLeadingPath (path, basePath):
	# Both inputs must be normalized already
	assert os.path.normpath(path) == path
	assert os.path.normpath(basePath) == basePath
	return path[len(basePath) + 1:]

def findFile (candidates):
	for file in candidates:
		if os.path.exists(file):
			return file
	return None

def getFileList (basePath):
	allFiles	= []
	basePath	= os.path.normpath(basePath)
	for root, dirs, files in os.walk(basePath):
		for file in files:
			relPath = removeLeadingPath(os.path.normpath(os.path.join(root, file)), basePath)
			allFiles.append(relPath)
	return allFiles

def toDatetime (dateTuple):
	Y, M, D = dateTuple
	return datetime.datetime(Y, M, D)

class PackageBuildInfo:
	def __init__ (self, releaseConfig, srcBasePath, dstBasePath, tmpBasePath):
		self.releaseConfig	= releaseConfig
		self.srcBasePath	= srcBasePath
		self.dstBasePath	= dstBasePath
		self.tmpBasePath	= tmpBasePath

	def getReleaseConfig (self):
		return self.releaseConfig

	def getReleaseVersion (self):
		return self.releaseConfig.getVersion()

	def getReleaseId (self):
		# Release id is crc32(releaseConfig + release)
		return zlib.crc32(self.releaseConfig.getName() + self.releaseConfig.getVersion()) & 0xffffffff

	def getSrcBasePath (self):
		return self.srcBasePath

	def getTmpBasePath (self):
		return self.tmpBasePath

class DstFile (object):
	def __init__ (self, dstFile):
		self.dstFile = dstFile

	def makeDir (self):
		dirName = os.path.dirname(self.dstFile)
		if not os.path.exists(dirName):
			os.makedirs(dirName)

	def make (self, packageBuildInfo):
		assert False # Should not be called

class CopyFile (DstFile):
	def __init__ (self, srcFile, dstFile):
		super(CopyFile, self).__init__(dstFile)
		self.srcFile = srcFile

	def make (self, packageBuildInfo):
		self.makeDir()
		if os.path.exists(self.dstFile):
			die("%s already exists" % self.dstFile)
		shutil.copyfile(self.srcFile, self.dstFile)

class GenReleaseInfoFileTarget (DstFile):
	def __init__ (self, dstFile):
		super(GenReleaseInfoFileTarget, self).__init__(dstFile)

	def make (self, packageBuildInfo):
		self.makeDir()

		scriptPath = os.path.normpath(os.path.join(packageBuildInfo.srcBasePath, "framework", "qphelper", "gen_release_info.py"))
		execute([
				"python",
				"-B", # no .py[co]
				scriptPath,
				"--name=%s" % packageBuildInfo.getReleaseVersion(),
				"--id=0x%08x" % packageBuildInfo.getReleaseId(),
				"--out=%s" % self.dstFile
			])

class GenCMake (DstFile):
	def __init__ (self, srcFile, dstFile, replaceVars):
		super(GenCMake, self).__init__(dstFile)
		self.srcFile		= srcFile
		self.replaceVars	= replaceVars

	def make (self, packageBuildInfo):
		self.makeDir()
		print "    GenCMake: %s" % removeLeadingPath(self.dstFile, packageBuildInfo.dstBasePath)
		src = readFile(self.srcFile)
		for var, value in self.replaceVars:
			src = re.sub('set\(%s\s+"[^"]*"' % re.escape(var),
						 'set(%s "%s"' % (var, value), src)
		writeFile(self.dstFile, src)

def createFileTargets (srcBasePath, dstBasePath, files, filters):
	usedFiles	= set() # Files that are already included by other filters
	targets		= []

	for isMatch, createFileObj in filters:
		# Build list of files that match filter
		matchingFiles = []
		for file in files:
			if not file in usedFiles and isMatch(file):
				matchingFiles.append(file)

		# Build file objects, add to used set
		for file in matchingFiles:
			usedFiles.add(file)
			targets.append(createFileObj(os.path.join(srcBasePath, file), os.path.join(dstBasePath, file)))

	return targets

# Generates multiple file targets based on filters
class FileTargetGroup:
	def __init__ (self, srcBasePath, dstBasePath, filters, srcBasePathFunc=PackageBuildInfo.getSrcBasePath):
		self.srcBasePath	= srcBasePath
		self.dstBasePath	= dstBasePath
		self.filters		= filters
		self.getSrcBasePath	= srcBasePathFunc

	def make (self, packageBuildInfo):
		fullSrcPath		= os.path.normpath(os.path.join(self.getSrcBasePath(packageBuildInfo), self.srcBasePath))
		fullDstPath		= os.path.normpath(os.path.join(packageBuildInfo.dstBasePath, self.dstBasePath))

		allFiles		= getFileList(fullSrcPath)
		targets			= createFileTargets(fullSrcPath, fullDstPath, allFiles, self.filters)

		# Make all file targets
		for file in targets:
			file.make(packageBuildInfo)

# Single file target
class SingleFileTarget:
	def __init__ (self, srcFile, dstFile, makeTarget):
		self.srcFile	= srcFile
		self.dstFile	= dstFile
		self.makeTarget	= makeTarget

	def make (self, packageBuildInfo):
		fullSrcPath		= os.path.normpath(os.path.join(packageBuildInfo.srcBasePath, self.srcFile))
		fullDstPath		= os.path.normpath(os.path.join(packageBuildInfo.dstBasePath, self.dstFile))

		target = self.makeTarget(fullSrcPath, fullDstPath)
		target.make(packageBuildInfo)

class BuildTarget:
	def __init__ (self, baseConfig, generator, targets = None):
		self.baseConfig	= baseConfig
		self.generator	= generator
		self.targets	= targets

	def make (self, packageBuildInfo):
		print "    Building %s" % self.baseConfig.getBuildDir()

		# Create config with full build dir path
		config = BuildConfig(os.path.join(packageBuildInfo.getTmpBasePath(), self.baseConfig.getBuildDir()),
							 self.baseConfig.getBuildType(),
							 self.baseConfig.getArgs(),
							 srcPath = os.path.join(packageBuildInfo.dstBasePath, "src"))

		assert not os.path.exists(config.getBuildDir())
		build(config, self.generator, self.targets)

class BuildAndroidTarget:
	def __init__ (self, dstFile):
		self.dstFile = dstFile

	def make (self, packageBuildInfo):
		print "    Building Android binary"

		buildRoot = os.path.join(packageBuildInfo.tmpBasePath, "android-build")

		assert not os.path.exists(buildRoot)
		os.makedirs(buildRoot)

		# Execute build script
		scriptPath = os.path.normpath(os.path.join(packageBuildInfo.dstBasePath, "src", "android", "scripts", "build.py"))
		execute([
				"python",
				"-B", # no .py[co]
				scriptPath,
				"--build-root=%s" % buildRoot,
			])

		srcFile		= os.path.normpath(os.path.join(buildRoot, "package", "bin", "dEQP-debug.apk"))
		dstFile		= os.path.normpath(os.path.join(packageBuildInfo.dstBasePath, self.dstFile))

		CopyFile(srcFile, dstFile).make(packageBuildInfo)

class FetchExternalSourcesTarget:
	def __init__ (self):
		pass

	def make (self, packageBuildInfo):
		scriptPath = os.path.normpath(os.path.join(packageBuildInfo.dstBasePath, "src", "external", "fetch_sources.py"))
		execute([
				"python",
				"-B", # no .py[co]
				scriptPath,
			])

class RemoveSourcesTarget:
	def __init__ (self):
		pass

	def make (self, packageBuildInfo):
		shutil.rmtree(os.path.join(packageBuildInfo.dstBasePath, "src"), ignore_errors=False)

class Module:
	def __init__ (self, name, targets):
		self.name		= name
		self.targets	= targets

	def make (self, packageBuildInfo):
		for target in self.targets:
			target.make(packageBuildInfo)

class ReleaseConfig:
	def __init__ (self, name, version, modules, sources = True):
		self.name			= name
		self.version		= version
		self.modules		= modules
		self.sources		= sources

	def getName (self):
		return self.name

	def getVersion (self):
		return self.version

	def getModules (self):
		return self.modules

	def packageWithSources (self):
		return self.sources

def matchIncludeExclude (includePatterns, excludePatterns, filename):
	components = os.path.normpath(filename).split(os.sep)
	for pattern in excludePatterns:
		for component in components:
			if fnmatch.fnmatch(component, pattern):
				return False

	for pattern in includePatterns:
		for component in components:
			if fnmatch.fnmatch(component, pattern):
				return True

	return False

def copyFileFilter (includePatterns, excludePatterns=[]):
	return (lambda f: matchIncludeExclude(includePatterns, excludePatterns, f),
			lambda s, d: CopyFile(s, d))

def makeFileCopyGroup (srcDir, dstDir, includePatterns, excludePatterns=[]):
	return FileTargetGroup(srcDir, dstDir, [copyFileFilter(includePatterns, excludePatterns)])

def makeTmpFileCopyGroup (srcDir, dstDir, includePatterns, excludePatterns=[]):
	return FileTargetGroup(srcDir, dstDir, [copyFileFilter(includePatterns, excludePatterns)], PackageBuildInfo.getTmpBasePath)

def makeFileCopy (srcFile, dstFile):
	return SingleFileTarget(srcFile, dstFile, lambda s, d: CopyFile(s, d))

def getReleaseFileName (configName, releaseName):
	today = datetime.date.today()
	return "dEQP-%s-%04d-%02d-%02d-%s" % (releaseName, today.year, today.month, today.day, configName)

def getTempDir ():
	dirName = os.path.join(tempfile.gettempdir(), "dEQP-Releases")
	if not os.path.exists(dirName):
		os.makedirs(dirName)
	return dirName

def makeRelease (releaseConfig):
	releaseName			= getReleaseFileName(releaseConfig.getName(), releaseConfig.getVersion())
	tmpPath				= getTempDir()
	srcBasePath			= DEQP_DIR
	dstBasePath			= os.path.join(tmpPath, releaseName)
	tmpBasePath			= os.path.join(tmpPath, releaseName + "-tmp")
	packageBuildInfo	= PackageBuildInfo(releaseConfig, srcBasePath, dstBasePath, tmpBasePath)
	dstArchiveName		= releaseName + ".tar.bz2"

	print "Creating release %s to %s" % (releaseName, tmpPath)

	# Remove old temporary dirs
	for path in [dstBasePath, tmpBasePath]:
		if os.path.exists(path):
			shutil.rmtree(path, ignore_errors=False)

	# Make all modules
	for module in releaseConfig.getModules():
		print "  Processing module %s" % module.name
		module.make(packageBuildInfo)

	# Remove sources?
	if not releaseConfig.packageWithSources():
		shutil.rmtree(os.path.join(dstBasePath, "src"), ignore_errors=False)

	# Create archive
	print "Creating %s" % dstArchiveName
	archive	= tarfile.open(dstArchiveName, 'w:bz2')
	archive.add(dstBasePath, arcname=releaseName)
	archive.close()

	# Remove tmp dirs
	for path in [dstBasePath, tmpBasePath]:
		if os.path.exists(path):
			shutil.rmtree(path, ignore_errors=False)

	print "Done!"

# Module declarations

SRC_FILE_PATTERNS	= ["*.h", "*.hpp", "*.c", "*.cpp", "*.m", "*.mm", "*.inl", "*.java", "*.aidl", "CMakeLists.txt", "LICENSE.txt", "*.cmake"]
TARGET_PATTERNS		= ["*.cmake", "*.h", "*.lib", "*.dll", "*.so", "*.txt"]

BASE = Module("Base", [
	makeFileCopy		("LICENSE",									"src/LICENSE"),
	makeFileCopy		("CMakeLists.txt",							"src/CMakeLists.txt"),
	makeFileCopyGroup	("targets",									"src/targets",							TARGET_PATTERNS),
	makeFileCopyGroup	("execserver",								"src/execserver",						SRC_FILE_PATTERNS),
	makeFileCopyGroup	("executor",								"src/executor",							SRC_FILE_PATTERNS),
	makeFileCopy		("modules/CMakeLists.txt",					"src/modules/CMakeLists.txt"),
	makeFileCopyGroup	("external",								"src/external",							["CMakeLists.txt", "*.py"]),

	# Stylesheet for displaying test logs on browser
	makeFileCopyGroup	("doc/testlog-stylesheet",					"doc/testlog-stylesheet",				["*"]),

	# Non-optional parts of framework
	makeFileCopy		("framework/CMakeLists.txt",				"src/framework/CMakeLists.txt"),
	makeFileCopyGroup	("framework/delibs",						"src/framework/delibs",					SRC_FILE_PATTERNS),
	makeFileCopyGroup	("framework/common",						"src/framework/common",					SRC_FILE_PATTERNS),
	makeFileCopyGroup	("framework/qphelper",						"src/framework/qphelper",				SRC_FILE_PATTERNS),
	makeFileCopyGroup	("framework/platform",						"src/framework/platform",				SRC_FILE_PATTERNS),
	makeFileCopyGroup	("framework/opengl",						"src/framework/opengl",					SRC_FILE_PATTERNS, ["simplereference"]),
	makeFileCopyGroup	("framework/egl",							"src/framework/egl",					SRC_FILE_PATTERNS),

	# android sources
	makeFileCopyGroup	("android/package/src",						"src/android/package/src",				SRC_FILE_PATTERNS),
	makeFileCopy		("android/package/AndroidManifest.xml",		"src/android/package/AndroidManifest.xml"),
	makeFileCopyGroup	("android/package/res",						"src/android/package/res",				["*.png", "*.xml"]),
	makeFileCopyGroup	("android/scripts",							"src/android/scripts", [
		"common.py",
		"build.py",
		"resources.py",
		"install.py",
		"launch.py",
		"debug.py"
		]),

	# Release info
	GenReleaseInfoFileTarget("src/framework/qphelper/qpReleaseInfo.inl")
])

DOCUMENTATION = Module("Documentation", [
	makeFileCopyGroup	("doc/pdf",									"doc",									["*.pdf"]),
	makeFileCopyGroup	("doc",										"doc",									["porting_layer_changes_*.txt"]),
])

GLSHARED = Module("Shared GL Tests", [
	# Optional framework components
	makeFileCopyGroup	("framework/randomshaders",					"src/framework/randomshaders",			SRC_FILE_PATTERNS),
	makeFileCopyGroup	("framework/opengl/simplereference",		"src/framework/opengl/simplereference",	SRC_FILE_PATTERNS),
	makeFileCopyGroup	("framework/referencerenderer",				"src/framework/referencerenderer",		SRC_FILE_PATTERNS),

	makeFileCopyGroup	("modules/glshared",						"src/modules/glshared",					SRC_FILE_PATTERNS),
])

GLES2 = Module("GLES2", [
	makeFileCopyGroup	("modules/gles2",							"src/modules/gles2",					SRC_FILE_PATTERNS),
	makeFileCopyGroup	("data/gles2",								"src/data/gles2",						["*.*"]),
	makeFileCopyGroup	("doc/testspecs/GLES2",						"doc/testspecs/GLES2",					["*.txt"])
])

GLES3 = Module("GLES3", [
	makeFileCopyGroup	("modules/gles3",							"src/modules/gles3",					SRC_FILE_PATTERNS),
	makeFileCopyGroup	("data/gles3",								"src/data/gles3",						["*.*"]),
	makeFileCopyGroup	("doc/testspecs/GLES3",						"doc/testspecs/GLES3",					["*.txt"])
])

GLES31 = Module("GLES31", [
	makeFileCopyGroup	("modules/gles31",							"src/modules/gles31",					SRC_FILE_PATTERNS),
	makeFileCopyGroup	("data/gles31",								"src/data/gles31",						["*.*"]),
	makeFileCopyGroup	("doc/testspecs/GLES31",					"doc/testspecs/GLES31",					["*.txt"])
])

EGL = Module("EGL", [
	makeFileCopyGroup	("modules/egl",								"src/modules/egl",						SRC_FILE_PATTERNS)
])

INTERNAL = Module("Internal", [
	makeFileCopyGroup	("modules/internal",						"src/modules/internal",					SRC_FILE_PATTERNS),
	makeFileCopyGroup	("data/internal",							"src/data/internal",					["*.*"]),
])

EXTERNAL_SRCS = Module("External sources", [
	FetchExternalSourcesTarget()
])

ANDROID_BINARIES = Module("Android Binaries", [
	BuildAndroidTarget	("bin/android/dEQP.apk"),
	makeFileCopyGroup	("targets/android",							"bin/android",							["*.bat", "*.sh"]),
])

COMMON_BUILD_ARGS	= ['-DPNG_SRC_PATH=%s' % os.path.realpath(os.path.join(DEQP_DIR, '..', 'libpng'))]
NULL_X32_CONFIG		= BuildConfig('null-x32',	'Release', ['-DDEQP_TARGET=null', '-DCMAKE_C_FLAGS=-m32', '-DCMAKE_CXX_FLAGS=-m32'] + COMMON_BUILD_ARGS)
NULL_X64_CONFIG		= BuildConfig('null-x64',	'Release', ['-DDEQP_TARGET=null', '-DCMAKE_C_FLAGS=-m64', '-DCMAKE_CXX_FLAGS=-m64'] + COMMON_BUILD_ARGS)
GLX_X32_CONFIG		= BuildConfig('glx-x32',	'Release', ['-DDEQP_TARGET=x11_glx', '-DCMAKE_C_FLAGS=-m32', '-DCMAKE_CXX_FLAGS=-m32'] + COMMON_BUILD_ARGS)
GLX_X64_CONFIG		= BuildConfig('glx-x64',	'Release', ['-DDEQP_TARGET=x11_glx', '-DCMAKE_C_FLAGS=-m64', '-DCMAKE_CXX_FLAGS=-m64'] + COMMON_BUILD_ARGS)

EXCLUDE_BUILD_FILES = ["CMakeFiles", "*.a", "*.cmake"]

LINUX_X32_COMMON_BINARIES = Module("Linux x32 Common Binaries", [
	BuildTarget			(NULL_X32_CONFIG, ANY_UNIX_GENERATOR),
	makeTmpFileCopyGroup(NULL_X32_CONFIG.getBuildDir() + "/execserver",		"bin/linux32",					["*"],	EXCLUDE_BUILD_FILES),
	makeTmpFileCopyGroup(NULL_X32_CONFIG.getBuildDir() + "/executor",		"bin/linux32",					["*"],	EXCLUDE_BUILD_FILES),
])

LINUX_X64_COMMON_BINARIES = Module("Linux x64 Common Binaries", [
	BuildTarget			(NULL_X64_CONFIG, ANY_UNIX_GENERATOR),
	makeTmpFileCopyGroup(NULL_X64_CONFIG.getBuildDir() + "/execserver",		"bin/linux64",					["*"],	EXCLUDE_BUILD_FILES),
	makeTmpFileCopyGroup(NULL_X64_CONFIG.getBuildDir() + "/executor",		"bin/linux64",					["*"],	EXCLUDE_BUILD_FILES),
])

# Special module to remove src dir, for example after binary build
REMOVE_SOURCES = Module("Remove sources from package", [
	RemoveSourcesTarget()
])

# Release configuration

ALL_MODULES		= [
	BASE,
	DOCUMENTATION,
	GLSHARED,
	GLES2,
	GLES3,
	GLES31,
	EGL,
	INTERNAL,
	EXTERNAL_SRCS,
]

ALL_BINARIES	= [
	LINUX_X64_COMMON_BINARIES,
	ANDROID_BINARIES,
]

RELEASE_CONFIGS	= {
	"src":		ALL_MODULES,
	"src-bin":	ALL_MODULES + ALL_BINARIES,
	"bin":		ALL_MODULES + ALL_BINARIES + [REMOVE_SOURCES],
}

def parseArgs ():
	parser = argparse.ArgumentParser(description = "Build release package")
	parser.add_argument("-c",
						"--config",
						dest="config",
						choices=RELEASE_CONFIGS.keys(),
						required=True,
						help="Release configuration")
	parser.add_argument("-n",
						"--name",
						dest="name",
						required=True,
						help="Package-specific name")
	parser.add_argument("-v",
						"--version",
						dest="version",
						required=True,
						help="Version code")
	return parser.parse_args()

if __name__ == "__main__":
	args	= parseArgs()
	config	= ReleaseConfig(args.name, args.version, RELEASE_CONFIGS[args.config])
	makeRelease(config)