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

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

import os
import sys
import string
import socket
import fnmatch
from datetime import datetime

BASE_NIGHTLY_DIR	= os.path.normpath(os.path.join(DEQP_DIR, "..", "deqp-nightly"))
BASE_BUILD_DIR		= os.path.join(BASE_NIGHTLY_DIR, "build")
BASE_LOGS_DIR		= os.path.join(BASE_NIGHTLY_DIR, "logs")
BASE_REFS_DIR		= os.path.join(BASE_NIGHTLY_DIR, "refs")

EXECUTOR_PATH		= "executor/executor"
LOG_TO_CSV_PATH		= "executor/testlog-to-csv"
EXECSERVER_PATH		= "execserver/execserver"

CASELIST_PATH		= os.path.join(DEQP_DIR, "Candy", "Data")

COMPARE_NUM_RESULTS	= 4
COMPARE_REPORT_NAME	= "nightly-report.html"

COMPARE_REPORT_TMPL = '''
<html>
<head>
<title>${TITLE}</title>
<style type="text/css">
<!--
body				{ font: serif; font-size: 1em; }
table				{ border-spacing: 0; border-collapse: collapse; }
td					{ border-width: 1px; border-style: solid; border-color: #808080; }
.Header				{ font-weight: bold; font-size: 1em; border-style: none; }
.CasePath			{ }
.Pass				{ background: #80ff80; }
.Fail				{ background: #ff4040; }
.QualityWarning		{ background: #ffff00; }
.CompabilityWarning	{ background: #ffff00; }
.Pending			{ background: #808080; }
.Running			{ background: #d3d3d3; }
.NotSupported		{ background: #ff69b4; }
.ResourceError		{ background: #ff4040; }
.InternalError		{ background: #ff1493; }
.Canceled			{ background: #808080; }
.Crash				{ background: #ffa500; }
.Timeout			{ background: #ffa500; }
.Disabled			{ background: #808080; }
.Missing			{ background: #808080; }
.Ignored			{ opacity: 0.5; }
-->
</style>
</head>
<body>
<h1>${TITLE}</h1>
<table>
${RESULTS}
</table>
</body>
</html>
'''

class NightlyRunConfig:
	def __init__(self, name, buildConfig, generator, binaryName, testset, args = [], exclude = [], ignore = []):
		self.name			= name
		self.buildConfig	= buildConfig
		self.generator		= generator
		self.binaryName		= binaryName
		self.testset		= testset
		self.args			= args
		self.exclude		= exclude
		self.ignore			= ignore

	def getBinaryPath(self, basePath):
		return os.path.join(self.buildConfig.getBuildDir(), self.generator.getBinaryPath(self.buildConfig.getBuildType(), basePath))

class NightlyBuildConfig(BuildConfig):
	def __init__(self, name, buildType, args):
		BuildConfig.__init__(self, os.path.join(BASE_BUILD_DIR, name), buildType, args)

class TestCaseResult:
	def __init__ (self, name, statusCode):
		self.name		= name
		self.statusCode	= statusCode

class MultiResult:
	def __init__ (self, name, statusCodes):
		self.name			= name
		self.statusCodes	= statusCodes

class BatchResult:
	def __init__ (self, name):
		self.name		= name
		self.results	= []

def parseResultCsv (data):
	lines	= data.splitlines()[1:]
	results	= []

	for line in lines:
		items = line.split(",")
		results.append(TestCaseResult(items[0], items[1]))

	return results

def readTestCaseResultsFromCSV (filename):
	return parseResultCsv(readFile(filename))

def readBatchResultFromCSV (filename, batchResultName = None):
	batchResult = BatchResult(batchResultName if batchResultName != None else os.path.basename(filename))
	batchResult.results = readTestCaseResultsFromCSV(filename)
	return batchResult

def getResultTimestamp ():
	return datetime.now().strftime("%Y-%m-%d-%H-%M")

def getCompareFilenames (logsDir):
	files = []
	for file in os.listdir(logsDir):
		fullPath = os.path.join(logsDir, file)
		if os.path.isfile(fullPath) and fnmatch.fnmatch(file, "*.csv"):
			files.append(fullPath)
	files.sort()

	return files[-COMPARE_NUM_RESULTS:]

def parseAsCSV (logPath, config):
	args = [config.getBinaryPath(LOG_TO_CSV_PATH), "--mode=all", "--format=csv", logPath]
	proc = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
	out, err = proc.communicate()
	return out

def computeUnifiedTestCaseList (batchResults):
	caseList	= []
	caseSet		= set()

	for batchResult in batchResults:
		for result in batchResult.results:
			if not result.name in caseSet:
				caseList.append(result.name)
				caseSet.add(result.name)

	return caseList

