#!/usr/bin/env python3
# Run this script from the root folder to generate algorithm and commandline parameters.
#
# The JSON parameter format:
# * name: name of the parameter used in code.
# * data_type: eg `double` or custom enum type.
# * default: value for the parameter when it's not supplied by the user.
# * enum: if given non-empty list, interpret `data_type` as `enum class`.
# * generate_enum: if `true`, construct a new enum using the `enum` list.
# * doc: documentation string.

import re
import os
import sys
import json
from collections import OrderedDict

warn_line = "/************************ WARNING ************************/\n"
warning = (warn_line*5) + """
// THIS FILE IS AUTOGENERATED. DO NOT EDIT DIRECTLY.
// Edit codegen/parameter_definitions files instead,

""" + (warn_line*5)

re_line_comment = re.compile('//.*')
re_empty = re.compile('^\s*$')

supported_cmdline_types = [
    'int',
    'unsigned',
    'float',
    'double',
    'bool',
    'std::string',
    'std::vector<int>',
    'std::vector<std::string>',
    'std::vector<double>'
]

def write_file_if_changed(data, filename):
    """
    Writes given data to file, but only if the data differs from the
    current contents of that file (if any). This allows the "last modified"
    time of the file to stay the same if the contents did not change, which
    is important for Make.
    """
    try:
        with open(filename, 'r') as old:
            old_contents = old.read()
    except IOError:
        print('%s not found' % filename)
        old_contents = ""

    if old_contents == data:
        print('%s not changed' % filename)
    else:
        print('writing %d byte(s) to %s' % (len(data), filename))
        with open(filename, 'w') as new:
            new.write(data)

def parse_definitions_json(definitions):
    return json.load(definitions, object_pairs_hook=OrderedDict)

def parse_definitions_c(definitions):
    parameter_groups = OrderedDict()
    for line in definitions:
        # Could also use C preprocessor to remove comments.
        line = re_line_comment.sub('', line)
        if re_empty.match(line):
            continue

        enum = []
        tokens = line.split()
        if tokens[0] == "enum":
            tokens.pop(0)
            enum = tokens[3:]
            assert(len(enum) > 0)
        elif len(tokens) != 3:
            print("Could not parse line: ", line)
            continue
        name_tokens = tokens[1].split('.')
        if len(name_tokens) != 2:
            print("Could not parse name: ", tokens[1])
            continue

        group = name_tokens[0]
        if not group in parameter_groups:
            parameter_groups[group] = []

        parameter_groups[group].append({
            "data_type": tokens[0],
            "name": name_tokens[1],
            "default": tokens[2],
            "enum": enum,
            # Always true because the format is too awkward to control it.
            "generate_enum": True,
            })
    return parameter_groups

