#!/bin/sh -e
# COPYRIGHT 2020 Schweitzer Engineering Laboratories, Inc.

PYTHON=$(which python 2>/dev/null || true)
if [ -z "$PYTHON" ]; then
  PYTHON=$(which python3 2>/dev/null || true)
  if [ -z "$PYTHON" ]; then
    echo Cannot find a working python installation. 1>&2
    exit 1
  fi
fi

$PYTHON - $@ <<EOF
from __future__ import print_function  # requires python >= 2.6

import getopt
import glob
import os
import sys

try:
    import ConfigParser as configparser
except ImportError:
    import configparser

# sysfs attribute path
SYSFS_ATTR_PATH = "/sys/class/selmultiuart/"


# keys are valid variables
# values are the valid values for each variable
# The first value in the valid values is the default
INI_VALUES = {
    'loopback': ['off', 'on'],
    'power': ['off', 'on'],
    'mode': ['RS232', 'RS485'],
    'half-duplex': ['off', 'on'],
    'half-flow': ['off', 'on'],
    'send-data-control': ['manual', 'auto', 'rts_on'],
}

# map the ini variable values to the sysfs attribute values
INI_ATTR_MAP = {
    'loopback': {
        'off': '0',
        'on': '1',
    },
    'power': {
        'off': '0',
        'on': '1',
    },
    'mode': {
        # send-data-control isn't meaningful when running
        # in RS232 mode, so it uses the same setting for both
        # manual and auto
        'RS232': {
            'manual': '0',
            'auto': '0',
            'rts_on': '0',
        },
        'RS485': {
            'manual': '1',
            'auto': 'a',
            'rts_on': 'b',
        },
    },
    'half-duplex': {
        'off': '0',
        'on': '1',
    },
    'half-flow': {
        'off': '0',
        'on': '1',
    },
}


def die(*args, **kwargs):
    """
    Exits the application with an error.

    Args:
        args: Multiple arguments to be printed prior to exit
        kwargs: Keyword arguments

    Remarks:
        This is just a convenience function to print an error and exit.
        Call it the same way you would the print function.
    """
    kwargs['file'] = sys.stderr
    print(*args, **kwargs)
    sys.exit(1)


def validate_settings(settings, port):
    """
    Verifies the settings are valid

    Args:
        settings: A dict of settings to be verified
        port: Name to list if an error is detected

    Remarks:
        Exits with an error message if an error is detected
    """
    """ verify that settings are valid
    @param settings  a dict of settings to verify
    @param port      name to list if an error is detected"""
    for setting in settings:
        if setting not in INI_VALUES:
            die("ERROR: non-standard setting '{0}' on '{1}'".
                format(setting, port))
        elif settings[setting] not in INI_VALUES[setting]:
            die("ERROR: invalid value '{0}' for setting '{1}' on '{2}'".format(
                settings[setting], setting, port))


def parse_ini(configfile):
    """
    Parses the INI file printing an error if it fails to parse

    Args:
        configfile: The INI file to be parsed

    Returns:
        The parsed INI file
    """
    ini = configparser.RawConfigParser()
    try:
        read_ok = ini.read(configfile)
        if read_ok != [configfile]:
            print("ERROR: file '{0}' does not exist".format(
                configfile), file=sys.stderr)
            sys.exit(1)
    except configparser.ParsingError as e:
        print(e, file=sys.stderr)
        sys.exit(1)
    return ini


