#!/usr/bin/python
"""
@file wrtd-tool.py
@copyright: Copyright (c) 2019 CERN (home.cern)

SPDX-License-Identifier: LGPL-2.1-or-later
"""

import sys
import signal
import time
import argparse
import re

from PyWrtd import PyWrtd

def signal_handler(sig, frame):
    sys.exit(0)

def __tstamp_normalise(tstamp):
    ret = tstamp
    while (ret['ns'] >= 1000000000):
        ret['ns']      -= 1000000000
        ret['seconds'] += 1
    while (ret['ns'] < 0):
        ret['ns']      += 1000000000
        ret['seconds'] -= 1
    return ret

def __tstamp_to_ns(tstamp):
    return tstamp['seconds'] * 1e9 + tstamp['ns'] + tstamp['frac'] * 2**-32

def __tstamp_to_str(tstamp):
    if list(tstamp.values()) == [0, 0, 0]:
        return 'Never'
    else:
        ret = __tstamp_normalise(tstamp)
        return '{0}.{1:03d}.{2:03d}.{3:03d}+{4:03d}'.format(
            time.strftime('%F,%T', time.gmtime(ret['seconds'])),
            int(ret['ns'] / 1e6),
            int(ret['ns'] / 1e3) % 1000,
            int(ret['ns'] / 1e1) % 1000,
            ret['frac'] >> (32 - 9))

def cmd_list_nodes(args):
    for i in range((PyWrtd.get_node_count())):
        node_id = PyWrtd.get_node_id(i+1)
        print('-> WRTD Node detected with ID: {0}'.format(node_id))
        if args.verbose:
            wrtd = PyWrtd(node_id)
            args.verbose = False
            cmd_sys_info(wrtd, args)
            args.verbose = True

def cmd_sys_info(wrtd, args):
    print('')
    cmd_sys_time(wrtd, args)
    print('')
    count = wrtd.get_attr_int32(wrtd.WRTD_GLOBAL_REP_CAP_ID,
                                wrtd.WRTD_ATTR_FW_COUNT)
    print ('{0} Application{1} detected{2}'.format(count,
                                                   '' if (count == 1) else 's',
                                                   '.' if (count == 0) else ':'))
    for i in range(count):
        if args.verbose:
            print ('')
        fw_name = wrtd.get_fw_name(i)
        print ('# {0}'.format(fw_name))
        if args.verbose:
            print("  + Version Info")
            print("    - Name..............: {0}".format(fw_name))
            print("    - FW version........: {0}.{1}".format(
                wrtd.get_attr_int32(fw_name, wrtd.WRTD_ATTR_FW_MAJOR_VERSION),
                wrtd.get_attr_int32(fw_name, wrtd.WRTD_ATTR_FW_MINOR_VERSION)))
            print("    - WRTD version......: {0}.{1}".format(
                wrtd.get_attr_int32(fw_name, wrtd.WRTD_ATTR_FW_MAJOR_VERSION_REQUIRED),
                wrtd.get_attr_int32(fw_name, wrtd.WRTD_ATTR_FW_MINOR_VERSION_REQUIRED)))
            print("  + Capabilities")
            print("    - Local RX Channels.: {0}".format(
                wrtd.get_attr_int32(fw_name, wrtd.WRTD_ATTR_FW_LOCAL_INPUTS)))
            print("    - Local TX Channels.: {0}".format(
                wrtd.get_attr_int32(fw_name, wrtd.WRTD_ATTR_FW_LOCAL_OUTPUTS)))
            caps = wrtd.get_attr_int32(fw_name, wrtd.WRTD_ATTR_FW_CAPABILITIES)
            print("    - Network RX........: {0}".format(caps & 0x1 != 0))
            print("    - Network TX........: {0}".format(caps & 0x2 != 0))

    print('')
    cmd_list_alarms(wrtd, args)
    print('')
    cmd_list_rules(wrtd, args)
    print('')

def cmd_sys_time(wrtd, args):
    print('Current time = {0}'.format(__tstamp_to_str(
        wrtd.get_attr_tstamp(wrtd.WRTD_GLOBAL_REP_CAP_ID,
                             wrtd.WRTD_ATTR_SYS_TIME))))

