#!/usr/bin/env python3

# Copyright (c) 2016-2022 Satlab A/S <satlab@satlab.com>
# All rights reserved. Do not distribute without permission.

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

import jinja2
import jsonschema

copyright_tpl = """\
/*
 * Copyright (c) 2017-{{ time.year }} Satlab A/S <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 }}>
{%- set prefix = props.prefix + '_PROP_' %}
{% for group in props.groups %}
{%- set grpdef = prefix + group.name.upper() %}
/* {{ group.name }}: {{ group.description }} */
static const struct prop_spec {{ props.name }}_{{ group.name }}_props[] = {
{%- for prop in group.props %}
{%- set propdef = grpdef + '_' + prop.name.upper().replace('.', '_') %}
\t/* {{ group.name }}.{{ prop.name }}: {{ prop.description }} */
{%- if prop.type == 'bool' %}
\tPROP_{{ prop.type.upper() }}(
\t\t"{{ prop.name }}",
\t\t{{ propdef }},
\t\t{{ translate_flags(prop) }},
\t\t{{ propdef }}_DEF_VAL
\t),
{%- elif prop.type == 'string' or prop.type == 'binary' %}
\tPROP_{{ prop.type.upper() }}(
\t\t"{{ prop.name }}",
\t\t{{ propdef }},
\t\t{{ translate_flags(prop) }},
\t\t{{ propdef }}_DEF_VAL,
\t\t{{ propdef }}_SIZE
\t),
{%- else %}
\tPROP_{{ prop.type.upper() }}(
\t\t"{{ prop.name }}",
\t\t{{ propdef }},
\t\t{{ translate_flags(prop) }},
\t\t{{ propdef }}_DEF_VAL,
\t\t{{ propdef }}_MIN_VAL,
\t\t{{ propdef }}_MAX_VAL,
\t\t{{ propdef }}_FMT,
\t\t{{ propdef }}_UNIT
\t),
{%- endif %}
{%- 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>
{% set incguard = '__SL_PROPS_' + props['name'].upper().replace(' ', '_') + '_H__' %}
#ifndef {{ incguard }}
#define {{ incguard }}
{%- set prefix = props.prefix + '_PROP_' %}
{% for group in props.groups %}
{%- set grpdef = prefix + group.name.upper() %}
{%- set grpiddef = grpdef + '_GROUP_ID' %}
/* {{ group.name }}: {{ group.description }} */
#define {{ grpiddef }} {{ group.id }}
#define {{ grpdef }}_GROUP PROP_MKGROUPID({{ grpiddef }})
{% for prop in group.props %}
{%- set propdef = grpdef + '_' + prop.name.upper().replace('.', '_') %}
{%- set propiddef = propdef + '_PROP_ID' %}
/* {{ group.name }}.{{ prop.name }}: {{ prop.description }} */
#define {{ propiddef }} {{ prop.id }}
#define {{ propdef }} PROP_MKID(PROP_TYPE_{{ prop.type.upper() }}, {{ grpiddef }}, {{ propiddef }})
{%- set vals = translate_values(prop) %}
#define {{ propdef }}_DEF_VAL {{ vals.def }}
{%- if 'min' in vals %}
#define {{ propdef }}_MIN_VAL {{ vals.min }}
{%- endif %}
{%- if 'max' in vals %}
#define {{ propdef }}_MAX_VAL {{ vals.max }}
{%- endif %}
{%- if 'size' in vals %}
#define {{ propdef }}_SIZE {{ vals.size }}
{%- endif %}
{%- if 'fmt' in vals %}
#define {{ propdef }}_FMT {{ vals.fmt }}
{%- endif %}
{%- if 'unit' in vals %}
#define {{ propdef }}_UNIT {{ vals.unit }}
{%- endif %}
{% 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',
        'secret': 'PROP_FLAG_SECRET'
    }
    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):
    out = {}
    if prop['type'] == 'bool':
        out['def'] = 'true' if prop.get('default', False) else 'false'
    elif prop['type'] == 'int8':
        out['def'] = prop.get('default', 0)
        out['min'] = prop.get('min', 'INT8_MIN')
        out['max'] = prop.get('max', 'INT8_MAX')
    elif prop['type'] == 'int16':
        out['def'] = prop.get('default', 0)
        out['min'] = prop.get('min', 'INT16_MIN')
        out['max'] = prop.get('max', 'INT16_MAX')
    elif prop['type'] == 'int32':
        out['def'] = prop.get('default', 0)
        out['min'] = prop.get('min', 'INT32_MIN')
        out['max'] = prop.get('max', 'INT32_MAX')
    elif prop['type'] == 'int64':
        out['def'] = prop.get('default', 0)
        out['min'] = prop.get('min', 'INT64_MIN')
        out['max'] = prop.get('max', 'INT64_MAX')
    elif prop['type'] == 'uint8':
        out['def'] = prop.get('default', 0)
        out['min'] = prop.get('min', '0')
        out['max'] = prop.get('max', 'UINT8_MAX')
    elif prop['type'] == 'uint16':
        out['def'] = prop.get('default', 0)
        out['min'] = prop.get('min', '0')
        out['max'] = prop.get('max', 'UINT16_MAX')
    elif prop['type'] == 'uint32':
        out['def'] = prop.get('default', 0)
        out['min'] = prop.get('min', '0')
        out['max'] = prop.get('max', 'UINT32_MAX')
    elif prop['type'] == 'uint64':
        out['def'] = prop.get('default', 0)
        out['min'] = prop.get('min', '0')
        out['max'] = prop.get('max', 'UINT64_MAX')
    elif prop['type'] == 'float':
        out['def'] = prop.get('default', 0.0)
        out['min'] = prop.get('min', '-FLT_MAX')
        out['max'] = prop.get('max', 'FLT_MAX')
    elif prop['type'] == 'double':
        out['def'] = prop.get('default', 0.0)
        out['min'] = prop.get('min', '-DBL_MAX')
        out['max'] = prop.get('max', 'DBL_MAX')
    elif prop['type'] == 'string':
        out['size'] = prop['size']
        if len(prop['default']) >= prop['size']:
            raise Exception('String property must fit in allocated size')
        out['def'] = '"' + prop['default'] + '"'
    elif prop['type'] == 'binary':
        out['size'] = prop['size']
        b = bytes.fromhex(prop['default'])
        if len(b) != prop['size']:
            raise Exception('Binary property defaults must match size')
        binary = ''.join(f'\\x{x:02x}' for x in b)
        out['def'] = '"' + binary + '"'
    else:
        raise TypeError(f'Unknown type {prop["type"]}')

    out['fmt'] = '"' + prop['format'] + '"' if 'format' in prop else 'NULL'
    out['unit'] = '"' + prop['unit'] + '"' if 'unit' in prop else 'NULL'

    return out


def validate_props(props):
    # Build default prefix
    if 'prefix' not in props:
        props['prefix'] = 'SL_' + props['name'].upper().replace(' ', '_')

    # Validate uniqueness
    group_ids = {}
    for group in props['groups']:
        gid = group['id']
        gname = group['name']

        # Validate uniqueness
        if gid in group_ids:
            raise Exception(f'ID {gid} for {gname} already used by {group_ids[gid]} group')
        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 uniqueness
            if pid in prop_ids:
                raise Exception(f'ID {pid} for {gname}.{pname} already used by {group_ids[gid]}.{prop_ids[pid]} property')
            prop_ids[pid] = pname


def validate_props_schema(props):
    schema = {
        'type': 'object',
        'properties': {
            'name': {
                'type': 'string',
                'pattern': '^[a-zA-Z][a-zA-Z0-9_]*$'
            },
            'description': {
                'type': 'string'
            },
            'brief': {
                'type': 'string'
            },
            'version': {
                'type': 'string',
                'pattern': '[0-9]\.[0-9]\.[0-9](-[$a-zA-Z0-9-_]+)?'
            },
            'prefix': {
                'type': 'string',
                'pattern': '^SL_[A-Z][A-Z0-9_]*$'
            },
            'groups': {
                'type': 'array',
                'items': {
                    'type': 'object',
                    'properties': {
                        'name': {
                            'type': 'string',
                            'pattern': '^[a-zA-Z]([_]?[a-zA-Z0-9]+)*$'
                        },
                        'description': {
                            'type': 'string'
                        },
                        'brief': {
                            'type': 'string'
                        },
                        'id': {
                            'type': 'integer',
                            'minimum': 1,
                            'maximum': 31
                        },
                        'props': {
                            'type': 'array',
                            'items': {
                                'type': 'object',
                                'properties': {
                                    'name': {
                                        'type': 'string',
                                        'pattern': '^[a-zA-Z]([_\.]?[a-zA-Z0-9]+)*$'
                                    },
                                    'description': {
                                        'type': 'string'
                                    },
                                    'brief': {
                                        'type': 'string'
                                    },
                                    'id': {
                                        'type': 'integer',
                                        'minimum': 1,
                                        'maximum': 127
                                    },
                                    'type': {
                                        'type': 'string',
                                        'enum': ['int8',
                                                 'int16',
                                                 'int32',
                                                 'int64',
                                                 'uint8',
                                                 'uint16',
                                                 'uint32',
                                                 'uint64',
                                                 'float',
                                                 'double',
                                                 'string',
                                                 'binary',
                                                 'bool']
                                    },
                                    'default': {
                                    },
                                    'min': {
                                    },
                                    'max': {
                                    },
                                    'size': {
                                        'type': 'integer',
                                        'minimum': 1,
                                    },
                                    'flags': {
                                        'type': 'string',
                                        'pattern': '^(readonly|nostore|secret)( (readonly|nostore|secret))*$'
                                    },
                                    'unit': {
                                        'type': 'string'
                                    },
                                    'format': {
                                        'type': 'string'
                                    }
                                },
                                'required': ['name', 'description', 'id', 'type'],
                                'additionalProperties': False
                            }
                        }
                    },
                    'required': ['name', 'description', 'id', 'props'],
                    'additionalProperties': False
                }
            }
        },
        'required': ['name', 'description', 'version'],
        'additionalProperties': False
    }

    jsonschema.validate(instance=props, schema=schema)


def mkprops(infile, header_out, source_out, verbose=False):
    # Load property JSON
    with open(infile, 'r') as fin:
        props = json.load(fin)

    # Validate properties schema
    validate_props_schema(props)

    # Validate properties values
    validate_props(props)

    time = datetime.datetime.now()

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

    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:
        header_dir = os.path.dirname(header_out)
        if header_dir and not os.path.exists(header_dir):
            os.mkdir(header_dir)
        with open(header_out, 'w') as fout:
            fout.write(out_header + '\n')

    if source_out is not None:
        source_dir = os.path.dirname(source_out)
        if source_dir and not os.path.exists(source_dir):
            os.mkdir(source_dir)
        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()
