#!/usr/bin/env python2.7
# @date   22 September, 2014
# @author cfuguet <cesar.fuguet-tortolero@lip6.fr>

import os
import shutil
import subprocess
import arch
import faultyprocs
import argparse
import multiprocessing
import math

def run(args):
    """ Execute the distributed bootloader providing permanent fault-recovery on
    the TSAR platform.

    Keyword arguments:
    path         -- platform's base directory path
    outpath      -- output's base directory path
    x            -- number of clusters on the X coordinate
    y            -- number of clusters on the Y coordinate
    nprocs       -- number of processors per cluster
    compileonly  -- stops after platform's compilation
    batchmode    -- TTY and FB are only redirected to FILES
    faultyrouter -- a list containing faulty routers' coordinates (t, x, y)
    faultymask   -- a mask of disabled routers' interfaces
    faultycore   -- a list containing faulty cores' coordinates (x, y, l)
    debug        -- a list with debug's start cycle, PID and MID
    threads      -- number of OpenMP threads
    firmdebug    -- activate the DEBUG compilation mode on software
    diskimage    -- relative or absolute path to the disk image
    force        -- create configuration files (or overwrite them)
    """

    # translate the relative path (if needed) into an absolute path
    basedir = os.path.abspath(args.path)
    outdir = os.path.abspath(args.outpath)
    print "[ run.py ] platform base directory: {0}".format(basedir)
    print "[ run.py ] output directory: {0}".format(outdir)

    # 1. generate configuration and ouput directories
    try:
        os.makedirs(os.path.join(outdir, "config"), 0755)
    except OSError:
        pass # directory already exists => do nothing

    # 2. generate hard_config.h and fault_config.h files
    faultpath = os.path.join(outdir, "config/fault_config.h")
    hardpath = os.path.join(outdir, "config/hard_config.h")
    xmlpath = os.path.join(outdir, "config/giet.map.xml")
    if args.linux:
        dtspath = os.path.join(outdir, "config/platform.dts")
    else:
        dtspath = None

    exist = True
    exist = exist and os.path.exists(faultpath)
    exist = exist and os.path.exists(hardpath)
    exist = exist and os.path.exists(xmlpath)
    if args.linux: exist = exist and os.path.exists(dtspath)

    if not args.force and exist:
        print "[ run.py ] Warning: Reusing existing configuration files. "
        print "[ run.py ] Script arguments will be ignored (no --force option)"
        cmd = raw_input('[ run.py ] Would you like to continue (y/n): ')
        if cmd == 'n': exit(0)
        if cmd == 'y': pass
        else: exit(1)
    else:
        arch.main(args.x, args.y, args.nprocs, hardpath, xmlpath, dtspath)
        faultyprocs.generate(args.faultycore, faultpath)

    # create a log file
    logfile = open(os.path.join(outdir, "log"), "w")

    # 3. compile simulator executable
    dst = os.path.join(basedir, "hard_config.h")
    if os.path.lexists(dst):
        os.unlink(dst)

    os.symlink(hardpath, dst)

    print "[ run.py ] compiling simulator"
    command = []
    command.extend(['make'])
    command.extend(['-C', basedir])
    subprocess.check_call(command, stdout=logfile, stderr=logfile)

    # 4. compile distributed boot executable
    dst = os.path.join(outdir, "config/boot_config.h")
    if os.path.lexists(dst):
        os.unlink(dst)

    os.symlink(os.path.join(basedir, "soft/test/config/boot_config.h"), dst)

    print "[ run.py ] compiling distributed boot procedure"
    command = []
    command.extend(['make'])
    command.extend(['-C', os.path.join(basedir, "soft")])
    command.extend(["CONFIG=" + outdir])
    command.extend(["DEBUG=" + str(args.firmdebug)])
    if args.linux:
        command.extend(["PATCHER_OS=1"])
    else:
        command.extend(["PATCHER_OS=0"])

    subprocess.check_call(command, stdout=logfile, stderr=logfile)

    # stop after compiling when the compile-only option is activated
    if args.compileonly == True: exit(0)

    # 5. execute simulator
    os.environ["DISTRIBUTED_BOOT"] = "1"
    os.environ["SOCLIB_FB"] = "HEADLESS"
    if args.batchmode:
        os.environ["SOCLIB_TTY"] = "FILES"

    print "[ run.py ] starting simulation"
    command = []
    command.extend([os.path.join(basedir, "simul.x")])
    command.extend(["-DSOFT", "soft/build/boot.elf"])
    command.extend(["-SOFT", "soft/build/loader.elf"])
    command.extend(["-DISK", args.diskimage])

    if args.faultyrouter != None:
        command.extend(["-FAULTY_MASK", str(args.faultymask)])
        for f in args.faultyrouter:
            command.extend(["-FAULTY_ROUTER", str(f[0]), str(f[1]), str(f[2])])

    if args.debug != None:
        command.extend(["-DEBUG", str(args.debug[0])]);
        command.extend(["-PROCID", str(args.debug[1])]);
        command.extend(["-MEMCID", str(args.debug[2])]);
        command.extend(["-IOB", "1"]);

    if args.ncycles > 0:
        command.extend(["-NCYCLES", str(args.ncycles)])

    elif os.environ.get('SOCLIB_GDB') == None:
        # the procedure grows linearly with the diameter of the mesh.
        # maxcycles = 1500000 + (args.x + args.y) * 20000
        # command.extend(["-NCYCLES", str(maxcycles)])
        command.extend(["-NCYCLES", str(200000000)])

    # OpenMP number of threads definition
    ompthreads = args.threads
    if ompthreads < 1:
        ompthreads = math.ceil(float(args.x * args.y) / 4)

        # be nice and don't use all available processors
        maxcpu = math.ceil(float(multiprocessing.cpu_count()) * 2/3)
        if ompthreads > maxcpu: ompthreads = maxcpu

    command.extend(["-THREADS", str(ompthreads)])

    logfile.write("Execute: {0}\n".format(" ".join(command)))
    logfile.flush()

    subprocess.check_call(command, stdout=logfile, stderr=logfile)

    logfile.close()

    # 6. move simulation terminal output into the target config dir
    if os.path.lexists("term0"):
        shutil.copy2("term0", os.path.join(outdir, "term"))

