#!/usr/bin/env python3
""" Script to generate params from module.yaml config file(s)
    Note: serial params are handled in Tools/serial/generate_config.py
"""

import argparse
import os
import sys
from copy import deepcopy

from output_groups_from_timer_config import get_timer_groups, get_output_groups

try:
    import yaml
except ImportError as e:
    print("Failed to import yaml: " + str(e))
    print("")
    print("You may need to install it using:")
    print("    pip3 install --user pyyaml")
    print("")
    sys.exit(1)

parser = argparse.ArgumentParser(description='Generate params from module.yaml file(s)')

parser.add_argument('--config-files', type=str, nargs='*', default=[],
                    help='YAML module config file(s)')
parser.add_argument('--params-file', type=str, action='store',
                    help='Parameter output file')
parser.add_argument('--timer-config', type=str, action='store',
                    help='board-specific timer_config.cpp file')
parser.add_argument('--ethernet', action='store_true',
                    help='Ethernet support')
parser.add_argument('--board', type=str, action='store',
                    help='board name, e.g. ')
parser.add_argument('--board-with-io', dest='board_with_io', action='store_true',
                    help='Indicate that the board as an IO for extra PWM',
                    default=False)
parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
                    help='Verbose Output')

args = parser.parse_args()

verbose = args.verbose
params_output_file = args.params_file
timer_config_file = args.timer_config
ethernet_supported = args.ethernet
board_with_io = args.board_with_io
board = args.board

root_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)),"../..")
output_functions_file = os.path.join(root_dir,"src/lib/mixer_module/output_functions.yaml")

def process_module_name(module_name):
    if module_name == '${PWM_MAIN_OR_AUX}':
        if board_with_io: return 'PWM AUX'
        return 'PWM MAIN'
    if '${' in module_name:
        raise Exception('unhandled variable in {:}'.format(module_name))
    return module_name

def process_param_prefix(param_prefix):
    if param_prefix == '${PWM_MAIN_OR_AUX}':
        if board_with_io: return 'PWM_AUX'
        return 'PWM_MAIN'
    if '${' in param_prefix:
        raise Exception('unhandled variable in {:}'.format(param_prefix))
    return param_prefix

def process_channel_label(module_name, channel_label, no_prefix):
    if channel_label == '${PWM_MAIN_OR_AUX_CAP}':
        return 'PWM Capture'
    if channel_label == '${PWM_MAIN_OR_AUX}':
        if board_with_io: return 'PWM Aux'
        return 'PWM Main'
    if '${' in channel_label:
        raise Exception('unhandled variable in {:}'.format(channel_label))
    if no_prefix: return channel_label
    return module_name + ' ' + channel_label


def parse_yaml_parameters_config(yaml_config, ethernet_supported):
    """ parse the parameters section from the yaml config file """
    if 'parameters' not in yaml_config:
        return ''
    parameters_section_list = yaml_config['parameters']
    ret = ''
    for parameters_section in parameters_section_list:
        if 'definitions' not in parameters_section:
            continue
        definitions = parameters_section['definitions']
        param_group = parameters_section.get('group', None)
        for param_name in definitions:
            # 'definitions' either contains the param definition directly (dict),
            # or a list of definitions with that name (multiple entries for a
            # multi-instance param with different instance_start)
            param_list = definitions[param_name]
            if not isinstance(param_list, list):
                param_list = [param_list]

            for param in param_list:
                if param.get('requires_ethernet', False) and not ethernet_supported:
                    continue
                num_instances = param.get('num_instances', 1)
                instance_start = param.get('instance_start', 0) # offset
                instance_start_label = param.get('instance_start_label', instance_start)

                # get the type and extract all tags
                tags = '@group {:}'.format(param_group)
                if param['type'] == 'enum':
                    param_type = 'INT32'
                    for key in param['values']:
                        tags += '\n * @value {:} {:}'.format(key, param['values'][key])
                elif param['type'] == 'bitmask':
                    param_type = 'INT32'
                    for key in param['bit']:
                        tags += '\n * @bit {:} {:}'.format(key, param['bit'][key])
                    max_val = max(key for key in param['bit'])
                    tags += '\n * @min 0'
                    tags += '\n * @max {:}'.format((1<<(max_val+1)) - 1)
                elif param['type'] == 'boolean':
                    param_type = 'INT32'
                    tags += '\n * @boolean'
                elif param['type'] == 'int32':
                    param_type = 'INT32'
                elif param['type'] == 'float':
                    param_type = 'FLOAT'
                else:
                    raise Exception("unknown param type {:}".format(param['type']))

                for tag in ['decimal', 'increment', 'category', 'volatile',
                            'min', 'max', 'unit', 'reboot_required']:
                    if tag in param:
                        tags += '\n * @{:} {:}'.format(tag, param[tag])

                for i in range(num_instances):
                    # default value
                    default_value = 0
                    if 'default' in param:
                        # default can be a list of num_instances or a single value
                        if type(param['default']) == list:
                            assert len(param['default']) == num_instances
                            default_value = param['default'][i]
                        else:
                            default_value = param['default']

                    if type(default_value) == bool:
                        default_value = int(default_value)

                    # output the existing C-style format
                    ret += '''
/**
 * {short_descr}
 *
 * {long_descr}
 *
 * {tags}
 */
PARAM_DEFINE_{param_type}({name}, {default_value});
'''.format(short_descr=param['description']['short'].replace("\n", "\n * "),
           long_descr=param['description'].get('long', "").replace("\n", "\n * "),
           tags=tags,
           param_type=param_type,
           name=param_name.replace('${i}', str(i+instance_start)),
           default_value=default_value,
          ).replace('${i}', str(i+instance_start_label))
    return ret


