#!/usr/bin/env python2

import argparse
import os
import subprocess
import re
import json
import itertools
import yaml

tmpDir = 'tmp'
kscOutputFn = tmpDir + '/test-target-ksc-output'
NO_COLORS = False

parser = argparse.ArgumentParser()
parser.add_argument('--lang', nargs='+')
parser.add_argument('--ksy', nargs='+')
parser.add_argument('--hide-success', action='store_true', default=False)
args = parser.parse_args()

if not os.path.exists(tmpDir):
    os.makedirs(tmpDir, 0700)

def shellExec(cmd):
    try:
        return (subprocess.check_output(cmd, shell=True), 0)
    except subprocess.CalledProcessError as e:
        return (e.output, e.returncode)

colors = { "default": "\033[0m", "fail": "\033[91m", "success": "\033[92m", "warning": "\033[33m" }
if NO_COLORS:
    for c in colors:
        colors[c] = ""

def fail(str):
    print colors["fail"] + str + colors["default"]

def success(str):
    print colors["success"] + str + colors["default"]

def warning(str):
    print colors["warning"] + str + colors["default"]

includes = {}

def getImportFn(baseFile, relPath):
    if relPath[0] == '/':
        return relPath[1:] + '.ksy'
    else:
        return baseFile.rsplit('/', 1)[0] + '/' +  relPath + '.ksy'

def discoverIncludes(ksyFn):
    if ksyFn in includes:
        return includes[ksyFn]

    try:
        with open("../" + ksyFn) as f: ksy = yaml.safe_load(f.read())
    except Exception as e:
        fail('Error while resolving include for %s: %s' % (ksyFn, e,))
        return

    ksyIncludes = [getImportFn(ksyFn, x) for x in ksy.get('meta',{}).get('imports', [])]

    allIncludes = []
    for fn in ksyIncludes:
        allIncludes.extend([fn] + discoverIncludes(fn))
    includes[ksyFn] = allIncludes

    return allIncludes

dependentKsys = []
if args.ksy:
    print "Discovering includes..."

    for rootKsyFn in args.ksy:
        dependentKsys.extend(discoverIncludes(rootKsyFn))

print "Compiling .ksy files..."

oneLang = args.lang and len(args.lang) == 1
kscLang = args.lang[0] if oneLang else "all"
kscTargetDir = "target" + ("" if kscLang == "all" else "/" + kscLang)
kscKsy = " ".join("../" + x for x in args.ksy + dependentKsys) if args.ksy else "../**/*.ksy"
kscCmdLine = 'ksc -- -t {kscLang} --outdir {kscTargetDir} --import-path ../ --ksc-json-output {kscKsy}'.format(kscLang=kscLang, kscTargetDir=kscTargetDir, kscKsy=kscKsy)

print "  cmd: %s\n" % kscCmdLine
(kscOutput, _) = shellExec(kscCmdLine)
with open(kscOutputFn, 'w') as f: f.write(kscOutput)
#kscOutput = open(kscOutputFn).read()

reNewKsy = re.compile(r'^reading (.*?\.ksy)\.\.\.', re.IGNORECASE)
reNewLang = re.compile(r'^\.\.\. compiling it for (\w+)\.\.\. \.\.\. => (.*)$', re.IGNORECASE)
reOneLangNewKsy = re.compile(r'^compiling (.*?) for (\w+)\.\.\.$', re.IGNORECASE)
reOneLangSourceFn = re.compile(r'^\.\.\. => (.*)$', re.IGNORECASE)

kscOutObj = json.loads(kscOutput)

results = {}
for (ksyFn, ksyData) in kscOutObj.iteritems():
    try:
        ksyFn = ksyFn.replace("../", "")
        results[ksyFn] = currKsy = {}

        if "errors" in ksyData:
            errorMsg = ksyData["errors"][0]["message"]
            currKsy["error"] = { "kscError": errorMsg, "kscErrorOutput": errorMsg } 
        
        if "output" in ksyData:
            for (langName, langData) in ksyData["output"].iteritems():
                realLangData = langData.values()[0]
                currKsy[langName] = currLang = { "kscError": None, "kscErrorOutput": "" }
                if "files" in realLangData:
                    currLang["sourceFn"] = "target/" + langName + "/" + realLangData["files"][0]["fileName"]
                if "errors" in realLangData:
                    currLang["kscError"] = currLang["kscErrorOutput"] = realLangData["errors"][0]["message"]
    except Exception as e:
        print "Error parsing file " + str(ksyFn) + ": " + str(e)

if not args.ksy:
    print "Discovering includes..."

allResults = []
for ksyFn in results:
    if args.ksy and ksyFn not in args.ksy:
        continue

    discoverIncludes(ksyFn)

    for lang in results[ksyFn]:
        result = results[ksyFn][lang]
        result["ksyFn"] = ksyFn
        result["lang"] = lang
        allResults.append(result)

kscFail = []
kscSuccess = []
for res in allResults:
    res["kscSuccess"] = not res["kscError"]
    if res["kscSuccess"]:
        kscSuccess.append(res)
    else:
        kscFail.append(res)

def saveResults():
    kscOutputJson = json.dumps(results, indent=4, sort_keys=True)
    with open(kscOutputFn + '.json', 'w') as f: f.write(kscOutputJson)
    #print kscOutputJson

