#!/usr/bin/env python

# Copyright (c) 2016-2017 Satlab Aps <satlab@satlab.com>
# All rights reserved. Do not distribute without permission.

from __future__ import print_function, division

import datetime
import json
import os
import os.path
import argparse

import jinja2

copyright_tpl = """\
/*
 * Copyright (c) 2017-{{ time.year }} Satlab Aps <satlab@satlab.com>
 * All rights reserved. Do not distribute without permission.
 *
 * Properties for {{ props['description'] }} version {{ props['version'] }}
 *
 * Autogenerated from {{ infile }} on {{ time.strftime("%Y-%m-%d %H:%M:%S") }}
 * Please do not edit manually.
 */

"""

source_tpl = copyright_tpl + """\
#include <{{ prophdr }}>
{% for group in props['groups'] %}
/* {{ group.description }} */
static const struct prop_spec {{ props['name'] }}_{{ group['name'] }}_props[] = {
{%- for prop in group['props'] %}
\tPROP_{{ prop['type'].upper() }}("{{ prop['name'] }}", {{ props['prefix'] }}_PROP_{{ group['name'].upper() }}_{{ prop['name'].upper().replace('.', '_') }}, {{ translate_flags(prop) }}, {{ translate_values(prop) }}),
{%- endfor %}
};
{% endfor %}
/* Groups */
static const struct prop_group_spec {{ props['name'] }}_prop_groups[] = {
{%- for group in props['groups'] %}
\tPROP_GROUP("{{ group['name'] }}", {{ props['prefix'] }}_PROP_{{ group['name'].upper() }}_GROUP, {{ props['name'] }}_{{ group['name'] }}_props, "{{ group.description }}"),
{%- endfor %}
};

/* Subsystem */
const struct prop_subsys_spec {{ props['name'] }}_props =
\tPROP_SUBSYSTEM("{{ props['name'] }}", "{{ props['description'] }}", {{ props['name'] }}_prop_groups);
"""

header_tpl = copyright_tpl + """\
#include <prop-client/prop_spec.h>

#ifndef {{ incguard }}
#define {{ incguard }}
{% for group in props['groups'] %}
/* {{ group.description }} */
enum {
\t{{ props['prefix'] }}_PROP_{{ group['name'].upper() }}_GROUP = PROP_MKGROUPID({{ group['id'] }}),
{%- for prop in group['props'] %}
\t{{ props['prefix'] }}_PROP_{{ group['name'].upper() }}_{{ prop['name'].upper().replace('.', '_') }} = PROP_MKID(PROP_TYPE_{{ prop['type'].upper() }}, {{ group['id'] }}, {{ prop['id'] }}),
{%- endfor %}
};
{% endfor %}
/* Subsystem */
extern const struct prop_subsys_spec {{ props['name'] }}_props;

#endif /* {{ incguard }} */
"""


def translate_flags(prop):
    flag_lut = {'readonly': 'PROP_FLAG_READONLY', 'nostore': 'PROP_FLAG_NOSTORE'}
    flags = 'PROP_FLAG_NONE'
    if 'flags' in prop:
        flags = []
        for f in prop['flags'].split(' '):
            flags.append(flag_lut[f])
        flags = ' | '.join(flags)
    return flags


