#!/usr/bin/env python3

import argparse
import os
import subprocess
from tempfile import NamedTemporaryFile


# Code tempaltes for insertion
POPPPY_HEADER = """
// ------------------------ This code was inserted by IPUpyc -------------------------- //
#include <poplar/Vertex.hpp>
#include "poplar/StackSizeDefs.hpp" 

// ------------------------ This code was inserted by IPUpyc -------------------------- //
"""
IPUpy_START_LINES = """
{indent}// ------------------------ This code was generated by IPUpyc -------------------------- //
{indent}char* poplar_stack_bottom;
{indent}asm volatile("mov %[poplar_stack_bottom], $m11" : [poplar_stack_bottom] "+r" (poplar_stack_bottom) ::);
{indent}IPUpy_set_stdout({set_stdout_args});
{indent}IPUpy_init(poplar_stack_bottom);
"""
IPUpy_VARIABLE_LINE = """{indent}IPUpy_add_memory_as_array("{pyname}", &{cppname}[0], {cppname}.size(), {dtype});"""
IPUpy_END_LINES = """
{indent}IPUpy_do_str("{payload}", 0);
{indent}IPUpy_deinit();
{indent}// ------------------------- This code was generated by IPUpyc ------------------------ //
"""


def parse_varname(varname):
    pyname, cppname = varname.split("=") if "=" in varname else (varname, varname)
    pyname, cppname = pyname.strip(), cppname.strip()
    if cppname.endswith("<float>"):
        cppname, dtype = cppname[:-7].strip(), ord("f")
    elif cppname.endswith("<int>"):
        cppname, dtype = cppname[:-5].strip(), ord("i")
    elif cppname.endswith("<char>"):
        cppname, dtype = cppname[:-6].strip(), ord("b")
    else:
        dtype = ord("i")
    return pyname, cppname, dtype


def preprocess_block(lines):
    header, python_lines = lines[0], lines[1:]

    # The position of the #pragma defines the indentation of the python block
    indent = header[:header.find('#pragma')]

    # Parse the variable name map, creating python arrays for each
    # Special handling for stdout, if present
    var_lines = []
    set_stdout_args = "NULL, 0"
    var_list = ' '.join(header.split()[2:])
    assert(var_list[0] == '[' and var_list[-1] == ']')
    for item in var_list[1:-1].split(','):
        pyname, cppname, dtype = parse_varname(item)
        if pyname == 'stdout':
            set_stdout_args = '&{cppname}[0], {cppname}.size()'.format(cppname=cppname)
            continue
        var_lines.append(IPUpy_VARIABLE_LINE.format(
            indent=indent, pyname=pyname, cppname=cppname, dtype=dtype))

    # Dedent python lines and fuse into a string for execution
    payload = ""
    for line in filter(lambda ln: ln.strip(), python_lines):  # (Non-empty lines only)
        assert(line.startswith(indent))
        line = line.replace('\\', '\\\\').replace('"', "\\\"")
        payload += line[len(indent):] + '\\n'

    out_lines = [
        IPUpy_START_LINES.format(indent=indent, set_stdout_args=set_stdout_args),
    ] + var_lines + [
        IPUpy_END_LINES.format(indent=indent, payload=payload),
    ]
    return out_lines


def preprocess(src):
    current_block = []
    output_lines = [POPPPY_HEADER]

    for line in src.splitlines():
        line_start = ' '.join(line.split()[:2])

        # Not already in a IPUpy block
        if not current_block:
            # Enter a new block
            if line_start == '#pragma IPUpy_start':
                current_block.append(line)
            elif line_start == '#pragma IPUpy_end':
                raise RuntimeError("Found python block end outside a python block")
            else:
                output_lines.append(line)
            continue

        # We're in a block, is this the end?
        if line_start == '#pragma IPUpy_end':
            output_lines.extend(preprocess_block(current_block))
            current_block = []
            continue

        # Stay in the block
        current_block.append(line)

    if current_block:
        raise RuntimeError("End of file found inside python block")

    return '\n'.join(output_lines)


def main(args, popc_args):
    src_file = open(args.infile, 'r')
    preproc_file = open(args.outfile, 'w') if args.preprocess_only else NamedTemporaryFile(
        'w', suffix='.cpp')
    compiled_file = open(args.outfile, 'w') if args.compile_only else NamedTemporaryFile(
        'w', suffix='.gp')

    with src_file, preproc_file, compiled_file:

        # Preprocess
        if args.external_preprocessor:
            result = subprocess.run(
                [args.external_preprocessor, src_file.name],
                stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="UTF-8"
            )
            if result.returncode:
                print('Preprocessing using', args.external_preprocessor, 
                    'failed with following stderr output:\n', result.stderr)
                return
            src = result.stdout
        else:
            src = preprocess(src_file.read())
        preproc_file.write(src)
        preproc_file.flush()
        if args.preprocess_only:
            return

        # Compile
        result = subprocess.run(
            ["popc", preproc_file.name, "-o", compiled_file.name] + popc_args,
            stderr=subprocess.PIPE, encoding="UTF-8"
        )
        print(["popc", preproc_file.name, "-o", compiled_file.name] + popc_args)
        if result.returncode:
            print('Compiling with popc failed with following stderr output:\n', result.stderr)
            return
        if args.compile_only:
            return

        # Link
        libname = os.path.join(os.path.dirname(__file__), 'build', 'libIPUpy.gp')
        result = subprocess.run(
            ["popc", compiled_file.name, libname, "-o", args.outfile] + popc_args,
            stderr=subprocess.PIPE, encoding="UTF-8"
        )
        print(["popc", compiled_file.name, libname, "-o", args.outfile] + popc_args)
        if result.returncode:
            print('Linking with popc failed with following stderr output:\n', result.stderr)



if __name__ == "__main__":

    parser = argparse.ArgumentParser(description='Run popc, with IPUpy preprocessing')
    parser.add_argument('--infile', '-i', required=True,
                        help='cpp file to compile')
    parser.add_argument('--outfile', '-o', required=True,
                        help='Compiled destination file')
    parser.add_argument('--external_preprocessor', default=None, 
                        help='External executable to do IPUpyc preprocessing')
    parser.add_argument('--preprocess_only', action='store_true',
                        help='Skip compilation and linking, output preprocessed src')
    parser.add_argument('--compile_only', action='store_true',
                        help='Skip linking, output compiled vertex object')
    parser.add_argument('--target', default='ipu21',
                        help="Default target flag to pass to popc")

    IPUpyc_args, popc_args = parser.parse_known_args()

    popc_args.append('--target={}'.format(IPUpyc_args.target))
    del IPUpyc_args.target

    main(IPUpyc_args, popc_args)