def process_ini(ini, num_ports):
    """
    Process the parsed INI file and generate defaults

    Args:
        ini: The parsed INI file
        num_ports: The number of SEL ports with settings

    Remarks:
        Exits with an error message if the INI file has settings
        that cannot be processed.

    Returns:
        Settings to be written for each port
    """

    # process default section
    defaults = dict((var, INI_VALUES[var][0]) for var in INI_VALUES)
    if ini.has_section('default'):
        defaults.update(dict(ini.items('default')))
        validate_settings(defaults, 'default')

    # process sections for settings
    port_settings = [defaults] * num_ports
    seen_ports = dict()
    for section in ini.sections():
        if section == 'default':
            continue

        settings = dict(defaults)
        settings.update(dict(ini.items(section)))

        split = section.split()
        if len(split) == 0 or split[0] != 'port':
            die("ERROR: non-standard section '{0}'".format(section))
        elif len(split) <= 1:
            die("ERROR: port sections must specify a port number")
        else:
            validate_settings(settings, section)
            for port in split[1:]:
                try:
                    i = int(port)

                    if not 0 <= i <= num_ports - 1:
                        raise ValueError()

                    port_settings[i] = settings
                    if i in seen_ports:
                        die("ERROR: port {0} multiply defined \
                            in '{1}' and '{2}'".format(
                                port, section, seen_ports[i]))
                    seen_ports[i] = section

                except ValueError:
                    die("ERROR: invalid port '{0}' on '{1}'".format(
                        port, section))

    return port_settings


def update_serial_settings(port_settings, dry_run):
    """
    Apply serial settings provided in the config file

    Args:
        port_settings: The settings from the config file to be applied
        dry_run: Print the settings change to stdout instead enacting them

    Remarks:
        Exits with an error message if an IOError occurs
    """
    # create a different set_pci() function if a dry_run
    if not dry_run:
        def set_pci(port, sysfile, value):
            with open(os.path.join(SYSFS_ATTR_PATH, sysfile), 'w') as f:
                f.write("{0} {1}\n".format(port, value))
    else:
        def set_pci(port, sysfile, value):
            print("echo {0} {1} > {2}".format(
                port, value, os.path.join(SYSFS_ATTR_PATH, sysfile)))

    try:
        for port, settings in enumerate(port_settings):
            set_pci(port, 'half_duplex',
                    INI_ATTR_MAP['half-duplex'][settings['half-duplex']])
            set_pci(port, 'half_flow',
                    INI_ATTR_MAP['half-flow'][settings['half-flow']])
            set_pci(port, 'loopback',
                    INI_ATTR_MAP['loopback'][settings['loopback']])
            set_pci(port, 'port_power',
                    INI_ATTR_MAP['power'][settings['power']])
            set_pci(port, 'rs485',
                    INI_ATTR_MAP['mode'][settings['mode']]
                    [settings['send-data-control']])
    except IOError as e:
        die("ERROR: failed to set serial ports:", e)


def get_num_ports():
    """
    Find how many /dev/ttySEL serial ports are available
    to apply settings for.

    Returns:
        The number of ttySEL serial ports
    """
    num_ports = 0
    for name in glob.glob('/dev/*ttySEL*'):
        num_ports = num_ports + 1

    return num_ports


def usage():
    """
    Print argument usage
    """
    print("Usage: sel-update-serial-ports [-c config_file.ini | -n ]")
    print("Update serial settings for SEL MultiUART \
        ports using a configuration file")
    print("")
    config_args = "-c, --config-file"
    config_desc = "file to load configuration from"
    print("\t{0:{width}}{1}".format(config_args, config_desc, width=30))

    config_args = "-n, --dry-run"
    config_desc = "Print what would happen without making settings changes"
    print("\t{0:{width}}{1}".format(config_args, config_desc, width=30))


def main():
    """
    Attempts to apply serial settings from a config file
    """

    try:
        opts, args = getopt.getopt(sys.argv[1:], "hnc:",
                                   ["help", "dry-run", "config-file="])
    except getopt.GetoptError as err:
        print(err)  # will print something like "option -a not recognized"
        usage()
        sys.exit(2)

    ini_file = "/etc/serial_port_settings"
    dry_run = False

    for opt, arg in opts:
        if opt in ("-c", "--config-file"):
            ini_file = arg
        elif opt in ("-n", "--dry-run"):
            dry_run = True
        elif opt in ("-h", "--help"):
            usage()
            sys.exit()
        else:
            assert False, "Error: Unhandled option"

    ini = parse_ini(ini_file)
    port_settings = process_ini(ini, get_num_ports())
    update_serial_settings(port_settings, dry_run)


if __name__ == '__main__':
    """The main entry point when run from the command line"""
    main()
EOF
