#!/usr/bin/env python3
""" Script to parse board-specific timer_config.cpp and print the output groups
and timer config params to stdout
"""

import argparse
import os
import sys
import re
from itertools import groupby
from copy import deepcopy


def find_matching_brackets(brackets, s, verbose):
    idx = 0
    opening = 0
    first_open = -1
    while idx < len(s):
        if s[idx] == brackets[0]:
            opening += 1
            if first_open == -1:
                first_open = idx
        if s[idx] == brackets[1]:
            opening -= 1
            if opening == 0:
                if verbose: print(first_open, idx, s[first_open:idx+1])
                return first_open+1, idx
        idx += 1
    raise Exception('Failed to find opening/closing brackets in {:}'.format(s))

def extract_timer(line):
    # Try format: initIOTimer(Timer::Timer5, DMA{DMA::Index1, DMA::Stream0, DMA::Channel6}),
    search = re.search('Timer::([0-9a-zA-Z_]+)[,\)]', line, re.IGNORECASE)
    if search:
        return search.group(1), 'generic'

    # nxp rt1062 format: initIOPWM(PWM::FlexPWM2),
    search = re.search('PWM::Flex([0-9a-zA-Z_]+)[,\)]', line, re.IGNORECASE)
    if search:
        return search.group(1), 'imxrt'

    return None, 'unknown'

def extract_timer_from_channel(line, num_channels_already_found):
    # Try format: initIOTimerChannel(io_timers, {Timer::Timer5, Timer::Channel1}, {GPIO::PortA, GPIO::Pin0}),
    search = re.search('Timer::([0-9a-zA-Z_]+), ', line, re.IGNORECASE)
    if search:
        return search.group(1)

    # nxp rt1062 format: initIOTimerChannel(io_timers, {PWM::PWM2_PWM_A, PWM::Submodule0}, IOMUX::Pad::GPIO_B0_06),
    search = re.search('PWM::(PWM[0-9]+)[_,\)]', line, re.IGNORECASE)
    if search:
        # imxrt uses a 1:1 timer group to channel association
        return str(num_channels_already_found)

    return None

def get_timer_groups(timer_config_file, verbose=False):
    with open(timer_config_file, 'r') as f:
        timer_config = f.read()

    # timers
    dshot_support = {} # key: timer
    timers_start_marker = 'io_timers_t io_timers'
    timers_start = timer_config.find(timers_start_marker)
    if timers_start == -1:
        raise Exception('"{:}" not found in {:}'.format(timers_start_marker, timer_config_file))
    timer_config = timer_config[timers_start:]
    open_idx, close_idx = find_matching_brackets(('{', '}'), timer_config, verbose)
    timers_str = timer_config[open_idx:close_idx]
    timers = []
    for line in timers_str.splitlines():
        line = line.strip()
        if len(line) == 0 or line.startswith('//'):
            continue
        timer, timer_type = extract_timer(line)

        if timer_type == 'imxrt':
            if verbose: print('imxrt timer found')
            max_num_channels = 16 # Just add a fixed number of timers
            timers = [str(i) for i in range(max_num_channels)]
            dshot_support = {str(i): False for i in range(max_num_channels)}
            break

        if timer:
            if verbose: print('found timer def: {:}'.format(timer))
            dshot_support[timer] = 'DMA' in line
            timers.append(timer)
        else:
            # Make sure we don't miss anything (e.g. for different syntax) or misparse (e.g. multi-line comments)
            raise Exception('Unparsed timer in line: {:}'.format(line))


    # channels
    channels_start_marker = 'timer_io_channels_t timer_io_channels'
    channels_start = timer_config.find(channels_start_marker)
    if channels_start == -1:
        raise Exception('"{:}" not found in {:}'.format(channels_start_marker, timer_config_file))

    timer_config = timer_config[channels_start:]
    open_idx, close_idx = find_matching_brackets(('{', '}'), timer_config, verbose)
    channels = timer_config[open_idx:close_idx]
    channel_timers = []
    channel_types = []

    for line in channels.splitlines():
        line = line.strip()
        if len(line) == 0 or line.startswith('//'):
            continue

        if verbose: print('--'+line+'--')
        timer = extract_timer_from_channel(line, len(channel_timers))

        if timer:
            if verbose: print('Found timer: {:} in channel line {:}'.format(timer, line))
            channel_types.append('cap' if 'capture' in line.lower() else 'pwm')
            channel_timers.append(timer)
        else:
            # Make sure we don't miss anything (e.g. for different syntax) or misparse (e.g. multi-line comments)
            raise Exception('Unparsed channel in line: {:}'.format(line))

    if len(channel_timers) == 0:
        raise Exception('No channels found in "{:}"'.format(channels))

    groups = [(timers.index(k), len(list(g)), dshot_support[k]) for k, g in groupby(channel_timers)]
    outputs = {
        'types': channel_types,
        'groups': groups
        }

    return outputs