def cmd_set_log(wrtd, args):
    wrtd.set_attr_bool(wrtd.WRTD_GLOBAL_REP_CAP_ID,
                       wrtd.WRTD_ATTR_EVENT_LOG_ENABLED,
                       args.set_log == True)

def cmd_show_log(wrtd, args):

    if (args.set_log):
        cmd_set_log(wrtd, args)

    n_read = 0

    while ((args.count == 0) or (n_read < args.count)):
        log_entry = wrtd.get_next_event_log_entry()
        if len(log_entry):
            print(log_entry)
            n_read += 1

def cmd_clear_log(wrtd, args):
    wrtd.clear_event_log_entries()

def cmd_list_rules(wrtd, args):
    count = wrtd.get_attr_int32(wrtd.WRTD_GLOBAL_REP_CAP_ID,
                                wrtd.WRTD_ATTR_RULE_COUNT)
    print ('{0} Rule{1} defined{2}'.format(count,
                                           '' if (count == 1) else 's',
                                           '.' if (count == 0) else ':'))
    for i in range(count):
        args.name = wrtd.get_rule_name(i)
        if args.verbose:
            print ('')
        print ('# {0}'.format(args.name))
        if args.verbose:
            cmd_rule_info(wrtd, args)

def cmd_add_rule(wrtd, args):
    wrtd.add_rule(args.name)

def cmd_set_rule(wrtd, args):
    time = {'seconds': 0, 'ns': args.delay, 'frac': 0}

    delay = __tstamp_normalise(time)
    wrtd.set_attr_tstamp(args.name, wrtd.WRTD_ATTR_RULE_DELAY,
                         seconds = delay['seconds'],
                         ns      = delay['ns'],
                         frac    = delay['frac'])

    wrtd.set_attr_string(args.name, wrtd.WRTD_ATTR_RULE_SOURCE, args.source)
    wrtd.set_attr_string(args.name, wrtd.WRTD_ATTR_RULE_DESTINATION, args.destination)

def cmd_remove_rule(wrtd, args):
    wrtd.remove_rule(args.name)

def cmd_remove_all_rules(wrtd, args):
    wrtd.remove_all_rules()

def cmd_enable_rule(wrtd, args):
    wrtd.set_attr_bool(args.name, wrtd.WRTD_ATTR_RULE_ENABLED, True)

def cmd_disable_rule(wrtd, args):
    wrtd.set_attr_bool(args.name, wrtd.WRTD_ATTR_RULE_ENABLED, False)

def cmd_disable_all_rules(wrtd, args):
    wrtd.disable_all_rules()

