###############################################################################
# Copyright 2020 - 2022 Trimble Inc.
# $Id: nopilive_config.py,v 1.7 2024/02/11 20:58:50 wlentz Exp $
# $Source: /home/wlentz/cvs_stinger/GPSTools/NoPiLive/nopilive_config.py,v $
###############################################################################

""" NoPiLive
    Configuration classes and methods for NoPiLive.
    Parses any command line options and any supplied .ini files to set the
    configuration.
"""

import argparse
import configparser
import textwrap

###############################################################################
# Constants

VERSION_STR = '2022.0.2'
COPYRIGHT_STR = '(c) Copyright Trimble Inc. 2020 - 2022'

###############################################################################
# Configuration parameter classes

class RXConfig():
    """ Class containing the base and rover receiver configuration parameters.
    """
    def __init__(self, name):
        self.name = name
        self.ip_addr = None
        self.port_num = None
        self.ant_name = None
        self.ant_xyz = [None, None, None]
        self.ant_num = 0
        self.cmn_clk = False
        self.rt27_file_name = None
    def rt27_from_ip_stream(self):
        """ Returns True/False if the RT27 measurements for this receivers are
            from a IP stream (and not a data file).
        """
        if self.ip_addr is not None and self.port_num is not None:
            return True
        return False
    def rt27_from_file(self):
        """ Returns True/False if the RT27 measurements for this receivers are
            from a data file (and not a IP stream).
        """
        if self.rt27_from_ip_stream():
            return False
        if self.rt27_file_name is None:
            return False
        return True
    def format_ip_port(self):
        """ Returns formatted string of the IP address and port if the RT27
            measurements for this receivers are from a IP stream.
        """
        if self.rt27_from_ip_stream():
            return self.ip_addr+':'+str(self.port_num)
        return None
    def rt27_source(self):
        """ Returns a string identifying the RT27 data source """
        source = self.format_ip_port()
        if source is not None:
            return source
        from_file = self.rt27_from_file()
        if from_file:
            return self.rt27_file_name
        return 'No RT27 data'

class CurveFitConfig():
    """ Class containing configuation parameters related to the curve-fit data.
    """
    def __init__(self):
        self.ip_addr = None
        self.user = "admin"
        self.password = "password"
        self.req_rate = 30
    def set_ip_addr(self, ip_addr):
        """ Set the receiver IP address to request the curve-fit data. """
        self.ip_addr = ip_addr
    def set_user_password(self, user, password):
        """ Set the receiver user name & password to request the curve-fit
            data.
        """
        self.user = user
        self.password = password
    def set_req_rate(self, req_rate):
        """ Set the request rate (s) for the curve-fit data. """
        self.req_rate = req_rate

class RT27Config():
    """ Class containing general RT27 data handling configuation parameters.
    """
    def __init__(self):
        self.timeout_secs = 3600
    def get_timeout(self):
        """ Get the RT27 timeout (s) """
        return self.timeout_secs
    def set_timeout(self, timeout):
        """ Set the RT27 timeout (s) """
        self.timeout_secs = timeout

class PlotConfig():
    """ Class containing configuation parameters related to the plot window.
    """
    def __init__(self):
        self.update_rate = 1.0
        self.plot_width = 8.0
        self.plot_height = 6.0
    def get_update_rate(self):
        """ Return the plot update rate (s). """
        return self.update_rate
    def get_plot_size(self):
        """ Return the plot size as tuple (width, height). """
        return (self.plot_width, self.plot_height)
    def set_plot_size(self, figsize):
        """ Set the plot size from tuple (width, height). """
        self.plot_width, self.plot_height = figsize

class StartUp():
    """ Class containing configuration parameters defining the start-up
        state: plot type etc.
    """
    def __init__(self):
        self.plot_type = 0
        self.menu_sdiff = False
    def get_plot_type(self):
        """ Return the start-up display plot type """
        return self.plot_type
    def get_menu_sdiff(self):
        """ Return the menu single difference, rather than the normal double
            difference, state
        """
        return self.menu_sdiff

class NoPiLiveConfig():
    """ Class containing all of the NoPiLive configuration parameters. """
    def __init__(self):
        self.base_cfg = RXConfig('Base')
        self.rovr_cfg = RXConfig('Rover')
        self.cfit_cfg = CurveFitConfig()
        self.rt27_cfg = RT27Config()
        self.plot_cfg = PlotConfig()
        self.startup = StartUp()
        self.func_rx_info = None
        self.func_get_diffs = None
        self.func_clear_rt27_data = None
        self.func_select_combo = None
        self.verbose = False
    def rx_cfg(self, is_base):
        """ Returns the base or rover receiver configuration. """
        if is_base:
            return self.base_cfg
        return self.rovr_cfg
    def zero_baseline(self):
        """ Returns True/False if the baseline is considered to be zero.
            i.e. if the base and rover share a common antenna.
        """
        if abs(self.base_cfg.ant_xyz[0] - self.rovr_cfg.ant_xyz[0]) > 1e-3:
            return False
        if abs(self.base_cfg.ant_xyz[1] - self.rovr_cfg.ant_xyz[1]) > 1e-3:
            return False
        if abs(self.base_cfg.ant_xyz[2] - self.rovr_cfg.ant_xyz[2]) > 1e-3:
            return False
        return True