def computeUnifiedResults (batchResults):

	def genResultMap (batchResult):
		resMap = {}
		for result in batchResult.results:
			resMap[result.name] = result
		return resMap

	resultMap	= [genResultMap(r) for r in batchResults]
	caseList	= computeUnifiedTestCaseList(batchResults)
	results		= []

	for caseName in caseList:
		statusCodes = []

		for i in range(0, len(batchResults)):
			result		= resultMap[i][caseName] if caseName in resultMap[i] else None
			statusCode	= result.statusCode if result != None else 'Missing'
			statusCodes.append(statusCode)

		results.append(MultiResult(caseName, statusCodes))

	return results

def allStatusCodesEqual (result):
	firstCode = result.statusCodes[0]
	for i in range(1, len(result.statusCodes)):
		if result.statusCodes[i] != firstCode:
			return False
	return True

def computeDiffResults (unifiedResults):
	diff = []
	for result in unifiedResults:
		if not allStatusCodesEqual(result):
			diff.append(result)
	return diff

def genCompareReport (batchResults, title, ignoreCases):
	class TableRow:
		def __init__ (self, testCaseName, innerHTML):
			self.testCaseName = testCaseName
			self.innerHTML = innerHTML

	unifiedResults	= computeUnifiedResults(batchResults)
	diffResults		= computeDiffResults(unifiedResults)
	rows			= []

	# header
	headerCol = '<td class="Header">Test case</td>\n'
	for batchResult in batchResults:
		headerCol += '<td class="Header">%s</td>\n' % batchResult.name
	rows.append(TableRow(None, headerCol))

	# results
	for result in diffResults:
		col = '<td class="CasePath">%s</td>\n' % result.name
		for statusCode in result.statusCodes:
			col += '<td class="%s">%s</td>\n' % (statusCode, statusCode)

		rows.append(TableRow(result.name, col))

	tableStr = ""
	for row in rows:
		if row.testCaseName is not None and matchesAnyPattern(row.testCaseName, ignoreCases):
			tableStr += '<tr class="Ignored">\n%s</tr>\n' % row.innerHTML
		else:
			tableStr += '<tr>\n%s</tr>\n' % row.innerHTML

	html = COMPARE_REPORT_TMPL
	html = html.replace("${TITLE}", title)
	html = html.replace("${RESULTS}", tableStr)

	return html

def matchesAnyPattern (name, patterns):
	for pattern in patterns:
		if fnmatch.fnmatch(name, pattern):
			return True
	return False

def statusCodesMatch (refResult, resResult):
	return refResult == 'Missing' or resResult == 'Missing' or refResult == resResult

def compareBatchResults (referenceBatch, resultBatch, ignoreCases):
	unifiedResults	= computeUnifiedResults([referenceBatch, resultBatch])
	failedCases		= []

	for result in unifiedResults:
		if not matchesAnyPattern(result.name, ignoreCases):
			refResult		= result.statusCodes[0]
			resResult		= result.statusCodes[1]

			if not statusCodesMatch(refResult, resResult):
				failedCases.append(result)

	return failedCases

def getUnusedPort ():
	# \note Not 100%-proof method as other apps may grab this port before we launch execserver
	s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	s.bind(('localhost', 0))
	addr, port = s.getsockname()
	s.close()
	return port

def runNightly (config):
	build(config.buildConfig, config.generator)

	# Run parameters
	timestamp		= getResultTimestamp()
	logDir			= os.path.join(BASE_LOGS_DIR, config.name)
	testLogPath		= os.path.join(logDir, timestamp + ".qpa")
	infoLogPath		= os.path.join(logDir, timestamp + ".txt")
	csvLogPath		= os.path.join(logDir, timestamp + ".csv")
	compareLogPath	= os.path.join(BASE_REFS_DIR, config.name + ".csv")
	port			= getUnusedPort()

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

	if os.path.exists(testLogPath) or os.path.exists(infoLogPath):
		raise Exception("Result '%s' already exists", timestamp)

	# Paths, etc.
	binaryName		= config.generator.getBinaryPath(config.buildConfig.getBuildType(), os.path.basename(config.binaryName))
	workingDir		= os.path.join(config.buildConfig.getBuildDir(), os.path.dirname(config.binaryName))

	execArgs = [
		config.getBinaryPath(EXECUTOR_PATH),
		'--start-server=%s' % config.getBinaryPath(EXECSERVER_PATH),
		'--port=%d' % port,
		'--binaryname=%s' % binaryName,
		'--cmdline=%s' % string.join([shellquote(arg) for arg in config.args], " "),
		'--workdir=%s' % workingDir,
		'--caselistdir=%s' % CASELIST_PATH,
		'--testset=%s' % string.join(config.testset, ","),
		'--out=%s' % testLogPath,
		'--info=%s' % infoLogPath,
		'--summary=no'
	]

	if len(config.exclude) > 0:
		execArgs += ['--exclude=%s' % string.join(config.exclude, ",")]

	execute(execArgs)

	# Translate to CSV for comparison purposes
	lastResultCsv		= parseAsCSV(testLogPath, config)
	writeFile(csvLogPath, lastResultCsv)

	if os.path.exists(compareLogPath):
		refBatchResult = readBatchResultFromCSV(compareLogPath, "reference")
	else:
		refBatchResult = None

	# Generate comparison report
	compareFilenames	= getCompareFilenames(logDir)
	batchResults		= [readBatchResultFromCSV(filename) for filename in compareFilenames]

	if refBatchResult != None:
		batchResults = [refBatchResult] + batchResults

	writeFile(COMPARE_REPORT_NAME, genCompareReport(batchResults, config.name, config.ignore))
	print "Comparison report written to %s" % COMPARE_REPORT_NAME

	# Compare to reference
	if refBatchResult != None:
		curBatchResult		= BatchResult("current")
		curBatchResult.results = parseResultCsv(lastResultCsv)
		failedCases			= compareBatchResults(refBatchResult, curBatchResult, config.ignore)

		print ""
		for result in failedCases:
			print "MISMATCH: %s: expected %s, got %s" % (result.name, result.statusCodes[0], result.statusCodes[1])

		print ""
		print "%d / %d cases passed, run %s" % (len(curBatchResult.results)-len(failedCases), len(curBatchResult.results), "FAILED" if len(failedCases) > 0 else "passed")

		if len(failedCases) > 0:
			return False

	return True