def generate_code(parameter_groups, hpp, cpp):
    text_hpp_sub_struct = ""
    text_hpp_struct = ""
    text_hpp_top = ""
    text_cpp_sub_struct = ""
    text_cpp_struct = ""
    text_cpp_set_parameter = ""
    text_cpp_help = ""

    # Generate the code.
    group_indent = 4 * " ";
    first_group = True
    for group_name, group in parameter_groups.items():
        group_struct_name = "Parameters" + group_name.capitalize()

        # Open group code.
        text_hpp_struct += "struct %s {\n" % group_struct_name
        text_cpp_struct += "%s::%s() :\n" % (group_struct_name, group_struct_name)
        if not first_group:
            text_cpp_sub_struct += ",\n"
        text_cpp_sub_struct += "%s()" % group_name

        # Generate each parameter.
        for i, p in enumerate(group):
            name = p["name"]
            short = p.get("short", "")
            default = p.get("default", "")
            data_type = p["data_type"]
            # enum = p["enum"] if "enum" in p else []
            enum = p.get("enum", [])
            generate_enum = p.get("generate_enum", False)
            if "vector" in data_type:
                default = "{" + default + "}"
            doc = p.get("doc", "")
            if i > 0:
                text_cpp_struct += ",\n"
            if enum:
                text_cpp_struct += group_indent + "{name}({type}::{default})".format(
                    name=name, type=data_type, default=default)
            else:
                text_cpp_struct += group_indent + "{name}({default})".format(
                    name=name, default=default)
            text_hpp_struct += group_indent + "%s %s;\n" % (data_type, name)
            text_cpp_help += '{ "%s", "%s", "%s", "%s" },\n' % (name, short, default, doc)

            keys = [name]
            if "short" in p:
                keys.append(short)
            for key in keys:
                if data_type in supported_cmdline_types:
                    text_cpp_set_parameter += 'if (parser.hasKey("{key}")) {{\n'.format(key=key)
                    text_cpp_set_parameter += '    p.{group_name}.{name} = parser.get<{type}>("{key}");\n'.format(
                        group_name=group_name, name=name, key=key, type=data_type)
                    text_cpp_set_parameter += '}\n';
                elif enum:
                    text_cpp_set_parameter += 'if (parser.hasKey("{key}")) {{\n'.format(key=key)
                    # Automatically convert uppercase because of coding style for C++ enums.
                    text_cpp_set_parameter += '    std::string v = ::util::toUpper(parser.get<std::string>("{key}"));\n'.format(
                        group_name=group_name, key=key)
                    for i, value in enumerate(enum):
                        el = "else " if i > 0 else ""
                        text_cpp_set_parameter += '    {el}if (v == "{value}") p.{group_name}.{name} = {type}::{value};\n'.format(
                            el=el, value=value, group_name=group_name, name=name, type=data_type)
                    text_cpp_set_parameter += '    else throw std::invalid_argument("Unknown parameter {type}::" + v);\n'.format(
                            value=value, type=data_type)
                    text_cpp_set_parameter += '}\n'
                else:
                    print("Ignored parameter", key)

            if enum and generate_enum:
                text_hpp_top += "enum class {type} {{\n".format(type=data_type)
                for (i, e) in enumerate(enum):
                    if i > 0:
                        text_hpp_top += ",\n"
                    text_hpp_top += "    {}".format(e)

                text_hpp_top += "\n};\n"


        # Close group code.
        text_hpp_struct += group_indent + "%s();\n};\n" % group_struct_name
        text_hpp_sub_struct += "%s %s;\n" % (group_struct_name, group_name)
        text_cpp_struct += "\n{}\n\n"

        first_group = False

    hpp["subs"].append({ "marker": "// CODEGEN-HPP-SUB-STRUCT", "text": text_hpp_sub_struct })
    hpp["subs"].append({ "marker": "// CODEGEN-HPP-STRUCT", "text": text_hpp_struct })
    hpp["subs"].append({ "marker": "// CODEGEN-HPP-TOP", "text": text_hpp_top })
    cpp["subs"].append({ "marker": "// CODEGEN-CPP-SUB-STRUCT", "text": text_cpp_sub_struct })
    cpp["subs"].append({ "marker": "// CODEGEN-CPP-STRUCT", "text": text_cpp_struct })
    cpp["subs"].append({ "marker": "// CODEGEN-CPP-SET-PARAMETER", "text": text_cpp_set_parameter })
    cpp["subs"].append({ "marker": "// CODEGEN-CPP-HELP", "text": text_cpp_help })

    # Substitute the generated code into base files.
    for subs_file in [hpp, cpp]:
        with open(subs_file['base'], 'r') as base_file:
            data = base_file.read()
            for sub in subs_file["subs"]:
                # Indent the replacement text the same amount the marker is indented.
                m = re.search("([ ]*)" + sub["marker"], data)
                if not m:
                    continue
                indent = m.group(1)
                sub["text"] = sub["text"].replace('\n', '\n' + indent).strip()

                data = data.replace(sub["marker"], sub["text"])

            # prepend warning
            data = warning + data

            write_file_if_changed(data, subs_file['output'])

if __name__ == "__main__":
    try:
        os.mkdir('output')
        print('created the output directory')
    except:
        pass

    if len(sys.argv) == 2 and sys.argv[1] == "cmd":
        hpp = {
            "base": "cmd_parameters_base.hpp",
            "output": "output/cmd_parameters.hpp"
        }
        cpp = {
            "base": "cmd_parameters_base.cpp",
            "output": "output/cmd_parameters.cpp"
        }
        with open('cmd_parameter_definitions.json', 'r') as definitions:
            parameter_groups = parse_definitions_json(definitions)
    else:
        hpp = {
            "base": "parameters_base.hpp",
            "output": "output/parameters.hpp"
        }
        cpp = {
            "base": "parameters_base.cpp",
            "output": "output/parameters.cpp"
        }
        with open('parameter_definitions.c', 'r') as definitions:
            parameter_groups = parse_definitions_c(definitions)

    hpp["subs"] = []
    cpp["subs"] = []

    generate_code(parameter_groups, hpp, cpp)