def cmd_rule_info(wrtd, args):
    print("  + Configuration")
    print("    - Name..............: {0}".format(args.name))
    print("    - Source............: {0}".format(
        wrtd.get_attr_string(args.name, wrtd.WRTD_ATTR_RULE_SOURCE)))
    print("    - Destination.......: {0}".format(
        wrtd.get_attr_string(args.name, wrtd.WRTD_ATTR_RULE_DESTINATION)))
    print("    - Enabled...........: {0}".format(
        wrtd.get_attr_bool(args.name, wrtd.WRTD_ATTR_RULE_ENABLED)))
    print("    - Send Late.........: {0}".format(
        wrtd.get_attr_bool(args.name, wrtd.WRTD_ATTR_RULE_SEND_LATE)))
    print("    - Repeat Count......: {0}".format(
        wrtd.get_attr_int32(args.name, wrtd.WRTD_ATTR_RULE_REPEAT_COUNT)))
    print("    - Delay.............: {0:.3f}ns".format(__tstamp_to_ns(
        wrtd.get_attr_tstamp(args.name, wrtd.WRTD_ATTR_RULE_DELAY))))
    print("    - Holdoff...........: {0:.3f}ns".format(__tstamp_to_ns(
        wrtd.get_attr_tstamp(args.name, wrtd.WRTD_ATTR_RULE_HOLDOFF))))
    print("    - Resync Period.....: {0:.3f}ns".format(__tstamp_to_ns(
        wrtd.get_attr_tstamp(args.name, wrtd.WRTD_ATTR_RULE_RESYNC_PERIOD))))
    print("    - Resync Factor.....: {0}".format(
        wrtd.get_attr_int32(args.name, wrtd.WRTD_ATTR_RULE_RESYNC_FACTOR)))
    print("  + Statistics")
    print("    - RX Events.........: {0}".format(
        wrtd.get_attr_int32(args.name, wrtd.WRTD_ATTR_STAT_RULE_RX_EVENTS)))
    print("    - Last RX...........: {0}".format(__tstamp_to_str(
        wrtd.get_attr_tstamp(args.name, wrtd.WRTD_ATTR_STAT_RULE_RX_LAST))))
    print("    - TX Events.........: {0}".format(
        wrtd.get_attr_int32(args.name, wrtd.WRTD_ATTR_STAT_RULE_TX_EVENTS)))
    print("    - Last TX...........: {0}".format(__tstamp_to_str(
        wrtd.get_attr_tstamp(args.name, wrtd.WRTD_ATTR_STAT_RULE_TX_LAST))))
    print("    - Latency (min).....: {0:.3f}ns".format(__tstamp_to_ns(
        wrtd.get_attr_tstamp(args.name,wrtd.WRTD_ATTR_STAT_RULE_RX_LATENCY_MIN))))
    print("    - Latency (avg).....: {0:.3f}ns".format(__tstamp_to_ns(
        wrtd.get_attr_tstamp(args.name, wrtd.WRTD_ATTR_STAT_RULE_RX_LATENCY_AVG))))
    print("    - Latency (max).....: {0:.3f}ns".format(__tstamp_to_ns(
        wrtd.get_attr_tstamp(args.name, wrtd.WRTD_ATTR_STAT_RULE_RX_LATENCY_MAX))))
    print("    - Missed (late).....: {0}".format(
        wrtd.get_attr_int32(args.name, wrtd.WRTD_ATTR_STAT_RULE_MISSED_LATE)))
    print("    - Missed (holdoff)..: {0}".format(
        wrtd.get_attr_int32(args.name, wrtd.WRTD_ATTR_STAT_RULE_MISSED_HOLDOFF)))
    print("    - Missed (no sync)..: {0}".format(
        wrtd.get_attr_int32(args.name, wrtd.WRTD_ATTR_STAT_RULE_MISSED_NOSYNC)))
    print("    - Missed (overflow).: {0}".format(
        wrtd.get_attr_int32(args.name, wrtd.WRTD_ATTR_STAT_RULE_MISSED_OVERFLOW)))
    print("    - Last Missed.......: {0}".format(__tstamp_to_str(
        wrtd.get_attr_tstamp(args.name, wrtd.WRTD_ATTR_STAT_RULE_MISSED_LAST))))

def cmd_reset_rule_stats(wrtd, args):
    wrtd.reset_rule_stats(args.name)

def cmd_list_alarms(wrtd, args):
    count = wrtd.get_attr_int32(wrtd.WRTD_GLOBAL_REP_CAP_ID,
                                wrtd.WRTD_ATTR_ALARM_COUNT)
    print ('{0} Alarm{1} defined{2}'.format(count,
                                            '' if (count == 1) else 's',
                                            '.' if (count == 0) else ':'))
    for i in range(count):
        args.name = wrtd.get_alarm_name(i)
        if args.verbose:
            print ('')
        print ('# {0}'.format(args.name))
        if args.verbose:
            cmd_alarm_info(wrtd, args)

def cmd_add_alarm(wrtd, args):
    wrtd.add_alarm(args.name)

def cmd_set_alarm(wrtd, args):
    time = wrtd.get_attr_tstamp(wrtd.WRTD_GLOBAL_REP_CAP_ID, wrtd.WRTD_ATTR_SYS_TIME)

    time['ns'] += args.delay
    delay = __tstamp_normalise(time)
    wrtd.set_attr_tstamp(args.name, wrtd.WRTD_ATTR_ALARM_TIME,
                         seconds = delay['seconds'],
                         ns      = delay['ns'],
                         frac    = delay['frac'])

    time['ns'] -= args.setup
    setup = __tstamp_normalise(time)
    wrtd.set_attr_tstamp(args.name, wrtd.WRTD_ATTR_ALARM_SETUP_TIME,
                         seconds = setup['seconds'],
                         ns      = setup['ns'],
                         frac    = setup['frac'])

    time['seconds'] = 0
    time['ns']      = args.period
    time['frac']    = 0

    period = __tstamp_normalise(time)
    wrtd.set_attr_tstamp(args.name, wrtd.WRTD_ATTR_ALARM_PERIOD,
                         seconds = setup['seconds'],
                         ns      = setup['ns'],
                         frac    = setup['frac'])

    wrtd.set_attr_int32(args.name, wrtd.WRTD_ATTR_ALARM_REPEAT_COUNT, args.count)

    if (args.enable):
        cmd_enable_alarm(wrtd, args)