def translate_values(prop):
    fmt = '"' + prop['format'] + '"' if 'format' in prop else 'NULL'
    unit = '"' + prop['unit'] + '"' if 'unit' in prop else 'NULL'
    if prop['type'] == 'bool':
        out = 'true' if prop.get('default', False) else 'false',
    elif prop['type'] == 'int8':
        mi = prop.get('min', 'INT8_MIN')
        ma = prop.get('max', 'INT8_MAX')
        out = prop.get('default', 0), mi, ma, fmt, unit
    elif prop['type'] == 'int16':
        mi = prop.get('min', 'INT16_MIN')
        ma = prop.get('max', 'INT16_MAX')
        out = prop.get('default', 0), mi, ma, fmt, unit
    elif prop['type'] == 'int32':
        mi = prop.get('min', 'INT32_MIN')
        ma = prop.get('max', 'INT32_MAX')
        out = prop.get('default', 0), mi, ma, fmt, unit
    elif prop['type'] == 'int64':
        mi = prop.get('min', 'INT64_MIN')
        ma = prop.get('max', 'INT64_MAX')
        out = prop.get('default', 0), mi, ma, fmt, unit
    elif prop['type'] == 'uint8':
        mi = prop.get('min', '0')
        ma = prop.get('max', 'UINT8_MAX')
        out = prop.get('default', 0), mi, ma, fmt, unit
    elif prop['type'] == 'uint16':
        mi = prop.get('min', '0')
        ma = prop.get('max', 'UINT16_MAX')
        out = prop.get('default', 0), mi, ma, fmt, unit
    elif prop['type'] == 'uint32':
        mi = prop.get('min', '0')
        ma = prop.get('max', 'UINT32_MAX')
        out = prop.get('default', 0), mi, ma, fmt, unit
    elif prop['type'] == 'uint64':
        mi = prop.get('min', '0')
        ma = prop.get('max', 'UINT64_MAX')
        out = prop.get('default', 0), mi, ma, fmt, unit
    elif prop['type'] == 'float':
        mi = prop.get('min', '-FLT_MAX')
        ma = prop.get('max', 'FLT_MAX')
        out = prop.get('default', 0.0), mi, ma, fmt, unit
    elif prop['type'] == 'double':
        mi = prop.get('min', '-DBL_MAX')
        ma = prop.get('max', 'DBL_MAX')
        out = prop.get('default', 0.0), mi, ma, fmt, unit
    elif prop['type'] == 'string':
        size = prop['size']
        out = '"' + prop['default'] + '"', size
    elif prop['type'] == 'binary':
        size = prop['size']
        if len(prop['default']) != 2*size:
            raise Exception("Binary property defaults must match size")
        binary = ''.join(['\\x' + prop['default'][i:i+2] for i in range(0, len(prop['default']), 2)])
        out = '"' + binary + '"', size
    else:
        raise TypeError("Unknown type {}".format(prop['type']))

    return ', '.join([str(x) for x in out])


def validate_props(props):
    group_ids = {}
    for group in props['groups']:
        gid = group['id']
        gname = group['name']

        # Validate range
        if gid < 1 or gid > 31:
            raise Exception("Invalid group ID {} for {} group, must be between 1 and 31".format(gid, gname))

        # Validate uniqueness
        if gid in group_ids:
            raise Exception("ID {} for {} already used by {} group".format(gid, gname, group_ids[gid]))
        group_ids[gid] = gname

        # And the same for properties in each group
        prop_ids = {}
        for prop in group['props']:
            pid = prop['id']
            pname = prop['name']

            # Validate range
            if pid < 1 or pid > 127:
                raise Exception("Invalid prop ID {} for {}.{} property, must be between 1 and 127".format(pid, group_ids[gid], pname))

            # Validate uniqueness
            if pid in prop_ids:
                raise Exception("ID {} for {}.{} already used by {}.{} property".format(pid, gname, pname, group_ids[gid], prop_ids[pid]))
            prop_ids[pid] = pname


def mkprops(infile, header_out, source_out, verbose=False):

    # Load property JSON
    with open(infile, 'r') as fin:
        props = json.load(fin)

    # Validate properties
    validate_props(props)

    # Render include guard
    incguard = '__SL_PROPS_' + props['name'].upper().replace(' ', '_') + '_H__'

    time = datetime.datetime.now()

    out_header = jinja2.Template(header_tpl).render(
        time=time,
        infile=os.path.basename(infile),
        incguard=incguard,
        props=props)

    out_source = jinja2.Template(source_tpl).render(
        time=time,
        infile=os.path.basename(infile),
        prophdr='satlab/' + os.path.basename(header_out),
        props=props,
        translate_flags=translate_flags,
        translate_values=translate_values)

    if verbose:
        print('------ HEADER ------')
        print(out_header)
        print('------ SOURCE ------')
        print(out_source)

    if header_out is not None:
        if not os.path.exists(os.path.dirname(header_out)):
            os.mkdir(header_out)
        with open(header_out, 'w') as fout:
            fout.write(out_header + '\n')

    if source_out is not None:
        if not os.path.exists(os.path.dirname(source_out)):
            os.mkdir(source_out)
        with open(source_out, 'w') as fout:
            fout.write(out_source + '\n')


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument('input', help='Input file name')
    parser.add_argument('-i', '--header-out', help='Output header file', default=None)
    parser.add_argument('-s', '--source-out', help='Output source file', default=None)
    parser.add_argument('-v', '--verbose', help='Show verbose output', action='store_true')

    args = parser.parse_args()

    mkprops(args.input, args.header_out, args.source_out, args.verbose)


if __name__ == '__main__':
    main()