def get_actuator_output_params(yaml_config, output_functions,
    timer_config_file, verbose):
    """ parse the actuator_output section from the yaml config file
        :return: dict of param definitions
    """
    if not 'actuator_output' in yaml_config:
        return {}
    output_groups = yaml_config['actuator_output']['output_groups']
    module_name = process_module_name(yaml_config['module_name'])
    all_params = {}
    group_idx = 0

    all_param_prefixes = {}

    def add_local_param(param_name, param_def):
        nonlocal all_params
        # add as a list, as there can be multiple entries with the same param_name
        if not param_name in all_params:
            all_params[param_name] = []
        all_params[param_name].append(param_def)

    while group_idx < len(output_groups):
        group = output_groups[group_idx]
        group_idx += 1

        if verbose: print("processing group: {:}".format(group))

        # Check for generator and generate additional data.
        # We do this by extending the output_groups list and parse in a later iteration
        if 'generator' in group:
            if group['generator'] == 'pwm':
                param_prefix = process_param_prefix(group['param_prefix'])
                no_prefix = not group.get('channel_label_module_name_prefix', True)
                channel_labels = [process_channel_label(module_name, label, no_prefix)
                    for label in group['channel_labels']]
                standard_params = group.get('standard_params', [])
                extra_function_groups = group.get('extra_function_groups', [])
                pwm_timer_param = group.get('pwm_timer_param', None)
                if 'timer_config_file' in group:
                    timer_config_file = os.path.join(root_dir, group['timer_config_file'])
                if timer_config_file is None:
                    raise Exception('trying to generate pwm outputs, but --timer-config not set')
                timer_groups = get_timer_groups(timer_config_file, verbose)
                timer_output_groups, timer_params = get_output_groups(timer_groups,
                    param_prefix, channel_labels,
                    standard_params, extra_function_groups, pwm_timer_param,
                    verbose=verbose)
                all_params.update(timer_params)
                output_groups.extend(timer_output_groups)

            else:
                raise Exception('unknown generator {:}'.format(group['generator']))
            continue

        num_channels = group['num_channels']
        param_prefix = process_param_prefix(group['param_prefix'])
        no_prefix = not group.get('channel_label_module_name_prefix', True)
        channel_label = process_channel_label(module_name, group['channel_label'], no_prefix)
        standard_params = group.get('standard_params', {})
        instance_start = group.get('instance_start', 1)
        instance_start_label = group.get('instance_start_label', instance_start)
        if len(param_prefix) > 9: # 16 - len('_FAIL') - 2 (2 digits for index)
            raise Exception("param prefix {:} too long (max length=10)".format(param_prefix))
        # collect the functions
        function_groups = ['common']
        function_groups.extend(group.get('extra_function_groups', []))
        output_function_values = {}
        for function_group in function_groups:
            group = output_functions[function_group]
            for function_name in group:
                function_name_label = function_name.replace('_', ' ')
                if isinstance(group[function_name], int):
                    output_function_values[group[function_name]] = function_name_label
                elif not 'count' in group[function_name]:
                    output_function_values[group[function_name]['start']] = function_name_label
                else:
                    start = group[function_name]['start']
                    count = group[function_name]['count']
                    for i in range(count):
                        output_function_values[start+i] = function_name_label+' '+str(i+1)

        if param_prefix not in all_param_prefixes:
            all_param_prefixes[param_prefix] = []
        all_param_prefixes[param_prefix].append((instance_start,
            instance_start_label, num_channels, channel_label))

        # function param
        param = {
            'description': {
                'short': channel_label+' ${i} Output Function',
                'long':
'''Select what should be output on {:} ${{i}}.

The default failsafe value is set according to the selected function:
- 'Min' for ConstantMin
- 'Max' for ConstantMax
- 'Max' for Parachute
- ('Max'+'Min')/2 for Servos
- 'Disarmed' for the rest
'''.format(channel_label),
                },
            'type': 'enum',
            'instance_start': instance_start,
            'instance_start_label': instance_start_label,
            'num_instances': num_channels,
            'default': 0,
            'values': output_function_values
            }
        add_local_param(param_prefix+'_FUNC${i}', param)

        # handle standard_params
        disarmed_description = \