# Configurations

DEFAULT_WIN32_GENERATOR				= ANY_VS_X32_GENERATOR
DEFAULT_WIN64_GENERATOR				= ANY_VS_X64_GENERATOR

WGL_X64_RELEASE_BUILD_CFG			= NightlyBuildConfig("wgl_x64_release", "Release", ['-DDEQP_TARGET=win32_wgl'])
ARM_GLES3_EMU_X32_RELEASE_BUILD_CFG	= NightlyBuildConfig("arm_gles3_emu_release", "Release", ['-DDEQP_TARGET=arm_gles3_emu'])

BASE_ARGS							= ['--deqp-visibility=hidden', '--deqp-watchdog=enable', '--deqp-crashhandler=enable']

CONFIGS = [
	NightlyRunConfig(
		name			= "wgl_x64_release_gles2",
		buildConfig		= WGL_X64_RELEASE_BUILD_CFG,
		generator		= DEFAULT_WIN64_GENERATOR,
		binaryName		= "modules/gles2/deqp-gles2",
		args			= ['--deqp-gl-config-name=rgba8888d24s8ms0'] + BASE_ARGS,
		testset			= ["dEQP-GLES2.info.*", "dEQP-GLES2.functional.*", "dEQP-GLES2.usecases.*"],
		exclude			= [
				"dEQP-GLES2.functional.shaders.loops.*while*unconditional_continue*",
				"dEQP-GLES2.functional.shaders.loops.*while*only_continue*",
				"dEQP-GLES2.functional.shaders.loops.*while*double_continue*",
			],
		ignore			= []
		),
	NightlyRunConfig(
		name			= "wgl_x64_release_gles3",
		buildConfig		= WGL_X64_RELEASE_BUILD_CFG,
		generator		= DEFAULT_WIN64_GENERATOR,
		binaryName		= "modules/gles3/deqp-gles3",
		args			= ['--deqp-gl-config-name=rgba8888d24s8ms0'] + BASE_ARGS,
		testset			= ["dEQP-GLES3.info.*", "dEQP-GLES3.functional.*", "dEQP-GLES3.usecases.*"],
		exclude			= [
				"dEQP-GLES3.functional.shaders.loops.*while*unconditional_continue*",
				"dEQP-GLES3.functional.shaders.loops.*while*only_continue*",
				"dEQP-GLES3.functional.shaders.loops.*while*double_continue*",
			],
		ignore			= [
				"dEQP-GLES3.functional.transform_feedback.*",
				"dEQP-GLES3.functional.occlusion_query.*",
				"dEQP-GLES3.functional.lifetime.*",
				"dEQP-GLES3.functional.fragment_ops.depth_stencil.stencil_ops",
			]
		),
	NightlyRunConfig(
		name			= "wgl_x64_release_gles31",
		buildConfig		= WGL_X64_RELEASE_BUILD_CFG,
		generator		= DEFAULT_WIN64_GENERATOR,
		binaryName		= "modules/gles31/deqp-gles31",
		args			= ['--deqp-gl-config-name=rgba8888d24s8ms0'] + BASE_ARGS,
		testset			= ["dEQP-GLES31.*"],
		exclude			= [],
		ignore			= [
				"dEQP-GLES31.functional.draw_indirect.negative.command_bad_alignment_3",
				"dEQP-GLES31.functional.draw_indirect.negative.command_offset_not_in_buffer",
				"dEQP-GLES31.functional.vertex_attribute_binding.negative.bind_vertex_buffer_negative_offset",
				"dEQP-GLES31.functional.ssbo.layout.single_basic_type.packed.mediump_uint",
				"dEQP-GLES31.functional.blend_equation_advanced.basic.*",
				"dEQP-GLES31.functional.blend_equation_advanced.srgb.*",
				"dEQP-GLES31.functional.blend_equation_advanced.barrier.*",
				"dEQP-GLES31.functional.uniform_location.*",
				"dEQP-GLES31.functional.debug.negative_coverage.log.state.get_framebuffer_attachment_parameteriv",
				"dEQP-GLES31.functional.debug.negative_coverage.log.state.get_renderbuffer_parameteriv",
				"dEQP-GLES31.functional.debug.error_filters.case_0",
				"dEQP-GLES31.functional.debug.error_filters.case_2",
			]
		),
	NightlyRunConfig(
		name			= "wgl_x64_release_gl3",
		buildConfig		= WGL_X64_RELEASE_BUILD_CFG,
		generator		= DEFAULT_WIN64_GENERATOR,
		binaryName		= "modules/gl3/deqp-gl3",
		args			= ['--deqp-gl-config-name=rgba8888d24s8ms0'] + BASE_ARGS,
		testset			= ["dEQP-GL3.info.*", "dEQP-GL3.functional.*"],
		exclude			= [
				"dEQP-GL3.functional.shaders.loops.*while*unconditional_continue*",
				"dEQP-GL3.functional.shaders.loops.*while*only_continue*",
				"dEQP-GL3.functional.shaders.loops.*while*double_continue*",
			],
		ignore			= [
				"dEQP-GL3.functional.transform_feedback.*"
			]
		),
	NightlyRunConfig(
		name			= "arm_gles3_emu_x32_egl",
		buildConfig		= ARM_GLES3_EMU_X32_RELEASE_BUILD_CFG,
		generator		= DEFAULT_WIN32_GENERATOR,
		binaryName		= "modules/egl/deqp-egl",
		args			= BASE_ARGS,
		testset			= ["dEQP-EGL.info.*", "dEQP-EGL.functional.*"],
		exclude			= [
				"dEQP-EGL.functional.sharing.gles2.multithread.*",
				"dEQP-EGL.functional.multithread.*",
			],
		ignore			= []
		),
	NightlyRunConfig(
		name			= "opencl_x64_release",
		buildConfig		= NightlyBuildConfig("opencl_x64_release", "Release", ['-DDEQP_TARGET=opencl_icd']),
		generator		= DEFAULT_WIN64_GENERATOR,
		binaryName		= "modules/opencl/deqp-opencl",
		args			= ['--deqp-cl-platform-id=2 --deqp-cl-device-ids=1'] + BASE_ARGS,
		testset			= ["dEQP-CL.*"],
		exclude			= ["dEQP-CL.performance.*", "dEQP-CL.robustness.*", "dEQP-CL.stress.memory.*"],
		ignore			= [
				"dEQP-CL.scheduler.random.*",
				"dEQP-CL.language.set_kernel_arg.random_structs.*",
				"dEQP-CL.language.builtin_function.work_item.invalid_get_global_offset",
				"dEQP-CL.language.call_function.arguments.random_structs.*",
				"dEQP-CL.language.call_kernel.random_structs.*",
				"dEQP-CL.language.inf_nan.nan.frexp.float",
				"dEQP-CL.language.inf_nan.nan.lgamma_r.float",
				"dEQP-CL.language.inf_nan.nan.modf.float",
				"dEQP-CL.language.inf_nan.nan.sqrt.float",
				"dEQP-CL.api.multithread.*",
				"dEQP-CL.api.callback.random.nested.*",
				"dEQP-CL.api.memory_migration.out_of_order_host.image2d.single_device_kernel_migrate_validate_abb",
				"dEQP-CL.api.memory_migration.out_of_order.image2d.single_device_kernel_migrate_kernel_validate_abbb",
				"dEQP-CL.image.addressing_filtering12.1d_array.*",
				"dEQP-CL.image.addressing_filtering12.2d_array.*"
			]
		)
]

if __name__ == "__main__":
	config = None

	if len(sys.argv) == 2:
		cfgName = sys.argv[1]
		for curCfg in CONFIGS:
			if curCfg.name == cfgName:
				config = curCfg
				break

	if config != None:
		isOk = runNightly(config)
		if not isOk:
			sys.exit(-1)
	else:
		print "%s: [config]" % sys.argv[0]
		print ""
		print "  Available configs:"
		for config in CONFIGS:
			print "    %s" % config.name
		sys.exit(-1)