def cmd_remove_alarm(wrtd, args):
    wrtd.remove_alarm(args.name)

def cmd_remove_all_alarms(wrtd, args):
    wrtd.remove_all_alarms()

def cmd_enable_alarm(wrtd, args):
    wrtd.set_attr_bool(args.name, wrtd.WRTD_ATTR_ALARM_ENABLED, True)

def cmd_disable_alarm(wrtd, args):
    wrtd.set_attr_bool(args.name, wrtd.WRTD_ATTR_ALARM_ENABLED, False)

def cmd_disable_all_alarms(wrtd, args):
    wrtd.disable_all_alarms()

def cmd_alarm_info(wrtd, args):
    print("  + Configuration")
    print("    - Name..............: {0}".format(args.name))
    print("    - Enabled...........: {0}".format(
        wrtd.get_attr_bool(args.name, wrtd.WRTD_ATTR_ALARM_ENABLED)))
    print("    - Trigger Time......: {0}".format(__tstamp_to_str(
        wrtd.get_attr_tstamp(args.name, wrtd.WRTD_ATTR_ALARM_TIME))))
    print("    - Setup Time........: {0}".format(__tstamp_to_str(
        wrtd.get_attr_tstamp(args.name, wrtd.WRTD_ATTR_ALARM_SETUP_TIME))))
    print("    - Period............: {0:.3f}ns".format(__tstamp_to_ns(
        wrtd.get_attr_tstamp(args.name, wrtd.WRTD_ATTR_ALARM_PERIOD))))
    print("    - Repeat Count......: {0}".format(
        wrtd.get_attr_int32(args.name, wrtd.WRTD_ATTR_ALARM_REPEAT_COUNT)))

def cmd_cli(wrtd, args):
    try:
        import readline
    except ImportError:
        readline = None
        print("Warning: readline not present")

    parser = create_parser(True)

    while True:
        try:
            l = input('wrtd-tool> ')
        except EOFError:
            print('exiting')
            break
        l = l.split()
        # We don't want to exit in case of error, so continue.
        # Ugly until we use python 3.9 and exit_on_error=False
        try:
            args = parser.parse_args(l)
        except SystemExit:
            continue
        # Do not exit on error.
        try:
            args.func(wrtd, args)
        except OSError as e:
            print(e)


def time_interval(string):
    m = re.match('\A([0-9]+)([nums]?)\Z', string)
    if m == None:
        msg = "%r is not a valid time interval specification" % string
        raise argparse.ArgumentTypeError(msg)
    mult = {
        '' : 1e0,
        'n': 1e0,
        'u': 1e3,
        'm': 1e6,
        's': 1e9,
    }
    return int(m.group(1)) * int(mult[m.group(2)])

def time_interval_help():
    return """Default unit is ns, but an 'n','u','m' or 's' can be appended \
    to the value to set it explicitly to nano, micro, milli or full seconds"""

def concat(*args):
    res = []
    for e in args:
        if e is not None:
            res.append(e)
    return res