saveResults()

colors = { "default": "\033[0m", "fail": "\033[91m", "success": "\033[92m", "warning": "\033[33m" }
if NO_COLORS:
    for c in colors:
        colors[c] = ""

def fail(str):
    print colors["fail"] + str + colors["default"]

def success(str):
    print colors["success"] + str + colors["default"]

def warning(str):
    print colors["warning"] + str + colors["default"]

if kscFail:
    fail("ksc compilation errors:\n" +
        ''.join([" - {ksyFn} ({lang}): {kscError}\n".format(**res) for res in kscFail]))
else:
    success("Yay there were no ksc compilation errors!")

langData = {
    "csharp": {
        "compileCmd": "mcs {sourceFns} test/Main.cs ../../runtime/csharp/*.cs",
        "errorRegex": r"Compilation failed: (\d+ error\(s\))"
    },
    "python": {
        "compileCmd": "PYTHONPATH='../../runtime/python/' python {sourceFns}",
        "errorRegex": r"\n\s+(.*?)\n\s+\^+\n+(.*?)\n",
        "errorRegexFormat": "{1}: '{0}'"
    },
    "ruby": {
        "compileCmd": "ruby -I../../runtime/ruby/lib {sourceFns}"
    },
    "php": {
        "compileCmd": "php ../../runtime/php/lib/Kaitai/Struct/*.php {sourceFns}"
    },
    "perl": {
        "compileCmd": "perl ../../runtime/perl/Kaitai/*.pm {sourceFns}"
    },
    "java": {
        "compileCmd": "javac ../../runtime/java/src/main/java/io/kaitai/struct/*.java {sourceFns}",
        "errorRegex": r"(\d+ errors?)"
    },
    "javascript": {
        "compileCmd": "node {sourceFns}",
        "errorRegex": r"\n\s+(.*?)\n\s+\^+\n+(.*?)\n",
        "errorRegexFormat": "{1}: '{0}'"
    },
    "graphviz": {
        "compileCmd": "dot -Tsvg {sourceFns} > /dev/null",
    },
    "cpp_stl": {
        "compileCmd": "g++ -I../../runtime/cpp_stl/ ../../runtime/cpp_stl/kaitai/*.cpp test/main.cpp {sourceFns} -lz",
    },
}

def groupby(items, keyFunc):
    return itertools.groupby(sorted(items, key=keyFunc), keyFunc)

for (ksyFn, langs) in groupby(kscSuccess, lambda x: x["ksyFn"]):
    for res in langs:
        res['ksyIncludes'] = includes[ksyFn]
        
unknownLangs = {}

for (langName, langItems) in groupby(kscSuccess, lambda x: x["lang"]):
    lang = langData.get(langName)
    if not lang:
        unknownLangs[langName] = True
        continue

    if args.lang and langName not in args.lang:
        warning("Skipping language (--lang argument): " + langName)
        continue

    langItems = sorted(langItems, key=lambda x: x["ksyFn"])
    print "Compiling {0} sources...\n".format(langName)
    #print json.dumps(list(langItems), indent=4)

    errorRegex = re.compile(lang["errorRegex"]) if "errorRegex" in lang else None
    errorRegexFormat = lang["errorRegexFormat"] if "errorRegexFormat" in lang else "{0}"
    failCount = 0

    for res in langItems:
        sourceIncludes = [results.get(ksyInc, {}).get(langName,{}).get("sourceFn") for ksyInc in res["ksyIncludes"]]
        res["sourceIncludes"] = sourceIncludes = [x for x in sourceIncludes if x]
        res["sourceFns"] = ' '.join(sourceIncludes + [res["sourceFn"]])

        compileCmd = lang["compileCmd"].format(**res)

        try:
            compileOutput = subprocess.check_output(compileCmd + " 2>&1", shell=True)
            compileExitCode = 0
        except subprocess.CalledProcessError as e:
            compileOutput = e.output
            compileExitCode = e.returncode

        res["compileOutput"] = compileOutput
        res["compileExitCode"] = compileExitCode

        prefix = "{ksyFn} ({lang})".format(**res)
        if compileExitCode != 0:
            errorFn = "{tmpDir}/error_{lang}_{ksyName}.txt".format(tmpDir = tmpDir, lang = res["lang"], ksyName = res["ksyFn"].replace("/", "_"))
            open(errorFn, "w").write(compileOutput)

            errorSummary = "";
            if errorRegex:
                m = errorRegex.search(compileOutput)
                if m:
                    errorSummary = errorRegexFormat.format(*m.groups())

            fail(" - [FAIL] " + prefix + ": code=" + str(compileExitCode) + ", fn=" + errorFn + (" (" + errorSummary + ")" if errorSummary else ""))
            failCount = failCount + 1
        elif not args.hide_success:
            success(' - [SUCCESS] ' + prefix)
    print

    if failCount == 0:
        success('Yay, there were no %s compilation errors' % langName)
    else:
        fail('There were %d %s compilation errors' % (failCount, langName))

    print

print
saveResults()

if unknownLangs:
    fail("The following languages are unknown and were not tested: " + ', '.join(unknownLangs))