'''This is the output value that is set when not armed.

Note that non-motor outputs might already be active in prearm state if COM_PREARM_MODE is set.
'''
        minimum_description = \
'''Minimum output value (when not disarmed).
'''
        maximum_description = \
'''Maxmimum output value (when not disarmed).
'''
        failsafe_description = \
'''This is the output value that is set when in failsafe mode.

When set to -1 (default), the value depends on the function (see {:}).
'''.format(param_prefix+'_FUNC${i}')
        standard_params_array = [
            ( 'disarmed', 'Disarmed', 'DIS', disarmed_description ),
            ( 'min', 'Minimum', 'MIN', minimum_description ),
            ( 'max', 'Maximum', 'MAX', maximum_description ),
            ( 'failsafe', 'Failsafe', 'FAIL', failsafe_description ),
            ]
        for key, label, param_suffix, description in standard_params_array:
            if key in standard_params:

                # values must be in range of an uint16_t
                if standard_params[key]['min'] < 0:
                    raise Exception('minimum value for {:} expected >= 0 (got {:})'.format(key, standard_params[key]['min']))
                if standard_params[key]['max'] >= 1<<16:
                    raise Exception('maximum value for {:} expected <= {:} (got {:})'.format(key, 1<<16, standard_params[key]['max']))

                if key == 'failsafe':
                    standard_params[key]['default'] = -1
                    standard_params[key]['min'] = -1

                param = {
                    'description': {
                        'short': channel_label+' ${i} '+label+' Value',
                        'long': description
                        },
                    'type': 'int32',
                    'instance_start': instance_start,
                    'instance_start_label': instance_start_label,
                    'num_instances': num_channels,
                    'min': standard_params[key]['min'],
                    'max': standard_params[key]['max'],
                    'default': standard_params[key]['default'],
                    }
                add_local_param(param_prefix+'_'+param_suffix+'${i}', param)

    # add reverse range param
    for param_prefix in all_param_prefixes:
        groups = all_param_prefixes[param_prefix]
        # collect the bits
        channel_bits = {}
        for instance_start, instance_start_label, num_instances, label in groups:
            for instance in range(instance_start, instance_start+num_instances):
                instance_label = instance - instance_start + instance_start_label
                channel_bits[instance-1] = label + ' ' + str(instance_label)

        param = {
            'description': {
                'short': 'Reverse Output Range for '+module_name,
                'long':
'''Allows to reverse the output range for each channel.
Note: this is only useful for servos.
'''.format(channel_label),
                },
            'type': 'bitmask',
            'default': 0,
            'bit': channel_bits
            }
        add_local_param(param_prefix+'_REV', param)

    if verbose: print('adding actuator params: {:}'.format(all_params))
    return all_params

def load_yaml_file(file_name):
    with open(file_name, 'r') as stream:
        try:
            return yaml.safe_load(stream)
        except yaml.YAMLError as exc:
            print(exc)
            raise

output_functions_yaml = load_yaml_file(output_functions_file)
output_functions = output_functions_yaml['functions']

all_params = ""

for yaml_file in args.config_files:
    yaml_config = load_yaml_file(yaml_file)

    # convert 'output_groups' section into additional params
    try:
        actuator_output_params = get_actuator_output_params(yaml_config,
                output_functions, timer_config_file, verbose)
    except Exception as e:
        print('Exception while parsing {:}:'.format(yaml_file))
        raise e
    # now add them to the yaml config
    if not 'parameters' in yaml_config:
        yaml_config['parameters'] = []
    group_name = 'Actuator Outputs'
    yaml_config['parameters'].append({'group': group_name, 'definitions': actuator_output_params})

    all_params += parse_yaml_parameters_config(yaml_config, ethernet_supported)


if verbose: print("Generating {:}".format(params_output_file))
with open(params_output_file, 'w') as fid:
    fid.write(all_params)