###############################################################################
# Utility functions

def _get_ant_xyz(ant_name):
    """ Return a list of X/Y/Z co-ordindates for the given antenna name, if
        this name is in the list. Otherwise terminates the program as NoPiLive
        requires a valid antenna X/Y/Z.
    """
    if ant_name == 'RS3':
        xyz = [-2689308.106, -4302881.090,  3851417.715]
    elif ant_name == 'RS9':
        xyz = [-2689304.200, -4302871.567,  3851431.087]
    elif ant_name == 'RS10':
        xyz = [-2689307.322, -4302869.643,  3851430.959]
    elif ant_name == 'SING':
        xyz = [-1523061.317,  6192123.180,   139797.098]
    elif ant_name == 'MELB1':
        xyz = [-4134302.919,  2879067.718, -3898415.716]
    elif ant_name == 'MELB2':
        xyz = [-4134303.327,  2879072.728, -3898417.291]
    else:
        print("Unknown antenna name "+ant_name)
        xyz = [None, None, None]
    return xyz

def _check_for_valid_config(config):
    """ Check the configuration parameters for a valid configuration.
        Certain parameters must be provided for NoPiLive to operate.
    """
    for coord in range(3):
        if config.base_cfg.ant_xyz[coord] is None or config.rovr_cfg.ant_xyz[coord] is None:
            return False
    if not config.base_cfg.rt27_from_ip_stream() and not config.base_cfg.rt27_from_file():
        return False
    if not config.rovr_cfg.rt27_from_ip_stream() and not config.rovr_cfg.rt27_from_file():
        return False

    # Copy some shared parameter values
    config.rovr_cfg.cmn_clk = config.base_cfg.cmn_clk
    config.startup.menu_sdiff = config.base_cfg.cmn_clk

    return True

###############################################################################
# Parse .ini files

def _parse_base_rovr_ini(rx_config, ini_data, section):
    """ Parse the BASE and ROVER sections of the .ini file.
        Not all configuration options need to be present in the .ini file.
    """
    label = 'IPAddr'
    if ini_data.has_option(section, label):
        rx_config.ip_addr = ini_data[section][label]
    label = 'Port'
    if ini_data.has_option(section, label):
        rx_config.port_num = int(ini_data[section][label])
    label = 'AntName'
    if ini_data.has_option(section, label):
        rx_config.ant_name = ini_data[section][label]
        rx_config.ant_xyz = _get_ant_xyz(rx_config.ant_name)
    elif ini_data.has_option(section, 'AntXYZ'):
        rx_config.ant_name = 'Custom'
        ant_xyz = ini_data[section]['AntXYZ'].split(',')
        rx_config.ant_xyz[0] = float(ant_xyz[0])
        rx_config.ant_xyz[1] = float(ant_xyz[1])
        rx_config.ant_xyz[2] = float(ant_xyz[2])
    label = 'AntNum'
    if ini_data.has_option(section, label):
        rx_config.ant_num = int(ini_data[section][label])
    label = 'CmnClk'
    if ini_data.has_option(section, label):
        if int(ini_data[section][label]):
            rx_config.cmn_clk = True
    label = 'RT27File'
    if ini_data.has_option(section, label):
        rx_config.rt27_file_name = ini_data[section][label]

def _parse_cfit_ini(cfit_cfg, ini_data):
    """ Parse the CFITDATA section of the .ini file, if present """
    section = 'CFITDATA'
    label = 'IPAddr'
    if ini_data.has_option(section, label):
        cfit_cfg.ip_addr = ini_data[section][label]
    label = 'User'
    if ini_data.has_option(section, label):
        cfit_cfg.user = ini_data[section][label]
    label = 'Password'
    if ini_data.has_option(section, label):
        cfit_cfg.password = ini_data[section][label]
    label = 'RequestRate'
    if ini_data.has_option(section, label):
        cfit_cfg.req_rate = int(ini_data[section][label])

def _parse_rt27_ini(rt27_cfg, ini_data):
    """ Parse the RT27DATA section of the .ini file, if present """
    section = 'RT27DATA'
    label = 'Timeout'
    if ini_data.has_option(section, label):
        rt27_cfg.timeout_secs = int(ini_data[section][label])

