#!/bin/env python3

import os
import sys
import traceback
from itertools import combinations
import random

SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
requirement_file = os.path.abspath(os.path.join(SCRIPT_DIR, "..", "requirements.txt"))
try:
    import json
    import argparse
    import shutil
    import time
    import datetime

except:
    print("Please install requried packages using: pip3 install -r %s" % (requirement_file))
    sys.exit(1)

from nsdk_utils import *
from nsdk_report import *
from nsdk_runner import nsdk_runner

FPGACIROOT = os.path.join(SCRIPT_DIR, "configs", "fpgaci")

def valid_cpuarch(cpuarch):
    cpuarch = cpuarch.lower()
    if not cpuarch.startswith("rv64") and not cpuarch.startswith("rv32"):
        print("CPU arch should start with rv64 or rv32")
        return False
    # remove rv64/rv32
    arch = re.sub(r'rv\d\d', "", cpuarch)
    rst = re.search(r'[^imafdcbpvg]', arch)
    if rst:
        print("%s not valid arch" %(cpuarch))
        return False
    return True

def gen_buildcfg(cpu, full_arch):
    buildcfg = dict()
    # if 'g'(imafd) exist
    rv_arch = re.sub(r'g', "imafd", full_arch)
    # remove rv64/rv32
    arch = re.sub(r'rv\d\d', "", rv_arch)
    arch_ext = re.sub(r'[imafdc]', "", arch)
    # core + f/p or fp
    core = cpu.replace('u', 'n') + re.sub(r'[imac%s]' %(arch_ext), "", arch)
    if "" == arch_ext:
        buildcfg = {full_arch:{"CORE": core}}
    else:
        buildcfg = {full_arch:{"CORE": core, "ARCH_EXT": arch_ext}}
    return buildcfg

def gencfg_from_arch(cfgloc, core, cpuarch, maxnum):
    DEFAULT_VALID_MAX = 0
    # single, just itself
    ARCH_VALID = 1
    ARCH_AT_LEAST_VALID = 2
    arch_ext_comb = []
    arch_full = []
    cpucfgdict = dict()

    cpuarch = cpuarch.lower()
    # remove rv64/rv32
    arch = re.sub(r'rv\d\d', "", cpuarch)

    cpu_series = re.search(r'\d+', core).group()
    arch_prefix = re.match(r'rv\d\d', cpuarch).group()

    arch_base = ["imac"]
    if "f" in arch:
        arch_base.append('imafc')
    # if "d" exists, "f" exists
    if "d" in arch:
        arch_base.append('imafdc')
    # b/p/v
    arch_ext = re.sub(r'[imafdc]', "", arch)
    for i in range(1, len(arch_ext) + 1):
        for val in combinations(arch_ext, i):
            arch_ext_comb.append(''.join(val))

    # empty string on purpose to generate ones without ARCH_EXT
    arch_ext_comb.append("")
    arch_ext_comb.sort(key = lambda val:len(val), reverse = False)

    if maxnum >= ARCH_AT_LEAST_VALID and len(arch_ext_comb) > 1:
        arch_base = [arch_base[-1]]
        print("ARCH_AT_LEAST_VALID base : %s" %(arch_base))
    if maxnum >= ARCH_AT_LEAST_VALID:
        arch_ext_rest_cnt = 0
        if len(arch_ext_comb) > 2:
            arch_ext_rest_cnt = random.randint(0, len(arch_ext_comb) - 2)
        rand_rest_ext = random.sample(arch_ext_comb[1:-1], arch_ext_rest_cnt)
        # empty string ensures at least one element
        arch_ext_comb = [arch_ext_comb[0],arch_ext_comb[-1]]
        print("ARCH_AT_LEAST_VALID at least ext : %s, random rest ext : %s" %(arch_ext_comb, rand_rest_ext))
        arch_ext_comb.extend(rand_rest_ext)

    arch_ext_comb = list(set(arch_ext_comb))
    arch_base.sort(key = lambda val:len(val), reverse = False)
    arch_ext_comb.sort(key = lambda val:len(val), reverse = False)
    for idx_base, base in enumerate(arch_base):
        if ARCH_VALID == maxnum and idx_base != len(arch_base) - 1:
            print("Not ARCH_VALID base type, skip %s" %(base))
            continue
        for idx_ext, ext in enumerate(arch_ext_comb):
            if ARCH_VALID == maxnum and idx_ext != len(arch_ext_comb) - 1:
                print("Not ARCH_VALID ext type, skip %s" %(ext))
                continue
            if "imac" == base and 'v' in ext:
                print("v ext should not be with base imac, skip")
                continue
            arch_full.append(arch_prefix+base+ext)

    cpucfgdict["build_config"] = {"CPU_SERIES": cpu_series}
    cpucfgdict["build_configs"] = dict()
    for arch in arch_full:
        buildcfg = gen_buildcfg(core, arch)
        cpucfgdict["build_configs"].update(buildcfg)
    print("CPU config generated from arch = %s, mode = %s, number = %d: %s" %(cpuarch, maxnum, len(cpucfgdict["build_configs"]), cpucfgdict))
    cpucfg_name = core + "_" + cpuarch + ".json"
    cpucfg = os.path.join(cfgloc, cpucfg_name)
    save_json(cpucfg, cpucfgdict)
    return cpucfg