def get_output_groups(timer_groups, param_prefix="PWM_MAIN",
        channel_labels=["PWM Main", "PWM Capture"],
        standard_params=[],
        extra_function_groups=[], pwm_timer_param=None,
        verbose=False):
    """ convert the timer groups into an output_groups section of module.yaml
    and extra timer params
    """

    instance_start = 1
    output_groups = []
    timer_params = {}
    instance_start_label = [ 1, 1 ]
    for timer_index, group_count, dshot_support in timer_groups['groups']:

        # check for capture vs normal pins for the label
        types = timer_groups['types'][instance_start-1:instance_start+group_count-1]
        if not all(types[0] == t for t in types):
            # Should this ever be needed, we can extend this script to handle that
            raise Exception('Implementation requires all channel types for a timer to be equal (types: {:})'.format(types))
        if types[0] == 'pwm':
            channel_type_idx = 0
        elif types[0] == 'cap':
            channel_type_idx = 1
        else:
            raise Exception('unsupported channel type: {:}'.format(types[0]))

        channel_label = channel_labels[channel_type_idx]
        channel_type_instance = instance_start_label[channel_type_idx]
        group_label = channel_label + ' ' + str(channel_type_instance)
        if group_count > 1:
            group_label += '-' + str(channel_type_instance+group_count-1)
        group = {
            'param_prefix': param_prefix,
            'channel_label': channel_label,
            'instance_start': instance_start,
            'instance_start_label': channel_type_instance,
            'extra_function_groups': deepcopy(extra_function_groups),
            'num_channels': group_count,
            'standard_params': deepcopy(standard_params),
            'group_label': group_label,
            'channel_label_module_name_prefix': False,
        }

        if pwm_timer_param is not None:
            pwm_timer_param_cp = deepcopy(pwm_timer_param)
            timer_param_name = param_prefix+'_TIM'+str(timer_index)

            group['config_parameters'] = [
                    {
                        'param': timer_param_name,
                        'function': 'primary',
                    }
                ]

            if dshot_support:
                # don't show pwm limit params when dshot enabled

                for standard_param in group['standard_params']:
                    group['standard_params'][standard_param]['show_if'] = timer_param_name + '>=-1'

                # indicate support for changing motor spin direction
                group['supported_actions'] = {
                        'set_spin_direction1': {
                            'supported_if': timer_param_name + '<-1',
                            'actuator_types': ['motor']
                        },
                        'set_spin_direction2': {
                            'supported_if': timer_param_name + '<-1',
                            'actuator_types': ['motor']
                        },
                    }
            else:
                # remove dshot entries if no dshot support
                values = pwm_timer_param_cp['values']
                for key in list(values.keys()):
                    if 'dshot' in values[key].lower():
                        del values[key]

            for descr_type in ['short', 'long']:
                descr = pwm_timer_param_cp['description'][descr_type]
                pwm_timer_param_cp['description'][descr_type] = \
                    descr.replace('${label}', group_label)
            timer_params[timer_param_name] = pwm_timer_param_cp
        output_groups.append(group)
        instance_start += group_count
        instance_start_label[channel_type_idx] += group_count
    return (output_groups, timer_params)

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Extract output groups from timer_config.cpp')

    parser.add_argument('--timer-config', type=str, action='store',
                        help='timer_config.cpp file', required=True)
    parser.add_argument('-v', '--verbose', dest='verbose', action='store_true',
                        help='Verbose Output')

    args = parser.parse_args()
    verbose = args.verbose
    timer_groups = get_timer_groups(args.timer_config, verbose)
    print('timer groups: {:}'.format(timer_groups))
    output_groups, timer_params = get_output_groups(timer_groups, verbose=verbose)
    print('output groups: {:}'.format(output_groups))
    print('timer params: {:}'.format(timer_params))