# get command-line arguments
if __name__ == "__main__":
    parser = argparse.ArgumentParser(description='Run simulation')

    parser.add_argument(
        '--path', '-p', type=str, dest='path', default=os.getcwd(),
        help='relative or absolute path to the platform')

    parser.add_argument(
        '--output', '-o', type=str, dest='outpath', default='./output',
        help='relative or absolute path to the output directory')

    parser.add_argument(
        '--xsize', '-x', type=int, dest='x', default=2,
        help='# of clusters in a row')

    parser.add_argument(
        '--ysize', '-y', type=int, dest='y', default=2,
        help='# of clusters in a column')

    parser.add_argument(
        '--nprocs', '-n', type=int, dest='nprocs', default=4,
        help='# of processors per cluster')

    parser.add_argument(
        '--compile-only', '-c', dest='compileonly', action='store_true',
        help='generate config files and compile the platform. Do not simulate')

    parser.add_argument(
        '--batch-mode', '-b', dest='batchmode', action='store_true',
        help='run simulation in batch mode: no interactive TTY or FrameBuffer')

    parser.add_argument(
        '--faulty-router', '-fr', dest='faultyrouter', action='append', nargs=3,
        help='ID (T,X,Y) of faulty router. \
        The T is 0:CMD, 1:RSP, 2:M2P, 3:P2M, 4:CLACK. \
        The three arguments are space-separated. \
        (e.g. -fr 2 1 1, router M2P (1,1) is deactivated')

    parser.add_argument(
        '--faulty-mask', '-m', dest='faultymask', default=0x1F,
        help='Disable mask for faulty router interfaces')

    parser.add_argument(
        '--faulty-core', '-fc', dest='faultycore', action='append', nargs=3,
        help='ID (X,Y,L) of faulty processor. \
        The three arguments are space-separated. \
        (e.g. -fc 0 1 3, core (0,1,3) is deactivated')

    parser.add_argument(
        '--debug', '-g', dest='debug', nargs=3,
        help='needs four arguments: from, procid, memcid')

    parser.add_argument(
        '--threads', '-t', type=int, dest='threads', default=1,
        help='number of OpenMP threads')

    parser.add_argument(
        '--firmware-debug', '-fg', dest='firmdebug', default=0,
        help='activate the DEBUG compilation mode on software')

    parser.add_argument(
        '--disk-image', '-di', dest='diskimage', default="/dev/null",
        help='relative or absolute path to the disk image used by the IOC')

    parser.add_argument(
        '--force', '-f', dest='force', action='store_true',
        help='create configuration files (or overwrite them)')

    parser.add_argument(
        '--linux', dest='linux', action='store_true',
        help='generate linux device tree and compile bootloader ' +
             'with linux specifics')

    parser.add_argument(
        '--ncycles', type=int, dest='ncycles', default=-1,
        help='max simulation cycles')

    run(parser.parse_args())

# vim: tabstop=4 : softtabstop=4 : shiftwidth=4 : expandtab