# caseconfig is a dict
# {
#    "core": "n300",
#    "locations": {
#            "fpgaloc": "",
#            "ncycmloc": "",
#            "cfgloc": ""
#            },
#    "ncycm": "xxxx",
#    "bitstream": "fpga.bit",
#    "fpga_serial": "xxxx",
#    "ftdi_serial": "xxxx",
#    "boardtype": "ddr200t",
#    "ocdcfg": "SoC/evalsoc/Board/nuclei_fpga_eval/openocd_evalsoc.cfg",
#    "cpucfg": "n300.json"
# }

def gen_runner_configs(casedir, caseconfig, genloc):
    if os.path.isdir(casedir) == False:
        return False
    if "core" not in caseconfig:
        print("No core is specified, please check!")
        return False
    core = caseconfig["core"]
    ocdcfg = caseconfig.get("ocdcfg", "SoC/evalsoc/Board/nuclei_fpga_eval/openocd_evalsoc.cfg")
    defbldcfg = dict()
    try:
        pathkeys = ocdcfg.replace("\\","/").split("/")
        defbldcfg = {"SOC": pathkeys[1], "BOARD": pathkeys[3]}
    except:
        defbldcfg = {"SOC": "evalsoc"}
    # get build_config from caseconfig
    glbldcfg = caseconfig.get("build_config", defbldcfg)
    runcfg = os.path.join(casedir, "%s.json" % (core))
    appcfg = os.path.join(casedir, "app.json")
    cfgcfg = os.path.join(casedir, "config.json")
    _, cfgconfig = load_json(cfgcfg)
    if cfgconfig is None:
        cfgconfig = {"choice": "mini"}

    choice = cfgconfig.get("choice", "mini")
    if os.path.isdir(genloc) == False:
        os.makedirs(genloc)

    cpuarch = caseconfig.get("cpuarch", "")
    # if specified user own cpu config use it
    if caseconfig.get("cpucfg", None):
        runcfgdict = gen_runcfg(caseconfig.get("cpucfg"), runcfg, glbldcfg)
    elif valid_cpuarch(cpuarch):
        # maxnum: 0, means generate default reasonable arch sets, max sets
        #         1, means generate cpuarch-same set
        #        >1, means generate at least reasonable arch sets
        maxnum = int(caseconfig.get("archmaxnum", 1))
        cpucfgpath = gencfg_from_arch(genloc, core, cpuarch, maxnum)
        runcfgdict = gen_runcfg(cpucfgpath, runcfg, glbldcfg)
    else:
        # if not cpuarch or cpucfg file specified, then select cpu config via choice defined path
        runcfgdict = gen_coreruncfg(core, runcfg, choice, glbldcfg, casedir)

    caseconfig["gencfgtimestamp"] = str(datetime.datetime.now())
    # save casecfg.json/app.json/hw.json
    save_json(os.path.join(genloc, "casecfg.json"), caseconfig)
    save_json(os.path.join(genloc, "hw.json"), runcfgdict)
    shutil.copy(appcfg, os.path.join(genloc, "app.json"))

    locs = caseconfig.get("locations", dict())
    locs["cfgloc"] = "."
    fpga_serial = caseconfig.get("fpga_serial", INVAILD_SERNO)
    ftdi_serial = caseconfig.get("ftdi_serial", INVAILD_SERNO)
    cycm = caseconfig.get("ncycm", None)
    fpgabit = caseconfig.get("bitstream", "fpga.bit")
    if core in ["n200", "n300"]:
        boardtype = "ddr200t"
    elif core in ["n600", "ux600"]:
        boardtype = "ku060"
    else:
        boardtype = "vcu118"
    for btype in ["mcu200t", "ddr200t", "ku060", "vcu118"]:
        if btype in fpgabit:
            boardtype = btype
            break
    boardtype = caseconfig.get("boardtype", boardtype)
    appcfg = "app.json"
    hwcfg = "hw.json"

    # generate required runner yaml
    runyamldict = gen_runyaml(core, locs, fpga_serial, ftdi_serial, cycm, fpgabit, boardtype, ocdcfg, appcfg, hwcfg)
    save_yaml(os.path.join(genloc, "core.yaml"), runyamldict)

    return True


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description="Nuclei SDK CPU Runner")
    parser.add_argument('--cases', required=True, help="Case names, splitted by comma, such as barebench,clibbench")
    parser.add_argument('--casecfg', required=True, help="Case configuration for cycle model or fpga")
    parser.add_argument('--caseroot', default=FPGACIROOT, help="Case configuration root")
    parser.add_argument('--logdir', default='logs', help="logs directory, default logs")
    parser.add_argument('--sdk', help="Where SDK located in")
    parser.add_argument('--make_options', help="Extra make options passed to overwrite default build configuration passed via appcfg and hwcfg")
    parser.add_argument('--runon', default='qemu', help="Where to run these application")
    parser.add_argument('--timeout', help="Runner timeout for each application run, if not specified will use default one specified in json configuration")
    parser.add_argument('--verbose', action='store_true', help="If specified, will show detailed build/run messsage")
    parser.add_argument('--uniqueid', default="", help="Pass pipeline$CI_PIPELINE_ID, such as pipeline123456")
    args = parser.parse_args()

    if args.sdk is None:
        sdk_path = os.environ.get("NUCLEI_SDK_ROOT")
        if sdk_path is None or os.path.isdir(sdk_path) == False:
            args.sdk = os.path.abspath(os.path.join(os.path.dirname(os.path.realpath(__file__)), "../../../"))
        else:
            args.sdk = sdk_path
    if not(args.sdk and os.path.isdir(args.sdk) and os.path.isfile(os.path.join(args.sdk, "npk.yml"))):
        print("SDK location %s is invalid, please check!" % (args.sdk))
        sys.exit(1)

    STOPONFAIL = get_env_flag("STOPONFAIL", True)
    print("Using sdk path in %s" % (args.sdk))

    ret, caseconfig = load_json(args.casecfg)
    if ret != JSON_OK:
        print("Invalid case configuration file %s, please check!" % (args.casecfg))
        sys.exit(1)
    passed_cases = []
    failed_cases = []
    torun_cases = list(set(args.cases.split(",")))
    tot_cases = []
    start_time = time.time()
    ret = True
    try:
        print("Prepare to do cases %s on %s, stop on fail %s" % (torun_cases, args.runon, STOPONFAIL))
        index = 0
        totlen = len(torun_cases)
        for case in torun_cases:
            case = case.strip()
            index += 1
            print("Run baremetal cpu test case %s now, progress %d/%d!" % (case, index, totlen))
            if case == "":
                print("Case %s is invaild, ignore it!" % (case))
                continue
            casedir = os.path.join(args.caseroot, case)
            if os.path.isdir(casedir) == False:
                print("Can't find case %s, ignore it!" % (casedir))
                continue
            tot_cases.append(case)
            caselogdir = os.path.join(args.logdir, case)
            casecfgdir = os.path.join(caselogdir, "gencfgs")
            caseconfig["execase"] = case
            if gen_runner_configs(casedir, caseconfig, casecfgdir) == False:
                print("No correct case configurations found in %s" % (casedir))
                ret = False
                # if stop on fail, it will break current execution and exit
                if STOPONFAIL:
                    print("Stop early due to case %s failed" % (case))
                    failed_cases.append(case)
                    break
                else:
                    print("Continue execution due to not stop on failed case")
                    continue
            runneryaml = os.path.join(casecfgdir, "core.yaml")
            locations = dict()
            nsdk_ext = nsdk_runner(args.sdk, args.make_options, runneryaml, locations, args.verbose, args.timeout)
            casepassed = True
            for config in nsdk_ext.get_configs():
                print("Run case %s for configuration %s specified in yaml %s" % (case, config, runneryaml))
                if nsdk_ext.run_config(config, caselogdir, runon=args.runon, createsubdir=False) == False:
                    print("Configuration %s failed" % (config))
                    print("Case %s for configuration %s specified in yaml %s: FAILED" % (case, config, runneryaml))
                    failed_cases.append(case)
                    casepassed = False
                else:
                    print("Case %s for configuration %s specified in yaml %s: PASSED" % (case, config, runneryaml))
                    passed_cases.append(case)
                # only one configuration should be executed, so just break here
                break

            if casepassed == False:
                ret = False
                # if stop on fail, it will break current execution and exit
                if STOPONFAIL:
                    print("Stop early due to case %s failed" % (case))
                    break
                else:
                    print("Continue execution due to not stop on failed case")
    except Exception as exc:
        print("Unexpected Error: %s during execute case %s" % (exc, case))
        if case not in failed_cases:
            failed_cases.append(case)
        # https://stackoverflow.com/questions/3702675/how-to-catch-and-print-the-full-exception-traceback-without-halting-exiting-the
        traceback.print_exc()
        ret = False

    runtime = round(time.time() - start_time, 2)
    print("Cost about %s seconds to do this running, passed %s!" % (runtime, ret))
    print("All the required cases are %s" % (torun_cases))
    print("Case %s passed out of executed %s" % (passed_cases, tot_cases))
    print("These following cases failed: %s" % (failed_cases))
    # At least passed or failed cases are not zero
    if (len(passed_cases) > 0 or len(failed_cases) > 0) and os.path.isdir(args.logdir):
        runcpustatustxt = os.path.join(args.logdir, "runcpustatus.txt")
        print("Save run cpu report status into %s" % (runcpustatustxt))
        with open(runcpustatustxt, "w") as rf:
            rf.write("PASSED:%s\n" % (",".join(passed_cases)))
            rf.write("FAILED:%s\n" % (",".join(failed_cases)))

    sys.stdout.flush()
    # Exit with ret value
    if ret:
        sys.exit(0)
    else:
        sys.exit(1)