def create_parser(is_cli):
    # Parent parsers for common args.
    if not is_cli:
        devid_parse = argparse.ArgumentParser(add_help=False)
        devid_parse.add_argument('devid', type=lambda x: int(x,0),
                                 metavar='<node_id>',
                                 help='The ID of the WRTD Node (int, can be hex with "0x" prefix)')
    else:
        # No devid with cli
        devid_parse = None

    verbose_parse = argparse.ArgumentParser(add_help=False)
    verbose_parse.add_argument('-v', '--verbose', action='store_true',
                               help='Show more details')

    rname_parse = argparse.ArgumentParser(add_help=False)
    rname_parse.add_argument('name', metavar='<rule_id>',
                             help='The ID of the Rule (string up to 15 characters)')

    aname_parse = argparse.ArgumentParser(add_help=False)
    aname_parse.add_argument('name', metavar='<alarm_id>',
                             help='The ID of the Alarm (string up to 15 characters)')

    # The parser (with its subparsers)
    parser = argparse.ArgumentParser(
        description='WRTD Node configuration tool')

    subparsers = parser.add_subparsers(title='Available commands',
                                       dest='command', metavar='<command>',
                                       help='(Use "<command> -h" to get more details)')
    subparsers.required = True;

    # list-nodes
    cmd_parser = subparsers.add_parser(
        'list-nodes',
        help='List the IDs of all detected WRTD Nodes',
        parents=[verbose_parse])
    cmd_parser.set_defaults(func=cmd_list_nodes, devid=None)

    # sys-info
    cmd_parser = subparsers.add_parser(
        'sys-info',
        help='Show system information',
        parents=concat(devid_parse, verbose_parse))
    cmd_parser.set_defaults(func=cmd_sys_info)

    # sys-time
    cmd_parser = subparsers.add_parser(
        'sys-time', help='Show current system time',
        parents=concat(devid_parse))
    cmd_parser.set_defaults(func=cmd_sys_time)

    # enable-log
    cmd_parser = subparsers.add_parser(
        'enable-log', help='Enable logging',
        parents=concat(devid_parse))
    cmd_parser.set_defaults(func=cmd_set_log, set_log=True)

    # disable-log
    cmd_parser = subparsers.add_parser(
        'disable-log', help='Disable logging',
        parents=concat(devid_parse))
    cmd_parser.set_defaults(func=cmd_set_log, set_log=False)

    # show-log
    cmd_parser = subparsers.add_parser(
        'show-log', help='Show log entries',
        parents=concat(devid_parse))
    cmd_parser.add_argument(
        '-c', '--count', dest='count', type=int, default=0,
        help='Number of entries to read (0 = infinite, the default)')
    cmd_parser.add_argument(
        '-e', '--enable', dest='set_log', action='store_true',
        help='Enable event logging on the Node if not already enabled')
    cmd_parser.set_defaults(func=cmd_show_log)

    # clear-log
    cmd_parser = subparsers.add_parser(
        'clear-log', help='Clear pending log entries',
        parents=concat(devid_parse))
    cmd_parser.set_defaults(func=cmd_clear_log)

    # list-rules
    cmd_parser = subparsers.add_parser(
        'list-rules', help='List all defined Rules',
        parents=concat(devid_parse, verbose_parse))
    cmd_parser.set_defaults(func=cmd_list_rules)

    # add-rule
    cmd_parser = subparsers.add_parser(
        'add-rule', help='Define a new Rule',
        parents=concat(devid_parse, rname_parse))
    cmd_parser.set_defaults(func=cmd_add_rule)

    # set-rule
    cmd_parser = subparsers.add_parser(
        'set-rule', help='Configure a Rule',
        parents=concat(devid_parse, rname_parse))
    cmd_parser.add_argument(
        '-d', '--delay', type=time_interval, default = 0,
        help='Set the delay for this Rule. ' + time_interval_help())
    cmd_parser.add_argument(
        'source', help='The source Event ID for this Rule.')
    cmd_parser.add_argument(
        'destination', help='The destination Event ID for this Rule.')
    cmd_parser.set_defaults(func=cmd_set_rule)

    # remove-rule
    cmd_parser = subparsers.add_parser(
        'remove-rule', help='Delete a Rule',
        parents=concat(devid_parse, rname_parse))
    cmd_parser.set_defaults(func=cmd_remove_rule)

    # remove-all-rules
    cmd_parser = subparsers.add_parser(
        'remove-all-rules', help='Delete all Rules',
        parents=concat(devid_parse))
    cmd_parser.set_defaults(func=cmd_remove_all_rules)

    # enable-rule
    cmd_parser = subparsers.add_parser(
        'enable-rule', help='Enable a Rule',
        parents=concat(devid_parse, rname_parse))
    cmd_parser.set_defaults(func=cmd_enable_rule)

    # disable-rule
    cmd_parser = subparsers.add_parser(
        'disable-rule', help='Disable a Rule',
        parents=concat(devid_parse, rname_parse))
    cmd_parser.set_defaults(func=cmd_disable_rule)

    # disable-all-rules
    cmd_parser = subparsers.add_parser(
        'disable-all-rules', help='Disable all Rules',
        parents=concat(devid_parse))
    cmd_parser.set_defaults(func=cmd_disable_all_rules)

    # rule-info
    cmd_parser = subparsers.add_parser(
        'rule-info', help='Display information about a Rule',
        parents=concat(devid_parse, rname_parse))
    cmd_parser.set_defaults(func=cmd_rule_info)

    # reset-rule-stats
    cmd_parser = subparsers.add_parser(
        'reset-rule-stats', help='Reset all statistics of a Rule',
        parents=concat(devid_parse, rname_parse))
    cmd_parser.set_defaults(func=cmd_reset_rule_stats)

    # list-alarms
    cmd_parser = subparsers.add_parser(
        'list-alarms', help='List all defined Alarms',
        parents=concat(devid_parse, verbose_parse))
    cmd_parser.set_defaults(func=cmd_list_alarms)

    # add-alarm
    cmd_parser = subparsers.add_parser(
        'add-alarm', help='Define a new Alarm',
        parents=concat(devid_parse, aname_parse))
    cmd_parser.set_defaults(func=cmd_add_alarm)

    # set-alarm
    cmd_parser = subparsers.add_parser(
        'set-alarm', help='Configure an Alarm',
        parents=concat(devid_parse, aname_parse))
    cmd_parser.add_argument(
        '-d', '--delay', type=time_interval, required = True,
        help='Set the delay for this Alarm wrt now. ' + time_interval_help())
    cmd_parser.add_argument(
        '-s', '--setup', type=time_interval, default = 0,
        help='Set the setup time for this Alarm. ' + time_interval_help())
    cmd_parser.add_argument(
        '-p', '--period', type=time_interval, default = 0,
        help='Set the period for this Alarm. ' + time_interval_help())
    cmd_parser.add_argument(
        '-c', '--count', type=int, default = 0,
        help='Set the repeat count for this Alarm')
    cmd_parser.add_argument(
        '-e', '--enable', action='store_true',
        help='Also enable the Alarm after configuring it.')
    cmd_parser.set_defaults(func=cmd_set_alarm)

    # remove-alarm
    cmd_parser = subparsers.add_parser(
        'remove-alarm', help='Delete an Alarm',
        parents=concat(devid_parse, aname_parse))
    cmd_parser.set_defaults(func=cmd_remove_alarm)

    # remove-all-alarms
    cmd_parser = subparsers.add_parser(
        'remove-all-alarms', help='Delete all Alarms',
        parents=concat(devid_parse))
    cmd_parser.set_defaults(func=cmd_remove_all_alarms)

    # enable-alarm
    cmd_parser = subparsers.add_parser(
        'enable-alarm', help='Enable an Alarm',
        parents=concat(devid_parse, aname_parse))
    cmd_parser.set_defaults(func=cmd_enable_alarm)

    # disable-alarm
    cmd_parser = subparsers.add_parser(
        'disable-alarm', help='Disable an Alarm',
        parents=concat(devid_parse, aname_parse))
    cmd_parser.set_defaults(func=cmd_disable_alarm)

    # disable-all-alarms
    cmd_parser = subparsers.add_parser(
        'disable-all-alarms', help='Disable all Alarms',
        parents=concat(devid_parse))
    cmd_parser.set_defaults(func=cmd_disable_all_alarms)

    # alarm-info
    cmd_parser = subparsers.add_parser(
        'alarm-info', help='Display information about an Alarm',
        parents=concat(devid_parse, aname_parse))
    cmd_parser.set_defaults(func=cmd_alarm_info)

    # cli
    if not is_cli:
        cmd_parser = subparsers.add_parser(
            'cli', help='Command line interface',
            parents=concat(devid_parse))
        cmd_parser.set_defaults(func=cmd_cli)

    return parser


def main():

    parser = create_parser(is_cli=False)
    args = parser.parse_args()

    if (args.devid != None):
        wrtd = PyWrtd(args.devid)
        args.func(wrtd, args)
    else:
        args.func(args)

if __name__ == "__main__":
    signal.signal(signal.SIGINT, signal_handler)
    main()