def _parse_plot_ini(plot_cfg, ini_data):
    """ Parse the PLOT section of the .ini file, if present """
    section = 'PLOT'
    label = 'UpdateRate'
    if ini_data.has_option(section, label):
        plot_cfg.update_rate = float(ini_data[section][label])
    label = 'PlotWidth'
    if ini_data.has_option(section, label):
        plot_cfg.plot_width = float(ini_data[section][label])
    label = 'PlotHeight'
    if ini_data.has_option(section, label):
        plot_cfg.plot_height = float(ini_data[section][label])

def _parse_plot_startup(startup, ini_data):
    """ Parse the STARTUP section of the .ini file, if present """
    section = 'STARTUP'
    label = 'PlotType'
    if ini_data.has_option(section, label):
        startup.plot_type = float(ini_data[section][label])

def _parse_ini_file(config, ini_file_name):
    """ Parse the NoPiLive .ini file """
    ini_data = configparser.ConfigParser()
    ini_data.read(ini_file_name)

    _parse_base_rovr_ini(config.base_cfg, ini_data, 'BASE')
    _parse_base_rovr_ini(config.rovr_cfg, ini_data, 'ROVER')
    _parse_rt27_ini(config.rt27_cfg, ini_data)
    _parse_plot_ini(config.plot_cfg, ini_data)
    _parse_plot_startup(config.startup, ini_data)

###############################################################################
# Parse the command line arguments

def _parse_cmd_args(config, args):
    """ Parse the command line arguments """
    if args.b_rt27 is not None:
        config.base_cfg.ip_addr = args.b_rt27[0]
        config.base_cfg.port_num = int(args.b_rt27[1])

    if args.r_rt27 is not None:
        config.rovr_cfg.ip_addr = args.r_rt27[0]
        config.rovr_cfg.port_num = int(args.r_rt27[1])

    if args.b_ant is not None:
        config.base_cfg.ant_name = args.b_ant[0]
        config.base_cfg.ant_xyz = _get_ant_xyz(config.base_cfg.ant_name)
    elif args.b_xyz is not None:
        config.base_cfg.ant_name = 'Custom'
        config.base_cfg.ant_xyz = (float(args.b_xyz[0]), float(args.b_xyz[1]), float(args.b_xyz[2]))

    if args.r_ant is not None:
        config.rovr_cfg.ant_name = args.r_ant[0]
        config.rovr_cfg.ant_xyz = _get_ant_xyz(config.rovr_cfg.ant_name)
    elif args.r_xyz is not None:
        config.rovr_cfg.ant_name = 'Custom'
        config.rovr_cfg.ant_xyz = (float(args.r_xyz[0]), float(args.r_xyz[1]), float(args.r_xyz[2]))

def nopilive_cli(config):
    """ NoPiLive command line interface. """
    cli_description = textwrap.dedent('''\
    Plot single & double difference measurements in real-time using RT27 streams

    Only set one of the x_ant and x_xyz options for base & rover.
    Known b/r_ant names: 
      RS3, RS9, RS10 [Stinger lab]; 
      SING [Singapore antennas]; 
      MELB1, MELB2 [Melbourne antennas].''')
    cli_epilog = textwrap.dedent('''\
    Examples:
    python nopilive.py --ini nopilive.ini
    python nopilive.py --b_rt27 10.1.150.1 5018 --r_rt27 10.1.150.2 5018 --b_ant RS9 --r_ant RS9''')
    br_strs = [('b', 'Base'), ('r', 'Rover')]
    cli = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter,
                                  description=cli_description, epilog=cli_epilog)
    cli.add_argument('--ini', default=False, nargs='?', const=True, help='Configuration .ini file')
    cli.add_argument('--verbose','-v', help='Verbose print() output', action="store_true")
    for br_char, br_str in br_strs:
        cli.add_argument('--'+br_char+'_rt27', default=None, nargs=2,
                         help=br_str+' receiver IP address and port for RT27')
        cli.add_argument('--'+br_char+'_ant', default=None, nargs=1,
                         help=br_str+' receiver antenna name to look-up XYZ, see above')
        cli.add_argument('--'+br_char+'_xyz', default=None, nargs=3,
                         help=br_str+' receiver antenna XYZ position, see above')
    cli_args = cli.parse_args()
    config.verbose = cli_args.verbose

    if cli_args.ini:
        # Decode configuration from .ini file
        _parse_ini_file(config, cli_args.ini)
    else:
        # Decode configuration from command line
        _parse_cmd_args(config, cli_args)

    config_valid = _check_for_valid_config(config)
    return config_valid

###############################################################################

def main():
    """ No runnable code in this file """

if __name__ == '__main__':
    main()
