"""
Utility functions for packet handling in CCS
"""

import gi
gi.require_version('Gtk', '3.0')
# gi.require_version('Notify', '0.7')

from gi.repository import Gtk, GLib, GdkPixbuf  #, Notify
import subprocess
import struct
import datetime
import dateutil.parser as duparser
import io
import types
import sys
import select
import json
import time
import dbus
import socket
import os
from pathlib import Path
import glob
import numpy as np
import logging.handlers
from database.tm_db import scoped_session_maker, DbTelemetry, DbTelemetryPool, RMapTelemetry, FEEDataTelemetry
from sqlalchemy.exc import OperationalError as SQLOperationalError
from sqlalchemy.sql.expression import func

from s2k_partypes import ptt, ptt_reverse, ptype_parameters, ptype_values
import confignator
import importlib


cfg = confignator.get_config(check_interpolation=False)

PCPREFIX = 'packet_config_'
CFG_SECT_PLOT_PARAMETERS = 'ccs-plot_parameters'
CFG_SECT_DECODE_PARAMETERS = 'ccs-decode_parameters'

# Set up logger
CFL_LOGGER_NAME = 'cfl'
logger = logging.getLogger(CFL_LOGGER_NAME)
logger.setLevel(getattr(logging, cfg.get('ccs-logging', 'level').upper()))
# sh = logging.StreamHandler()
# sh.setFormatter(logging.Formatter('%(levelname)s %(message)s'))
# logger.addHandler(sh)

communication = {name: 0 for name in cfg['ccs-dbus_names']}

scoped_session_idb = scoped_session_maker('idb', idb_version=None)
scoped_session_storage = scoped_session_maker('storage')

# check if MIB schema exists
try:
    scoped_session_idb.execute('show schemas').fetchall()
except SQLOperationalError as err:
    logger.critical(err)
    sys.exit()

# MIB caches to reduce SQL load
_pcf_cache = {}
_cap_cache = {}
_txp_cache = {}

project = cfg.get('ccs-database', 'project')
pc = importlib.import_module(PCPREFIX + str(project).upper())

# project specific parameters, must be present in all packet_config_* files
try:
    PUS_VERSION, TMHeader, TCHeader, PHeader, TM_HEADER_LEN, TC_HEADER_LEN, P_HEADER_LEN, PEC_LEN, MAX_PKT_LEN, timepack, \
        timecal, calc_timestamp, CUC_OFFSET, CUC_EPOCH, crc, PLM_PKT_PREFIX_TC_SEND, PLM_PKT_SUFFIX, FMT_TYPE_PARAM = \
        [pc.PUS_VERSION, pc.TMHeader, pc.TCHeader, pc.PHeader,
         pc.TM_HEADER_LEN, pc.TC_HEADER_LEN, pc.P_HEADER_LEN, pc.PEC_LEN,
         pc.MAX_PKT_LEN, pc.timepack, pc.timecal, pc.calc_timestamp,
         pc.CUC_OFFSET, pc.CUC_EPOCH, pc.puscrc, pc.PLM_PKT_PREFIX_TC_SEND, pc.PLM_PKT_SUFFIX, pc.FMT_TYPE_PARAM]

    s13_unpack_data_header = pc.s13_unpack_data_header
    SPW_PROTOCOL_IDS_R = {pc.SPW_PROTOCOL_IDS[key]: key for key in pc.SPW_PROTOCOL_IDS}

    tmtc = pc.TMTC
    tsync_flag = pc.TSYNC_FLAG

except AttributeError as err:
    logger.critical(err)
    raise err

SREC_MAX_BYTES_PER_LINE = 250
SEG_HEADER_FMT = '>III'
SEG_HEADER_LEN = struct.calcsize(SEG_HEADER_FMT)
SEG_SPARE_LEN = 2
SEG_CRC_LEN = 2

pid_offset = int(cfg.get('ccs-misc', 'pid_offset'))

fmtlist = {'INT8': 'b', 'UINT8': 'B', 'INT16': 'h', 'UINT16': 'H', 'INT32': 'i', 'UINT32': 'I', 'INT64': 'q',
           'UINT64': 'Q', 'FLOAT': 'f', 'DOUBLE': 'd', 'INT24': 'i24', 'UINT24': 'I24', 'uint*': 'uint',
           'ascii*': 'ascii', 'oct*': 'oct'}

personal_fmtlist = []

fmtlengthlist = {'b': 1, 'B': 1, 'h': 2, 'H': 2, 'i': 4, 'I': 4, 'q': 8,
                 'Q': 8, 'f': 4, 'd': 8, 'i24': 3, 'I24': 3}

# get format and offset of SIDs/discriminants
SID_FORMAT = {8: '>B', 16: '>H', 32: '>I'}
try:
    _sidfmt = scoped_session_idb.execute('SELECT PIC_TYPE,PIC_STYPE,PIC_APID,PIC_PI1_OFF,PIC_PI1_WID FROM pic').fetchall()
    if len(_sidfmt) != 0:
        SID_LUT = {tuple(k[:3]): tuple(k[3:]) for k in _sidfmt}
    else:
        SID_LUT = {}
        logger.warning('SID definitions not found in MIB!')
except SQLOperationalError:
    _sidfmt = scoped_session_idb.execute('SELECT PIC_TYPE,PIC_STYPE,PIC_PI1_OFF,PIC_PI1_WID FROM pic').fetchall()
    SID_LUT = {tuple([*k[:2], None]): tuple(k[2:]) for k in _sidfmt}
    logger.warning('MIB structure not fully compatible, no APID in PIC for SID format definition.')


# get names of TC parameters that carry data pool IDs, i.e. have CPC_CATEG=P
DATA_POOL_ID_PARAMETERS = [par[0] for par in scoped_session_idb.execute('SELECT cpc_pname FROM cpc WHERE cpc_categ="P"').fetchall()]


counters = {}  # keeps track of PUS TC packet sequence counters (one per APID)

if cfg.has_section('ccs-user_defined_packets'):
    user_tm_decoders = {k: json.loads(cfg['ccs-user_defined_packets'][k]) for k in cfg['ccs-user_defined_packets']}
else:
    user_tm_decoders = {}


def _reset_mib_caches():
    _pcf_cache.clear()
    _cap_cache.clear()
    _txp_cache.clear()


def _add_log_socket_handler():
    global logger
    # check if a handler is already present
    for hdlr in logger.handlers:
        if isinstance(hdlr, logging.handlers.SocketHandler):
            return
    sh = logging.handlers.SocketHandler('localhost', logging.handlers.DEFAULT_TCP_LOGGING_PORT)
    sh.setFormatter(logging.Formatter('%(asctime)s: %(name)-15s %(levelname)-8s %(message)s'))
    logger.addHandler(sh)


def _remove_log_socket_handlers():
    global logger
    for hdlr in logger.handlers:
        if isinstance(hdlr, logging.handlers.SocketHandler):
            logger.removeHandler(hdlr)


def set_scoped_session_idb_version(idb_version=None):
    """

    :param idb_version:
    """
    global scoped_session_idb
    scoped_session_idb.close()
    scoped_session_idb = scoped_session_maker('idb', idb_version=idb_version)

    _reset_mib_caches()

    logger.info('MIB SQL reconnect ({})'.format(idb_version))


def get_scoped_session_storage():
    """

    :return:
    """
    return scoped_session_maker('storage')


def start_app(file_path, wd, *args, console=False, **kwargs):
    """
    
    :param file_path:
    :param wd:
    :param args:
    :param console:
    :param kwargs:
    """
    # gui argument only used for poolmanager since it does not have an automatic gui
    if not os.path.isfile(file_path):
        raise FileNotFoundError('File not found: {}'.format(file_path))

    if kwargs:
        logger.info('{}: some parameters are not handled: {}'.format(file_path, kwargs))

    if not console:
        command = ''
        command += 'nohup python3 '
        command += file_path
        for arg in args:
            command += ' '
            command += arg
        command += ' >/dev/null 2>&1 &'
        logger.debug('Command to be executed: {}'.format(command))
        os.system(command)
    else:
        subprocess.Popen(['python3', file_path, *args], cwd=wd)


# Start the poolviewer
def start_pv(pool_name=None, console=False, **kwargs):
    """
    Gets the path of the Startfile for the Poolviewer and executes it
    
    :param console: If False will be run in Console, otherwise will be run in separate Environment
    :return:
    """

    directory = cfg.get('paths', 'ccs')
    file_path = os.path.join(directory, 'poolview_sql.py')
    if pool_name is not None:
        start_app(file_path, directory, pool_name, console=console, **kwargs)
    else:
        start_app(file_path, directory, console=console, **kwargs)


# Start only PoolManager
def start_pmgr(gui=True, console=False, **kwargs):
    """
    Gets the path of the Startfile for the Poolmanager and executes it
    
    :param console: If False will be run in Console, otherwise will be run in separate Environment
    :return:
    """

    directory = cfg.get('paths', 'ccs')
    file_path = os.path.join(directory, 'pus_datapool.py')

    if not gui:
        start_app(file_path, directory, '--nogui', console=console, **kwargs)
    else:
        start_app(file_path, directory, console=console, **kwargs)


# Start Editor
# Argumnet gives the possibility to run file in the console to see print comands
def start_editor(*files, console=False, **kwargs):
    """
    Gets the path of the Startfile for the Editor and executes it
    
    :param console: If False will be run in Console, otherwise will be run in separate Environment
    :return:
    """

    directory = cfg.get('paths', 'ccs')
    file_path = os.path.join(directory, 'editor.py')

    start_app(file_path, directory, *files, console=console, **kwargs)


# Start Parameter Monitor
# Argumnet gives the possibility to run file in the console to see print comands
def start_monitor(pool_name=None, parameter_set=None, console=False, **kwargs):
    """
    Gets the path of the Startfile for the Monitor and executes it
    
    :param console: If False will be run in Console, otherwise will be run in separate Environment
    :return:
    """

    directory = cfg.get('paths', 'ccs')
    file_path = os.path.join(directory, 'monitor.py')

    if pool_name is not None and parameter_set is not None:
        start_app(file_path, directory, pool_name, parameter_set, console=console, **kwargs)
    elif pool_name is not None:
        start_app(file_path, directory, pool_name, console=console, **kwargs)
    else:
        start_app(file_path, directory, console=console, **kwargs)


# Start Parameter Plotter
# Argumnet gives the possibility to run file in the console to see print comands
def start_plotter(pool_name=None, console=False, **kwargs):
    """
    Gets the path of the Startfile for the Plotter and executes it
    
    :param console: If False will be run in Console, otherwise will be run in separate Environment
    :return:
    """
    directory = cfg.get('paths', 'ccs')
    file_path = os.path.join(directory, 'plotter.py')

    if pool_name is not None:
        start_app(file_path, directory, pool_name, console=console, **kwargs)
    else:
        start_app(file_path, directory, console=console, **kwargs)

def start_tst(console=False, **kwargs):
    """

    :param console:
    :param kwargs:
    """
    directory = cfg.get('paths', 'tst')
    file_path = os.path.join(directory, 'tst/main.py')
    start_app(file_path, directory, console=console, **kwargs)


def start_progress_view(console=False, **kwargs):
    """

    :param console:
    :param kwargs:
    """
    directory = cfg.get('paths', 'tst')
    file_path = os.path.join(directory, 'progress_view/progress_view.py')
    start_app(file_path, directory, console=console, **kwargs)


def start_log_viewer(console=False, **kwargs):
    """

    :param console:
    :param kwargs:
    """
    directory = cfg.get('paths', 'tst')
    file_path = os.path.join(directory, 'log_viewer/log_viewer.py')
    start_app(file_path, directory, console=console, **kwargs)


def start_config_editor(console=False, **kwargs):
    """

    :param console:
    :param kwargs:
    """
    file_path = cfg.get('start-module', 'config-editor')
    directory = os.path.dirname(file_path)
    start_app(file_path, directory, console=console, **kwargs)


def start_tst(console=False, **kwargs):
    """

    :param console:
    :param kwargs:
    """
    file_path = os.path.join(cfg.get('paths', 'base'), 'start_tst')
    directory = os.path.dirname(file_path)
    start_app(file_path, directory, console=console, **kwargs)


# This sets up a logging client for the already running TCP-logging Server,
# The logger is returned with the given name an can be used like a normal logger
def start_logging(name):
    """

    :param name:
    :return:
    """
    level = cfg.get('ccs-logging', 'level')
    loglevel = getattr(logging, level.upper())

    rootLogger = logging.getLogger('')
    rootLogger.setLevel(loglevel)
    socketHandler = logging.handlers.SocketHandler('localhost', logging.handlers.DEFAULT_TCP_LOGGING_PORT)

    # don't bother with a formatter, since a socket handler sends the event as an unformatted pickle
    rootLogger.addHandler(socketHandler)
    log = logging.getLogger(name)

    return log


# This returns a dbus connection to a given Application-Name
def dbus_connection(name, instance=1):
    """

    :param name:
    :param instance:
    :return:
    """
    if instance == 0:
        logger.warning('No instance of {} found.'.format(name))
        return False

    if not instance:
        instance = 1

    dbus_type = dbus.SessionBus()
    try:
        Bus_Name = cfg.get('ccs-dbus_names', name)
    except (ValueError, confignator.config.configparser.NoOptionError):
        logger.warning(str(name) + ' is not a valid DBUS name.')
        logger.warning(str(name) + ' not found in config file.')
        raise NameError('"{}" is not a valid module name'.format(name))

    Bus_Name += str(instance)

    try:
        dbuscon = dbus_type.get_object(Bus_Name, '/MessageListener')
        return dbuscon
    except:
        # print('Please start ' + str(name) + ' if it is not running')
        logger.info('Connection to ' + str(name) + ' is not possible.')
        return False


# Returns True if application is running or False if not
def is_open(name, instance=1):
    """

    :param name:
    :param instance:
    :return:
    """
    dbus_type = dbus.SessionBus()
    try:
        # dbus_connection(name, instance)
        Bus_Name = cfg.get('ccs-dbus_names', name)
        Bus_Name += str(instance)
        dbus_type.get_object(Bus_Name, '/MessageListener')
        return True
    except Exception as err:
        logger.debug(err)
        return False


def show_functions(conn, filter=None):
    """
    Show all available functions for a CCS application
    
    :param conn: A Dbus connection
    :param filter: A string which filters the results
    :return: A list of available functions
    """
    '''
    if app_nbr and not isinstance(app_nbr, int):
        filter = app_nbr
        conn = dbus_connection(app_name)
    else:
        conn = dbus_connection(app_name, app_nbr)
    '''

    if filter:
        method_list = conn.show_functions(filter)
    else:
        method_list = conn.show_functions()

    method_list2 = []
    for i in method_list:
        method_list2.append(str(i))

    return method_list2


def ConnectionCheck(dbus_con, argument=None):
    """
    The user friendly version to use the ConnectionCheck method exported by all CCS applications via DBus, checks if the
    connection is made
    
    :param dbus_con: A Dbus connection
    :param argument: An argument which can be sent for testing purposes
    :return: If the connection is made
    """
    argument = python_to_dbus(argument, True)

    if argument:
        result = dbus_con.ConnectionCheck(argument)
    else:
        result = dbus_con.ConnectionCheck()

    result = dbus_to_python(result, True)

    return result


def Functions(dbus_con, function_name, *args, **kwargs):
    """
    The user friendly version to use the Functions method exported by all CCS applications via DBus, lets one call all
    Functions in a CCS application

    :param dbus_con: A Dbus connection
    :param function_name: The function to call as a string
    :param args: The arguments for the function
    :param kwargs: The keyword arguments for the function as as Dict
    :return:
    """
    args = (python_to_dbus(value, True) for value in args)

    if kwargs:
        result = dbus_con.Functions(str(function_name), 'user_console_is_True', *args, dict_to_dbus_kwargs(kwargs, True))
    else:
        result = dbus_con.Functions(str(function_name), 'user_console_is_True', *args)

    result = dbus_to_python(result, True)

    return result

def Variables(dbus_con, variable_name, *args):
    """
    The user friendly version to use the Variables method exported by all CCS applications via DBus, lets one change and
    get all Variables of a CCs application

    :param dbus_con: A Dbus connection
    :param variable_name: The variable
    :param args: The value to change the variable to, if nothing is given the value of the Variable is returned
    :return: Either the variable value or None if Variable was changed
    """
    args = (python_to_dbus(value, True) for value in args)

    result = dbus_con.Variables(str(variable_name), 'user_console_is_True', *args)

    result = dbus_to_python(result, True)

    return result

def Dictionaries(dbus_con, dictionary_name, *args):
    """
    The user friendly version to use the Dictionaries method exported by all CCS applications via DBus, lets one change
    and get values or the entire Dictionary for all availabe Dictionaries of a CCS application

    :param dbus_con: A Dbus connection
    :param dictionary_name: The dictionary name
    :param args: A key of the dictionary to get the corresponding value, or a key and a value to change the value for a
    key, if not given the entire dictionary is returned
    :return: The entire dictionary, a value for a given key or None if a value was changed
    """
    args = (python_to_dbus(value, True) for value in args)

    result = dbus_con.Dictionaries(str(dictionary_name), 'user_console_is_True', *args)

    result = dbus_to_python(result, True)

    return result

def dict_to_dbus_kwargs(arguments={}, user_console = False):
    """
    Converts a dictionary to kwargs dbus does understand and if necessary and requested changes NoneType to 'NoneType'

    :param arguments: The to converting dictionary
    :return: The dbus Dictionary which simulates the kwargs
    """
    if user_console:
        for key in arguments.keys():
            if arguments[key] is None:
                arguments[key] = 'NoneType'

    return dbus.Dictionary({'kwargs': dbus.Dictionary(arguments, signature='sv')})


# Converts dbus types to python types
def dbus_to_python(data, user_console=False):
    """
    Converts DBus types to Python types

    :param data: Dbus Type variables or containers
    :param user_console: Flag to check for NoneType arguments
    :return: Same data as python variables or containers
    """
    # NoneType string is transformed to a python None type
    if user_console and data == 'NoneType':
        data = None
    elif isinstance(data, dbus.String):
        data = str(data)
    elif isinstance(data, dbus.Boolean):
        data = bool(data)
    elif isinstance(data, (dbus.Int16, dbus.UInt16, dbus.Int32, dbus.UInt32, dbus.Int64, dbus.UInt64)):
        data = int(data)
    elif isinstance(data, dbus.Double):
        data = float(data)
    elif isinstance(data, dbus.Array):
        data = [dbus_to_python(value, user_console) for value in data]
    elif isinstance(data, dbus.Dictionary):
        new_data = dict()
        for key in data.keys():
            new_data[str(key)] = dbus_to_python(data[key], user_console)
        data = new_data
    elif isinstance(data, dbus.ByteArray):
        data = bytes(data)
    elif isinstance(data, dbus.Struct):
        result = tuple()
        for value in data:
            new = dbus_to_python(value, user_console)
            result = result + (new,)
        data = result
    return data


def python_to_dbus(data, user_console=False):
    """
    Converts Python Types to Dbus Types, only containers, since 'normal' data types are converted automatically by dbus

    :param data: Dbus Type variables or containers
    :param user_console: Flag to check for NoneType arguments
    :return: Same data for python variables, same data for container types as dbus containers
    """

    if user_console and data is None:
        data = dbus.String('NoneType')
    elif isinstance(data, list):
        data = dbus.Array([python_to_dbus(value, user_console) for value in data], signature='v')
    elif isinstance(data, dict):
        data = dbus.Dictionary(data, signature='sv')
        for key in data.keys():
            data[key] = python_to_dbus(data[key], user_console)
    elif isinstance(data, tuple):
        data = dbus.Struct([python_to_dbus(value, user_console) for value in data], signature='v')
    elif isinstance(data, (int, str, float, bool, bytes, bytearray)):
        pass
    else:
        logger.info("Object of type " + str(type(data)) + " can probably not be sent via dbus")
    return data


def convert_to_python(func):
    """
    The Function dbus_to_python can be used as a decorator where all return values are changed to python types

    :param func: The function where the decorator should be used
    :return: The wrapped function
    """
    def wrapper(*args, **kwargs):
        """

        :param args:
        :param kwargs:
        :return:
        """
        return dbus_to_python(func(*args, **kwargs))
    return wrapper


def set_monitor(pool_name=None, param_set=None):
    """

    :param pool_name:
    :param param_set:
    :return:
    """
    if is_open('monitor'):
        monitor = dbus_connection('monitor', communication['monitor'])
    else:
        # print('The Parmameter Monitor is not running')
        logger.error('The Parmameter Monitor is not running')
        return

    if pool_name is not None:
        monitor.Functions('set_pool', pool_name)
    else:
        # print('Pool Name has to be specified (cfl.set_monitor(pool_name, parmeter_set))')
        logger.error('Pool Name has to be specified (cfl.set_monitor(pool_name, parmeter_set))')
        return

    if param_set is not None:
        # Ignore_reply is ok here
        monitor.Functions('monitor_setup', param_set, ignore_reply=True)
    else:
        monitor.Functions('monitor_setup', ignore_reply=True)

    return


# def ptt_reverse(typ):
#
#     """
#     Returns the ptc location (first layer) of a Type stored in s2k_partypes 'ptt'
#     :param typ: Has to be a type given in s2k_partypes 'ptt'
#     :return: ptc location
#     """
#     if typ.startswith('oct'):
#         return [7, typ[3:]]
#     elif typ.startswith('ascii'):
#         return [8, typ[5:]]
#
#     for i in ptt: # First Section
#         for j in ptt[i]: # Second Section
#             if ptt[i][j] == typ: # Check for type
#                 return [i, j]
#
#     return False


def user_tm_decoders_func():
    """

    :return:
    """
    if cfg.has_section('ccs-user_defined_packets'):
        user_tm_decoders = {k: json.loads(cfg['ccs-user_defined_packets'][k]) for k in cfg['ccs-user_defined_packets']}
    else:
        user_tm_decoders = {}
    return user_tm_decoders


def Tmformatted(tm, separator='\n', sort_by_name=False, textmode=True, udef=False, nocal=False):
    """
    Return a formatted string containing all the decoded source data of TM packet _tm_

    :param tm:
    :param separator:
    :param sort_by_name:
    :param textmode:
    :param udef:
    :param nocal:
    :return:
    """
    sourcedata, tmtcnames = Tmdata(tm, udef=udef)
    tmtcname = " / ".join(tmtcnames)

    if nocal:
        # check if packet size is variable (because of different returned data structure)
        if not isinstance(sourcedata[0][-1], tuple):
            def _get_val_func(x):
                return [str(x[2]), str(x[4]), '']
        else:
            def _get_val_func(x):
                return [str(x[2]), str(x[4][0]), '']
    else:
        def _get_val_func(x):
            return [str(x[2]), str(x[0]), none_to_empty(x[1])]

    if textmode:
        if sourcedata is not None:
            formattedlist = ['{}:  {} {}'.format(*_get_val_func(i)) for i in sourcedata]
            if sort_by_name:
                formattedlist.sort()
        else:
            formattedlist = []
        return separator.join([Tm_header_formatted(tm)] + [tmtcname] + [100 * "-"] + formattedlist)
    else:
        if sourcedata is not None:
            try:
                formattedlist = [[*_get_val_func(i), parameter_tooltip_text(i[-1][0])] for i in sourcedata]
            # for variable length packets
            except (IndexError, TypeError):
                formattedlist = [[*_get_val_func(i), parameter_tooltip_text(i[-1])] for i in sourcedata]
        else:
            formattedlist = [[]]

        return formattedlist, tmtcname


##
#  TM source data
#
#  Decode source data field of TM packet
#  @param tm TM packet bytestring
def Tmdata(tm, udef=False):
    """

    :param tm:
    :param udef:
    :return:
    """
    tpsd = None
    params = None
    dbcon = scoped_session_idb

    # check if a UDEF exists and use to decode, if not the IDB will be checked
    if udef:
        try:
            header, data, crc = Tmread(tm)
            st, sst, apid = header.SERV_TYPE, header.SERV_SUB_TYPE, header.APID
            que = 'SELECT pic_pi1_off,pic_pi1_wid from pic where pic_type=%s and pic_stype=%s' % (st, sst)
            # que = 'SELECT pic_pi1_off,pic_pi1_wid from pic where pic_type=%s and pic_stype=%s and pic_apid=%s' % (st, sst, apid)
            dbres = dbcon.execute(que)
            pi1, pi1w = dbres.fetchall()[0]

            pi1val = int.from_bytes(tm[pi1:pi1 + pi1w//8], 'big')
            tag = '{}-{}-{}-{}'.format(st, sst, apid, pi1val)
            user_label, params = user_tm_decoders[tag]
            spid = None

            # Length of a parameter which should be decoded acording to given position
            if len(params[0]) == 9:
                vals_params = decode_pus(data, params)
            # Decode according to given order, length is then 11
            else:
                vals_params = read_variable_pckt(data, params)

            tmdata = [(get_calibrated(i[0], j[0]), i[6], i[1], pidfmt(i[7]), j) for i, j in zip(params, vals_params)]
            tmname = ['USER DEFINED: {}'.format(user_label)]

            return tmdata, tmname

        except Exception as err:
            logger.info('UDEF could not be found, search in IDB ({})'.format(err))
        finally:
            dbcon.close()

    try:

        if (tm[0] >> 4) & 1:
            return Tcdata(tm)

        header, data, crc = Tmread(tm)
        st, sst, apid = header.SERV_TYPE, header.SERV_SUB_TYPE, header.APID
        que = 'SELECT pic_pi1_off,pic_pi1_wid from pic where pic_type=%s and pic_stype=%s' % (st, sst)
        dbres = dbcon.execute(que)
        pi1, pi1w = dbres.fetchall()[0]

        if pi1 != -1:
            pi1val = int.from_bytes(tm[pi1:pi1 + pi1w//8], 'big')
            que = 'SELECT pid_spid,pid_tpsd,pid_dfhsize from pid where pid_type=%s and pid_stype=%s and ' \
                  'pid_apid=%s and pid_pi1_val=%s' % (st, sst, apid, pi1val)
        else:
            que = 'SELECT pid_spid,pid_tpsd,pid_dfhsize from pid where pid_type=%s and pid_stype=%s and ' \
                  'pid_apid=%s' % (st, sst, apid)

        dbres = dbcon.execute(que)
        fetch = dbres.fetchall()

        # if APID or SID does not match:
        if len(fetch) != 0:
            spid, tpsd, dfhsize = fetch[0]
        else:
            if (st, sst) != (3, 25):
                logger.info('APID {} not found for TM{},{} in I-DB -- not using APID'.format(apid, st, sst))
            if pi1 != -1:
                try:
                    tag = '{}-{}-{}-{}'.format(st, sst, apid, pi1val)
                    user_label, params = user_tm_decoders[tag]
                    spid = None
                except KeyError:
                    que = 'SELECT pid_spid,pid_tpsd,pid_dfhsize from pid where pid_type=%s and pid_stype=%s\
                           and pid_pi1_val=%s' % (st, sst, pi1val)
            else:
                que = 'SELECT pid_spid,pid_tpsd,pid_dfhsize from pid where pid_type=%s and pid_stype=%s' % (st, sst)
            if params is None:
                dbres = dbcon.execute(que)
                spid, tpsd, dfhsize = dbres.fetchall()[0]

        # TODO: proper handling of super-commutated parameters
        if tpsd == -1 and params is None:
            que = 'SELECT pcf.pcf_name,pcf.pcf_descr,plf_offby,plf_offbi,pcf.pcf_ptc,pcf.pcf_pfc,\
            pcf.pcf_unit,pcf.pcf_pid,pcf.pcf_width FROM plf LEFT JOIN pcf ON plf.plf_name=pcf.pcf_name WHERE \
            plf.plf_spid={} AND pcf_name NOT LIKE "DPTG%" AND pcf_name NOT LIKE "SCTG%" \
            ORDER BY plf_offby,plf_offbi'.format(spid)
            dbres = dbcon.execute(que)
            params = dbres.fetchall()
            vals_params = decode_pus(data, params)
            tmdata = [(get_calibrated(i[0], j[0]), i[6], i[1], pidfmt(i[7]), j) for i, j in zip(params, vals_params)]

        elif params is not None:
            # Length of a parameter which should be decoded according to given position
            if len(params[0]) == 9:
                vals_params = decode_pus(data, params)
            # Decode according to given order, length is then 11
            else:
                vals_params = read_variable_pckt(data, params)

            tmdata = [(get_calibrated(i[0], j[0]), i[6], i[1], pidfmt(i[7]), j) for i, j in zip(params, vals_params)]

        else:
            que = 'SELECT pcf.pcf_name,pcf.pcf_descr,pcf.pcf_ptc,pcf.pcf_pfc,pcf.pcf_curtx,pcf.pcf_width,\
            pcf.pcf_unit,pcf.pcf_pid,vpd_pos,vpd_grpsize,vpd_fixrep from vpd left join pcf on \
            vpd.vpd_name=pcf.pcf_name where vpd_tpsd={} AND pcf_name NOT LIKE "DPTG%" \
            AND pcf_name NOT LIKE "SCTG%" ORDER BY vpd_pos'.format(tpsd)
            dbres = dbcon.execute(que)
            params_in = dbres.fetchall()

            vals_params = read_variable_pckt(data, params_in)
            tmdata = [(get_calibrated(i[0], j), i[6], i[1], pidfmt(i[7]), j) for j, i in vals_params]
            # tmdata = [(get_calibrated(i[0], j[0]), i[6], i[1], pidfmt(i[7]), j) for i, j in zip(params, vals_params)]

        if spid is not None:
            dbres = dbcon.execute("SELECT pid_descr FROM pid WHERE pid_spid={}".format(spid))
            tmname = dbres.fetchall()[0]
        else:
            tmname = ['USER DEFINED: {}'.format(user_label)]

    except Exception as failure:
        raise Exception('Packet data decoding failed: ' + str(failure))

    finally:
        dbcon.close()

    return tmdata, tmname


def read_pus(data):
    """
    Read single PUS packet from buffer

    @param data: has to be peekable
    @return: single PUS packet as byte string or *None*
    """
    pus_size = data.peek(10)

    if len(pus_size) >= 6:
        pus_size = pus_size[4:6]
    elif 0 < len(pus_size) < 6:
        start_pos = data.tell()
        pus_size = data.read(6)[4:6]
        data.seek(start_pos)
    elif len(pus_size) == 0:
        return

    # packet size is header size (6) + pus size field + 1
    pckt_size = int.from_bytes(pus_size, 'big') + 7
    return data.read(pckt_size)


def extract_pus(data):
    """

    @param data:
    @return:
    """
    pckts = []
    if isinstance(data, bytes):
        data = io.BufferedReader(io.BytesIO(data))

    while True:
        pckt = read_pus(data)
        if pckt is not None:
            pckts.append(pckt)
        else:
            break
    return pckts


def extract_pus_brute_search(data, filename=None, trashcnt=None):
    """

    :param data:
    :param filename:
    :param trashcnt:
    :return:
    """
    pckts = []
    if trashcnt is None:
        trashcnt = {filename: 0}  # dummy counter if no trashcnt dict is given

    if isinstance(data, bytes):
        data = io.BufferedReader(io.BytesIO(data))
    elif isinstance(data, io.BufferedReader):
        pass
    else:
        raise TypeError('Cannot handle input of type {}'.format(type(data)))

    while True:
        pos = data.tell()
        pckt = read_pus(data)
        if pckt is not None:
            if not crc_check(pckt):
                pckts.append(pckt)
            else:
                data.seek(pos + 1)
                trashcnt[filename] += 1
        else:
            break

    return pckts


def unpack_pus(pckt, use_pktlen=False, logger=logger):
    """
    Decode PUS and return header parameters and data field

    :param pckt:
    :param use_pktlen: whether to use packet length info in header or just take all of the data after the header as payload
    :param logger:
    :return:
    """
    try:
        tmtc = pckt[0] >> 4 & 1
        dhead = pckt[0] >> 3 & 1

        if tmtc == 0 and dhead == 1 and (len(pckt) >= TM_HEADER_LEN):
            header = TMHeader()
            header.bin[:] = pckt[:TM_HEADER_LEN]
            if not use_pktlen:
                data = pckt[TM_HEADER_LEN:-PEC_LEN]
                crc = pckt[-PEC_LEN:]
            else:
                data = pckt[TM_HEADER_LEN:header.bits.PKT_LEN + 7 - PEC_LEN]
                crc = pckt[header.bits.PKT_LEN + 7 - PEC_LEN:header.bits.PKT_LEN + 7]

        elif tmtc == 1 and dhead == 1 and (len(pckt) >= TC_HEADER_LEN):
            header = TCHeader()
            header.bin[:] = pckt[:TC_HEADER_LEN]
            if not use_pktlen:
                data = pckt[TC_HEADER_LEN:-PEC_LEN]
                crc = pckt[-PEC_LEN:]
            else:
                data = pckt[TC_HEADER_LEN:header.bits.PKT_LEN + 7 - PEC_LEN]
                crc = pckt[header.bits.PKT_LEN + 7 - PEC_LEN:header.bits.PKT_LEN + 7]

        else:
            header = PHeader()
            header.bin[:P_HEADER_LEN] = pckt[:P_HEADER_LEN]
            if not use_pktlen:
                data = pckt[P_HEADER_LEN:]
            else:
                data = pckt[P_HEADER_LEN:header.bits.PKT_LEN + 7]
            crc = None

        head_pars = header.bits

    except Exception as err:
        logger.warning('Error unpacking PUS packet: {}\n{}'.format(pckt, err))
        head_pars = None
        data = None
        crc = None

    finally:
        return head_pars, data, crc


def decode_pus(tm_data, parameters, decode_tc=False):
    """

    :param tm_data:
    :param parameters:
    :param decode_tc:
    :return:
    """
    # checkedfmts = [fmtcheck(i[1]) for i in idb]
    # if not any(checkedfmts):
    # fmts = []
    # for par in parameters:
    #    if not par[4] in [7,8]:
    #        fmts.append(ptt[par[4]][par[5]])
    #    elif par[4] == 7:
    #        fmts.append('oct' + str(par[5]) + 's')
    #    elif par[4] == 8:
    #        fmts.append('ascii' + str(par[5]) + 's')
    if not decode_tc:
        fmts = [parameter_ptt_type_tm(par) for par in parameters]
    else:
        fmts = [parameter_ptt_type_tc_read(par) for par in parameters]

    try:
        return zip(struct.unpack('>' + ''.join(fmts), tm_data), parameters)
    except struct.error:
        tms = io.BytesIO(tm_data)
        if not decode_tc:
            return [(read_stream(tms, fmt, pos=par[2] - TM_HEADER_LEN, offbi=par[3]), par) for fmt, par in zip(fmts, parameters)]
        else:
            return [(read_stream(tms, fmt, pos=par[-1]/8, offbi=0 if par[5] % 8 == 0 else 8 - par[5] % 8), par) for fmt, par in zip(fmts, parameters)]


##
#  Read_stream
#
#  Reads out the imported Byte object
#  Returns the Unpackt parameter depending on the format of it
#  @param stream Input Bytes Object
#  @param fmt Input String that defines the format of the bytes
#  @param pos Input The BytePosition in the input bytes
#  @param offbi
def read_stream(stream, fmt, pos=None, offbi=0):
    """

    :param stream:
    :param fmt:
    :param pos:
    :param offbi:
    :return:
    """
    if pos is not None:
        stream.seek(int(pos))

    readsize = csize(fmt, offbi)
    data = stream.read(readsize)

    if not data:
        raise BufferError('No data left to read from [{}]!'.format(fmt))

    if fmt == 'I24':
        x = int.from_bytes(data, 'big')
    elif fmt == 'i24':
        x = int.from_bytes(data, 'big', signed=True)
    # for bit-sized unsigned parameters:
    elif fmt.startswith('uint'):
        bitlen = int(fmt[4:])
        # bitsize = (bitlen // 8 + 1) * 8
        bitsize = len(data) * 8
        x = (int.from_bytes(data, 'big') & (2 ** (bitsize - offbi) - 1)) >> (bitsize - offbi - bitlen)
    elif fmt.startswith('oct'):
        x = struct.unpack('>{}s'.format(fmt[3:]), data)[0]
    elif fmt.startswith('ascii'):
        x = struct.unpack('>{}s'.format(fmt[5:]), data)[0]
        try:
            x = x.decode('ascii')
        except UnicodeDecodeError as err:
            logger.warning(err)
            x = x.decode('utf-8', errors='replace')
    elif fmt == timepack[0]:
        x = timecal(data)
    else:
        x = struct.unpack('>' + fmt, data)[0]

    return x


def csize(fmt, offbi=0, bitsize=False):
    """
    Returns the amount of bytes required for the input format

    :param fmt: Input String that defines the format
    :param offbi:
    :return:
    """

    if bitsize:
        bits = 8
    else:
        bits = 1

    if fmt in ('i24', 'I24'):
        return 3
    elif fmt.startswith('uint'):
        return (int(fmt[4:]) + offbi - 1) // 8 + 1
    elif fmt == timepack[0]:
        return timepack[1] - timepack[3]
    elif fmt.startswith('oct'):
        return int(fmt[3:])
    elif fmt.startswith('ascii'):
        return int(fmt[5:])
    else:
        try:
            return struct.calcsize(fmt)
        except struct.error:
            raise NotImplementedError(fmt)


##
# parameter_ptt_type
#
# Returns the format of the input bytes for TM (list has to be formated the correct way)
# @param parameters Input List of one parameter
def parameter_ptt_type_tm(par):
    """

    :param par:
    :return:
    """
    return ptt(par[4], par[5])


##
# parameter_ptt_type
#
# Returns the format of the input bytes for TC (list has to be formated the correct way)
# @param parameters Input List of one parameter
def parameter_ptt_type_tc_read(par):
    """

    :param par:
    :return:
    """
    if par[2] is None:
        return ptt('SPARE_visible', par[5])
    else:
        return ptt(par[2], par[3])


##
#  Nonetoempty
#
#  Return empty string "" if input is _None_, else return input string
#  @param s Input string
def none_to_empty(s):
    """

    :param s:
    :return:
    """
    return '' if s is None else s


def str_to_int(itr):
    """

    :param itr:
    :return:
    """
    return int(itr) if itr.lower() != 'none' else None


def Tm_header_formatted(tm, detailed=False):
    """

    :param tm:
    :param detailed:
    :return:
    """

    # if len(tm) < TC_HEADER_LEN:
    #     return 'Cannot decode header - packet has only {} bytes!'.format(len(tm))
    # params = list(struct.unpack('>HHHBBB', tm[:9]))
    # params[0] &= 2047
    # params[1] &= 16383
    # del (params[3])

    head = Tmread(tm)[0]
    if head is None:
        return 'Cannot decode header - packet has only {} bytes!'.format(len(tm))

    if detailed:
        hparams = get_header_parameters_detailed(tm)
        hlist = '\n'.join(['{}: {}'.format(par, val) for par, val in hparams])
        details = '\n\n{}\n\n{}'.format(hlist, head._b_base_.raw.hex().upper())
    else:
        details = ''

    if head.PKT_TYPE == 1:
        return 'APID:{}|SEQ:{}|LEN:{}|TYPE:{}|STYPE:{}{}'.format(head.APID, head.PKT_SEQ_CNT, head.PKT_LEN,
                                                               head.SERV_TYPE, head.SERV_SUB_TYPE, details)
    else:
        return 'APID:{}|SEQ:{}|LEN:{}|TYPE:{}|STYPE:{}|CUC:{}{}'.format(
            head.APID, head.PKT_SEQ_CNT, head.PKT_LEN, head.SERV_TYPE, head.SERV_SUB_TYPE, mkcucstring(tm), details)


def spw_header_formatted(spw_header):
    """

    :param spw_header:
    :return:
    """
    buf = spw_header.__class__.__name__ + '\n\n'
    buf += spw_header.raw.hex()
    return buf


def get_header_parameters_detailed(pckt):
    """
    Return values of all header elements

    :param pckt:
    """
    head = Tmread(pckt)[0]
    hparams = [(x[0], getattr(head, x[0])) for x in head._fields_]
    return hparams


##
# CUC timestring
#
#  Generate timestring (seconds.microseconds) with (un-)synchronised flag (U/S) appended from TM packet (header data)
#  @param tml List of decoded TM packet header parameters or TM packet
def mkcucstring(tml):
    """

    :param tml:
    :return:
    """
    return timecal(tml[CUC_OFFSET:CUC_OFFSET+timepack[1]], string=True)


def get_cuc_now():
    """
    Returns the current UTC time in seconds since the reference epoch

    :return:
    """
    cuc = datetime.datetime.now(datetime.timezone.utc) - CUC_EPOCH
    return cuc.total_seconds()


def utc_to_cuc(utc):
    """
    Returns the time provided in seconds since the reference epoch

    :param utc: ISO formatted date-time string or timezone aware datetime object
    :return:
    """
    if isinstance(utc, str):
        cuc = datetime.datetime.fromisoformat(utc) - CUC_EPOCH
    else:
        cuc = utc - CUC_EPOCH
    return cuc.total_seconds()


def cuc_to_utc(cuc):
    """
    Returns the UTC date-time corresponding to the provided second offset from the reference epoch

    :param cuc: Seconds since the reference epoch
    :return:
    """
    utc = CUC_EPOCH + datetime.timedelta(seconds=cuc)
    return utc.isoformat()


def cuc_time_str(head, logger=logger):
    """
    Return PUS header timestamp as string

    :param head: TMHeader instance
    :param logger:
    :return:
    """
    try:
        if head.PKT_TYPE == 0 and head.SEC_HEAD_FLAG == 1:
            if head.TIMESYNC in tsync_flag:
                return '{:.6f}{}'.format(head.CTIME + head.FTIME / timepack[2], tsync_flag[head.TIMESYNC])
            else:
                logger.warning('Unknown timesync flag value {} in packet {}'.format(head.TIMESYNC, head.raw[:4].hex()))
                return '{:.6f}{}'.format(head.CTIME + head.FTIME / timepack[2], 'U')
        else:
            return ''
    except Exception as err:
        logger.info(err)
        return ''


##
#  Parametertooltiptext
#
#  Takes numerical value and returns corresponding hex and decimal values as a string.
#  Intended for parameter view tooltips.
def parameter_tooltip_text(x):
    """

    :param x:
    :return:
    """
    if isinstance(x, int):
        h = hex(x)[2:].upper()
        if np.sign(x) == -1:
            h = hex(x)[3:].upper()
    elif isinstance(x, float):
        h = struct.pack('>f', x).hex().upper()
    elif isinstance(x, bytes):
        return x.hex().upper()
    else:
        # h = str(x)
        return str(x)
    return 'HEX: 0x{}\nDEC: {}'.format(h, x)


def Tcdata(tm):
    """

    :param tm:
    :return:
    """
    header, data, crc = Tmread(tm)
    st, sst, apid = header.SERV_TYPE, header.SERV_SUB_TYPE, header.APID
    dbcon = scoped_session_idb

    # check if TC contains fixed value parameter for discrimination
    que = 'SELECT ccf_cname,cdf_bit,cdf_value,cpc_ptc,cpc_pfc, cpc_pafref FROM ccf LEFT JOIN cdf ON ccf_cname=cdf_cname ' \
          'LEFT JOIN cpc ON cdf_pname=cpc_pname WHERE ccf_type={} AND ccf_stype={} AND ccf_apid={} AND cdf_eltype="F"'.format(st, sst, apid)

    finfo = dbcon.execute(que).fetchall()
    if finfo:
        cname, offbit, cdfval, ptc, pfc, paf = finfo[0]
        fvalue = read_stream(io.BytesIO(data), ptt(ptc, pfc), pos=offbit // 8)

        for paf in [info[-1] for info in finfo]:
            fname = tc_param_alias_reverse(paf, None, fvalue)
            if fname != fvalue:
                break
        try:
            cname = [p[0] for p in finfo if p[2] == fname][0]
        except IndexError:
            raise ValueError('Unknown discriminant: {}'.format(fvalue))

        que = 'SELECT ccf_cname, ccf_descr, cpc_ptc, cpc_pfc, ccf_npars, cdf_ellen, cdf_pname, cpc_descr,cpc_prfref,' \
              ' cpc_pafref, cpc_ccaref, cdf_grpsize, cdf_bit FROM ccf left join cdf on ccf_cname=cdf_cname left join' \
              ' cpc on cdf_pname=cpc_pname where ccf_cname="{}" order by cdf_bit, ccf_cname'.format(cname)

    else:
        que = 'SELECT ccf_cname, ccf_descr, cpc_ptc, cpc_pfc, ccf_npars, cdf_ellen, cdf_pname, cpc_descr,\
             cpc_prfref, cpc_pafref, cpc_ccaref, cdf_grpsize, cdf_bit FROM ccf left join cdf on \
             ccf_cname=cdf_cname left join cpc on cdf_pname=cpc_pname where\
             ccf_type={} and ccf_stype={} and ccf_apid={} order by cdf_bit, ccf_cname'.format(st, sst, apid)

    # TODO: project agnostic implementation for decoding ambiguous TCs

    params = dbcon.execute(que).fetchall()

    if len(params) == 0:
        #dbres = dbcon.execute(
        #    'SELECT ccf_cname, ccf_descr, cpc_ptc, cpc_pfc, ccf_npars, cdf_ellen, cdf_pname, cpc_descr, cpc_prfref,\
        #     cpc_pafref, cpc_ccaref, cdf_grpsize, 0 FROM ccf left join cdf on ccf_cname=cdf_cname left join cpc on\
        #     cdf_pname=cpc_pname where ccf_type={} and ccf_stype={}'.format(st, sst))
        dbres = dbcon.execute(
            'SELECT ccf_cname, ccf_descr, cpc_ptc, cpc_pfc, ccf_npars, cdf_ellen, cdf_pname, cpc_descr, cpc_prfref,\
             cpc_pafref, cpc_ccaref, cdf_grpsize, cdf_bit FROM ccf left join cdf on ccf_cname=cdf_cname left join cpc on\
             cdf_pname=cpc_pname where ccf_type={} and ccf_stype={}'.format(st, sst))

        params = dbres.fetchall()

    dbcon.close()
    tcnames = list({x[1] for x in params})

    # return if no TC can be unambiguously assigned
    _npars = {x[4] for x in params}
    if len(tcnames) and len(_npars) > 1:
        tcdata = None
        tcnames.append("\n\nAmbiguous packet type - cannot decode.")
        return tcdata, tcnames

    # select one parameter set if IFSW and DBS have entry
    if len(tcnames) > 1:
        params = params[::len(tcnames)]
    if params[0][4] == 0:
        return None, tcnames
    # check for ambiguity in CCF table and choose a version
    # params = [x for x in params if x[0] == params[-1][0]]

    # check for spare parameters and insert format info
    for par in params:
        if par[2] is None:
            newpar = list(par)
            newpar[2] = 'SPARE_visible'
            newpar[3] = par[5]
            newpar[7] = 'Spare{}'.format(par[5])
            params[params.index(par)] = newpar

    # check for variable length packet
    var_len = any([p[-2] for p in params])

    if not var_len:
        #extr_fmt = ','.join([ptt[p[2]][p[3]] for p in params]) ### only 2010/6 key error 20
        try:
            #vals_params = data.unpack(extr_fmt)
            # The Parameters needed are on different locations for TM and TC but for read_variable_pct they need to have this order, therefor we change the order of the list shortly and than change it back
            vals_params = decode_pus(data, params, decode_tc=True)

        except IndexError:
            vals_params = None
            #print(traceback.format_exc())
    else:
        #repeat = 0
        #outlist = []
        #datastream = BitStream(data)
        try:
            vals_params = read_variable_pckt(data, params, tc=True)
        except IndexError:
            vals_params = None
    if vals_params:
        tcdata = [(tc_param_alias_reverse(*p[9:11], val, p[6]), None, p[7], val) for val, p in vals_params]
        #tcdata = [(tc_param_alias_reverse(*p[9:11], val, p[6]), None, p[7], val) for p, val in zip(params, o)]
        #print(tcdata)
    else:
        tcdata = None
        tcnames.append("\n\nAmbiguous packet type - cannot decode.")
    return tcdata, tcnames


# Decode TM bytestring into list of TM packet values
#   @param pckt TM bytestring
def Tmread(pckt):
    """

    :param pckt:
    :return:
    """
    return unpack_pus(pckt)
    # try:
    #     tmtc = pckt[0] >> 4 & 1
    #     dhead = pckt[0] >> 3 & 1
    #
    #     if tmtc == 0 and dhead == 1 and (len(pckt) >= TM_HEADER_LEN):
    #         header = TMHeader()
    #         header.bin[:] = pckt[:TM_HEADER_LEN]
    #         data = pckt[TM_HEADER_LEN:-PEC_LEN]
    #         crc = pckt[-PEC_LEN:]
    #
    #     elif tmtc == 1 and dhead == 1 and (len(pckt) >= TC_HEADER_LEN):
    #         header = TCHeader()
    #         header.bin[:] = pckt[:TC_HEADER_LEN]
    #         data = pckt[TC_HEADER_LEN:-PEC_LEN]
    #         crc = pckt[-PEC_LEN:]
    #
    #     else:
    #         header = TCHeader()
    #         header.bin[:P_HEADER_LEN] = pckt[:P_HEADER_LEN]
    #         data = pckt[P_HEADER_LEN:]
    #         crc = None
    #
    #     head_pars = header.bits
    #
    # except Exception as err:
    #     # print('Error unpacking packet: {}\n{}'.format(pckt, err))
    #     logger.warning('Error unpacking packet: {}\n{}'.format(pckt, err))
    #     head_pars = None
    #     data = None
    #     crc = None
    #
    # finally:
    #     return head_pars, data, crc


##
#  Generate (space separated) hexstring from byte/bitstring
#  @param inbytes   bytestring or bitstring object to be converted
#  @param separator string by which the hex doublettes are joined, default=' '
def prettyhex(inbytes, separator=' '):
    """

    :param inbytes:
    :param separator:
    :return:
    """
    if not isinstance(inbytes, bytes):
        inbytes = inbytes.bytes
    return separator.join(['%02X' % x for x in inbytes])


##
#  Varpack
#
#  Decode variable-length part of TM/TC source data
#  @param data    input data of bitstring.BitStream type
#  @param parameters  list of parameter properties present in data
#  @param paramid parameter counter
#  @param outlist list of decoded source data parameter values
#  @param parlist list of decoded source data parameter properties
def read_varpack(data, parameters, paramid, outlist, parlist):
    """

    :param data:
    :param parameters:
    :param paramid:
    :param outlist:
    :param parlist:
    :return:
    """
    while paramid < len(parameters):
        fmt = ptt(parameters[paramid][2], parameters[paramid][3])
        if parameters[paramid][2] == 11:  # TODO: handle deduced parameter types
            raise NotImplementedError('Deduced parameter type PTC=11')
            # fmt = fmt[ptype]
            # if ptype == 7:  # ptt fmt string for bool not parsable with .read
            #     fmt = 'uint8'
        outdata = data.read(fmt)
        grpsize = parameters[paramid][-2]
        if parameters[paramid][6] == FMT_TYPE_PARAM:
            ptype = outdata
        outlist.append(outdata)
        parlist.append(parameters[paramid])

        if grpsize == 0:
            paramid += 1
        else:
            if parlist[-1][-1] == 0:
                repeat = outlist[-1]
            else:
                repeat = parlist[-1][-1]
                data.pos -= parlist[-1][5]
                # delete counter entry from lists
                outlist.pop(-1)
                parlist.pop(-1)

            while repeat > 0:
                outlist, parlist = read_varpack(data, parameters[paramid + 1:paramid + grpsize + 1], 0,
                                                outlist, parlist)
                repeat -= 1
            paramid += grpsize + 1
    return outlist, parlist


def read_variable_pckt(tm_data, parameters, tc=False):
    """
    Read parameters from a variable length packet

    :param tm_data:
    :param parameters:
    :return:
    """
    tms = io.BytesIO(tm_data)
    result = []

    result = read_stream_recursive(tms, parameters, decoded=result, tc=tc)

    return result


def read_stream_recursive(tms, parameters, decoded=None, bit_off=0, tc=False):
    """
    Recursively operating function for decoding variable length packets

    :param tms:
    :param parameters:
    :param decoded:
    :param bit_off:
    :param tc:
    :return:
    """

    decoded = [] if decoded is None else decoded

    skip = 0
    for par_idx, par in enumerate(parameters):
        if skip > 0:
            skip -= 1
            continue
        grp = par[-2]

        if grp is None:  # None happens for UDEF
            grp = 0

        fmt = ptt(par[2], par[3])
        if fmt == 'deduced':
            raise NotImplementedError('Deduced parameter type PTC=11')

        fixrep = par[-1]

        # don't use fixrep in case of a TC, since it is only defined for TMs
        if grp and fixrep and not tc:
            value = fixrep
            logger.debug('{} with fixrep={} used'.format(par[1], value))
        else:
            bits = par[5]
            unaligned = bits % 8

            value = read_stream(tms, fmt, offbi=bit_off)

            bit_off = (bit_off + unaligned) % 8
            # re-read byte if read position is bit-offset after previous parameter
            if bit_off:
                tms.seek(tms.tell() - 1)

            decoded.append((value, par))

        if grp != 0:
            skip = grp
            rep = value
            while rep > 0:
                decoded = read_stream_recursive(tms, parameters[par_idx + 1:par_idx + 1 + grp], decoded, bit_off=bit_off, tc=tc)
                rep -= 1

    return decoded


def tc_param_alias_reverse(paf, cca, val, pname=None):
    """

    :param paf:
    :param cca:
    :param val:
    :param pname:
    :return:
    """
    if paf is not None:
        dbcon = scoped_session_idb
        que = 'SELECT pas_altxt from pas where pas_numbr="%s" and pas_alval="%s"' % (paf, val)
        dbres = dbcon.execute(que)
        alval = dbres.fetchall()
        dbcon.close()
        if len(alval) == 0:
            return val
        return alval[0][0]
    elif cca is not None:
        dbcon = scoped_session_idb
        que = 'SELECT ccs_xvals,ccs_yvals from ccs where ccs_numbr="%s"' % (cca)
        dbres = dbcon.execute(que)
        xvals, yvals = np.array([x for x in zip(*dbres.fetchall())], dtype=float)
        dbcon.close()
        alval = np.interp(val, yvals, xvals)
        return alval
    # get name for ParamID if datapool item (in MIB)
    elif pname in DATA_POOL_ID_PARAMETERS:
        return get_pid_name(pidfmt_reverse(val))
    else:
        return val


def get_pid_name(pid):
    """

    :param pid:
    :return:
    """
    # if isinstance(pid, str):
    #     return pid
    if isinstance(pid, int):
        pids = [pid]

    try:
        names = [DP_IDS_TO_ITEMS[p] for p in pids]
    except KeyError as err:
        logger.warning('Unknown datapool ID')
        raise err

    # if pid in DP_IDS_TO_ITEMS:
    #     return DP_IDS_TO_ITEMS[pid]
    # else:
    #     logger.warning('Unknown datapool ID: {}'.format(pid))
    #     return pid
    return names if len(names) > 1 else names[0]


##
#  Format PID from I-DB value to int
def pidfmt(val):
    """

    :param val:
    :return:
    """
    return int(val - pid_offset) if val is not None else None


def pidfmt_reverse(val):
    """

    :param val:
    :return:
    """
    return int(val + pid_offset) if val is not None else None


## Parameter calibration
#  Calibrate raw parameter values
#  @param pcf_name PCF_NAME
#  @param rawval   Raw value of the parameter
def get_calibrated(pcf_name, rawval, properties=None, numerical=False, dbcon=None, nocal=False):
    """

    :param pcf_name:
    :param rawval:
    :param properties:
    :param numerical:
    :param dbcon:
    :param nocal:
    :return:
    """
    if properties is None:

        # cache
        if pcf_name in _pcf_cache:
            if _pcf_cache[pcf_name] is None:
                return rawval if isinstance(rawval, (int, float, str, bytes)) else rawval[0]
            else:
                ptc, pfc, categ, curtx = _pcf_cache[pcf_name]

        else:
            que = 'SELECT pcf.pcf_ptc,pcf.pcf_pfc,pcf.pcf_categ,pcf.pcf_curtx from pcf where pcf_name="%s"' % pcf_name
            dbres = scoped_session_idb.execute(que)
            fetch = dbres.fetchall()
            scoped_session_idb.close()
            if len(fetch) == 0:
                _pcf_cache[pcf_name] = None
                return rawval if isinstance(rawval, (int, float, str, bytes)) else rawval[0]

            ptc, pfc, categ, curtx = fetch[0]
            _pcf_cache[pcf_name] = (ptc, pfc, categ, curtx)

    else:
        ptc, pfc, categ, curtx = properties

    try:
        type_par = ptt(ptc, pfc)
    except NotImplementedError:
        try:
            return rawval if isinstance(rawval, (int, float)) else rawval[0]
        except IndexError:
            return rawval

    if type_par == timepack[0]:
        return timecal(rawval)
    # elif categ == 'T' or type_par.startswith('ascii'):
    #     return rawval
    elif type_par.startswith('oct'):
        return rawval.hex().upper()
    elif curtx is None:
        try:
            return rawval if isinstance(rawval, (int, float)) else rawval[0]
        except IndexError:
            return rawval
    elif curtx is not None and categ == 'N':
        if nocal:
            return rawval
        else:
            return get_cap_yval(pcf_name, rawval)
    elif curtx is not None and categ == 'S':
        if numerical or nocal:
            return rawval
        return get_txp_altxt(pcf_name, rawval)
    else:
        return rawval


##
#  Numerical calibration
#
#  Calibrate raw parameter values
#  @param pcf_name PCF_NAME
#  @param xval     Raw value of the parameter
def get_cap_yval(pcf_name, xval, properties=None, dbcon=None):
    """

    :param pcf_name:
    :param xval:
    :param properties:
    :param dbcon:
    :return:
    """

    # cache
    if pcf_name in _cap_cache:
        if _cap_cache[pcf_name] is None:
            return xval
        xvals, yvals = _cap_cache[pcf_name]

    else:
        que = 'SELECT cap.cap_xvals,cap.cap_yvals from pcf left join cap on pcf.pcf_curtx=cap.cap_numbr\
                where pcf.pcf_name="%s"' % pcf_name
        dbres = scoped_session_idb.execute(que)

        try:
            xvals, yvals = np.array([x for x in zip(*dbres.fetchall())], dtype=float)
            if np.isnan(xvals).any() or np.isnan(yvals).any():
                logger.error('Error in CAP support points for {}'.format(pcf_name))
                _cap_cache[pcf_name] = None
                return xval
            sortidx = xvals.argsort()
            xvals, yvals = xvals[sortidx], yvals[sortidx]  # make sure value pairs are sorted in x-ascending order
            _cap_cache[pcf_name] = (xvals, yvals)
        except IndexError:
            return xval
        finally:
            scoped_session_idb.close()

    yval = np.interp(xval, xvals, yvals, left=np.nan, right=np.nan)  # return NAN if outside defined calibration range

    # if yval == np.nan:
    #     logger.info('Calibration of {} failed. Value {} outside calibrated range {}-{}'.format(pcf_name, xval, xvals.min(), xvals.max()))

    return format(yval, 'g')


##
#  Textual calibration
#
#  Calibrate raw parameter values
#  @param pcf_name PCF_NAME
#  @param alval    Raw value of the parameter
def get_txp_altxt(pcf_name, alval, dbcon=None):
    """

    :param pcf_name:
    :param alval:
    :param dbcon:
    :return:
    """

    # cache
    if (pcf_name, alval) in _txp_cache:
        altxt = _txp_cache[(pcf_name, alval)]
        return altxt

    dbcon = scoped_session_idb
    que = 'SELECT txp.txp_altxt from pcf left join txp on pcf.pcf_curtx=txp.txp_numbr where\
            pcf.pcf_name="%s" and txp.txp_from=%s' % (pcf_name, alval if isinstance(alval, int) else alval[0])
    dbres = dbcon.execute(que)
    try:
        altxt, = dbres.fetchall()[0]
        _txp_cache[(pcf_name, alval)] = altxt
    except IndexError:
        altxt = alval
    finally:
        dbcon.close()
    return altxt


##
#  Dump list of TM packets
#
#  Save list of TM packets to file as either "binary" or "hex"
#  @param filename  Path to file
#  @param tmlist    List of TM packets
#  @param mode      Save as "binary" file or "hex" values with one packet per line
#  @param st_filter Save only packets of this service type
def Tmdump(filename, tmlist, mode='hex', st_filter=None, check_crc=False):
    """

    :param filename:
    :param tmlist:
    :param mode:
    :param st_filter:
    :param check_crc:
    """
    if st_filter is not None:
        tmlist = Tm_filter_st(tmlist, **st_filter)

    if check_crc:
        tmlist = (tm for tm in tmlist if not crc_check(tm))

    if mode.lower() == 'hex':
        with open(filename, 'w') as f:
            f.write('\n'.join([prettyhex(tm) for tm in tmlist]))
    elif mode.lower() == 'binary':
        if isinstance(tmlist, types.GeneratorType):
            with open(filename, 'wb') as f:
                for tm in tmlist:
                    f.write(tm)
        else:
            with open(filename, 'wb') as f:
                f.write(b''.join(tmlist))
    elif mode.lower() == 'text':
        txtlist = []
        for tm in tmlist:
            try:
                txtlist.append(Tmformatted(tm, separator='; '))
            except Exception as err:
                # logger.warning(err)
                txtlist.append(Tm_header_formatted(tm) + '; ' + str(tm[TM_HEADER_LEN:]))
        with open(filename, 'w') as f:
            f.write('\n'.join(txtlist))


def Tm_filter_st(tmlist, st=None, sst=None, apid=None, sid=None, time_from=None, time_to=None):
    """
    From tmlist return list of packets that match the specified criteria

    :param tmlist:
    :param st:
    :param sst:
    :param apid:
    :param sid:
    :param time_from:
    :param time_to:
    :return:
    """

    if st is not None:
        tmlist = [tm for tm in tmlist if tm[7] == st]

    if sst is not None:
        tmlist = [tm for tm in tmlist if tm[8] == sst]

    if apid is not None:
        # tmlist = [tm for tm in list(tmlist) if ((struct.unpack('>H', tm[:2])[0] & 2047) == apid)]
        tmlist = [tm for tm in list(tmlist) if (int.from_bytes(tm[:2], 'big') & 0x7FF) == apid]

    if sid:
        if st is None or sst is None or apid is None:
            raise ValueError('Must provide st, sst and apid if filtering by sid')

        sid_offset, sid_bitlen = get_sid(st, sst, apid)
        tobyte = sid_offset + sid_bitlen // 8
        tmlist = [tm for tm in list(tmlist) if int.from_bytes(tm[sid_offset:tobyte], 'big') == sid]

    if time_from is not None:
        tmlist = [tm for tm in list(tmlist) if (time_from <= get_cuctime(tm))]

    if time_to is not None:
        tmlist = [tm for tm in list(tmlist) if (get_cuctime(tm) <= time_to)]

    return tmlist


def filter_rows(rows, st=None, sst=None, apid=None, sid=None, time_from=None, time_to=None, idx_from=None, idx_to=None,
                tmtc=None, get_last=False):
    """
    Filter SQL query object by any of the given arguments, return filtered query.

    :param rows:
    :param st:
    :param sst:
    :param apid:
    :param sid:
    :param time_from:
    :param time_to:
    :param idx_from:
    :param idx_to:
    :param tmtc:
    :param get_last:
    """

    if st is not None:
        rows = rows.filter(DbTelemetry.stc == st)

    if sst is not None:
        rows = rows.filter(DbTelemetry.sst == sst)

    if apid is not None:
        rows = rows.filter(DbTelemetry.apid == apid)

    if sid:
        if st is None or sst is None or apid is None:
            raise ValueError('Must provide st, sst and apid if filtering by sid')

        sid_offset, sid_bitlen = get_sid(st, sst, apid)
        if sid_offset != -1:
            sid_size = sid_bitlen // 8
            rows = rows.filter(
                func.mid(DbTelemetry.data, sid_offset - TM_HEADER_LEN + 1, sid_size) == sid.to_bytes(sid_size, 'big'))
        else:
            logger.error('SID ({}) not applicable for {}-{}-{}'.format(sid, st, sst, apid))

    if time_from is not None:
        rows = rows.filter(func.left(DbTelemetry.timestamp, func.length(DbTelemetry.timestamp) - 1) >= time_from)

    if time_to is not None:
        rows = rows.filter(func.left(DbTelemetry.timestamp, func.length(DbTelemetry.timestamp) - 1) <= time_to)

    if idx_from is not None:
        rows = rows.filter(DbTelemetry.idx >= idx_from)

    if idx_to is not None:
        rows = rows.filter(DbTelemetry.idx <= idx_to)

    if tmtc is not None:
        rows = rows.filter(DbTelemetry.is_tm == tmtc)

    if get_last:
        rows = rows.order_by(DbTelemetry.idx.desc()).first()

    return rows


##
#  CRC check
#
#  Perform a CRC check on the _packet_. Returns True if CRC!=0.
#  @param packet TM/TC packet or any bytestring or bitstring object to be CRCed.
def crc_check(packet):
    """
    This function returns *True* if the CRC result is non-zero

    :param packet:
    :return:
    """
    return bool(crc(packet))


def get_cuctime(tml):
    """

    :param tml:
    :return:
    """
    cuc_timestamp = None
    if tml is not None:
        if isinstance(tml, bytes):
            return timecal(tml[CUC_OFFSET:CUC_OFFSET + timepack[1]], string=False)
            #ct, ft = struct.unpack('>IH', tml[TM_HEADER_LEN - 7:TM_HEADER_LEN - 1])
            #ft >>= 1
        elif isinstance(tml, pc.TMHeaderBits):
            ct = tml.CTIME
            ft = tml.FTIME
        elif isinstance(tml, list):
            if isinstance(tml[0], tuple):
                ct = tml[0][0][13]
                ft = tml[0][0][14]
            else:
                ct, ft = tml[13:15]
        elif isinstance(tml, tuple):
            ct = tml[0][13]
            ft = tml[0][14]
        else:
            raise TypeError("Can't handle input of type '{}'".format(type(tml)))

        # calculate the timestamp
        # the fine time, consisting out of 2 octets (2x 8bit), has a resolution of 2^16 - 1 bit = 2^15
        resolution = timepack[2]
        if ft > resolution:
            logger.warning('get_cuctime: the finetime value {} is larger than its resolution of {}'.format(ft, resolution))
            raise ValueError(
                'get_cuctime: the finetime value {} is larger than its resolution of {}'.format(ft, resolution))

        cuc_timestamp = ct + ft / resolution

    return cuc_timestamp


def get_pool_rows(pool_name, check_existence=False):
    """

    :param pool_name:
    :param check_existence:
    :return:
    """
    dbcon = scoped_session_storage()

    if check_existence:
        check = dbcon.query(DbTelemetryPool).filter(DbTelemetryPool.pool_name == pool_name)
        if not check.count():
            dbcon.close()
            raise ValueError('Pool "{}" does not exist.'.format(pool_name))

    rows = dbcon.query(
        DbTelemetry
    ).join(
        DbTelemetryPool,
        DbTelemetry.pool_id == DbTelemetryPool.iid
    ).filter(
        DbTelemetryPool.pool_name == pool_name
    )

    dbcon.close()

    return rows


# get values of parameter from HK packets
def get_param_values(tmlist=None, hk=None, param=None, last=0, numerical=False, tmfilter=True, pool_name=None, mk_array=True, nocal=False):
    """

    :param tmlist:
    :param hk:
    :param param:
    :param last:
    :param numerical:
    :param tmfilter:
    :param pool_name:
    :param mk_array:
    :param nocal:
    :return:
    """
    if param is None:
        return

    if tmlist is None and pool_name is not None:
        tmlist = get_pool_rows(pool_name, check_existence=True)

    dbcon = scoped_session_idb()
    if hk is None:
        que = 'SELECT plf.plf_name,plf.plf_spid,plf.plf_offby,plf.plf_offbi,pcf.pcf_ptc,pcf.pcf_pfc,pcf.pcf_unit,\
                   pcf.pcf_descr,pid.pid_apid,pid.pid_type,pid.pid_stype,pid.pid_descr,pid.pid_pi1_val from pcf\
                   left join plf on pcf.pcf_name=plf.plf_name left join pid on pid.pid_spid=plf.plf_spid\
                   where plf.plf_name="{}"'.format(param)
        dbres = dbcon.execute(que)
        name, spid, offby, offbi, ptc, pfc, unit, descr, apid, st, sst, hk, sid = dbres.fetchall()[0]
        if not isinstance(tmlist, list):
            tmlist_rows = filter_rows(tmlist, st=st, sst=sst, apid=apid, sid=sid)
            if tmlist_rows is not None:
                if last > 1:
                    tmlist = [tm.raw for tm in tmlist_rows.yield_per(1000)[-last:]]
                else:
                    tmlist = [tmlist_rows.order_by(DbTelemetry.idx.desc()).first().raw]
            else:
                tmlist = []
        else:
            sid = None if sid == 0 else sid

        ufmt = ptt(ptc, pfc)

    elif hk != 'User defined' and not hk.startswith('UDEF|'):
        que = 'SELECT pid_descr, pid_type,pid_stype,pid_pi1_val,pid_apid,plf.plf_name,plf.plf_spid,plf.plf_offby,plf.plf_offbi,\
                   pcf.pcf_ptc,pcf.pcf_pfc,pcf.pcf_unit,pcf.pcf_descr,pcf.pcf_pid from pid left join plf on\
                   pid.pid_spid=plf.plf_spid left join pcf on plf.plf_name=pcf.pcf_name where\
                   pcf.pcf_descr="%s" and pid.pid_descr="%s"' % (param, hk)
        dbres = dbcon.execute(que)
        hkdescr, st, sst, sid, apid, name, spid, offby, offbi, ptc, pfc, unit, descr, pid = dbres.fetchall()[0]

        sid = None if sid == 0 else sid
        ufmt = ptt(ptc, pfc)

    elif hk.startswith('UDEF|'):
        label = hk.replace('UDEF|', '')
        hkref = [k for k in user_tm_decoders if user_tm_decoders[k][0] == label][0]
        pktinfo = user_tm_decoders[hkref][1]
        parinfo = [x for x in pktinfo if x[1] == param][0]
        pktkey = hkref.split('-')

        st = int(pktkey[0])
        sst = int(pktkey[1])
        apid = int(pktkey[2]) if pktkey[2] != 'None' else None
        sid = int(pktkey[3]) if pktkey[3] != 'None' else None
        # name, descr, _, offbi, ptc, pfc, unit, _, bitlen = parinfo
        _, descr, ptc, pfc, curtx, bitlen, _, _, _, _, _ = parinfo
        unit = None
        name = None
        offbi = 0

        offby = sum([x[5] for x in pktinfo[:pktinfo.index(parinfo)]]) // 8 + TM_HEADER_LEN  # +TM_HEADER_LEN for header
        # tmlist_filt = Tm_filter_st(tmlist, st, sst, apid, sid)[-last:] if tmfilter else tmlist[-last:]
        ufmt = ptt(ptc, pfc)

    else:
        userpar = json.loads(cfg[CFG_SECT_PLOT_PARAMETERS][param])
        st = int(userpar['ST'])
        sst = int(userpar['SST'])
        apid = int(userpar['APID'])
        sid = None if (('SID' not in userpar) or (userpar['SID'] is None)) else int(userpar['SID'])
        # tmlist_filt = Tm_filter_st(tmlist, userpar['ST'], userpar['SST'], apid=userpar['APID'], sid=sid)[-last:] if tmfilter else tmlist[-last:]
        offby, ufmt = userpar['bytepos'], userpar['format']
        offbi = userpar['offbi'] if 'offbi' in userpar else 0
        descr, unit, name = param, None, None

    bylen = csize(ufmt)
    tmlist_filt = Tm_filter_st(tmlist, st=st, sst=sst, apid=apid, sid=sid)[-last:] if tmfilter else tmlist[-last:]

    if name is not None:
        que = 'SELECT pcf.pcf_categ,pcf.pcf_curtx from pcf where pcf_name="%s"' % name
        dbres = dbcon.execute(que)
        fetch = dbres.fetchall()

        if not fetch:
            logger.error('Parameter {} not found in MIB.'.format(name))
            return

        categ, curtx = fetch[0]
        xy = [(get_cuctime(tm),
               get_calibrated(name, read_stream(io.BytesIO(tm[offby:offby + bylen]), ufmt, offbi=offbi),
                              properties=[ptc, pfc, categ, curtx], numerical=numerical, nocal=True)) for tm in tmlist_filt]  # no calibration here, done below on array

    else:
        xy = [(get_cuctime(tm), read_stream(io.BytesIO(tm[offby:offby + bylen]), ufmt, offbi=offbi)) for tm in tmlist_filt]

    dbcon.close()

    if not mk_array:
        return xy, (descr, unit)

    try:
        arr = np.array(np.array(xy).T, dtype='float')
        # calibrate y values
        if not nocal and name is not None:
            get_cap_yval(name, arr[1, 0])  # calibrate one value to get name into _cap_cache
            if _cap_cache[name] is None:
                return arr, (descr, unit)
            xvals, yvals = _cap_cache[name]
            arr[1, :] = np.interp(arr[1, :], xvals, yvals, left=np.nan, right=np.nan)
        return arr, (descr, unit)

    except (ValueError, IndexError):
        return np.array(xy, dtype='float, U32'), (descr, unit)


def Hk_filter(tmlist, st, sst, apid=None, sid=None):
    """

    :param tmlist:
    :param st:
    :param sst:
    :param apid:
    :param sid:
    :return:
    """
    # if apid in (None, '') and sid not in (0, None):
    #     return [tm for tm in tmlist if (
    #             len(tm) > TM_HEADER_LEN and (tm[7], tm[8], tm[TM_HEADER_LEN]) == (st, sst, sid))]
    # elif sid not in (0, None):
    #     return [tm for tm in tmlist if (
    #             len(tm) > TM_HEADER_LEN and (tm[7], tm[8], struct.unpack('>H', tm[:2])[0] & 0b0000011111111111,
    #                                          tm[TM_HEADER_LEN]) == (st, sst, apid, sid))]
    return Tm_filter_st(tmlist, st=st, sst=sst, apid=apid, sid=sid)


def show_extracted_packet():
    """
    Get packet data selected in Pool Viewer

    :return:
    """
    pv = dbus_connection('poolviewer', communication['poolviewer'])
    if not pv:
        logger.warning('Could not obtain selected packets from PV!')
        return

    return eval(pv.Functions('selected_packet'))


def packet_selection():
    """
    Alias for show_extracted_packet call

    :return:
    """
    return show_extracted_packet()


def get_module_handle(module_name, instance=1, timeout=5):
    """
    Try getting the DBUS proxy object for the module_name module for timeout seconds.

    :param module_name:
    :param instance:
    :param timeout:
    :return:
    """
    if instance is None:
        instance = communication[module_name]

    module = None

    t1 = time.time()
    while (time.time() - t1) < timeout:
        try:
            # module = dbus.SessionBus().get_object(cfg.get('ccs-dbus_names', module_name) + str(instance), '/Messagelistner')
            module = dbus_connection(module_name, instance)
            if module:
                break
            else:
                time.sleep(.2)
        except dbus.DBusException as err:
            logger.info(err)
            module = False
            time.sleep(0.5)

    if module:
        return module
    else:
        logger.error('No running {} instance found'.format(module_name.upper()))
        return False


def _get_ccs_dbus_names(exclude=None):
    if exclude is None:
        exclude = []

    dbus_names = dbus.SessionBus().list_names()
    ccs_names = [cfg['ccs-dbus_names'][mod] for mod in cfg['ccs-dbus_names'] if mod not in exclude]
    ccs_modules = [mod for mod in dbus_names if mod.startswith(tuple(ccs_names))]

    return ccs_modules


def _quit_module(module_name, instance=1):
    mod = get_module_handle(module_name, instance=instance)

    if not mod:
        logger.error('{}{} not found on DBus').format(module_name, instance)
        return False

    try:
        mod.Functions('quit_func')
        return True
    except Exception as err:
        logger.exception(err)
        return False


def _close_modules():
    dbus_names = _get_ccs_dbus_names(exclude=['editor'])
    while dbus_names:
        print(dbus_names)
        for module in dbus_names:
            _, name, iid = module.split('.')
            success = _quit_module(name, int(iid.replace('communication', '')))
            if success:
                logger.info('Closed {}'.format(module))
            else:
                logger.error('Could not close {}'.format(module))

        dbus_names = _get_ccs_dbus_names(exclude=['editor'])

    logger.info('Closed all modules')


def connect(pool_name, host, port, protocol='PUS', is_server=False, timeout=10, delete_abandoned=False, try_delete=True,
            pckt_filter=None, options='', drop_rx=False, drop_tx=False):
    """
    Accessibility function for 'connect' in pus_datapool

    :param pool_name:
    :param host:
    :param port:
    :param return_socket:
    :param is_server:
    :param timeout:
    :param delete_abandoned:
    :param try_delete:
    :param pckt_filter:
    :param options:
    :param drop_rx:
    :param drop_tx:
    :param protocol:
    :return:
    """
    pmgr = get_module_handle('poolmanager')

    if not pmgr:
        return

    kwarguments = str({'protocol': protocol,
                       'is_server': is_server,
                       'timeout': timeout,
                       'delete_abandoned': delete_abandoned,
                       'try_delete': try_delete,
                       'pckt_filter': pckt_filter,
                       'options': options,
                       'drop_rx': drop_rx,
                       'drop_tx': drop_tx})

    # kwarguments = {'return_socket': return_socket, 'is_server': is_server, 'timeout': timeout,
    #                'delete_abandoned': delete_abandoned, 'try_delete': try_delete, 'pckt_filter': pckt_filter,
    #                'options': options, 'drop_rx': drop_rx, 'drop_tx': drop_tx, 'protocol': protocol}

    pmgr.Functions('connect', pool_name, host, port, {'kwargs': dbus.Dictionary({'options': kwarguments,
                                                                                 'override_with_options': '1'})})


def connect_tc(pool_name, host, port, protocol='PUS', drop_rx=True, timeout=10, is_server=False, use_socket=None,
               options=''):
    """
    Accessibility function for 'connect_tc' in pus_datapool

    :param pool_name:
    :param host:
    :param port:
    :param drop_rx:
    :param protocol:
    :param timeout:
    :param is_server:
    :param use_socket:
    :param options:
    :return:
    """
    pmgr = get_module_handle('poolmanager')

    if not pmgr:
        return

    kwarguments = str({'protocol': protocol,
                       'is_server': is_server,
                       'timeout': timeout,
                       'options': options,
                       'drop_rx': drop_rx,
                       'use_socket': use_socket})

    pmgr.Functions('connect_tc', pool_name, host, port, {'kwargs': dbus.Dictionary({'options': kwarguments,
                                                                                    'override_with_options': '1'})})


##
#  TC send (DB)
#
#  @param cmd       CCF_DESCR string of the TC to be issued
#  @param args      Parameters required by the TC specified with _cmd_
#  @param ack       Override the I-DB TC acknowledment value (4-bit binary string, e.g., '0b1011')
#  @param pool_name Name of pool bound to socket connected to the C&C port
#  @param sleep     Idle time in seconds after the packet has been sent. Useful if function is called repeatedly in a
#  loop to prevent too many packets are being sent over the socket in a too short time interval.
def Tcsend_DB(cmd, *args, ack=None, pool_name=None, sleep=0., no_check=False, pkt_time=False, **kwargs):
    """
    Build and send a TC packet whose structure is defined in the MIB. Note that for repeating parameter groups
    the arguments are interleaved, e.g., ParID1, ParVal1, ParID2, ParVal2,... Use the function *interleave_lists*
    to create a list ordered that way.

    :param cmd: command name as specified in *CCF_DESCR*
    :type cmd: str
    :param args: unpacked list of (calibrated) TC parameter values, order is as specified in the MIB
    :param ack: override acknowledge flags in PUS header
    :type ack: int
    :param pool_name:
    :param sleep:
    :param no_check:
    :param pkt_time:
    :param kwargs:
    :return:
    """
    t1 = time.time()

    pmgr = dbus_connection('poolmanager', communication['poolmanager'])
    if not pmgr:
        return

    try:
        tc, (st, sst, apid) = Tcbuild(cmd, *args, ack=ack, no_check=no_check, **kwargs)
    except TypeError as e:
        raise e

    if pool_name is None:
        pool_name = pmgr.Variables('tc_name')

    sent = _tcsend_common(tc, apid, st, sst, pool_name=pool_name, pkt_time=pkt_time)

    dt = time.time() - t1
    time.sleep(max(sleep - dt, 0))

    return sent


##
#  Generate TC
def Tcbuild(cmd, *args, sdid=0, ack=None, no_check=False, hack_value=None, source_data_only=False, **kwargs):
    """
    Create TC bytestring for CMD with corresponding parameters

    :param cmd: CCF_DESCR string of the requested TC
    :param args: Parameters required by the cmd
    :param sdid:
    :param ack: Override the I-DB TC acknowledment value (4-bit binary, e.g., 0b1011)
    :param no_check:
    :param hack_value:
    :param source_data_only:
    :param kwargs:
    :return:
    """
    # with self.poolmgr.lock:
    # que = 'SELECT ccf_type,ccf_stype,ccf_apid,ccf_npars,cdf.cdf_grpsize,cdf.cdf_eltype,cdf.cdf_ellen,' \
    #       'cdf.cdf_value,cpc.cpc_ptc,cpc.cpc_pfc,cpc.cpc_descr,cpc.cpc_pname FROM ccf LEFT JOIN cdf ON ' \
    #       'cdf.cdf_cname=ccf.ccf_cname LEFT JOIN cpc ON cpc.cpc_pname=cdf.cdf_pname ' \
    #       'WHERE BINARY ccf_descr="%s"' % cmd
    # dbcon = scoped_session_idb
    # params = dbcon.execute(que).fetchall()
    # dbcon.close()

    try:
        params = _get_tc_params(cmd)
    except SQLOperationalError:
        scoped_session_idb.close()
        params = _get_tc_params(cmd)

    try:
        st, sst, apid, npars = params[0][:4]
    except IndexError:
        # print('Unknown command "{}"'.format(cmd))
        # Notify.Notification.new('Unknown command "{}"'.format(cmd)).show()
        raise NameError('Unknown command "{}"'.format(cmd))

    if ack is None:
        ack = bin(Tcack(cmd))

    if npars == 0:
        pdata = b''

        if source_data_only:
            return pdata

    else:
        # check for padded parameters
        padded, = np.where(np.array([i[5] for i in params]) == 'A')
        if len(padded) != 0:
            for n in padded:
                x = list(params[n])
                x[-4:-2] = 'SPARE', x[-6]
                params.remove(params[n])
                params.insert(n, tuple(x))

        if np.any([i[4] for i in params]):
            varpos, = np.where([i[4] for i in params])
            grpsize = params[varpos[0]][4]
            # are there any spares/fixed before the rep. counter? TODO: nested/multiple repetition counters?
            pars_noedit = [p for p in params[:varpos[0]] if p[5] in ('A', 'F')]
            npars_noedit = len(pars_noedit)
            repfac = int(args[varpos[0] - npars_noedit])

            fix = encode_pus(params[:varpos[0] + 1], *[tc_param_alias(p[-1], v, no_check=no_check) for p, v in
                         zip_no_pad(params[:varpos[0] + 1], args[:varpos[0] - npars_noedit + 1])])

            if not [i[8] for i in params].count(11):
                var = encode_pus(repfac * params[varpos[0] + 1:varpos[0] + 1 + grpsize], *[tc_param_alias(p[-1], v, no_check=no_check) for p, v in
                                                    zip_no_pad(repfac * params[varpos[0] + 1:varpos[0] + 1 + grpsize],
                                                    args[varpos[0] - npars_noedit + 1:varpos[0] - npars_noedit + 1 + grpsize * repfac])])

            # for derived type parameters, not supported for SMILE
            else:
                raise NotImplementedError("Deduced parameter types in TCs are not supported!")
                # formats, args2 = build_packstr_11(st, sst, apid, params, varpos[0], grpsize, repfac, *args,
                #                                        no_check=no_check)
                # #var = pack(fstring, *args2[varpos[0] + 1:varpos[0] + 1 + grpsize * repfac])
                # var = encode_pus(formats, *args2[varpos[0] + 1:varpos[0] + 1 + grpsize * repfac])

            # add the parameters after the variable part, if any
            npars_with_var = varpos[0] + grpsize + 1
            if len(params) > npars_with_var:
                fix2 = encode_pus(params[npars_with_var:],
                                  *[tc_param_alias(p[-1], v, no_check=no_check) for p, v in
                                    zip_no_pad(params[npars_with_var:],
                                               args[npars_with_var - npars_noedit + grpsize * (repfac - 1):])])
            else:
                fix2 = b''

            pdata = fix + var + fix2

            if source_data_only:
                return pdata

        else:
            if hack_value is None:
                values = [tc_param_alias(p[-1], v, no_check=no_check) for p, v in zip_no_pad(params, args)]
            else:
                values = hack_value

            pdata = encode_pus(params, *values)

            if source_data_only:
                return pdata

    return Tcpack(st=st, sst=sst, apid=int(apid), data=pdata, sdid=sdid, ack=ack, **kwargs), (st, sst, apid)


def _get_tc_params(cmd, paf_cal=False):

    if paf_cal:
        que = 'SELECT ccf_type,ccf_stype,ccf_apid,ccf_npars,cdf.cdf_grpsize,cdf.cdf_eltype,cdf.cdf_ellen,' \
              'cdf.cdf_value,cpc.cpc_ptc,cpc.cpc_pfc,cpc.cpc_descr,cpc.cpc_pname,cpc.cpc_pafref FROM ccf LEFT JOIN cdf ON ' \
              'cdf.cdf_cname=ccf.ccf_cname LEFT JOIN cpc ON cpc.cpc_pname=cdf.cdf_pname ' \
              'WHERE BINARY ccf_descr="%s"' % cmd
    else:
        que = 'SELECT ccf_type,ccf_stype,ccf_apid,ccf_npars,cdf.cdf_grpsize,cdf.cdf_eltype,cdf.cdf_ellen,' \
              'cdf.cdf_value,cpc.cpc_ptc,cpc.cpc_pfc,cpc.cpc_descr,cpc.cpc_pname FROM ccf LEFT JOIN cdf ON ' \
              'cdf.cdf_cname=ccf.ccf_cname LEFT JOIN cpc ON cpc.cpc_pname=cdf.cdf_pname ' \
              'WHERE BINARY ccf_descr="%s"' % cmd

    params = scoped_session_idb.execute(que).fetchall()
    scoped_session_idb.close()
    return params


def encode_pus(params, *values, params_as_fmt_string=False):
    """

    :param params:
    :param values:
    :param params_as_fmt_string:
    :return:
    """
    if params_as_fmt_string or isinstance(params, str):
        return struct.pack(params, *values)

    if not isinstance(values, list):
        values = list(values)

    ed_pars = [param for param in params if param[5] not in ['A', 'F']]
    if len(ed_pars) != len(values):
        raise ValueError('Wrong number of parameters: Expected {}, but got {}.\n{}'.format(len(ed_pars), len(values), ', '.join(x[10] for x in ed_pars)))

    params_nospares = [param for param in params if param[5] not in ['A']]

    # insert fixed parameter values, cdf_value=param[7]
    for i, par in enumerate(params_nospares):
        if par[5] == 'F':
            values.insert(i, tc_param_alias(par[-1], par[7]))

    fmts = [parameter_ptt_type_tc(par) for par in params]

    # deduced parameter types are not supported for TCs
    if 'deduced' in fmts:
        raise NotImplementedError("Deduced parameter types in TCs are not supported! ({})".format(', '.join([p[-2] for p in params if p[8] == 11])))

    try:
        fmt_string = '>'+''.join(fmts)
        return struct.pack(fmt_string, *values)

    except struct.error as err:
        logger.debug(err)
        # proper insertion of spares
        vals_iter = iter(values)
        return b''.join([pack_bytes(fmt, next(vals_iter)) if not fmt.endswith('x') else struct.pack(fmt) for fmt in fmts])


def pack_bytes(fmt, value, bitbuffer=0, offbit=0):
    """

    :param fmt:
    :param value:
    :param bitbuffer:
    :param offbit:
    :return:
    """
    if fmt == 'I24':
        x = value.to_bytes(3, 'big')

    elif fmt == 'i24':
        x = value.to_bytes(3, 'big', signed=True)

    elif fmt.startswith('uint'):
        bitlen = int(fmt[4:])
        bitsize = (bitlen // 8 + 1) * 8
        shifted = (value << (bitsize - bitlen - offbit)) + bitbuffer
        if (bitsize - bitlen - offbit) == 0:
            x = shifted.to_bytes(bitsize // 8, 'big')
        else:
            return shifted

    elif fmt.startswith('oct'):
        if not isinstance(value, (bytes, bytearray)):
            raise TypeError('Value packed with fmt "{}" is not an octet string: {} {}!'.format(fmt, value, type(value)))
        if len(value) != int(fmt[3:]):
            logger.warning('Length of octet string ({}) does not match format {}!'.format(len(value), fmt))
        x = struct.pack('>{}s'.format(fmt[3:]), value)

    elif fmt.startswith('ascii'):
        if not isinstance(value, str):
            raise TypeError('Value packed with fmt "{}" is not a string: {} {}!'.format(fmt, value, type(value)))
        if len(value) != int(fmt[5:]):
            logger.warning('Length of string ({}) does not match format {}!'.format(len(value), fmt))
        x = struct.pack('>{}s'.format(fmt[5:]), value.encode(encoding='ascii'))

    elif fmt == timepack[0]:
        x = calc_timestamp(value, sync=None, return_bytes=True)

    elif value is None:
        x = struct.pack('>' + fmt)

    else:
        x = struct.pack('>' + fmt, value)

    return x


def date_to_cuc_bytes(date, sync=None):
    """
    Create CUC time bytes from date string.

    :param date: date as ISO formatted string
    :param sync: CUC sync flag, if None sync byte is omitted
    """
    if sync in [1, True]:
        sync = 'S'
    elif sync in [0, False]:
        sync = 'U'

    date = duparser.parse(date)

    if date.utcoffset() is None:
        date = date.replace(tzinfo=datetime.timezone.utc)

    float_time = utc_to_cuc(date)
    return calc_timestamp(float_time, sync=sync, return_bytes=True)


##
# parameter_ptt_type
#
# Returns the format of the input bytes for TC (list has to be formated the correct way)
# @param parameters Input List of one parameter
def parameter_ptt_type_tc(par):
    """

    :param par:
    :return:
    """
    return ptt(par[-4], par[-3])


##
#  Acknowledgement
#
#  Get type acknowledgement type for give service (sub-)type and APID from I-DB
#  @param st   Service type
#  @param sst  Service sub-type
#  @param apid APID of TC
def Tcack(cmd):
    """
    Get type acknowledgement type for give service (sub-)type and APID from I-DB

    :param cmd:
    :return:
    """
    que = 'SELECT ccf_ack FROM ccf WHERE BINARY ccf_descr="{}"'.format(cmd)
    dbcon = scoped_session_idb
    ack = int(dbcon.execute(que).fetchall()[0][0])
    dbcon.close()
    return ack

##
#  Parameter alias
#
#  Numerical/textual calibration and range check for value val of parameter param
#  @param param CPC_PNAME
#  @param val   Parameter value
def tc_param_alias(param, val, no_check=False):
    """
    Numerical/textual calibration and range check for value val of parameter param

    :param param:
    :param val:
    :param no_check:
    :return:
    """
    que = 'SELECT cpc_prfref,cpc_ccaref,cpc_pafref,cpc_descr,cpc_categ from cpc where cpc_pname="%s"' % param
    dbcon = scoped_session_idb
    prf, cca, paf, pdesc, categ = dbcon.execute(que).fetchall()[0]
    # this is a workaround for datapool items not being present in PAF/PAS table # DEPRECATED!
    # if param in ['DPP70004', 'DPP70043']:  # DataItemID in TC(211,1)
    #     val = get_pid(val)
    # else:
    #     pass

    # check if parameter holds a data pool ID (categ=P) and look up numerical value in case it is given as string
    if categ == 'P' and isinstance(val, str):
        try:
            val = DP_ITEMS_TO_IDS[val]
        except KeyError:
            raise KeyError('Unknown data pool item "{}"'.format(val))

    if (not no_check) and (prf is not None):
        in_range, error = tc_param_in_range(prf, val, pdesc)
        if not in_range:
            raise ValueError('Range check failed\n{}'.format(error))
        else:
            # subtract offset from PID to be compatible with IASW (CHEOPS only)
            if categ == 'P':
                val -= pid_offset
    else:
        if categ == 'P':
            val -= pid_offset

    if paf is not None:

        que = 'SELECT pas_alval from pas where pas_numbr="%s" and pas_altxt="%s"' % (paf, val)
        dbres = dbcon.execute(que)
        try:
            alval, = dbres.fetchall()[0]
        except IndexError as error:
            if no_check:
                alval = val
                logger.info('Inserting unchecked value for {}: {}'.format(pdesc, val))
            else:
                que = 'SELECT pas_altxt from pas where pas_numbr="%s"' % paf
                alvals = [x[0] for x in dbcon.execute(que).fetchall()]
                raise ValueError('Invalid {} value: {}. Allowed values are: {}.'.format(pdesc, val, ', '.join(alvals)))
        finally:
            dbcon.close()

        return int(alval)
    elif cca is not None:

        que = 'SELECT ccs_xvals,ccs_yvals from ccs where ccs_numbr="%s"' % cca
        dbres = dbcon.execute(que)
        xvals, yvals = np.array([x for x in zip(*dbres.fetchall())], dtype=float)
        dbcon.close()
        alval = int(np.interp(val, xvals, yvals))

        return alval
    else:

        dbcon.close()

        return val


##
#  Get PID
#  Translates name of data pool variable to corresponding ID, based on DP_ITEMS_TO_IDS look-up table
#  @param paramname Name of the data pool variables
def get_pid(parnames):
    """

    :param parnames:
    :return:
    """
    # if isinstance(parnames, int):
    #     return parnames
    if isinstance(parnames, str):
        parnames = [parnames]

    # if len(set(parnames)) != len(parnames):
    #     msg = "Duplicate parameters will be ignored! {}".format(set([p for p in parnames if parnames.count(p) > 1]))
    #     logger.warning(msg)

    pids = [DP_ITEMS_TO_IDS[parname] for parname in parnames]

    return pids if len(pids) > 1 else pids[0]


def get_sid(st, sst, apid):
    """

    :param st:
    :param sst:
    :param apid:
    :return:
    """
    if (st, sst, apid) in SID_LUT:
        return SID_LUT[(st, sst, apid)]
    else:
        try:
            logger.warning('APID {} not known'.format(apid))
            return SID_LUT[(st, sst, None)]
        except KeyError:
            return


##
#  Parameter range check
#
#  Check if parameter is within specified range
#  @param prf   PRV_NUMBR
#  @param val   Parameter value
#  @param pdesc Parameter DESCR
def tc_param_in_range(prf, val, pdesc):
    """

    :param prf:
    :param val:
    :param pdesc:
    :return:
    """
    que = 'SELECT prf_dspfmt,prf_radix,prv_minval,prv_maxval FROM prv INNER JOIN prf ON prf_numbr=prv_numbr WHERE prv_numbr="{}"'.format(prf)
    dbcon = scoped_session_idb
    prfs = dbcon.execute(que).fetchall()
    dbcon.close()
    if prfs[0][0] in ['I', 'U', 'R']:  # numerical range check if not text encoded (A) parameter
        if prfs[0][1] == 'D':
            ranges = [(int(pval[2], 10), int(pval[3], 10)) for pval in prfs]
        elif prfs[0][1] == 'H':
            ranges = [(int(pval[2], 16), int(pval[3], 16)) for pval in prfs]
        if not any([rng[0] <= float(val) <= rng[1] for rng in ranges]):
            limits = ' | '.join(['{:}-{:}'.format(*rng) for rng in ranges])
            # print('Parameter %s out of range: %s [valid: %s]' % (pdesc, val, limits))
            logger.warning('Parameter %s out of range: %s [valid: %s]' % (pdesc, val, limits))
            # Notify.Notification.new('Parameter %s out of range: %s [valid: %s]' % (pdesc, val, limits)).show()
            return False, 'Parameter %s out of range: %s [valid: %s]' % (pdesc, val, limits)
    elif prfs[0][0] == 'A':
        if val not in [i[2] for i in prfs]:
            valid = ' | '.join([i[2] for i in prfs])
            # print('Invalid parameter value for %s: %s [valid: %s]' % (pdesc, val, valid))
            logger.warning('Invalid parameter value for %s: %s [valid: %s]' % (pdesc, val, valid))
            # Notify.Notification.new('Invalid parameter value for %s: %s [valid: %s]' % (pdesc, val, valid)).show()
            return False, 'Invalid parameter value for %s: %s [valid: %s]' % (pdesc, val, valid)
    else:
        logger.warning('Range check for parameter type "{}" not implemented [{}]'.format(prfs[0][0], pdesc))  # TODO: no check for time ranges yet
    return True, ''


##
#  ZIPnoPAD
#
#  Zip parameter-value pairs, skipping padding parameters
#  @param params List of TC parameter properties
#  @param args   List of supplied parameter values
def zip_no_pad(params, args):
    """

    :param params:
    :param args:
    :return:
    """
    # [params.pop(i) for i, j in enumerate(params) if j[-4] in ['SPARE', 'PAD']]
    params = [param for param in params if param[5] not in ['A', 'F']]
    return zip(params, args)


##
#  Generate PUS packet
#
#  Create TC packet conforming to PUS
#  @param version version number
#  @param typ     packet type (TC/TM)
#  @param dhead   data field header flag
#  @param apid    application data ID
#  @param gflags  sequence flags
#  @param sc      sequence count
#  @param pktl    packet length
#  @param tmv     PUS version
#  @param ack     acknowledgement flags
#  @param st      service type
#  @param sst     service sub-type
#  @param destid  source/destination ID
#  @param data    application data
def Tmpack(data=b'', apid=321, st=1, sst=1, destid=0, version=0, typ=0, timestamp=0, dhead=1, gflags=0b11,
           sc=None, tmv=PUS_VERSION, tref_stat=0, msg_type_cnt=0, pktl=None, chksm=None, **kwargs):
    """
    Create TM packet conforming to PUS

    :param data:
    :param apid:
    :param st:
    :param sst:
    :param destid:
    :param version:
    :param typ:
    :param timestamp:
    :param dhead:
    :param gflags:
    :param sc:
    :param tmv:
    :param tref_stat:
    :param msg_type_cnt:
    :param pktl:
    :param chksm:
    :param kwargs:
    :return:
    """
    if pktl is None:
        # pktl = len(data) * 8 + (TC_HEADER_LEN + PEC_LEN - 7)  # 7=-1(convention)+6(datahead)+2(CRC) # len(data) *8, data in bytes has to be bits
        pktl = len(data) + (TM_HEADER_LEN + PEC_LEN - 7)

    if sc is None:
        sc = counters.setdefault(int(str(apid), 0), 1) % 2 ** 14  # wrap around counter to fit in 14 bits
        if sc == 0:
            sc += 1
            counters[int(str(apid))] += 1  # 0 is not allowed for seq cnt

    tm = PUSpack(version=version, typ=typ, dhead=dhead, apid=apid, gflags=gflags, sc=sc, pktl=pktl,
                 tmv=tmv, st=st, sst=sst, sdid=destid, timestamp=timestamp, tref_stat=tref_stat,
                 msg_type_cnt=msg_type_cnt, data=data, **kwargs)

    if chksm is None:
        chksm = crc(tm)
    tm += struct.pack('>H', chksm)

    return tm


##
#  Generate PUS packet
#
#  Create TC packet conforming to PUS
#  @param version version number
#  @param typ     packet type (TC/TM)
#  @param dhead   data field header flag
#  @param apid    application data ID
#  @param gflags  sequence flags
#  @param sc      sequence count
#  @param pktl    packet length
#  @param tmv     PUS version
#  @param ack     acknowledgement flags
#  @param st      service type
#  @param sst     service sub-type
#  @param sdid    source/destination ID
#  @param data    application data
def Tcpack(data=b'', apid=0x14c, st=1, sst=1, sdid=0, version=0, typ=1, dhead=1, gflags=0b11, sc=None,
           tmv=PUS_VERSION, ack=0b1001, pktl=None, chksm=None, **kwargs):
    """
    Create TC packet conforming to PUS

    :param data:
    :param apid:
    :param st:
    :param sst:
    :param sdid:
    :param version:
    :param typ:
    :param dhead:
    :param gflags:
    :param sc:
    :param tmv:
    :param ack:
    :param pktl:
    :param chksm:
    :param kwargs:
    :return:
    """
    if pktl is None:
        pktl = len(data) + (TC_HEADER_LEN + PEC_LEN - 7)  # 7=-1(convention)+6(datahead)+2(CRC)

    if sc is None:
        sc = counters.setdefault(int(str(apid), 0), 1) % 2 ** 14  # wrap around counter to fit in 14 bits
        if sc == 0:
            sc += 1
            counters[int(str(apid))] += 1  # 0 is not allowed for seq cnt
    tc = PUSpack(version=version, typ=typ, dhead=dhead, apid=int(str(apid), 0), gflags=int(str(gflags), 0),
                 sc=sc, pktl=pktl, tmv=tmv, ack=int(str(ack), 0), st=st, sst=sst, sdid=sdid, data=data, **kwargs)

    if chksm is None:
        chksm = crc(tc)

    tc += chksm.to_bytes(2, 'big')  # 16 bit CRC

    return tc


##
#  Generate PUS packet
#
#  @param version version number
#  @param typ     packet type (TC/TM)
#  @param dhead   data field header flag
#  @param apid    application data ID
#  @param gflags  sequence flags
#  @param sc      sequence count
#  @param pktl    packet length
#  @param tmv     PUS version
#  @param ack     acknowledgement flags
#  @param st      service type
#  @param sst     service sub-type
#  @param sdid    source/destination ID
#  @param data    application data
def PUSpack(version=0, typ=0, dhead=0, apid=0, gflags=0b11, sc=0, pktl=0,
            tmv=PUS_VERSION, ack=0, st=0, sst=0, sdid=0, tref_stat=0, msg_type_cnt=0, timestamp=0, data=b'', **kwargs):
    """
    Create bytestring conforming to PUS with no CRC appended, for details see PUS documentation

    :param version:
    :param typ:
    :param dhead:
    :param apid:
    :param gflags:
    :param sc:
    :param pktl:
    :param tmv:
    :param ack:
    :param st:
    :param sst:
    :param sdid:
    :param tref_stat:
    :param msg_type_cnt:
    :param timestamp:
    :param data:
    :param kwargs:
    :return:
    """
    if typ == 1 and dhead == 1:
        header = TCHeader()
    elif typ == 0 and dhead == 1:
        header = TMHeader()
    else:
        header = PHeader()

    header.bits.PKT_VERS_NUM = version
    header.bits.PKT_TYPE = typ
    header.bits.SEC_HEAD_FLAG = dhead
    header.bits.APID = apid
    header.bits.SEQ_FLAGS = gflags
    header.bits.PKT_SEQ_CNT = sc
    header.bits.PKT_LEN = pktl

    # PUS-A
    if PUS_VERSION == 1:
        if typ == 1 and dhead == 1:
            header.bits.CCSDS_SEC_HEAD_FLAG = 0
            header.bits.PUS_VERSION = tmv
            header.bits.ACK = ack
            header.bits.SERV_TYPE = st
            header.bits.SERV_SUB_TYPE = sst
            header.bits.SOURCE_ID = sdid

        elif typ == 0 and dhead == 1:
            header.bits.SPARE1 = 0
            header.bits.PUS_VERSION = tmv
            header.bits.SPARE2 = 0
            header.bits.SERV_TYPE = st
            header.bits.SERV_SUB_TYPE = sst
            header.bits.DEST_ID = sdid
            ctime, ftime, sync = calc_timestamp(timestamp)
            sync = 0 if sync is None else sync
            header.bits.CTIME = ctime
            header.bits.FTIME = ftime
            header.bits.TIMESYNC = sync
            # header.bits.SPARE = 0

    # PUS-C
    elif PUS_VERSION == 2:
        if typ == 1 and dhead == 1:
            header.bits.PUS_VERSION = tmv
            header.bits.ACK = ack
            header.bits.SERV_TYPE = st
            header.bits.SERV_SUB_TYPE = sst
            header.bits.SOURCE_ID = sdid

        elif typ == 0 and dhead == 1:
            header.bits.PUS_VERSION = tmv
            header.bits.SC_REFTIME = tref_stat
            header.bits.SERV_TYPE = st
            header.bits.SERV_SUB_TYPE = sst
            header.bits.MSG_TYPE_CNT = msg_type_cnt
            header.bits.DEST_ID = sdid
            ctime, ftime, sync = calc_timestamp(timestamp)
            sync = 0 if sync is None else sync
            header.bits.CTIME = ctime
            header.bits.FTIME = ftime
            header.bits.TIMESYNC = sync

    else:
        raise NotImplementedError('Invalid PUS version: {}'.format(PUS_VERSION))

    return bytes(header.bin) + data


##
#  Build Packstring 11
#
#  Create pack string if datatypes are defined in the packet iti.e. PTC/PTF=11/0
#  @param st      Service type
#  @param sst     Service sub-tpye
#  @param apid    APID of TC
#  @param params  List of parameter properties
#  @param varpos  Position of the parameter indicating repetition
#  @param grpsize Parameter group size
#  @param repfac  Number of parameter (group) repetitions
def build_packstr_11(st, sst, apid, params, varpos, grpsize, repfac, *args, no_check=False):
    """

    :param st:
    :param sst:
    :param apid:
    :param params:
    :param varpos:
    :param grpsize:
    :param repfac:
    :param args:
    :param no_check:
    :return:
    """
    ptypeindex = [i[-1] == FMT_TYPE_PARAM for i in params].index(True)  # check where fmt type defining parameter is
    ptype = args[varpos + ptypeindex::grpsize]
    args2 = list(args)
    args2[varpos + 1:] = [tc_param_alias(param[-1], val, no_check=no_check) for param, val in
                          zip(params[varpos + 1:] * repfac, args[varpos + 1:])]
    ptc = 0
    varlist = []
    for par in params[varpos + 1:] * repfac:
        if par[-4] != 11:
            #varlist.append(ptt[par[-4]][par[-3]])
            varlist.append(parameter_ptt_type_tc(par))
        else:
            varlist.append(
                ptt(par[-4], par[-3])[tc_param_alias(FMT_TYPE_PARAM, ptype[ptc], no_check=no_check)])
            #varlist.append(ptt[par[-4]][par[-3]][tc_param_alias('DPP70044', ptype[ptc], no_check=no_check)])
            ptc += 1
    return varlist, args2


##
# TC send (common part of Tcsend_DB and Tcsend)
#
#  @param tc_bytes  TC as byte array to send
#  @param apid      APID of the TC, as hex-string
#  @param st        Service type of TC
#  @param sst       Service sub-type of TC
#  @param sleep     Idle time in seconds after the packet has been sent. Useful if function is called repeatedly in a loop to prevent too many packets are being sent over the socket in a too short time interval.
#  @param pool_name Name of pool bound to socket connected to the C&C port
def _tcsend_common(tc_bytes, apid, st, sst, sleep=0., pool_name='LIVE', pkt_time=False):

    global counters

    # Note: in general, it is not possible to obtain the OBC time, thus the last packet time is used if available
    if pkt_time:
        t = get_last_pckt_time(pool_name=pool_name, string=False)
        if t is None:
            t = 0
    # Alternatively, use local time (JD)
    else:
        t = time.time()

    sent = Tcsend_bytes(tc_bytes, pool_name)
    if not sent:
        return

    # get the SSC of the sent packet
    ssc = counters[int(str(apid), 0)]
    # increase the SSC counter
    counters[int(str(apid), 0)] += 1
    # More specific Logging format that is compatible with the TST
    log_dict = dict([('st', st),('sst', sst),('ssc', ssc),('apid', apid),('timestamp', t)])
    json_string = '{} {}'.format('#SENT TC', json.dumps(log_dict))
    logger.debug(json_string)
    # time.sleep(sleep)
    return apid, ssc, t


# get the CUC timestamp of the lastest TM packet
#   @param pool_name: name of the pool
#   @param string: <boolean> if true the CUC timestamp is returned as a string, otherwise as a float
#   @return: <CUC> timestamp or None if failing
def get_last_pckt_time(pool_name='LIVE', string=True):
    """

    :param pool_name:
    :param string:
    :return:
    """
    pmgr = dbus_connection('poolmanager', communication['poolmanager'])

    if not pmgr:
        logger.warning('Accessing PMGR failed!')
        return

    packet = None
    # fetch the pool_name
    try:
        poolname = pmgr.Dictionaries('loaded_pools', pool_name)
    except (dbus.DBusException, KeyError):
        logger.error('Pool {} is not connected/accessible!'.format(pool_name))
        return

    filename = poolname[2]  # 3rd entry is the filename of the named tuple, named tuple not possible via dbus
    if not filename:
        filename = pool_name

    # get the first packet from the pool
    dbcon = scoped_session_storage
    row = dbcon.query(
        DbTelemetry
    ).join(
        DbTelemetryPool,
        DbTelemetry.pool_id == DbTelemetryPool.iid
    ).filter(
        DbTelemetryPool.pool_name == filename, DbTelemetry.is_tm == 0
    ).order_by(
        DbTelemetry.idx.desc()
    ).first()
    dbcon.close()
    if row is not None:
        packet = row.raw
    else:
        logger.warning('get_packet_from_pool: failed to get packets from query')

    # extract the CUC timestamp
    if string:
        if packet is None:
            cuc = ''
        else:
            cuc = mkcucstring(packet)
    else:
        if packet is None:
            cuc = None
        else:
            try:
                cuc = get_cuctime(packet)
            except:
                logger.error(
                    'This TM packet does not have valid CUC timestamp fields:\n\t\tHeader: {}\n\t\tData: {}'
                    .format(Tmread(packet), Tmdata(packet)))
                cuc = None
    return cuc


def _has_tc_connection(pool_name, pmgr_handle):
    try:
        if not pmgr_handle.Functions('_is_tc_connection_active', pool_name):
            logger.error('"{}" is not connected to any TC socket!'.format(pool_name))
            return False
        else:
            return True
    except Exception as err:
        logger.error(err)
        return False


def _get_pmgr_handle(tc_pool=None):
    pmgr = dbus_connection('poolmanager', communication['poolmanager'])

    if not pmgr:
        return False

    # check if pool is connected
    if tc_pool is not None and not _has_tc_connection(tc_pool, pmgr):
        return False

    return pmgr


def Tcsend_bytes(tc_bytes, pool_name='LIVE', pmgr_handle=None):
    """

    :param tc_bytes:
    :param pool_name:
    :param pmgr_handle:
    :return:
    """
    if not pmgr_handle:
        pmgr = _get_pmgr_handle(pool_name)
    else:
        pmgr = pmgr_handle

    # Tell dbus with signature = that you send a byte array (ay), otherwise does not allow null bytes
    try:
        pmgr.Functions('tc_send', pool_name, tc_bytes, signature='ssay')
        return True
    except (dbus.DBusException, AttributeError):
        logger.error('Failed to send packet of length {} to {}!'.format(len(tc_bytes), pool_name))
        return False
    # logger.debug(msg)
    # pmgr.Functions('tc_send', pool_name, tc_bytes, ignore_reply=True)


##
#  Send C&C command
#
#  Send command to C&C socket
#  @param pool_name Name of the pool bound to the socket for CnC/TC communication
#  @param cmd         Command string to be sent to C&C socket
def CnCsend(cmd, pool_name=None, apid=1804):
    """

    :param cmd:
    :param pool_name:
    :param apid:
    :return:
    """
    global counters  # One can only Change variable as global since we are static
    # pmgr = dbus_connection('poolmanager', communication['poolmanager'])
    pmgr = get_module_handle('poolmanager')
    if pool_name is None:
        pool_name = pmgr.Variables('tc_name')

    pid = (apid >> 4) & 0x7F
    cat = apid & 0xF
    packed_data = CnCpack(data=cmd, pid=pid, cat=cat, sc=counters.setdefault(apid, 1))

    received = pmgr.Functions('socket_send_packed_data', packed_data, pool_name, signature='says')
    logger.info('[CNC sent:]' + str(packed_data))

    try:
        msg = bytes(received)
    except TypeError as err:
        logger.error(err)
        return

    if msg:
        counters[apid] += 1
        try:
            msg = msg.decode('ascii', errors='replace')
        except Exception as err:
            logger.error(err)
            return

        logger.info('[CNC response:] ' + msg)
        return msg


##
#  Generate CnC packet
#
#  Create packet conforming to C&C definition
#  @param data    Application data, as ASCII string
#  @param version Version number ('011' binary)
#  @param typ     Packet type, 0=TM, 1=TC
#  @param dhead   Data field header flag. C&C has no DFH (=0)
#  @param pid     Application PID
#  @param cat     Category
#  @param gflags  Segmentation flags
#  @param sc      Sequence counter
def CnCpack(data=b'', version=0b011, typ=1, dhead=0, pid=112, cat=12, gflags=0b11, sc=0):
    """

    :param data:
    :param version:
    :param typ:
    :param dhead:
    :param pid:
    :param cat:
    :param gflags:
    :param sc:
    :return:
    """
    if isinstance(data, str):
        data = data.encode('ascii')

    header = PHeader()
    header.bits.PKT_VERS_NUM = version
    header.bits.PKT_TYPE = typ
    header.bits.SEC_HEAD_FLAG = dhead
    header.bits.APID = (pid << 4) + cat
    header.bits.SEQ_FLAGS = gflags
    header.bits.PKT_SEQ_CNT = sc
    header.bits.PKT_LEN = len(data) - 1

    return bytes(header.bin) + data


##
#  Send data to socket
#
#  Send bytestring to specified socket
#  @param data      Bytestring to be sent to socket
#  @param pool_name Name of pool bound to Python socket for CnC/TC communication
def Datasend(data, pool_name):
    """

    :param data:
    :param pool_name:
    :return:
    """
    pmgr = dbus_connection('poolmanager', communication['poolmanager'])

    if not pmgr:
        return

    if _has_tc_connection(pool_name, pmgr):
        pmgr.Functions('tc_send', pool_name, data)


##
#  Limits check
#
#  Check if TM parameter is within specified limits. Return 0 if ok, 1 if out of soft limit, 2 if out of hard limit.
#  @param param OCF_NAME
#  @param val   Parameter value
def Tm_limits_check(param, val, user_limit: dict = None, dbcon=None):
    """

    :param param:
    :param val:
    :param user_limit:
    :param dbcon:
    :return:
    """
    if user_limit is not None:
        val = float(val)
        limits = [user_limit[i][0] <= val <= user_limit[i][1] for i in user_limit]

        if not any(limits):
            return 2
        elif all(limits):
            return 0
        else:
            return 1

    dbcon = scoped_session_idb
    que = 'SELECT ocf_nbool,ocf_codin,ocp_pos,ocp_type,ocp_lvalu,ocp_hvalu from ocf\
            left join ocp on ocf_name=ocp_name where ocf_name="%s"' % param
    dbres = dbcon.execute(que)
    oc = dbres.fetchall()
    dbcon.close()
    if len(oc) == 0:
        return 0
    fmt = oc[0][1]
    if oc[0][3] == 'C':
        return 2 if val not in [i[4] for i in oc] else 0
    elif oc[0][3] in ['S', 'H']:
        oolimits = [str_to_num(pval[4], fmt) <= str_to_num(val, fmt) <= str_to_num(pval[5], fmt) for
                    pval in oc]
        if not any(oolimits):
            return 2
        elif all(oolimits):
            return 0
        elif any(oolimits) and ([lt[3] for lt in oc].count('S') == 0):
            return 0
        else:
            return 1


##
#  st_to_num
#
#  Convert string to either int or float, return input str if fails
#  @param string input string
#  @param fmt    format specifier for conversion, 'I' for int, 'R' for float
def str_to_num(string, fmt=None):
    """

    :param string:
    :param fmt:
    :return:
    """
    if fmt == 'I':
        num = int(string)
    elif fmt == 'R':
        num = float(string)
    else:
        return string
    return num


def calc_param_crc(cmd, *args, no_check=False, hack_value=None):
    """
    Calculates the CRC over the packet source data (excluding the checksum parameter).
    Uses the same CRC algo as packet CRC and assumes the checksum is at the end of the packet source data.

    :param cmd:
    :param args:
    :param no_check:
    :param hack_value:
    :return:
    """
    pdata = Tcbuild(cmd, *args, no_check=no_check, hack_value=hack_value, source_data_only=True)
    return crc(pdata[:-PEC_LEN])


def load_to_memory(data, memid, memaddr, max_pkt_size=1000, sleep=0.125, ack=0b1001, pool_name='LIVE', tcname=None,
                   progress=True, calc_crc=True, byte_align=4):
    """
    Function for loading data to DPU memory. Splits the input _data_ into slices and sequentially sends them
    to the specified location _memid_, _mempos_ by repeatedly calling the _Tcsend_bytes_ function until
    all _data_ is transferred. Data is zero-padded if not aligned to _byte_align_ bytes.

    :param data:
    :param memid:
    :param memaddr:
    :param max_pkt_size:
    :param sleep:
    :param ack:
    :param pool_name:
    :param tcname:
    :param progress:
    :param calc_crc:
    :param byte_align:
    :return:
    """

    if not isinstance(data, bytes):
        if isinstance(data, str):
            data = open(data, 'rb').read()
        else:
            raise TypeError('Data is not bytes or str')

    if byte_align and (len(data) % byte_align):
        logger.warning('Data is not {}-byte aligned, padding.'.format(byte_align))
        data += bytes(byte_align - (len(data) % byte_align))

    # get service 6,2 info from MIB
    apid, memid_ref, fmt, endspares = _get_upload_service_info(tcname)
    pkt_overhead = TC_HEADER_LEN + struct.calcsize(fmt) + len(endspares) + PEC_LEN
    payload_len = max_pkt_size - pkt_overhead

    memid = get_mem_id(memid, memid_ref)

    # get permanent pmgr handle to avoid requesting one for each packet
    pmgr = _get_pmgr_handle(tc_pool=pool_name)

    data_size = len(data)
    startaddr = memaddr

    upload_bytes = b''
    pcnt = 0
    ptot = None

    slices = [data[i:i + payload_len] for i in range(0, len(data), payload_len)]
    if (payload_len + pkt_overhead) > MAX_PKT_LEN:
        logger.warning('PKTSIZE > {} bytes, this might not work!'.format(MAX_PKT_LEN))

    for sli in slices:
        t1 = time.time()

        # create PUS packet
        packetdata = struct.pack(fmt, memid, startaddr, len(sli)) + sli + endspares
        seq_cnt = counters.setdefault(apid, 0)
        puspckt = Tcpack(data=packetdata, st=6, sst=2, apid=apid, sc=seq_cnt, ack=ack)

        if len(puspckt) > MAX_PKT_LEN:
            logger.warning('Packet length ({}) exceeding MAX_PKT_LEN of {} bytes!'.format(len(puspckt), MAX_PKT_LEN))

        Tcsend_bytes(puspckt, pool_name=pool_name, pmgr_handle=pmgr)
        # collect all uploaded segments for CRC at the end
        upload_bytes += sli
        pcnt += 1

        if progress:
            if ptot is None:
                ptot = int(np.ceil(data_size / len(sli)))  # packets needed to transfer data
            print('{}/{} packets sent\r'.format(pcnt, ptot), end='')

        dt = time.time() - t1
        time.sleep(max(sleep - dt, 0))

        startaddr += len(sli)
        counters[apid] += 1

    print('\nUpload finished, {} bytes sent in {} packets.'.format(len(upload_bytes), pcnt))

    if calc_crc:
        # return total length of uploaded data  and CRC over entire uploaded data
        return len(upload_bytes), crc(upload_bytes)


def get_tc_descr_from_stsst(st, sst):
    """

    :param st:
    :param sst:
    :return:
    """
    res = scoped_session_idb.execute('SELECT ccf_descr FROM ccf where ccf_type={} and ccf_stype={}'.format(st, sst)).fetchall()
    return [x[0] for x in res]


def bin_to_hex(fname, outfile):
    """

    :param fname:
    :param outfile:
    """
    # bash alternative: hexdump -e '16/1 "%3.2X"' fname > outfile
    bindata = open(fname, 'rb').read()
    buf = prettyhex(bindata)
    with open(outfile, 'w') as fd:
        fd.write(buf)
        # print('Wrote {} bytes as HEX-ASCII to {}.'.format(len(bindata), outfile))
        logger.info('Wrote {} bytes as HEX-ASCII to {}.'.format(len(bindata), outfile))


def get_mem_id(memid, memid_ref):
    """

    :param memid:
    :param memid_ref:
    :return:
    """
    if not isinstance(memid, int):
        dbcon = scoped_session_idb
        dbres = dbcon.execute('SELECT pas_alval from pas where pas_numbr="{}" and pas_altxt="{}"'.format(memid_ref, memid))
        try:
            memid, = dbres.fetchall()[0]
        except IndexError:
            que = 'SELECT pas_altxt from pas where pas_numbr="{}"'.format(memid_ref)
            alvals = [x[0] for x in dbcon.execute(que).fetchall()]
            raise ValueError('Invalid MemID "{}". Allowed values are: {}.'.format(memid, ', '.join(alvals)))
        finally:
            dbcon.close()
        memid = int(memid)

    return memid


#######################################################
##
#  Convert srec file to sequence of PUS packets (TM6,2) and save them in hex-files or send them to socket _tcsend_
#  @param fname        Input srec file
#  @param outname      Root name ouf the output files, if _None_, _fname_ is used
#  @param memid        Memory ID packets are destined to, number or name (e.g. "DPU_RAM")
#  @param memaddr      Memory start address where packets are patched to
#  @param segid        Segment ID
#  @param tcsend       Name of pool bound to TC socket to send the packets to, files are created instead if _False_
#  @param linesperpack Number of lines in srec file to concatenate in one PUS packet
#  @param pcount       Initial sequence counter for packets
#  @param sleep        Timeout after each packet if packets are sent directly to socket
def srectohex(fname, memid, memaddr, segid, tcsend=False, outname=None, linesperpack=61, pcount=0, sleep=0.,
              source_only=False, add_memaddr_to_source=False):
    """

    :param fname:
    :param memid:
    :param memaddr:
    :param segid:
    :param tcsend:
    :param outname:
    :param linesperpack:
    :param pcount:
    :param sleep:
    :param source_only:
    :param add_memaddr_to_source:
    :return:
    """
    source_list = []
    if outname is None:
        outname = fname.replace('.srec', '')

    # get service 6,2 info from MIB
    apid, memid_ref, fmt, endspares = _get_upload_service_info()

    if not isinstance(memid, int):
        dbcon = scoped_session_idb
        dbres = dbcon.execute('SELECT pas_alval from pas where pas_numbr="{}" and pas_altxt="{}"'.format(memid_ref, memid))
        try:
            memid, = dbres.fetchall()[0]
        except IndexError:
            raise ValueError('MemID "{}" does not exist. Aborting.'.format(memid))
        finally:
            dbcon.close()
        memid = int(memid)

    f = open(fname, 'r').readlines()[1:]
    lines = [p[12:-3] for p in f]
    startaddr = int(f[0][4:12], 16)

    # npacks=len(lines)//int(linesperpack)
    if not isinstance(pcount, int):
        pcount = 0

    linecount = 0
    while linecount < len(f) - 1:

        t1 = time.time()

        linepacklist = []
        for n in range(linesperpack):
            if linecount >= (len(lines) - 1):
                break
            linepacklist.append(lines[linecount])
            linelength = len(lines[linecount]) // 2
            if int(f[linecount + 1][4:12], 16) != (int(f[linecount][4:12], 16) + linelength):
                linecount += 1
                newstartaddr = int(f[linecount][4:12], 16)
                break
            else:
                linecount += 1
                newstartaddr = int(f[linecount][4:12], 16)

        linepack = bytes.fromhex(''.join(linepacklist))
        dlen = len(linepack)
        data = struct.pack(SEG_HEADER_FMT, segid, startaddr, dlen // 4) + linepack + bytes(SEG_SPARE_LEN)
        data = data + crc(data).to_bytes(SEG_CRC_LEN, 'big')
        if source_only:
            if add_memaddr_to_source:
                source_list.append(prettyhex(memaddr.to_bytes(4, 'big') + data))
            else:
                source_list.append(prettyhex(data))
            startaddr = newstartaddr
            memaddr += len(data)
            continue
        packetdata = struct.pack('>HII', memid, memaddr, len(data)) + data
        PUS = Tcpack(data=packetdata, st=6, sst=2, sc=pcount, apid=apid, ack=0b1001)
        if len(PUS) > 1024:
            logger.warning('Packet length ({:}) exceeding 1024 bytes!'.format(len(PUS)))
        if tcsend:
            Tcsend_bytes(PUS, pool_name=tcsend)
            dt = time.time() - t1
            time.sleep(max(sleep - dt, 0))
        else:
            with open(outname + '%04i.tc' % pcount, 'w') as ofile:
                # ofile.write(PUS.hex.upper())
                ofile.write(prettyhex(PUS))
        startaddr = newstartaddr
        # startaddr += dlen
        memaddr += len(data)
        pcount += 1
    if source_only:
        if add_memaddr_to_source:
            source_list.append(prettyhex(memaddr.to_bytes(4, 'big') + bytes(12)))
        else:
            source_list.append(prettyhex(bytes(12)))
        with open(outname + '_source.TC', 'w') as fd:
            fd.write('\n'.join(source_list))
        return
    packetdata = struct.pack('>HII', memid, memaddr, 12) + bytes(12)
    PUS = Tcpack(data=packetdata, st=6, sst=2, sc=pcount, apid=apid, ack=0b1001)
    if tcsend:
        Tcsend_bytes(PUS, pool_name=tcsend)
    else:
        with open(outname + '%04i.tc' % pcount, 'w') as ofile:
            # ofile.write(PUS.hex.upper())
            ofile.write(prettyhex(PUS))


def srectosrecmod(input_srec, output_srec, imageaddr=0x40180000, linesperpack=61):
    """
    Repack source data from srec file into 'DBS structure' and save it to new srec file.

    :param input_srec:
    :param output_srec:
    :param imageaddr:
    :param linesperpack:
    :return:
    """
    # get source data from original srec and add memory address
    srectohex(input_srec, outname='srec_binary', memaddr=0xDEADBEEF, source_only=True, linesperpack=linesperpack)

    # write source data to new srec
    source_to_srec('srec_binary_source.TC', output_srec, memaddr=imageaddr)


def srec_to_s6(fname, memid, memaddr, segid, tcname=None, linesperpack=50, max_pkt_size=MAX_PKT_LEN, image_crc=True):
    # get service 6,2 info from MIB
    apid, memid_ref, fmt, endspares = _get_upload_service_info(tcname)
    pkt_overhead = TC_HEADER_LEN + struct.calcsize(fmt) + SEG_HEADER_LEN + SEG_SPARE_LEN + SEG_CRC_LEN + len(
        endspares) + PEC_LEN
    payload_len = max_pkt_size - pkt_overhead

    memid = get_mem_id(memid, memid_ref)

    pckts = []

    f = open(fname, 'r').readlines()[1:]
    lines = [p[12:-3] for p in f]
    data_size = len(''.join(lines)) // 2
    startaddr = int(f[0][4:12], 16)

    upload_bytes = b''
    linecount = 0
    bcnt = 0
    pcnt = 0
    ptot = None

    while linecount < len(f) - 1:

        t1 = time.time()

        linepacklist = []
        for n in range(linesperpack):
            if linecount >= (len(lines) - 1):
                break

            if (len(''.join(linepacklist)) + len(lines[linecount])) // 2 > payload_len:  # ensure max_pkt_size
                break

            linepacklist.append(lines[linecount])
            linelength = len(lines[linecount]) // 2
            if int(f[linecount + 1][4:12], 16) != (int(f[linecount][4:12], 16) + linelength):
                linecount += 1
                newstartaddr = int(f[linecount][4:12], 16)
                break
            else:
                linecount += 1
                newstartaddr = int(f[linecount][4:12], 16)

        linepack = bytes.fromhex(''.join(linepacklist))
        dlen = len(linepack)
        bcnt += dlen
        # segment header, see IWF DBS HW SW ICD
        data = struct.pack(SEG_HEADER_FMT, segid, startaddr, dlen // 4) + linepack + bytes(SEG_SPARE_LEN)
        data = data + crc(data).to_bytes(SEG_CRC_LEN, 'big')

        # create PUS packet
        packetdata = struct.pack(fmt, memid, memaddr, len(data)) + data + endspares
        seq_cnt = counters.setdefault(apid, 0)
        puspckt = Tcpack(data=packetdata, st=6, sst=2, apid=apid, sc=seq_cnt, ack=0b1001)

        if len(puspckt) > MAX_PKT_LEN:
            logger.warning('Packet length ({}) exceeding MAX_PKT_LEN of {} bytes!'.format(len(puspckt), MAX_PKT_LEN))

        pckts.append(puspckt)

        # collect all uploaded segments for CRC at the end
        upload_bytes += data
        pcnt += 1

        startaddr = newstartaddr
        memaddr += len(data)
        counters[apid] += 1

    packetdata = struct.pack(fmt, memid, memaddr, 12) + bytes(12) + endspares
    puspckt = Tcpack(data=packetdata, st=6, sst=2, apid=apid, sc=counters[apid], ack=0b1001)
    counters[apid] += 1
    pckts.append(puspckt)

    if image_crc:
        # return total length of uploaded data (without termination segment) and CRC over entire image, including segment headers
        return pckts, len(upload_bytes), crc(upload_bytes)

    return pckts


def upload_srec(fname, memid, memaddr, segid, pool_name='LIVE', tcname=None, linesperpack=50, sleep=0.125,
                max_pkt_size=MAX_PKT_LEN, progress=True, image_crc=True):
    """
    Upload data from an SREC file to _memid_ via S6,2

    :param fname:
    :param memid:
    :param memaddr:
    :param segid:
    :param pool_name:
    :param tcname:
    :param linesperpack:
    :param sleep:
    :param max_pkt_size:
    :param progress:
    :param image_crc:
    """
    # get service 6,2 info from MIB
    apid, memid_ref, fmt, endspares = _get_upload_service_info(tcname)
    pkt_overhead = TC_HEADER_LEN + struct.calcsize(fmt) + SEG_HEADER_LEN + SEG_SPARE_LEN + SEG_CRC_LEN + len(endspares) + PEC_LEN
    payload_len = max_pkt_size - pkt_overhead

    memid = get_mem_id(memid, memid_ref)

    # get permanent pmgr handle to avoid requesting one for each packet
    pmgr = _get_pmgr_handle(tc_pool=pool_name)

    f = open(fname, 'r').readlines()[1:]
    lines = [p[12:-3] for p in f]
    data_size = len(''.join(lines)) // 2
    startaddr = int(f[0][4:12], 16)

    upload_bytes = b''
    linecount = 0
    bcnt = 0
    pcnt = 0
    ptot = None

    while linecount < len(f) - 1:

        t1 = time.time()

        linepacklist = []
        for n in range(linesperpack):
            if linecount >= (len(lines) - 1):
                break

            if (len(''.join(linepacklist)) + len(lines[linecount])) // 2 > payload_len:  # ensure max_pkt_size
                break

            linepacklist.append(lines[linecount])
            linelength = len(lines[linecount]) // 2
            if int(f[linecount + 1][4:12], 16) != (int(f[linecount][4:12], 16) + linelength):
                linecount += 1
                newstartaddr = int(f[linecount][4:12], 16)
                break
            else:
                linecount += 1
                newstartaddr = int(f[linecount][4:12], 16)

        linepack = bytes.fromhex(''.join(linepacklist))
        dlen = len(linepack)
        bcnt += dlen
        # segment header, see IWF DBS HW SW ICD
        data = struct.pack(SEG_HEADER_FMT, segid, startaddr, dlen // 4) + linepack + bytes(SEG_SPARE_LEN)
        data = data + crc(data).to_bytes(SEG_CRC_LEN, 'big')

        # create PUS packet
        packetdata = struct.pack(fmt, memid, memaddr, len(data)) + data + endspares
        seq_cnt = counters.setdefault(apid, 0)
        puspckt = Tcpack(data=packetdata, st=6, sst=2, apid=apid, sc=seq_cnt, ack=0b1001)

        if len(puspckt) > MAX_PKT_LEN:
            logger.warning('Packet length ({}) exceeding MAX_PKT_LEN of {} bytes!'.format(len(puspckt), MAX_PKT_LEN))

        Tcsend_bytes(puspckt, pool_name=pool_name, pmgr_handle=pmgr)
        # collect all uploaded segments for CRC at the end
        upload_bytes += data
        pcnt += 1

        if progress:
            if ptot is None:
                ptot = int(np.ceil(data_size / dlen))  # packets needed to transfer SREC payload
            print('{}/{} packets sent\r'.format(pcnt, ptot), end='')

        dt = time.time() - t1
        time.sleep(max(sleep - dt, 0))

        startaddr = newstartaddr
        memaddr += len(data)
        counters[apid] += 1

    # send all-zero termination segment of length 12
    packetdata = struct.pack(fmt, memid, memaddr, 12) + bytes(12) + endspares
    puspckt = Tcpack(data=packetdata, st=6, sst=2, apid=apid, sc=counters[apid], ack=0b1001)
    Tcsend_bytes(puspckt, pool_name=pool_name, pmgr_handle=pmgr)
    counters[apid] += 1

    print('\nUpload finished, {} bytes sent in {}(+1) packets.'.format(bcnt, pcnt))

    if image_crc:
        # return total length of uploaded data (without termination segment) and CRC over entire image, including segment headers
        return len(upload_bytes), crc(upload_bytes)


def segment_data(data, segid, addr, seglen=480):
    """
    Split data into segments (as defined in IWF DPU HW SW ICD) with segment header and CRC.
    Segment data has to be two-word aligned.

    :param data:
    :param segid:
    :param addr:
    :param seglen:
    :return: list of segments
    """

    if isinstance(data, str):
        data = open(data, 'rb').read()

    if not isinstance(data, bytes):
        raise TypeError
        
    datalen = len(data)
    if datalen % 4:
        raise ValueError('Data length is not two-word aligned')
    data = io.BytesIO(data)
    
    segments = []
    segaddr = addr
    
    while data.tell() < datalen:
        chunk = data.read(seglen - (SEG_HEADER_LEN + SEG_SPARE_LEN + SEG_CRC_LEN))
        chunklen = len(chunk)

        if chunklen % 4:
            raise ValueError('Segment data length is not two-word aligned')
            
        sdata = struct.pack(SEG_HEADER_FMT, segid, segaddr, chunklen // 4) + chunk + bytes(SEG_SPARE_LEN)
        sdata += crc(sdata).to_bytes(SEG_CRC_LEN, 'big')
        segments.append(sdata)
        segaddr += chunklen

    # add 12 byte termination segment
    segments.append(bytes(SEG_HEADER_LEN))
    
    return segments


def source_to_srec(data, outfile, memaddr, header=None, bytes_per_line=32, skip_bytes=0):
    """

    :param data:
    :param outfile:
    :param memaddr:
    :param header:
    :param bytes_per_line:
    :param skip_bytes:
    :return:
    """

    def srec_chksum(x):
        """

        :param x:
        :return:
        """
        return sum(bytes.fromhex(x)) & 0xff ^ 0xff

    if bytes_per_line > SREC_MAX_BYTES_PER_LINE:
        raise ValueError("Maximum number of bytes per line is {}!".format(SREC_MAX_BYTES_PER_LINE))

    if isinstance(data, str):
        data = open(data, 'rb').read()
        
    if not isinstance(data, bytes):
        raise TypeError

    data = data[skip_bytes:]

    if header is None:
        fname = outfile.split('/')[-1][-60:]
        header = 'S0{:02X}0000{:}'.format(len(fname.encode('ascii')) + 3, fname.encode('ascii').ljust(24).hex().upper())
        header += '{:02X}'.format(srec_chksum(header[2:]))

    datalen = len(data)
    data = io.BytesIO(data)

    sreclist = []
    terminator = 'S705{:08X}'.format(memaddr)
    terminator += '{:02X}'.format(srec_chksum(terminator[2:]))

    while data.tell() < datalen:
        chunk = data.read(bytes_per_line)
        chunklen = len(chunk)
        line = '{:02X}{:08X}{:}'.format(chunklen + 5, memaddr, chunk.hex().upper())
        # add chksum according to SREC standard
        line = 'S3' + line + '{:02X}'.format(srec_chksum(line))
        sreclist.append(line)
        memaddr += chunklen

    with open(outfile, 'w') as fd:
        fd.write(header + '\n')
        fd.write('\n'.join(sreclist) + '\n')
        fd.write(terminator)

    print('Data written to file: "{}", skipped first {} bytes.'.format(outfile, skip_bytes))
    logger.info('Data written to file: "{}", skipped first {} bytes.'.format(outfile, skip_bytes))


def srec_direct(fname, memid, pool_name='LIVE', max_pkt_size=MAX_PKT_LEN, tcname=None, sleep=0.125, progress=True,
                image_crc=True, byte_align=2, ack=0b1001, dryrun=False):
    """
    Upload data from SREC file directly to memory _memid_, no additional segment headers (like for DBS) are added.

    :param fname:
    :param memid:
    :param pool_name:
    :param max_pkt_size:
    :param tcname:
    :param sleep:
    :param progress:
    :param image_crc:
    :param byte_align:
    :param ack:
    :param dryrun:
    :return:
    """
    if dryrun:
        print('DRYRUN -- NO PACKETS ARE BEING SENT!')

    # get service 6,2 info from MIB
    apid, memid_ref, fmt, endspares = _get_upload_service_info(tcname)
    pkt_overhead = TC_HEADER_LEN + struct.calcsize(fmt) + len(endspares) + PEC_LEN
    payload_len = max_pkt_size - pkt_overhead

    memid = get_mem_id(memid, memid_ref)

    # get permanent pmgr handle to avoid requesting one for each packet
    if not dryrun:
        pmgr = _get_pmgr_handle(tc_pool=pool_name)

    upload_bytes = b''
    bcnt = 0
    pcnt = 0
    ptot = None

    f = open(fname, 'r').readlines()[1:-1]  # omit header and footer line
    lines = [p[12:-3] for p in f]
    data_size = len(''.join(lines)) // 2
    memaddr = int(f[0][4:12], 16)

    linecount = 0
    nextlinelength = len(lines[linecount]) // 2
    while linecount < len(f) - 1:

        t1 = time.time()

        linepacklist = []
        packlen = 0
        while (packlen + nextlinelength) <= payload_len:
            if linecount >= (len(lines) - 1):
                break
            linepacklist.append(lines[linecount])
            linelength = len(lines[linecount]) // 2
            packlen += linelength
            if int(f[linecount + 1][4:12], 16) != (int(f[linecount][4:12], 16) + linelength):
                linecount += 1
                newstartaddr = int(f[linecount][4:12], 16)
                break
            else:
                linecount += 1
                newstartaddr = int(f[linecount][4:12], 16)

            nextlinelength = len(lines[linecount]) // 2

        data = bytes.fromhex(''.join(linepacklist))

        dlen = len(data)
        bcnt += dlen

        # create PUS packet
        packetdata = struct.pack(fmt, memid, memaddr, len(data)) + data + endspares
        seq_cnt = counters.setdefault(apid, 0)
        puspckt = Tcpack(data=packetdata, st=6, sst=2, apid=apid, sc=seq_cnt, ack=ack)

        if len(puspckt) > MAX_PKT_LEN:
            logger.warning('Packet length ({}) exceeding MAX_PKT_LEN of {} bytes!'.format(len(puspckt), MAX_PKT_LEN))

        if not dryrun:
            Tcsend_bytes(puspckt, pool_name=pool_name, pmgr_handle=pmgr)

        # collect all uploaded segments for CRC at the end
        upload_bytes += data
        pcnt += 1

        if progress:
            if ptot is None:
                ptot = int(np.ceil(data_size / dlen))  # packets needed to transfer SREC payload
            print('{}/{} packets sent\r'.format(pcnt, ptot), end='')

        dt = time.time() - t1
        time.sleep(max(sleep - dt, 0))

        if data == b'':
            print('No data left, exit upload.')
            return

        memaddr = newstartaddr
        if not dryrun:
            counters[apid] += 1

    # check if entire data is x-byte-aligned
    if len(upload_bytes) % byte_align:
        padding = byte_align - (len(upload_bytes) % byte_align)
        print('\nData is not {}-byte aligned. Sending padding data ({})'.format(byte_align, padding))

        # create PUS packet
        packetdata = struct.pack(fmt, memid, memaddr, padding) + bytes(padding) + endspares
        seq_cnt = counters.setdefault(apid, 0)
        puspckt = Tcpack(data=packetdata, st=6, sst=2, apid=apid, sc=seq_cnt, ack=ack)

        if not dryrun:
            Tcsend_bytes(puspckt, pool_name=pool_name, pmgr_handle=pmgr)
            counters[apid] += 1

        memaddr += padding
        upload_bytes += bytes(padding)
        bcnt += padding
        pcnt += 1

    print('\nUpload finished, {} bytes sent in {} packets.'.format(bcnt, pcnt))

    if image_crc:
        # return total length of uploaded data (without termination segment) and CRC over entire image, including segment headers
        return len(upload_bytes), crc(upload_bytes)


def _get_upload_service_info(tcname=None):
    """
    Get info about service 6,2 from MIB

    :param tcname:
    :return:
    """
    if tcname is None:
        cmd = get_tc_descr_from_stsst(6, 2)[0]
    else:
        cmd = tcname

    params = _get_tc_params(cmd, paf_cal=True)

    apid = params[0][2]

    # try to find paf_ref for MEMID
    try:
        memid_ref = [p[-1] for p in params if p[-1] is not None][0]
    except KeyError:
        memid_ref = None

    # get format info for fixed block
    fmt = '>'
    idx = 0
    for par in params:
        fmt += ptt(*par[8:10])
        if par[4] != 0:
            idx = params.index(par)
            break

    # check for spares after variable part
    endspares = b''
    for par in params[idx:]:
        if par[5] == 'A':
            endspares += bytes(par[6] // 8)

    return apid, memid_ref, fmt, endspares


def get_tc_list(ccf_descr=None):
    """

    :param ccf_descr:
    :return:
    """
    if ccf_descr is None:
        cmds = scoped_session_idb.execute('SELECT ccf_cname, ccf_descr, ccf_descr2, ccf_type, ccf_stype, ccf_npars, '
                                          'cpc_descr, cpc_dispfmt, cdf_eltype, cpc_pname, cdf_value, cpc_inter, '
                                          'cpc_radix FROM ccf LEFT JOIN cdf ON cdf.cdf_cname=ccf.ccf_cname '
                                          'LEFT JOIN cpc ON cpc.cpc_pname=cdf.cdf_pname '
                                          'ORDER BY SUBSTRING(ccf_cname, 1, 2), ccf_type, ccf_stype, ccf_cname').fetchall()
    else:
        cmds = scoped_session_idb.execute('SELECT ccf_cname, ccf_descr, ccf_descr2, ccf_type, ccf_stype, ccf_npars, '
                                          'cpc_descr, cpc_dispfmt, cdf_eltype, cpc_pname, cdf_value, cpc_inter, '
                                          'cpc_radix FROM ccf LEFT JOIN cdf ON cdf.cdf_cname=ccf.ccf_cname '
                                          'LEFT JOIN cpc ON cpc.cpc_pname=cdf.cdf_pname '
                                          'WHERE ccf_descr="{}"'.format(ccf_descr)).fetchall()

    scoped_session_idb.close()

    cmd_dict = {}

    for row in cmds:
        cmd_dict.setdefault(row[0:5], []).append(row[6:])

    return cmd_dict


def get_tc_calibration_and_parameters(ccf_descr=None):
    """

    :param ccf_descr:
    :return:
    """
    if ccf_descr is None:
        calibrations = scoped_session_idb.execute('SELECT ccf_cname, ccf_descr, cdf_eltype, cdf_descr, cdf_ellen, '
                                                  'cdf_value, cdf_pname, cpc_descr, cpc_categ, cpc_ptc, '
                                                  'cpc_pfc, prv_minval, prv_maxval, pas_altxt, pas_alval '
                                                  'FROM ccf LEFT JOIN cdf ON ccf.ccf_cname=cdf.cdf_cname '
                                                  'LEFT JOIN cpc ON cdf.cdf_pname=cpc.cpc_pname '
                                                  'LEFT JOIN prv ON cpc.cpc_prfref=prv.prv_numbr '
                                                  'LEFT JOIN pas ON cpc.cpc_pafref=pas.pas_numbr '
                                                  'ORDER BY ccf_type, ccf_stype, '
                                                  'ccf_cname, cdf_bit, pas_alval').fetchall()

    else:
        calibrations = scoped_session_idb.execute('SELECT ccf_cname, ccf_descr, cdf_eltype, cdf_descr, cdf_ellen, '
                                                  'cdf_value, cdf_pname, cpc_descr, cpc_categ, cpc_ptc, '
                                                  'cpc_pfc, prv_minval, prv_maxval, pas_altxt, pas_alval '
                                                  'FROM ccf LEFT JOIN cdf ON ccf.ccf_cname=cdf.cdf_cname '
                                                  'LEFT JOIN cpc ON cdf.cdf_pname=cpc.cpc_pname '
                                                  'LEFT JOIN prv ON cpc.cpc_prfref=prv.prv_numbr '
                                                  'LEFT JOIN pas ON cpc.cpc_pafref=pas.pas_numbr '
                                                  'WHERE ccf_descr="{}"'.format(ccf_descr)).fetchall()

    scoped_session_idb.close()

    calibrations_dict = {}

    for row in calibrations:
        calibrations_dict.setdefault(row[:9], []).append(row[9:])

    return calibrations_dict


def get_tm_parameter_list(st, sst, apid=None, pi1val=0):
    """

    :param st:
    :param sst:
    :param apid:
    :param pi1val:
    :return:
    """
    spid, tpsd = _get_spid(st, sst, apid=apid, pi1val=pi1val)

    if tpsd == -1:
        que = 'SELECT plf_name, pcf_descr, plf_offby, pcf_ptc, pcf_pfc FROM plf LEFT JOIN pcf ON plf_name=pcf_name WHERE plf_spid={} ORDER BY plf_offby, plf_offbi'.format(spid)
    else:
        que = 'SELECT vpd_name, pcf_descr, NULL, pcf_ptc, pcf_pfc FROM vpd LEFT JOIN pcf ON vpd_name=pcf_name WHERE vpd_tpsd={} ORDER BY vpd_pos'.format(tpsd)

    res = scoped_session_idb.execute(que).fetchall()

    return res


def get_tm_parameter_info(pname):
    """

    :param pname:
    :return:
    """
    que = 'SELECT ocp_lvalu, ocp_hvalu, ocp_type, txp_from, txp_altxt FROM pcf LEFT JOIN ocp ON pcf_name=ocp_name ' \
          'LEFT JOIN txp ON pcf_curtx=txp_numbr WHERE pcf_name="{}" ORDER BY txp_from, ocp_pos'.format(pname)
    res = scoped_session_idb.execute(que).fetchall()

    return res


def get_tm_id(pcf_descr=None):
    """

    :param pcf_descr:
    :return:
    """
    if pcf_descr is None:
        tms = scoped_session_idb.execute('SELECT pid_type, pid_stype, pid_apid, pid_pi1_val, pid_descr, pid_tpsd, '
                                         'pid_spid, pcf_name, pcf_descr, pcf_curtx, txp_from, txp_altxt, plf_offby,'
                                         'pcf_ptc, pcf_pfc '
                                         'FROM pid '
                                         'LEFT JOIN plf '
                                         'ON pid_spid = plf_spid AND pid_tpsd = -1 '
                                         'LEFT JOIN vpd '
                                         'ON pid_tpsd = vpd_tpsd AND pid_tpsd <> -1 '
                                         'LEFT JOIN pcf '
                                         'ON pcf_name = COALESCE(plf_name, vpd_name) '
                                         'LEFT JOIN txf '
                                         'ON pcf_curtx = txf_numbr '
                                         'LEFT JOIN txp '
                                         'ON txf_numbr = txp.txp_numbr '
                                         'ORDER BY pid_type, pid_stype, pid_apid, pid_pi1_val').fetchall()

    else:
        tms = scoped_session_idb.execute('SELECT pid_type, pid_stype, pid_apid, pid_pi1_val, pid_descr , pid_tpsd, '
                                         'pid_spid, pcf_name, pcf_descr, pcf_curtx, txp_from, txp_altxt, plf_offby,'
                                         'pcf_ptc, pcf_pfc '
                                         'FROM pid '
                                         'LEFT JOIN plf '
                                         'ON pid_spid = plf_spid AND pid_tpsd = -1 '
                                         'LEFT JOIN vpd '
                                         'ON pid_tpsd = vpd_tpsd AND pid_tpsd <> -1 '
                                         'LEFT JOIN pcf '
                                         'ON pcf_name = COALESCE(plf_name, vpd_name) '
                                         'LEFT JOIN txf '
                                         'ON pcf_curtx = txf_numbr '
                                         'LEFT JOIN txp '
                                         'ON txf_numbr = txp.txp_numbr '
                                         'WHERE pcf_descr="{}"'.format(pcf_descr)).fetchall()

    scoped_session_idb.close()

    tms_dict = {}

    for row in tms:
        tms_dict.setdefault(row[0:5], []).append(row[5:])

    return tms_dict


def get_tm_parameter_sizes(st, sst, apid=None, pi1val=0):
    """
    Returns a list of parameters and their sizes. For variable length TMs only the first fixed part is considered.

    :param st:
    :param sst:
    :param apid:
    :param pi1val:
    :return:
    """

    spid, tpsd = _get_spid(st, sst, apid=apid, pi1val=pi1val)

    if tpsd == -1:
        que = 'SELECT plf_name, pcf_descr, pcf_ptc, pcf_pfc, NULL FROM plf LEFT JOIN pcf ON plf_name=pcf_name WHERE plf_spid={} ORDER BY plf_offby, plf_offbi'.format(spid)
    else:
        que = 'SELECT vpd_name, pcf_descr, pcf_ptc, pcf_pfc, vpd_grpsize FROM vpd LEFT JOIN pcf ON vpd_name=pcf_name WHERE vpd_tpsd={} ORDER BY vpd_pos'.format(tpsd)

    res = scoped_session_idb.execute(que).fetchall()

    pinfo = []
    for p in res:
        pinfo.append((p[1], csize(ptt(*p[2:4]))))
        # break after first "counter" parameter
        if p[-1] != 0:
            break

    return pinfo


def _get_spid(st, sst, apid=None, pi1val=0):
    """

    :param st:
    :param sst:
    :param apid:
    :param pi1val:
    :return:
    """
    if apid is None:
        apid = ''
    else:
        apid = ' AND pid_apid={}'.format(apid)

    que = 'SELECT pid_spid, pid_tpsd FROM pid WHERE pid_type={} AND pid_stype={}{} AND pid_pi1_val={}'.format(st, sst, apid, pi1val)
    spid, tpsd = scoped_session_idb.execute(que).fetchall()[0]

    return spid, tpsd


def get_data_pool_items(pcf_descr=None, src_file=None, as_dict=False):
    """

    :param pcf_descr:
    :param src_file:
    :param as_dict:
    :return:
    """
    if not isinstance(src_file, (str, type(None))):
        raise TypeError('src_file must be str, is {}.'.format(type(src_file)))

    if src_file:
        with open(src_file, 'r') as fd:
            lines = fd.readlines()[2:]  # skip first two header rows
        data_pool = []
        for line in lines:
            if not line.startswith('#'):
                dp_item = line.strip().split('|')
                # check for format
                if len(dp_item) == 9:
                    data_pool.append(dp_item[:2][::-1] + [dp_item[2]] + [dp_item[4]] + dp_item[6:8])  # PID, NAME, TYPE, MULT, VALUE, DESCR
                else:
                    raise ValueError('Wrong format of input line in {}.'.format(src_file))

        if as_dict:
            data_pool_dict = {int(row[0]): {'descr': row[1], 'fmt': fmtlist[row[2]]} for row in data_pool if row[2] in fmtlist}
            if len(data_pool_dict) != len(data_pool):
                logger.warning('Data pool items were rejected because of unknown format ({})'.format(len(data_pool) - len(data_pool_dict)))
            return data_pool_dict
        else:
            return data_pool, src_file

    elif pcf_descr is None and not src_file:
        data_pool = scoped_session_idb.execute('SELECT pcf_pid, pcf_descr, pcf_ptc, pcf_pfc '
                                               'FROM pcf WHERE pcf_pid IS NOT NULL').fetchall()

    else:
        data_pool = scoped_session_idb.execute('SELECT pcf_pid, pcf_descr, pcf_ptc, pcf_pfc '
                                               'FROM pcf WHERE pcf_pid IS NOT NULL AND pcf_descr="{}"'.format(pcf_descr)).fetchall()

    scoped_session_idb.close()

    if not as_dict:
        return data_pool, src_file

    data_pool_dict = {int(row[0]): {'descr': row[1], 'fmt': ptt(row[2], row[3])} for row in data_pool}

    # for row in data_pool:
    #     data_pool_dict.setdefault(row[0:4], []).append(row[5:])

    return data_pool_dict


def get_dp_fmt_info(dp_name):
    """

    :param dp_name:
    :return:
    """
    que = 'SELECT pcf_name FROM pcf where pcf_pid is not NULL and pcf_descr="{}"'.format(dp_name)
    mib_name = scoped_session_idb.execute(que).fetchall()[0]
    return mib_name


# def get_dp_items(source='mib'):
#     fmt = {3: {4: 'UINT8', 12: 'UINT16', 14: 'UINT32'}, 4: {4: 'INT8', 12: 'INT16', 14: 'INT32'}, 5: {1: 'FLOAT'}, 9: {18: 'CUC'}, 7: {1: '1OCT'}}
#
#     if source.lower() == 'mib':
#         dp = scoped_session_idb.execute('SELECT pcf_pid, pcf_descr, pcf_ptc, pcf_pfc FROM pcf WHERE pcf_pid IS NOT NULL ORDER BY pcf_pid').fetchall()
#         dp_ed = [(*i[:2], fmt[i[2]][i[3]]) for i in dp]
#         return dp_ed
#     else:
#         raise NotImplementedError


def make_tc_template(ccf_descr, pool_name='LIVE', preamble='cfl.Tcsend_DB', options='', comment=True, add_parcfg=False):
    """

    :param ccf_descr:
    :param pool_name:
    :param preamble:
    :param options:
    :param comment:
    :param add_parcfg:
    :return:
    """
    try:
        cmd, pars = list(get_tc_list(ccf_descr).items())[0]
    except IndexError:
        raise IndexError('"{}" not found in IDB.'.format(ccf_descr))
    # print(tc_template(cmd, pars, pool_name=pool_name, preamble=preamble, options=options, comment=True))
    return tc_template(cmd, pars, pool_name=pool_name, preamble=preamble, options=options, comment=comment, add_parcfg=add_parcfg)


def tc_template(cmd, pars, pool_name='LIVE', preamble='cfl.Tcsend_DB', options='', comment=True, add_parcfg=False):
    """

    :param cmd:
    :param pars:
    :param pool_name:
    :param preamble:
    :param options:
    :param comment:
    :param add_parcfg:
    :return:
    """
    if comment:
        commentstr = "# TC({},{}): {} [{}]\n# {}\n".format(*cmd[3:], cmd[1], cmd[0], cmd[2])
        newline = '\n'
    else:
        commentstr = ''
        newline = ''

    parcfg = ''
    if add_parcfg:
        for par in pars:
            if par[2] == 'E':
                if par[4] is not None:
                    if par[5] == 'E':
                        parval = '"{}"'.format(par[4])
                    elif par[6] == 'H':
                        parval = '0x{}'.format(par[4])
                    else:
                        parval = par[4]
                else:
                    parval = par[4]
                line = '{} = {}  # {}\n'.format(par[0], parval, par[3])
            elif par[2] == 'F':
                line = '# {} = {}  # {} [NOT EDITABLE]\n'.format(par[0], par[4], par[3])
            else:
                line = ''
            parcfg += line

    parstr = ', '.join(parsinfo_to_str(pars))
    if len(parstr) > 0:
        parstr = ', ' + parstr
    exe = "{}('{}'{}, pool_name='{}'{})".format(preamble, cmd[1], parstr, pool_name, options)
    return commentstr + parcfg + exe + newline


def parsinfo_to_str(pars, separator=None):
    """
    Return list of editable parameter names based on get_tc_list info

    :param pars:
    :param separator:
    :return:
    """
    if separator is None:
        return [par[0] for par in pars if par[2] not in ['A', 'F', None]]  # != (None, None)]
    else:
        return [separator.join(par) for par in pars if par[2] not in ['A', 'F', None]]  # != (None, None)]


def on_open_univie_clicked(button):
    """
    Called by all applications, and called by the univie button to set up the starting options

    :param button:
    :return:
    """
    dict = {'editor': start_editor, 'poolmanager': start_pmgr, 'plotter': start_plotter,
            'monitor': start_monitor, 'poolviewer': start_pv}
    dict[button.get_label().split()[1].lower()]()


def about_dialog(parent=None, action=None):
    """
    Called by the Univie Button, option About, pops up the Credits Window

    :param parent: Instance of the calling Gtk Window, for the Gtk.Dialog
    :param action: Simply the calling button
    :return:
    """
    if not parent:
        return

    pics_path = cfg.get('paths', 'ccs')
    pics_path += '/pixmap'

    dialog = Gtk.AboutDialog()
    dialog.set_transient_for(parent)

    dialog.set_program_name('UVIE Central Checkout System')

    dialog.set_copyright('UVIE 08/2022')
    dialog.set_license_type(Gtk.License.MPL_2_0)
    dialog.set_authors(('Marko Mecina', 'Dominik Moeslinger', 'Thanassis Tsiodras', 'Armin Luntzer'))
    dialog.set_version('2.0')
    dialog.set_logo(GdkPixbuf.Pixbuf.new_from_file(os.path.join(pics_path, 'IfA_Logo_48.png')))
    dialog.set_website('https://space.univie.ac.at')
    dialog.set_website_label('space.univie.ac.at')

    dialog.run()
    dialog.destroy()


def change_communication_func(main_instance=None,new_main=None,new_main_nbr=None,application=None,application_nbr=1,parentwin=None):
    """
    Called by the UVIE button, option Communication. Used to change the main_application for each project
    (main_instance), also possible to only change main communication for one application

    :param new_main:The new main_application to be called every time in the future
    :param new_main_nbr: The instance of the new main_application
    :param application:The application to change the main communication for, None if chang for all
    :param application_nbr:The instance of *application*
    :param main_instance:The project in which the changes should apply
    :param parentwin:Instance of a Gtk.Window for the Gtk.Dialog, *None* if called from a command line
    :return:
    """
    save_com = {}
    # Check if a main_instance (project) is given otherwise try to get one, this is a necessary information
    if not main_instance:
        if is_open(application.lower(), application_nbr):
            conn = dbus_connection(application.lower(), application_nbr)
            main_instance = conn.Variables('main_instance')
        else:
            # print('Please give a value for "main_instance" (project name)')
            logger.warning('No main_instance was given therefore main communication could not be changed')
            return
    # Instance from an application is given call the dialog
    if parentwin:
        dialog = ChangeCommunicationDialog(parent=parentwin, cfg=cfg, main_instance=main_instance) # Start Dialog
        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            if dialog.change_button.get_active(): #Change for the entire main_instance project
                for conn in dialog.new_communication:
                    change_main_communication(conn, dialog.new_communication[conn], main_instance, parentwin.my_bus_name)
            else:   # Change only for the calling application
                communication = dialog.new_communication

        dialog.destroy()
    # Change the main communication for one application
    elif new_main and new_main_nbr and application and application_nbr:
        if is_open(application.lower(), application_nbr) and is_open(new_main.lower(), new_main_nbr):
            conn = dbus_connection(application.lower(), application_nbr)
            conn.Functions('change_communication', application.lower(), application_nbr)
        else:
            # print('Given application instance or new main communication instance is not open')
            logger.warning('Could not change main communication instance to {} for {} since one of these is not '
                        'running'.format(str(new_main)+str(new_main_nbr), str(application)+str(application_nbr)))
    # Change the main communication for the entire project
    elif new_main and new_main_nbr:
        change_main_communication(new_main, new_main_nbr, main_instance)
    else:
        # print('Please give a new main application and the instance number')
        logger.warning('Not enough information was given to change the main communication')

    return


def change_main_communication(new_main, new_main_nbr, main_instance, own_bus_name=None):
    """
    Changes the main_communication for the entire main_instance (project)

    :param new_main: The new main_application to be called every time in the future
    :param new_main_nbr: The instance of the new main_application
    :param main_instance: The application to change the main communication for, None if chang for all
    :param own_bus_name: The instance of :param application
    :return:
    """
    # Change all communication to the new main
    for service in dbus.SessionBus().list_names(): # Check all running dbus clients
        #print(cfg['ccs-dbus_names'])
        if service[:-1] in cfg['ccs-dbus_names'].values():  # Filter running for the CCS
            if service == own_bus_name:     # If it is the calling application dbus can not be used, simply change
                communication[new_main] = new_main_nbr
                continue
            # For the rest in the same project do the change via dbus
            conn = dbus_connection(service.split('.')[1], service[-1])
            if conn.Variables('main_instance') == main_instance:
                conn.Functions('change_communication', new_main, int(new_main_nbr), False)

    return


def add_decode_parameter(parentwin=None):  # , label=None, fmt=None, bytepos=None):
    """
    Add a parameter which can be used in a User DEFined packet, only defined by the format and can therefore only be
    used if the package is decoded in the given order

    :param parentwin: For graphical usage
    :return:
    """

    if parentwin is not None:
        dialog = TmParameterDecoderDialog(parent=parentwin)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            label = dialog.label.get_text()
            fmt = dialog.format.get_active_text()
            if fmt in fmtlist:
                fmt = fmtlist[fmt]
                if fmt in ('uint', 'ascii', 'oct'):
                    fmt += str(dialog.bitlen.get_text())

            else:
                fmt += str(dialog.bitlen.get_text())
                fmt = fmt.replace('*', '')
                if fmt.upper() in fmtlist:
                    fmt = fmtlist[fmt.upper()]
            dialog.destroy()
        else:
            dialog.destroy()
            return

    else:
        logger.error('Please give a valid format')
        return

    cfg.save_option_to_file('ccs-decode_parameters', label, json.dumps({'format': str(fmt)}))

    if fmt:
        return {'format': str(fmt)}
    else:
        return


def add_tm_decoder(label=None, st=None, sst=None, apid=None, sid=None, parameters=None, parentwin=None):
    """
    Add User DEFined packet with decoding info for TM not defined in IDB

    :param label: Name of new defined packet
    :param st: Service Type
    :param sst: Sub Service Type
    :param apid:
    :param sid:
    :param parameters: list of parameters
    :param parentwin:
    :return:
    """

    if label and st and sst and apid:
        sid = sid if sid else 0
        tag = '{}-{}-{}-{}'.format(st, sst, apid, sid)

    elif parentwin is not None:
        dialog = TmDecoderDialog(logger=logger, parent=parentwin)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            parameters = [par for par in dialog.parameter_list]
            sid = dialog.sid.get_text() if dialog.sid.get_text() else 0
            tag = '{}-{}-{}-{}'.format(dialog.st.get_text(), dialog.sst.get_text(), dialog.apid.get_text(), sid)
            label = dialog.label.get_text()
            dialog.destroy()
        else:
            dialog.destroy()
            return
    else:
        logger.error('Please give: label, st, sst and apid')
        return

    if not parameters:
        logger.warning('No parameters given, cannot create custom TM')
        return

    params = [_parameter_decoding_info(par, check_curtx=True) for par in parameters]
    logger.debug('Created custom TM decoder {} with parameters: {}'.format(label, [x[1] for x in params]))
    user_tm_decoders[tag] = (label, params)

    if not cfg.has_section('ccs-user_defined_packets'):
        cfg.add_section('ccs-user_defined_packets')
    cfg.save_option_to_file('ccs-user_defined_packets', tag, json.dumps((label, [tuple(x) for x in params])))

    return label


def _parameter_decoding_info(param, check_curtx=False):
    """
    Return parameter info tuple used for TM decoding

    :param param:
    :return:
    """

    if param[1] not in ['user_defined', 'user_defined_nopos', 'dp_item']:
        que = 'SELECT pcf.pcf_name,pcf.pcf_descr,pcf.pcf_ptc,pcf.pcf_pfc,pcf.pcf_curtx,pcf.pcf_width,pcf.pcf_unit,' \
              'pcf.pcf_pid,null,null,null from pcf WHERE pcf_name ="{}"'.format(param[1])
        dinfo = scoped_session_idb.execute(que).fetchall()[0]

    elif param[1] == 'user_defined':
        fmt = json.loads(cfg[CFG_SECT_PLOT_PARAMETERS][param[0]])['format']
        ptc, pfc = ptt_reverse(fmt)
        dinfo = [param[1], param[0], ptc, pfc, None, csize(fmt) * 8, None, None, None, None, None]

    elif param[1] == 'user_defined_nopos':
        fmt = json.loads(cfg[CFG_SECT_DECODE_PARAMETERS][param[0]])['format']
        ptc, pfc = ptt_reverse(fmt)
        dinfo = [param[1], param[0], ptc, pfc, None, csize(fmt) * 8, None, None, None, None, None]

    elif param[1] == 'dp_item':
        if isinstance(param[0], int):
            dp_id = param[0]
            dp_descr = DP_IDS_TO_ITEMS[param[0]]
        else:
            dp_id = DP_ITEMS_TO_IDS[param[0].split(' ')[0]]  # strip IDs in parentheses if present from parameter dialog model
            dp_descr = DP_IDS_TO_ITEMS[dp_id]

        if check_curtx:
            try:
                que = 'SELECT pcf.pcf_name,pcf.pcf_descr,pcf.pcf_ptc,pcf.pcf_pfc,pcf.pcf_curtx,pcf.pcf_width,pcf.pcf_unit,' \
                      'pcf.pcf_pid,null,null,null from pcf WHERE pcf_pid ="{}"'.format(dp_id)
                dinfo = scoped_session_idb.execute(que).fetchall()[0]
                return dinfo
            except IndexError:
                logger.debug('PID {} not in MIB.'.format(dp_id))

        ptc, pfc = ptt_reverse(_dp_items[dp_id]['fmt'])
        dinfo = [param[1], dp_descr, ptc, pfc, None, csize(_dp_items[dp_id]['fmt']) * 8, None, None, None, None, None]

    else:
        logger.warning('Info for parameter "{}" cannot be obtained'.format(param[0]))
        dinfo = None

    return dinfo


def create_hk_decoder(sid, *dp_ids, apid=None):
    """
    Create a decoder to interpret custom HK packets not defined in the MIB

    :param sid: SID of the custom HK
    :param dp_ids: list of parameters in the custom HK
    :param apid: APID of the custom HK packet
    :return:

    """
    parameters = [(dp_id, 'dp_item') for dp_id in dp_ids]

    if apid is None:
        que = 'SELECT pic_apid FROM pic WHERE pic_type=3 AND pic_stype=25'
        res = scoped_session_idb.execute(que).fetchall()
        apid = int(res[0][0])


    sid_off, sid_width = SID_LUT[(3, 25, apid)]

    que = 'SELECT plf_name, pcf_descr FROM pid left join plf on PLF_SPID=PID_SPID left join pcf on ' \
                'PCF_NAME=PLF_NAME where PID_TYPE=3 and PID_STYPE=25 and PID_APID={} and plf_offby={}'.format(apid, sid_off)
    sid_name, sid_descr = scoped_session_idb.execute(que).fetchall()[0]

    if sid_off != TM_HEADER_LEN:
        logger.warning('Inconsistent definition of SID parameter')

    parameters = [(sid_descr, sid_name)] + parameters
    label = add_tm_decoder(label='HK_{}'.format(sid), st=3, sst=25, apid=apid, parameters=parameters, sid=sid)
    return label


def add_user_parameter(parameter=None, apid=None, st=None, sst=None, sid=None, bytepos=None, fmt=None, offbi=None,
                       bitlen=None, parentwin=None):
    """
    Add a stand-alone (i.e. with positional info) User DEFined parameter

    :param parameter:
    :param apid:
    :param st:
    :param sst:
    :param sid:
    :param bytepos:
    :param fmt:
    :param offbi:
    :param bitlen:
    :param parentwin:
    :return:
    """
    # If a Gtk Parent Window is given, open the Dialog window to specify the details for the parameter
    if parentwin is not None:
        dialog = UserParameterDialog(parent=parentwin)

        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            try:
                label = dialog.label.get_text()
                apid = int(dialog.apid.get_text(), 0) if dialog.apid.get_text() != '' else None
                st, sst, sid = int(dialog.st.get_text(), 0), int(dialog.sst.get_text(), 0), dialog.sid.get_text()
                offbi = dialog.offbi.get_text()

                sid = int(sid, 0) if sid != '' else None
                offbi = int(offbi, 0) if offbi != '' else 0

                bytepos, fmt = int(dialog.bytepos.get_text(), 0), fmtlist[dialog.format.get_active_text()]
                if fmt in ('uint', 'ascii', 'oct'):
                    fmt += dialog.bitlen.get_text()
            except Exception as err:
                logger.error(err)
                dialog.destroy()
                return None

            if not cfg.has_section(CFG_SECT_PLOT_PARAMETERS):
                cfg.add_section(CFG_SECT_PLOT_PARAMETERS)
            cfg.save_option_to_file(CFG_SECT_PLOT_PARAMETERS, label, json.dumps(
                {'APID': apid, 'ST': st, 'SST': sst, 'SID': sid, 'bytepos': bytepos, 'format': fmt, 'offbi': offbi}))

            dialog.destroy()
            return label, apid, st, sst, sid, bytepos, fmt, offbi

        dialog.destroy()
        return

    # Else if parameter is given the others have to exist as well and the parameter is created
    if isinstance(parameter, str):
        label = parameter
        if isinstance(apid, int) and isinstance(st, int) and isinstance(sst, int) and isinstance(bytepos, int) and fmt:
            if fmt in ('uint', 'ascii', 'oct'):
                if bitlen:
                    fmt += bitlen
                else:
                    # print('Please give a bitlen (Amount of Bits) if fmt (Parameter Type) is set to "bit"')
                    logger.error('Parameter could not be created, no length was given.')
                    return

            if not isinstance(sid,int):
                sid = int(sid, 0) if sid is not None else None
            if not isinstance(offbi, int):
                offbi = int(offbi, 0) if offbi is not None else 0
        else:
            # print('Please give all neaded parameters in the correct format')
            logger.error('Parameter could not be created, because not all specifications were given correctly')
            return
    # Else if the Parameter is given as a Dictionary get all the needed informations and create the parameter
    elif isinstance(parameter, dict):
        label = parameter['label']
        apid = parameter['apid']
        st = parameter['st']
        sst = parameter['sst']
        byteps = parameter['bytepos']
        fmt = parameter['fmt']
        if isinstance(label, str) and isinstance(apid, int) and isinstance(st, int) and isinstance(sst, int) and isinstance(bytepos, int) and fmt:
            if fmt in ('uint', 'ascii', 'oct'):
                if bitlen:
                    fmt += bitlen
                else:
                    # print('Please give a bitlen (Amount of Bits) if fmt (Parameter Type) is set to "bit"')
                    logger.error('Parameter could not be created, no length was given.')
                    return

            if not isinstance(parameter['sid'], int):
                sid = int(parameter['sid'], 0) if parameter['sid'] else None
            if not isinstance(parameter['offbi'], int):
                offbi = int(parameter['offbi'], 0) if parameter['offbi'] else 0
        else:
            # print('Please give all neaded parameters in the correct format')
            logger.error('Parameter could not be created, because not all specifications were given correctly')
            return

    else:
        logger.error('Please give all arameters correctly')
        return
    # Add the created Parameter to the config file egse.cfg
    if not cfg.has_section(CFG_SECT_PLOT_PARAMETERS):
        cfg.add_section(CFG_SECT_PLOT_PARAMETERS)

    cfg.save_option_to_file(CFG_SECT_PLOT_PARAMETERS, label, json.dumps(
        {'APID': apid, 'ST': st, 'SST': sst, 'SID': sid, 'bytepos': bytepos, 'format': fmt, 'offbi': offbi}))

    return label, apid, st, sst, sid, bytepos, fmt, offbi


# Removes a user defined Parameter
def remove_user_parameter(parname=None, parentwin=None):
    """

    :param parname:
    :param parentwin:
    :return:
    """
    # If a Parameter is given delete the parameter
    if parname and cfg.has_option(CFG_SECT_PLOT_PARAMETERS, parname):
        cfg.remove_option_from_file(CFG_SECT_PLOT_PARAMETERS, parname)

        return parname

    # Else if a Parent Gtk window is given open the dialog to select a parameter
    # elif parentwin is not None:
    #     dialog = RemoveUserParameterDialog(cfg, parentwin)
    #     response = dialog.run()
    #     if response == Gtk.ResponseType.OK:
    #         param = dialog.remove_name.get_active_text()
    #
    #         cfg.remove_option_from_file(CFG_SECT_PLOT_PARAMETERS, param)
    #
    #         return param
    #
    #     else:
    #         dialog.destroy()
    #
    #     return

    elif parname is not None:
        logger.error('Unknown parameter {}. Cannot remove.'.format(parname))


# Edit an existing user defined Parameter
def edit_user_parameter(parentwin=None, parname=None):
    """

    :param parentwin:
    :param parname:
    :return:
    """

    # if an existing parameter is given, open same window as for adding a parameter, but pass along the existing information
    # simply overwrite the existing parameter with the new one
    if parname and cfg.has_option(CFG_SECT_PLOT_PARAMETERS, parname):
        dialog = UserParameterDialog(parentwin, parname)
        response = dialog.run()
        if response == Gtk.ResponseType.OK:
            try:
                label = dialog.label.get_text()
                apid = int(dialog.apid.get_text(), 0) if dialog.apid.get_text() != '' else None
                st, sst, sid = int(dialog.st.get_text(), 0), int(dialog.sst.get_text(), 0), dialog.sid.get_text()
                offbi = dialog.offbi.get_text()

                sid = int(sid, 0) if sid != '' else None
                offbi = int(offbi, 0) if offbi != '' else 0

                bytepos, fmt = int(dialog.bytepos.get_text(), 0), fmtlist[dialog.format.get_active_text()]
                if fmt in ('uint', 'ascii', 'oct'):
                    fmt += dialog.bitlen.get_text()
            except ValueError as error:
                logger.error(error)
                dialog.destroy()
                return

            if label != parname:
                cfg.remove_option_from_file(CFG_SECT_PLOT_PARAMETERS, parname)

            if not cfg.has_section(CFG_SECT_PLOT_PARAMETERS):
                cfg.add_section(CFG_SECT_PLOT_PARAMETERS)
            cfg.save_option_to_file(CFG_SECT_PLOT_PARAMETERS, label, json.dumps(
                {'APID': apid, 'ST': st, 'SST': sst, 'SID': sid, 'bytepos': bytepos, 'format': fmt, 'offbi': offbi}))

            dialog.destroy()

            return label, apid, st, sst, sid, bytepos, fmt, offbi
        else:
            dialog.destroy()
            return

    # Else Open a Window to select a parameter and call the same function again with an existing parameter
    # The upper code will be executed
    else:
        logger.error('Unknown parameter {}'.format(parname))
        return
        # if parname is not None:
        #     logger.warning('User defined parameter "{}" could not be found, please select a new one'.format(parname))
        #
        # dialog = EditUserParameterDialog(cfg, parentwin)
        # response = dialog.run()
        # if response == Gtk.ResponseType.OK:
        #     param = dialog.edit_name.get_active_text()
        #     dialog.destroy()
        #     ret = edit_user_parameter(parentwin, param)
        #     if ret:
        #         label, apid, st, sst, sid, bytepos, fmt, offbi = ret
        #         return label, apid, st, sst, sid, bytepos, fmt, offbi
        #     else:
        #         return
        # else:
        #     dialog.destroy()
        #     return


def read_plm_gateway_data(raw):
    """
    Interprets raw data from SMILE SXI PLM SpW Gateway data port (5000) and returns SpW packet(s) plus decoded PLM header data (see H8823-UM-HVS-0001)

    :param raw: binary data as received from PLM Gateway
    :return:
    """
    ct, ft, sw, data = raw[:4], raw[4:8], raw[8:12], raw[12:]
    t = int.from_bytes(ct, 'little') + int.from_bytes(ft, 'little') / 1e9
    tatype = bin(sw[-1] & 0b11)
    plen = int.from_bytes(sw[:3], 'little')
    return data, plen, t, tatype


def pack_plm_gateway_data(raw):
    """
    Pack data for TC to SMILE SXI PLM SpW Gateway data port (5000) (see H8823-UM-HVS-0001)

    :param raw: binary SpW packet
    :return:
    """
    cw = len(raw).to_bytes(3, 'little') + b'\x00'  # command word for EOP terminated packet
    return cw + raw


def get_spw_from_plm_gw(sock_plm, sock_gnd, strip_spw=4):
    """

    :param sock_plm:
    :param sock_gnd:
    :param strip_spw:
    """
    # print('> SPW PCKT routing started! <')
    # while sock_plm.fileno() > 0 and sock_gnd.fileno() > 0:

    data = b''
    while len(data) < 12:
        data += sock_plm.recv(12 - len(data))

    spwdata, plen, _, _ = read_plm_gateway_data(data)
    while len(spwdata) < plen:
        spwdata += sock_plm.recv(plen - len(spwdata))

    if strip_spw:
        sock_gnd.send(spwdata[strip_spw:])  # strip SpW header before routing packet
    else:
        sock_gnd.send(spwdata)
    logger.info(plen, len(spwdata), spwdata.hex())


def setup_gw_spw_routing(gw_hp, gnd_hp, tc_hp=None, spw_head=b'\xfe\x02\x00\x00'):
    """
    A router for the single-port HVS SpW Brick that handles the HVS and SpW protocol for the CCS

    :param gw_hp:
    :param gnd_hp:
    :param tc_hp:
    :param spw_head:
    """
    gw = socket.socket()
    gw.settimeout(10)
    gw.connect(gw_hp)

    gnd = socket.socket()
    gnd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    gnd.bind(gnd_hp)
    gnd.listen()

    if tc_hp is not None:
        tcsock = socket.socket()
        tcsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        tcsock.bind(tc_hp)
        tcsock.listen()
    else:
        tcsock = gnd

    while gw.fileno() > 0:
        logger.info(gw, gnd)
        gnd_s, addr = gnd.accept()
        tc_s, addr2 = tcsock.accept()

        while True:
            try:
                r, w, e = select.select([gw, tc_s], [], [])
                for sockfd in r:
                    if sockfd == gw:
                        while select.select([gw], [], [], 0)[0]:
                            get_spw_from_plm_gw(gw, gnd_s, strip_spw=len(spw_head))
                    elif sockfd == tc_s:
                        while select.select([tc_s], [], [], 0)[0]:
                            rawtc = tc_s.recv(1024)
                            if rawtc == b'':
                                raise socket.error('Lost connection to port '.format(tc_s.getsockname()))
                            else:
                                logger.info('# TC:', spw_head + rawtc)
                                msg = pack_plm_gateway_data(spw_head + rawtc)
                                gw.send(msg)

            # t_tc = threading.Thread(target=get_spw_from_plm_gw, args=[gw, tc_s])

            except socket.timeout:
                continue
            except socket.error:
                gnd_s.close()
                tc_s.close()
                logger.info('Closed TM/TC ports. Reopening...')
                break

        time.sleep(1)

    gnd.close()
    tcsock.close()
    gw.close()


def _gresb_unpack(raw, hdr_endianess='big'):
    pid = raw[0]
    pktlen = int.from_bytes(raw[1:4], hdr_endianess)
    return raw[4:], pktlen, pid


def _gresb_pack(pkt, protocol_id=0, hdr_endianess='big'):
    return protocol_id.to_bytes(1, hdr_endianess) + len(pkt).to_bytes(3, hdr_endianess) + pkt


def get_gresb_pkt(gresb, gnd_s, hdr_endianess='big'):
    """

    :param gresb:
    :param gnd_s:
    :param hdr_endianess:
    """
    data = b''
    while len(data) < 4:
        data += gresb.recv(4 - len(data))

    spwdata, plen, pid = _gresb_unpack(data, hdr_endianess=hdr_endianess)
    while len(spwdata) < plen:
        spwdata += gresb.recv(plen - len(spwdata))

    gnd_s.send(spwdata)

    logger.debug(plen, len(spwdata), spwdata.hex())
    print(plen, len(spwdata), spwdata.hex())


def setup_gresb_routing(gresb_hp, gnd_hp, tc_hp=None, protocol_id=0, hdr_endianess='big'):
    """
    Handle GRESB protocol for CCS

    :param gresb_hp:
    :param gnd_hp:
    :param tc_hp:
    :param protocol_id:
    :param hdr_endianess:
    """
    gresb = socket.socket()
    gresb.settimeout(10)
    gresb.connect(gresb_hp)

    gnd = socket.socket()
    gnd.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    gnd.bind(gnd_hp)
    gnd.listen()

    if tc_hp is not None:
        tcsock = socket.socket()
        tcsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        tcsock.bind(tc_hp)
        tcsock.listen()
    else:
        tcsock = gnd

    while gresb.fileno() > 0:
        logger.info(gresb, gnd)
        gnd_s, addr = gnd.accept()
        tc_s, addr2 = tcsock.accept()

        while True:
            try:
                print('START')
                r, w, e = select.select([gresb, tc_s], [], [])
                print(r)
                for sockfd in r:
                    if sockfd == gresb:
                        while select.select([gresb], [], [], 0)[0]:
                            get_gresb_pkt(gresb, gnd_s, hdr_endianess=hdr_endianess)
                    elif sockfd == tc_s:
                        while select.select([tc_s], [], [], 0)[0]:
                            rawtc = tc_s.recv(1024)
                            if rawtc == b'':
                                raise socket.error('Lost connection to port '.format(tc_s.getsockname()))
                            else:
                                logger.info('# TC:', rawtc)
                                print('# TC:', rawtc)
                                msg = _gresb_pack(rawtc, protocol_id=protocol_id, hdr_endianess=hdr_endianess)
                                print(gresb)
                                gresb.send(msg)
                                print(msg)

            except socket.timeout:
                continue
            except socket.error:
                gnd_s.close()
                tc_s.close()
                logger.info('Closed TM/TC ports. Reopening...')
                break
            print('########')
        time.sleep(1)

    gnd.close()
    tcsock.close()
    gresb.close()


def extract_spw(stream):
    """
    Read SpW packets from a byte stream

    :param stream:
    :return:
    """

    pkt_size_stream = b''
    pckts = []
    headers = []

    while True:
        pkt_size_stream += stream.read(2)
        if len(pkt_size_stream) < 2:
            break
        tla, pid = pkt_size_stream[:2]
        logger.debug('{}, {}'.format(tla, pid))

        # if (tla == pc.SPW_DPU_LOGICAL_ADDRESS) and (pid in SPW_PROTOCOL_IDS_R):
        if pid in SPW_PROTOCOL_IDS_R:
            buf = pkt_size_stream
        else:
            pkt_size_stream = pkt_size_stream[1:]
            continue

        if SPW_PROTOCOL_IDS_R[pid] == "FEEDATA":
            header = pc.FeeDataTransferHeader()
        elif SPW_PROTOCOL_IDS_R[pid] == "RMAP":
            while len(buf) < 3:
                instruction = stream.read(1)
                if not instruction:
                    return pckts, buf
                buf += instruction

            instruction = buf[2]

            if (instruction >> 6) & 1:
                header = pc.RMapCommandHeader()
            elif (instruction >> 5) & 0b11 == 0b01:
                header = pc.RMapReplyWriteHeader()
            elif (instruction >> 5) & 0b11 == 0b00:
                header = pc.RMapReplyReadHeader()

        hsize = header.__class__.bits.size

        while len(buf) < hsize:
            buf += stream.read(hsize - len(buf))

        header.bin[:] = buf[:hsize]

        if SPW_PROTOCOL_IDS_R[pid] == "FEEDATA":
            pktsize = header.bits.DATA_LEN
        elif (header.bits.PKT_TYPE == 1 and header.bits.WRITE == 0) or (
                header.bits.PKT_TYPE == 0 and header.bits.WRITE == 1):
            pktsize = hsize
        else:
            pktsize = hsize + header.bits.DATA_LEN + pc.RMAP_PEC_LEN  # TODO: no data CRC from FEEsim?

        while len(buf) < pktsize:
            data = stream.read(pktsize - len(buf))
            if not data:
                return headers, pckts, pkt_size_stream
            buf += data

        buf = buf[:pktsize]
        pkt_size_stream = buf[pktsize:]
        pckts.append(buf)
        headers.append(header)

    return headers, pckts, pkt_size_stream


##
#  Save pool
#
#  Dump content of data pool _pname_ to file _fname_, either as a concatenated binary sequence or in hexadecimal
#  representation and one packet per line. Selective saving (by service type) possible.
#  @param fname     File name for the dump
#  @param pname     Name of pool to be saved
#  @param mode      Type of the saved file. _binary_ or _hex_
#  @param st_filter Packets of that service type will be saved
def savepool(filename, pool_name, mode='binary', st_filter=None):
    """

    :param filename:
    :param pool_name:
    :param mode:
    :param st_filter:
    """
    # get new session for saving process
    logger.info('Saving pool content to disk...')
    tmlist = list(get_packets_from_pool(pool_name))

    Tmdump(filename, tmlist, mode=mode, st_filter=st_filter, check_crc=False)
    logger.info('Pool {} saved as {} in {} mode'.format(pool_name, filename, mode.upper()))


def get_packets_from_pool(pool_name, indices=None, st=None, sst=None, apid=None, **kwargs):
    """

    :param pool_name:
    :param indices:
    :param st:
    :param sst:
    :param apid:
    :return:
    """
    new_session = scoped_session_storage

    rows = new_session.query(
        DbTelemetry
    ).join(
        DbTelemetryPool,
        DbTelemetry.pool_id == DbTelemetryPool.iid
    ).filter(
        DbTelemetryPool.pool_name == pool_name
    )

    if indices is not None and len(indices) > 0:
        rows = rows.filter(
            DbTelemetry.idx.in_(indices)
        )

    if st is not None:
        rows = rows.filter(DbTelemetry.stc == st)
    if sst is not None:
        rows = rows.filter(DbTelemetry.sst == sst)
    if apid is not None:
        rows = rows.filter(DbTelemetry.apid == apid)

    ret = [row.raw for row in rows.yield_per(1000)]
    new_session.close()
    return ret


def add_tst_import_paths():
    """
    Include all paths to TST files that could potentially be used.

    :return:
    """
    # Add general tst path
    sys.path.append(cfg.get('paths', 'tst'))
    # Add all subfolders
    sys.path.append(cfg.get('paths', 'tst') + '/codeblockreusefeature')
    sys.path.append(cfg.get('paths', 'tst') + '/config_editor')
    sys.path.append(cfg.get('paths', 'tst') + '/confignator')
    sys.path.append(cfg.get('paths', 'tst') + '/doc')
    sys.path.append(cfg.get('paths', 'tst') + '/icon_univie')
    sys.path.append(cfg.get('paths', 'tst') + '/images')
    sys.path.append(cfg.get('paths', 'tst') + '/log_viewer')
    sys.path.append(cfg.get('paths', 'tst') + '/notes')
    sys.path.append(cfg.get('paths', 'tst') + '/progress_view')
    sys.path.append(cfg.get('paths', 'tst') + '/sketch_desk')
    sys.path.append(cfg.get('paths', 'tst') + '/test_specs')
    sys.path.append(cfg.get('paths', 'tst') + '/testing_library')
    # insert this to import the tst view.py, not the one in .local folder
    sys.path.insert(0, cfg.get('paths', 'tst') + '/tst')


def interleave_lists(*args):
    """

    :param args:
    :return:
    """
    if len({len(x) for x in args}) > 1:
        logger.warning('Iterables are not of the same length, result will be truncated to the shortest input!')
    return [i for j in zip(*args) for i in j]


def create_format_model():
    """

    :return:
    """
    store = Gtk.ListStore(str)
    for fmt in fmtlist.keys():
        if fmt != 'bit*':
            store.append([fmt])
    for pers in personal_fmtlist:
        store.append([pers])
    return store


def _get_displayed_pool_path(pool_name=None):
    """
    Try to get name of pool currently displayed in poolviewer or loaded in current poolmanager session

    :param pool_name:
    :return:
    """

    if pool_name is None:
        pv = get_module_handle('poolviewer', timeout=1)

        if not pv:
            return

        return str(pv.Variables('active_pool_info')[0])

    pmgr = get_module_handle('poolmanager', timeout=1)
    if not pmgr:
        return

    pools = pmgr.Dictionaries('loaded_pools')
    if pool_name in pools:
        return str(pools[pool_name][0])
    else:
        return


def collect_13(pool_name, starttime=None, endtime=None, startidx=None, endidx=None, join=True, collect_all=False,
               sdu=None, verbose=True):
    """
    Collect and group S13 down transfer packet trains

    :param pool_name:
    :param starttime:
    :param endtime:
    :param startidx:
    :param endidx:
    :param join:
    :param collect_all:
    :param sdu:
    :param verbose:
    :return:
    """
    if not os.path.isfile(pool_name):
        logger.debug('{} is not a file, looking it up in DB'.format(pool_name))
        # try fetching pool info from pools opened in viewer
        # pname = _get_displayed_pool_path(pool_name)
        # if pname:
        #     pool_name = pname

    rows = get_pool_rows(pool_name, check_existence=True)

    # if starttime is None:
    #     starttime = 0

    # if start is not None:
    #     starttime = float(rows.filter(DbTelemetry.idx == start).first().timestamp[:-1])

    # if endtime is None:
    #     endtime = get_last_pckt_time(pool_name, string=False)

    # if end is not None:
    #     endtime = float(rows.filter(DbTelemetry.idx == end).first().timestamp[:-1])

    # if starttime is None or endtime is None:
    #     raise ValueError('Specify start(time) and end(time)!')

    ces = {}
    # faster method to collect already completed TM13 transfers
    tm_bounds = rows.filter(DbTelemetry.stc == 13, DbTelemetry.sst.in_([1, 3])).order_by(DbTelemetry.idx)

    if starttime is not None:
        tm_bounds = tm_bounds.filter(func.left(DbTelemetry.timestamp, func.length(DbTelemetry.timestamp) - 1) >= starttime)

    if endtime is not None:
        tm_bounds = tm_bounds.filter(func.left(DbTelemetry.timestamp, func.length(DbTelemetry.timestamp) - 1) <= endtime)

    if startidx is not None:
        tm_bounds = tm_bounds.filter(DbTelemetry.idx >= startidx)

    if endidx is not None:
        tm_bounds = tm_bounds.filter(DbTelemetry.idx <= endidx)

    if sdu:
        tm_bounds = tm_bounds.filter(func.left(DbTelemetry.data, 1) == sdu.to_bytes(SDU_PAR_LENGTH, 'big'))

    # quit if no start and end packet are found
    if tm_bounds.count() < 2:
        return {None: None}

    tm_132 = rows.filter(DbTelemetry.stc == 13, DbTelemetry.sst == 2).order_by(DbTelemetry.idx)

    if starttime is not None:
        tm_132 = tm_132.filter(func.left(DbTelemetry.timestamp, func.length(DbTelemetry.timestamp) - 1) >= starttime)

    if endtime is not None:
        tm_132 = tm_132.filter(func.left(DbTelemetry.timestamp, func.length(DbTelemetry.timestamp) - 1) <= endtime)

    if startidx is not None:
        tm_132 = tm_132.filter(DbTelemetry.idx >= startidx)

    if endidx is not None:
        tm_132 = tm_132.filter(DbTelemetry.idx <= endidx)

    if sdu:
        tm_132 = tm_132.filter(func.left(DbTelemetry.data, 1) == sdu.to_bytes(SDU_PAR_LENGTH, 'big'))

    # make sure to start with a 13,1
    while tm_bounds[0].sst != 1:
        tm_bounds = tm_bounds[1:]
        if len(tm_bounds) < 2:
            return {None: None}
    # and end with a 13,3
    while tm_bounds[-1].sst != 3:
        tm_bounds = tm_bounds[:-1]
        if len(tm_bounds) < 2:
            return {None: None}

    if not collect_all:
        tm_bounds = tm_bounds[:2]
    else:
        tm_bounds = tm_bounds[:]  # cast Query to list if not list already

    # check for out of order 1s and 3s TODO: there can be short transfers with only TM13,1 and no TM13,3
    idcs = [i.sst for i in tm_bounds]
    outoforder = []
    for i in range(len(idcs) - 1):
        if idcs[i + 1] == idcs[i]:
            if idcs[i] == 1:
                outoforder.append(i)
            elif idcs[i] == 3:
                outoforder.append(i + 1)
    dels = 0
    for i in outoforder:
        del tm_bounds[i - dels]
        dels += 1
    # check if start/end (1/3) strictly alternating
    if not np.diff([i.sst for i in tm_bounds]).all():
        print('Detected inconsistent transfers')
        return {None: None}

    k = 0
    n = len(tm_bounds)
    cnt = 0
    while k < n - 1:
        i, j = tm_bounds[k:k + 2]
        if i.sst == j.sst:
            k += 1
            logger.warning('Identical consecutive SSTs found (idx={})'.format(i.idx))
            continue
        else:
            pkts = [a.raw[S13_HEADER_LEN_TOTAL:-PEC_LEN] for a in tm_132.filter(DbTelemetry.idx > i.idx, DbTelemetry.idx < j.idx)]
            if join:
                ces[float(i.timestamp[:-1])] = i.raw[S13_HEADER_LEN_TOTAL:-PEC_LEN] + b''.join(pkts) + j.raw[S13_HEADER_LEN_TOTAL:-PEC_LEN]
            else:
                ces[float(i.timestamp[:-1])] = [i.raw[S13_HEADER_LEN_TOTAL:-PEC_LEN]] + [b''.join(pkts)] + [j.raw[S13_HEADER_LEN_TOTAL:-PEC_LEN]]
            k += 2

        cnt += 1
        if verbose:
            print('Collected {} of {} S13 transfers.'.format(cnt, n // 2), end='\r')

    # for (i, j) in zip(tm_bounds[::2], tm_bounds[1::2]):
    #     pkts = [a.raw[21:-2] for a in tm_132.filter(DbTelemetry.idx > i.idx, DbTelemetry.idx < j.idx)]
    #     if join:
    #         ces[float(i.timestamp[:-1])] = i.raw[21:-2] + b''.join(pkts) + j.raw[21:-2]
    #     else:
    #         ces[float(i.timestamp[:-1])] = [i.raw[21:-2]] + [b''.join(pkts)] + [j.raw[21:-2]]

    return ces


def dump_large_data(pool_name, starttime=0, endtime=None, outdir="", dump_all=False, sdu=None, startidx=None,
                    endidx=None, verbose=True):
    """
    Dump S13 down transfer data to disk. For pools loaded from a file, pool_name must be the absolute path of that file.

    :param pool_name:
    :param starttime:
    :param endtime:
    :param outdir:
    :param dump_all:
    :param sdu:
    :param startidx:
    :param endidx:
    :param verbose:
    """

    if not os.path.exists(outdir):
        raise FileNotFoundError('Directory "{}" does not exist'.format(outdir))

    filedict = {}
    ldt_dict = collect_13(pool_name, starttime=starttime, endtime=endtime, join=True, collect_all=dump_all,
                          startidx=startidx, endidx=endidx, sdu=sdu, verbose=verbose)

    ldt_cnt = 0
    for i, buf in enumerate(ldt_dict, 1):
        if ldt_dict[buf] is None:
            continue

        try:
            obsid, ctime, ftime, ctr = s13_unpack_data_header(ldt_dict[buf])
        except ValueError as err:
            logger.error('Incompatible definition of S13 data header.')
            raise err

        fname = os.path.join(outdir, "LDT_{:03d}_{:010d}_{:06d}.ce".format(obsid, ctime, ctr))

        with open(fname, "wb") as fdesc:
            fdesc.write(ldt_dict[buf])
            filedict[buf] = fdesc.name
            ldt_cnt += 1

    if ldt_cnt != 0:
        logger.info('Dumped {} CEs to {}'.format(ldt_cnt, outdir))
    logger.debug('{} CEs found'.format(ldt_cnt))
    # print('Dumped {} CEs to {}'.format(ldt_cnt, outdir))

    return filedict


class DbTools:
    """
    SQL database management helpers
    """

    @staticmethod
    def recover_from_db(pool_name=None, iid=None, dump=False):
        """
        Recover TMTC packets not stored on disk from DB
        @param pool_name:
        @param iid:
        @param dump:
        @return:
        """
        new_session = scoped_session_storage
        if pool_name:
            rows = new_session.query(
                DbTelemetry).join(
                DbTelemetryPool, DbTelemetry.pool_id == DbTelemetryPool.iid).filter(
                DbTelemetryPool.pool_name == pool_name)
        elif iid:
            rows = new_session.query(
                DbTelemetry).join(
                DbTelemetryPool, DbTelemetry.pool_id == DbTelemetryPool.iid).filter(
                DbTelemetryPool.iid == iid)
        else:
            logger.error('Must give pool_name or iid')
            return None

        if dump:
            with open(dump, 'wb') as fdesc:
                fdesc.write(b''.join([row.raw for row in rows]))
        new_session.close()
        return rows

    @staticmethod
    def clear_from_db(pool_name, answer=''):
        """
        Remove pool pool_name from DB
        @param pool_name:
        @param answer:
        @return:
        """
        # answer = ''
        while answer.lower() not in ['yes', 'no', 'y', 'n']:
            answer = input("Clear pool\n >{}<\nfrom DB? (yes/no)\n".format(pool_name))
        if answer.lower() in ['yes', 'y']:
            new_session = scoped_session_storage
            indb = new_session.execute('select * from tm_pool where pool_name="{}"'.format(pool_name))
            if len(indb.fetchall()) == 0:
                logger.error('POOL\n >{}<\nNOT IN DB!'.format(pool_name))
                return
            new_session.execute(
                'delete tm from tm inner join tm_pool on tm.pool_id=tm_pool.iid where tm_pool.pool_name="{}"'.format(
                    pool_name))
            new_session.execute('delete tm_pool from tm_pool where tm_pool.pool_name="{}"'.format(pool_name))
            # new_session.flush()
            new_session.commit()
            new_session.close()
            logger.info('DELETED POOL\n >{}<\nFROM DB'.format(pool_name))
        return

    @staticmethod
    def _clear_db():
        """
        Delete all pools from DB
        @return:
        """
        answer = ''
        while answer.lower() not in ['yes', 'no', 'y', 'n']:
            answer = input(" > > > Clear all TMTC data from DB? < < < (yes/no)\n".upper())
        if answer.lower() in ['yes', 'y']:
            new_session = scoped_session_storage
            new_session.execute('delete tm from tm inner join tm_pool on tm.pool_id=tm_pool.iid')
            new_session.execute('delete tm_pool from tm_pool')
            # new_session.flush()
            new_session.commit()
            new_session.close()
            logger.info('>>> DELETED ALL POOLS FROM DB <<<')
        return

    @staticmethod
    def _purge_db_logs(date=None):
        """
        Purge binary MySQL logs before _date_
        @param date: ISO formatted date string; defaults to now, if None
        """
        if date is None:
            now = datetime.datetime.now()
            date = datetime.datetime.strftime(now, '%Y-%m-%d %H:%M:%S')
        new_session = scoped_session_storage
        new_session.execute('PURGE BINARY LOGS BEFORE "{:s}"'.format(date))
        new_session.close()

    @staticmethod
    def delete_abandoned_rows(timestamp=None):
        new_session = scoped_session_storage
        try:
            if timestamp is None:
                new_session.execute(
                    'DELETE tm FROM tm INNER JOIN tm_pool ON tm.pool_id=tm_pool.iid WHERE \
                    tm_pool.pool_name LIKE "---TO-BE-DELETED%"')
                new_session.execute(
                    'DELETE rmap_tm FROM rmap_tm INNER JOIN tm_pool ON rmap_tm.pool_id=tm_pool.iid WHERE \
                    tm_pool.pool_name LIKE "---TO-BE-DELETED%"')
                new_session.execute(
                    'DELETE feedata_tm FROM feedata_tm INNER JOIN tm_pool ON feedata_tm.pool_id=tm_pool.iid WHERE \
                    tm_pool.pool_name LIKE "---TO-BE-DELETED%"')
                new_session.execute('DELETE tm_pool FROM tm_pool WHERE tm_pool.pool_name LIKE "---TO-BE-DELETED%"')
            else:
                new_session.execute(
                    'DELETE tm FROM tm INNER JOIN tm_pool ON tm.pool_id=tm_pool.iid WHERE \
                    tm_pool.pool_name="---TO-BE-DELETED{}"'.format(timestamp))
                new_session.execute('DELETE tm_pool FROM tm_pool WHERE tm_pool.pool_name="---TO-BE-DELETED{}"'.format(
                    timestamp))

            new_session.commit()
            return 0
        except Exception as err:
            logger.error("Error trying to delete old DB rows: {}".format(err))
            new_session.rollback()
            return 1
        finally:
            new_session.close()


class TestReport:
    """
    Provides functions for interactive test reporting
    """
    def __init__(self, filename, version, idb_version, gui=False, delimiter='|'):
        super(TestReport, self).__init__()
        self.specfile = filename
        self.delimiter = delimiter
        self.gui = gui
        self.report = dict()

        self.version = int(version)
        self.idb_version = str(idb_version)

        self.step_rowid = dict()
        self._read_test_spec(filename)

        self.testname = self.report[1][0]

    def _read_test_spec(self, filename):
        with open(filename, 'r') as fd:
            csv = fd.readlines()

        for i, line in enumerate(csv):
            items = line.strip().split(self.delimiter)
            self.report[i] = items
            if items[0].startswith('Step '):
                self.step_rowid[items[0]] = i

    def execute_step(self, step, ask=True):
        """

        :param step:
        :param ask:
        :return:
        """
        if not ask:
            return

        try:
            exe_msg = '{}:\n{}'.format(step.upper(), self.report[self.step_rowid[str(step)]][1])
            if self.gui:
                dialog = TestExecGUI(self.report[1][0], exe_msg)
                response = dialog.run()

                if response == Gtk.ResponseType.YES:
                    dialog.destroy()
                    execute = True
                else:
                    dialog.destroy()
                    execute = False

            else:
                execute = input(exe_msg + ':\n(y/n)? > ')
                while execute.lower() not in ('y', 'yes', 'n', 'no'):
                    execute = input(exe_msg + ':\n(y/n)? > ')

                if execute in ('y', 'yes'):
                    execute = True
                else:
                    execute = False

            if execute:
                return
            else:
                pass  # TODO: abort step execution

        except KeyError:
            logger.error('"{}": no such step defined!'.format(str(step)))
            return

    def verify_step(self, step):
        """

        :param step:
        :return:
        """
        try:
            ver_msg = '{}:\n{}'.format(step.upper(), self.report[self.step_rowid[str(step)]][2])
            if self.gui:
                dialog = TestReportGUI(self.report[1][0], ver_msg)
                response = dialog.run()

                if response == Gtk.ResponseType.YES:
                    result = 'VERIFIED'
                    comment = dialog.comment.get_text()
                    dialog.destroy()
                elif response == Gtk.ResponseType.NO:
                    result = 'FAILED'
                    comment = dialog.comment.get_text()
                    dialog.destroy()
                else:
                    dialog.destroy()
                    return

                if comment:
                    result += ' ({})'.format(comment)

            else:
                result = input(ver_msg + ':\n>')

        except KeyError:
            logger.error('"{}": no such step defined!'.format(str(step)))
            return

        self.report[self.step_rowid[str(step)]][3] = result

    def export(self, reportdir=None, reportfile=None):
        """

        :param reportdir:
        :param reportfile:
        """
        if reportfile is None:
            if reportdir is None:
                reportfile = self.specfile.replace('.csv_PIPE', '-TR-{:03d}.csv_PIPE'.format(self.version)).replace('/testspec/', '/testrep/')
            else:
                reportdir += '/' if not reportdir.endswith('/') else ''
                reportfile = reportdir + self.specfile.split('/')[-1].replace('.csv_PIPE', '-TR-{:03d}.csv_PIPE'.format(self.version))

        self.report[1][3] += ' TR-{:03d}, MIB v{}'.format(self.version, self.idb_version)
        self.report[2][3] = time.strftime('%Y-%m-%d')

        buf = '\n'.join([self.delimiter.join(self.report[line]) for line in range(len(self.report))])

        Path(os.path.dirname(reportfile)).mkdir(parents=True, exist_ok=True)  # create directory if it does not exist

        with open(reportfile, 'w') as fd:
            fd.write(buf + '\n')
        logger.info('Report written to {}.'.format(reportfile))
        print('Report written to {}.'.format(reportfile))


class TestReportGUI(Gtk.MessageDialog):
    """
    GUI for the TestReport class
    """
    def __init__(self, testlabel, message):
        super(TestReportGUI, self).__init__(title=testlabel,
                                            buttons=(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                                                     Gtk.STOCK_NO, Gtk.ResponseType.NO,
                                                     Gtk.STOCK_YES, Gtk.ResponseType.YES,))

        head, body = self.get_message_area().get_children()
        head.set_text(message)

        cancel, fail, verify = self.get_action_area().get_children()

        cancel.get_child().get_child().get_children()[1].set_label('Skip')
        fail.get_child().get_child().get_children()[1].set_label('FAILED')
        verify.get_child().get_child().get_children()[1].set_label('VERIFIED')

        self.comment = Gtk.Entry()
        self.comment.set_placeholder_text('Optional comment')
        self.get_message_area().add(self.comment)

        verify.grab_focus()

        self.show_all()


class TestExecGUI(Gtk.MessageDialog):
    """
    Dialog window to confirm test step execution
    """
    def __init__(self, testlabel, message):
        super(TestExecGUI, self).__init__(title=testlabel,
                                          buttons=(# Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                                                   Gtk.STOCK_YES, Gtk.ResponseType.YES,))

        head, body = self.get_message_area().get_children()
        head.set_text(message)

        # abort, exe = self.get_action_area().get_children()
        exe, = self.get_action_area().get_children()

        # abort.get_child().get_child().get_children()[1].set_label('ABORT')
        exe.get_child().get_child().get_children()[1].set_label('EXECUTE')

        exe.grab_focus()

        self.show_all()


class TmParameterDecoderDialog(Gtk.Dialog):
    """
    Interface to define custom paramters
    """
    def __init__(self, parent=None):
        Gtk.Dialog.__init__(self, "Add User Parameter", parent, 0,
                            buttons=(Gtk.STOCK_OK, Gtk.ResponseType.OK, Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL))

        self.set_border_width(5)

        box = self.get_content_area()
        ok_button = self.get_action_area().get_children()[0]
        ok_button.set_sensitive(False)

        bytebox = Gtk.HBox()

        self.format = Gtk.ComboBoxText()
        self.format.set_model(create_format_model())
        self.format.set_tooltip_text('Format type')
        self.format.connect('changed', self.bitlen_active)
        self.format.connect('changed', self.check_ok_sensitive, ok_button)
        # self.offbi = Gtk.Entry()
        # self.offbi.set_placeholder_text('Bit offset')
        # self.offbi.set_tooltip_text('Bit offset from byte alignment')
        # self.offbi.set_sensitive(False)
        self.bitlen = Gtk.Entry()
        self.bitlen.set_placeholder_text('Length')
        self.bitlen.set_tooltip_text('Length in bits (for uint*) or bytes (ascii*, oct*)')
        self.bitlen.set_sensitive(False)

        bytebox.pack_start(self.format, 0, 0, 0)
        # bytebox.pack_start(self.offbi, 0, 0, 0)
        bytebox.pack_start(self.bitlen, 0, 0, 0)
        bytebox.set_spacing(5)

        self.label = Gtk.Entry()
        self.label.set_placeholder_text('Parameter Label')
        self.label.connect('changed', self.check_ok_sensitive, ok_button)

        box.pack_start(self.label, 0, 0, 0)
        box.pack_end(bytebox, 0, 0, 0)
        box.set_spacing(10)

        self.show_all()

    def check_ok_sensitive(self, unused_widget, button):
        """

        :param unused_widget:
        :param button:
        """
        if len(self.label.get_text()) == 0 or not self.format.get_active_text():
            button.set_sensitive(False)
        else:
            button.set_sensitive(True)

    def bitlen_active(self, widget):
        """

        :param widget:
        """
        if widget.get_active_text().endswith('*'):
            self.bitlen.set_sensitive(True)
            # if widget.get_active_text().startswith(('ascii', 'oct')):
            #     self.offbi.set_sensitive(False)
            # else:
            #     self.offbi.set_sensitive(True)
        else:
            self.bitlen.set_sensitive(False)


class TmDecoderDialog(Gtk.Dialog):
    """
    Interface to define custom packet structures
    """
    def __init__(self, logger, parameter_set=None, parent=None):
        Gtk.Dialog.__init__(self, "Build User Defined Packet Structure", parent, 0)
        self.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK, Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)

        # self.set_default_size(780,560)
        self.set_border_width(5)
        self.set_resizable(True)

        self.logger = logger
        self.cfg = cfg

        self.session_factory_idb = scoped_session_idb
        self.session_factory_storage = scoped_session_storage

        box = self.get_content_area()

        slots = self.create_view(parameter_set=parameter_set)
        box.pack_start(slots, 1, 1, 0)

        self.ok_button = self.get_widget_for_response(Gtk.ResponseType.OK)
        self.ok_button.set_sensitive(False)

        self.show_all()

    def create_view(self, parameter_set=None):
        """

        :param parameter_set:
        :return:
        """
        parameter_view = self.create_param_view()
        packet = None

        slotbox = Gtk.HBox()
        slotbox.set_homogeneous(True)
        slotbox.set_spacing(50)

        entrybox = Gtk.HBox()

        self.apid = Gtk.Entry()
        self.st = Gtk.Entry()
        self.sst = Gtk.Entry()
        self.label = Gtk.Entry()

        self.apid.set_placeholder_text('APID')
        self.st.set_placeholder_text('Service Type')
        self.sst.set_placeholder_text('Service Subtype')
        self.label.set_placeholder_text('Label')
        self.label.set_tooltip_text('Label for the current configuration')

        self.sid = Gtk.Entry()
        self.sid.set_placeholder_text('SID')
        self.sid.set_tooltip_text('Discriminant, only applicable if ST and SST have a SID defined in the MIB.')

        self.apid.connect('changed', self.check_entry)
        self.st.connect('changed', self.check_entry)
        self.sst.connect('changed', self.check_entry)
        self.label.connect('changed', self.check_entry)

        entrybox.pack_start(self.label, 0, 0, 0)
        entrybox.pack_start(self.apid, 0, 0, 0)
        entrybox.pack_start(self.st, 0, 0, 0)
        entrybox.pack_start(self.sst, 0, 0, 0)
        entrybox.pack_start(self.sid, 0, 0, 0)

        entrybox.set_homogeneous(True)
        entrybox.set_spacing(5)

        # decisionbox = Gtk.HBox()
        #
        # self.given_poition = Gtk.RadioButton.new_with_label_from_widget(None, 'Local')
        # self.given_poition.set_tooltip_text('Decode in given order')
        # self.idb_position = Gtk.RadioButton.new_with_label_from_widget(self.given_poition, 'IDB')
        # self.idb_position.set_tooltip_text('Decode by parameter position given in IDB')
        #
        # decisionbox.pack_start(self.given_poition, 0, 0, 0)
        # decisionbox.pack_start(self.idb_position, 0, 0, 0)

        if parameter_set is not None:

            if self.cfg.has_option('ccs-user_defined_packets', parameter_set):
                packet = json.loads(self.cfg['ccs-user_defined_packets'][parameter_set])
                value = parameter_set
            else:
                for pack in self.cfg['ccs-user_defined_packets']:
                    pack_val = json.loads(self.cfg['ccs-user_defined_packets'][pack])
                    if pack_val[0] == parameter_set:
                        packet = pack_val
                        value = pack

            if packet:
                value = value.split('-')

                self.st.set_text(value[0])
                self.sst.set_text(value[1])
                self.apid.set_text(value[2])
                self.sid.set_text(value[3])

                param_name = []
                for j in packet[1]:
                    param_name.append(j[1])

                slot = self.create_slot(param_name)
                slotbox.pack_start(slot, 1, 1, 0)
            else:
                slot = self.create_slot()
                slotbox.pack_start(slot, 1, 1, 0)

        else:
            slot = self.create_slot()
            slotbox.pack_start(slot, 1, 1, 0)

        # note = Gtk.Label(label="Note: User-Defined_IDB parameter can only be used if IDB order is chosen, User-Defined_Local only for Local order")

        box = Gtk.VBox()
        box.pack_start(parameter_view, 1, 1, 5)
        # box.pack_start(note, 0,0,0)
        box.pack_start(slotbox, 1, 1, 2)
        # box.pack_start(decisionbox, 1, 1, 2)
        box.pack_start(entrybox, 0, 0, 3)

        return box

    def create_param_view(self):
        """

        :return:
        """
        self.treeview = Gtk.TreeView(self.create_parameter_model())

        self.treeview.append_column(Gtk.TreeViewColumn("Parameters", Gtk.CellRendererText(), text=0))
        hidden_column = Gtk.TreeViewColumn("PCF_NAME", Gtk.CellRendererText(), text=1)
        hidden_column.set_visible(False)
        self.treeview.append_column(hidden_column)

        sw = Gtk.ScrolledWindow()
        sw.set_size_request(200, 200)
        # workaround for allocation warning GTK bug
        # grid = Gtk.Grid()
        # grid.attach(self.treeview, 0, 0, 1, 1)
        # sw.add(grid)
        sw.add(self.treeview)

        return sw

    def create_slot(self, group=None):
        """

        :param group:
        :return:
        """
        self.parameter_list = Gtk.ListStore(str, str)
        treeview = Gtk.TreeView(self.parameter_list)
        treeview.set_reorderable(True)

        treeview.append_column(Gtk.TreeViewColumn("Parameters", Gtk.CellRendererText(), text=0))
        hidden_column = Gtk.TreeViewColumn("PCF_NAME", Gtk.CellRendererText(), text=1)
        hidden_column.set_visible(False)
        treeview.append_column(hidden_column)
        treeview.set_headers_visible(False)

        # add parameters if modifying existing configuration
        if group is not None:
            for item in group:
                descr, name = self.name_to_descr(item)
                if descr is not None:
                    self.parameter_list.append([descr, name])

        sw = Gtk.ScrolledWindow()
        sw.set_size_request(100, 200)
        sw.add(treeview)

        bbox = Gtk.HBox()
        bbox.set_homogeneous(True)
        add_button = Gtk.Button(label='Add')
        add_button.connect('clicked', self.add_parameter, self.parameter_list)
        rm_button = Gtk.Button(label='Remove')
        rm_button.connect('clicked', self.remove_parameter, treeview)

        bbox.pack_start(add_button, 1, 1, 0)
        bbox.pack_start(rm_button, 1, 1, 0)

        vbox = Gtk.VBox()
        vbox.pack_start(bbox, 0, 0, 3)
        vbox.pack_start(sw, 1, 1, 0)

        return vbox

    def name_to_descr(self, name):
        """

        :param name:
        :return:
        """
        dbcon = self.session_factory_idb
        dbres = dbcon.execute('SELECT pcf_descr, pcf_name FROM pcf WHERE pcf_name="{}"'.format(name))
        name = dbres.fetchall()
        dbcon.close()
        if len(name) != 0:
            return name[0]
        else:
            return None, None

    # def create_parameter_model_old(self):
    #     parameter_model = Gtk.TreeStore(str, str)
    #
    #     dbcon = self.session_factory_idb
    #     #dbres = dbcon.execute('SELECT pid_descr,pid_spid from pid where pid_type=3 and pid_stype=25')
    #     dbres = dbcon.execute('SELECT pid_descr,pid_spid from pid order by pid_type,pid_pi1_val')
    #     hks = dbres.fetchall()
    #     for hk in hks:
    #         it = parameter_model.append(None, [hk[0], None])
    #         dbres = dbcon.execute('SELECT pcf.pcf_descr, pcf.pcf_name from pcf left join plf on\
    #          pcf.pcf_name=plf.plf_name left join pid on plf.plf_spid=pid.pid_spid where pid.pid_spid={}'.format(hk[1]))
    #         params = dbres.fetchall()
    #         [parameter_model.append(it, [par[0], par[1]]) for par in params]
    #     dbcon.close()
    #     self.useriter_IDB = parameter_model.append(None, ['User-defined_IDB', None])
    #     self.useriter_local = parameter_model.append(None, ['User-defined_local', None])
    #     for userpar in self.cfg['ccs-user_decoders']:
    #         parameter_model.append(self.useriter_IDB, [userpar, None])
    #     for userpar in self.cfg['ccs-decode_parameters']:
    #         parameter_model.append(self.useriter_local, [userpar, None])
    #
    #     return parameter_model

    def create_parameter_model(self):
        """

        :return:
        """
        parameter_model = Gtk.TreeStore(str, str)
        self.store = parameter_model

        dbcon = self.session_factory_idb
        dbres = dbcon.execute('SELECT pid_descr,pid_spid,pid_type from pid order by pid_type,pid_stype,pid_pi1_val')
        hks = dbres.fetchall()

        topleveliters = {}
        for hk in hks:

            if not hk[2] in topleveliters:
                serv = parameter_model.append(None, ['Service ' + str(hk[2]), None])
                topleveliters[hk[2]] = serv

            it = parameter_model.append(topleveliters[hk[2]], [hk[0], None])

            dbres = dbcon.execute('SELECT pcf.pcf_descr, pcf.pcf_name from pcf left join plf on pcf.pcf_name=plf.plf_name left join pid on \
                                   plf.plf_spid=pid.pid_spid where pid.pid_spid={} ORDER BY pcf.pcf_descr'.format(hk[1]))
            params = dbres.fetchall()
            for par in params:
                parameter_model.append(it, [*par])

        dbcon.close()

        # # add user defined PACKETS
        # self.user_tm_decoders = user_tm_decoders_func()
        # topit = parameter_model.append(None, ['UDEF'])
        # for hk in self.user_tm_decoders:
        #     it = parameter_model.append(topit, ['UDEF|{}'.format(self.user_tm_decoders[hk][0])])
        #     for par in self.user_tm_decoders[hk][1]:
        #         parameter_model.append(it, [par[1]])

        # add data pool items
        self.useriter = parameter_model.append(None, ['Data pool', None])
        for dp in _dp_items:
            dp_item = '{} ({})'.format(_dp_items[dp]['descr'], dp)
            parameter_model.append(self.useriter, [dp_item, 'dp_item'])

        # add user defined PARAMETERS with positional info
        self.useriter = parameter_model.append(None, ['User defined', None])
        for userpar in self.cfg[CFG_SECT_PLOT_PARAMETERS]:
            parameter_model.append(self.useriter, [userpar, 'user_defined'])

        # add user defined PARAMETERS without positional info
        for userpar in self.cfg[CFG_SECT_DECODE_PARAMETERS]:
            parameter_model.append(self.useriter, [userpar, 'user_defined_nopos'])

        return parameter_model

    def add_parameter(self, widget, listmodel):
        """

        :param widget:
        :param listmodel:
        :return:
        """
        par_model, par_iter = self.treeview.get_selection().get_selected()
        if par_model[par_iter][1] is None:
            return

        # hk = par_model[par_iter].parent[0]

        if par_model[par_iter].parent is None:
            return

        # elif hk not in ['User-defined_IDB', 'User-defined_local']:
        #     param = par_model[par_iter]
        #     listmodel.append([*param])

        else:
            param = par_model[par_iter]
            listmodel.append([*param])

        return

    def remove_parameter(self, widget, listview):
        """

        :param widget:
        :param listview:
        :return:
        """
        model, modeliter = listview.get_selection().get_selected()

        if modeliter is None:
            return

        model.remove(modeliter)
        return

    def check_entry(self, widget):
        """

        :param widget:
        """
        if self.apid.get_text_length() and self.st.get_text_length() and self.sst.get_text_length \
                and self.label.get_text_length():
            self.ok_button.set_sensitive(True)
        else:
            self.ok_button.set_sensitive(False)


class UserParameterDialog(Gtk.MessageDialog):
    """
    Interface to edit a user-defined parameter
    """
    def __init__(self, parent=None, edit=None):
        Gtk.Dialog.__init__(self, "Edit User Parameter", parent, 0,
                            buttons=(Gtk.STOCK_OK, Gtk.ResponseType.OK, Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL))

        self.cfg = cfg
        self.set_border_width(5)

        box = self.get_content_area()
        ok_button = self.get_action_area().get_children()[0]
        ok_button.set_sensitive(False)

        hbox = Gtk.HBox()

        self.apid = Gtk.Entry()
        self.st = Gtk.Entry()
        self.sst = Gtk.Entry()
        self.apid.set_placeholder_text('APID')
        self.apid.connect('changed', self.check_entry, ok_button)
        self.st.set_placeholder_text('Service Type')
        self.st.connect('changed', self.check_entry, ok_button)
        self.sst.set_placeholder_text('Service Subtype')
        self.sst.connect('changed', self.check_entry, ok_button)
        self.sid = Gtk.Entry()
        self.sid.set_placeholder_text('SID')
        self.sid.set_tooltip_text('Discriminant (i.e. PI1VAL)')

        hbox.pack_start(self.apid, 0, 0, 0)
        hbox.pack_start(self.st, 0, 0, 0)
        hbox.pack_start(self.sst, 0, 0, 0)
        hbox.pack_start(self.sid, 0, 0, 0)
        hbox.set_homogeneous(True)
        hbox.set_spacing(5)

        bytebox = Gtk.HBox()

        self.bytepos = Gtk.Entry()
        self.bytepos.set_placeholder_text('Byte Offset')
        self.bytepos.set_tooltip_text('Including {} ({} for TCs) header bytes, e.g. byte 0 in TM source data -> offset={}'
                                      .format(TM_HEADER_LEN, TC_HEADER_LEN, TM_HEADER_LEN))
        self.bytepos.connect('changed', self.check_entry, ok_button)
        self.format = Gtk.ComboBoxText()
        self.format.set_model(create_format_model())
        self.format.set_tooltip_text('Format type')
        self.format.connect('changed', self.bitlen_active)
        self.offbi = Gtk.Entry()
        self.offbi.set_placeholder_text('Bit Offset')
        self.offbi.set_tooltip_text('Bit Offset (optional)')
        self.offbi.set_sensitive(False)
        self.bitlen = Gtk.Entry()
        self.bitlen.set_placeholder_text('Length')
        self.bitlen.set_tooltip_text('Length in bits (for uint*) and bytes (ascii*, oct*)')
        self.bitlen.set_sensitive(False)

        bytebox.pack_start(self.bytepos, 0, 0, 0)
        bytebox.pack_start(self.format, 0, 0, 0)
        bytebox.pack_start(self.offbi, 0, 0, 0)
        bytebox.pack_start(self.bitlen, 0, 0, 0)
        bytebox.set_spacing(5)

        self.label = Gtk.Entry()
        self.label.set_placeholder_text('Parameter Label')
        self.label.connect('changed', self.check_entry, ok_button)

        box.pack_start(self.label, 0, 0, 0)
        box.pack_end(bytebox, 0, 0, 0)
        box.pack_end(hbox, 0, 0, 0)
        box.set_spacing(10)

        if edit is not None:
            pars = json.loads(self.cfg[CFG_SECT_PLOT_PARAMETERS][edit])
            self.label.set_text(edit)
            if 'ST' in pars:
                self.st.set_text(str(pars['ST']))
            if 'SST' in pars:
                self.sst.set_text(str(pars['SST']))
            if 'APID' in pars:
                self.apid.set_text(str(pars['APID']))
            if 'SID' in pars and pars['SID'] is not None:
                self.sid.set_text(str(pars['SID']))
            if 'bytepos' in pars:
                self.bytepos.set_text(str(pars['bytepos']))
            if 'format' in pars:
                fmt_dict = {a: b for b, a in fmtlist.items()}
                fmt = pars['format']
                for fk in ('uint', 'ascii', 'oct'):
                    if fmt.startswith(fk):
                        self.bitlen.set_text(fmt.replace(fk, ''))
                        fmt = fk
                        break
                model = self.format.get_model()
                it = [row.iter for row in model if row[0] == fmt_dict[fmt]][0]
                self.format.set_active_iter(it)
            if 'offbi' in pars:
                self.offbi.set_text(str(pars['offbi']))

        self.show_all()

    def check_entry(self, widget, ok_button):
        """

        :param widget:
        :param ok_button:
        """
        if self.apid.get_text_length() and self.st.get_text_length() and self.sst.get_text_length \
                and self.label.get_text_length() and self.bytepos.get_text_length():
            ok_button.set_sensitive(True)
        else:
            ok_button.set_sensitive(False)

    def bitlen_active(self, widget):
        """

        :param widget:
        """
        if widget.get_active_text() == 'uint*':
            self.bitlen.set_sensitive(True)
            self.offbi.set_sensitive(True)
        elif widget.get_active_text() in ('ascii*', 'oct*'):
            self.bitlen.set_sensitive(True)
            self.offbi.set_sensitive(False)
        else:
            self.bitlen.set_sensitive(False)
            self.offbi.set_sensitive(False)


# class RemoveUserParameterDialog(Gtk.Dialog):
#     def __init__(self, cfg, parent=None):
#         Gtk.Dialog.__init__(self, "Remove User Defined Parameter", parent, 0)
#         self.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK, Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
#
#         self.cfg = cfg
#
#         box = self.get_content_area()
#
#         self.ok_button = self.get_widget_for_response(Gtk.ResponseType.OK)
#         self.ok_button.set_sensitive(False)
#
#         self.remove_name = Gtk.ComboBoxText.new_with_entry()
#         self.remove_name.set_tooltip_text('Parameter')
#         self.remove_name_entry = self.remove_name.get_child()
#         self.remove_name_entry.set_placeholder_text('Label')
#         self.remove_name_entry.set_width_chars(5)
#         self.remove_name.connect('changed', self.fill_remove_mask)
#
#         self.remove_name.set_model(self.create_remove_model())
#
#         box.pack_start(self.remove_name, 0, 0, 0)
#
#         self.show_all()
#
#     def create_remove_model(self):
#         model = Gtk.ListStore(str)
#
#         for decoder in self.cfg[CFG_SECT_PLOT_PARAMETERS].keys():
#             model.append([decoder])
#         return model
#
#     def fill_remove_mask(self, widget):
#         decoder = widget.get_active_text()
#
#         if self.cfg.has_option(CFG_SECT_PLOT_PARAMETERS, decoder):
#             self.ok_button.set_sensitive(True)
#         else:
#             self.ok_button.set_sensitive(False)


# class EditUserParameterDialog(Gtk.Dialog):
#     def __init__(self, cfg, parent=None):
#         Gtk.Dialog.__init__(self, "Edit User Defined Parameter", parent, 0)
#         self.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK, Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)
#
#         self.cfg = cfg
#
#         box = self.get_content_area()
#
#         self.ok_button = self.get_widget_for_response(Gtk.ResponseType.OK)
#         self.ok_button.set_sensitive(False)
#
#         self.edit_name = Gtk.ComboBoxText.new_with_entry()
#         self.edit_name.set_tooltip_text('Parameter')
#         self.edit_name_entry = self.edit_name.get_child()
#         self.edit_name_entry.set_placeholder_text('Label')
#         self.edit_name_entry.set_width_chars(5)
#         self.edit_name.connect('changed', self.fill_edit_mask)
#
#         self.edit_name.set_model(self.create_edit_model())
#
#         box.pack_start(self.edit_name, 0, 0, 0)
#
#         self.show_all()
#
#     def create_edit_model(self):
#         model = Gtk.ListStore(str)
#
#         for decoder in self.cfg[CFG_SECT_PLOT_PARAMETERS].keys():
#             model.append([decoder])
#         return model
#
#     def fill_edit_mask(self, widget):
#         decoder = widget.get_active_text()
#
#         if self.cfg.has_option(CFG_SECT_PLOT_PARAMETERS, decoder):
#             self.ok_button.set_sensitive(True)
#         else:
#             self.ok_button.set_sensitive(False)


class ChangeCommunicationDialog(Gtk.Dialog):
    """
    This dialog is used to manage the main_communication in the CCS via a GUI
    """
    def __init__(self, cfg, main_instance, parent=None):
        """

        :param cfg: Is the config file
        :param main_instance: Is the project name
        :param parent: Given Gtk.Window instance
        """
        Gtk.Dialog.__init__(self, "Communication", parent, 0)
        self.add_buttons(Gtk.STOCK_OK, Gtk.ResponseType.OK, Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL)

        self.box = self.get_content_area()  # Dialogs are not running without this

        self.label = Gtk.Label('"Main Communication" \n is to these Applications')

        self.cfg = cfg
        self.parent = parent

        # Set up a variable which will hold the end results
        self.new_communication = communication

        # Filter all connection of the CCS out of all dbus connections
        self.our_con = []
        for service in dbus.SessionBus().list_names():
            if service[:-1] in self.cfg['ccs-dbus_names'].values():
                self.our_con.append(service)

        self.change_box = self.setup_change_box()

        self.tick_box = self.get_tick_box()

        self.box.pack_start(self.label, 0, 0, 4)
        self.box.pack_start(self.change_box, 0, 0, 4)
        self.box.pack_start(self.tick_box, 0, 0, 4)
        self.show_all()

    def setup_change_box(self):
        """
        Sets up the main Box in which the change happens

        :return: a Gtk.Box
        """
        main_box = Gtk.HBox()

        label_box = Gtk.VBox()  # Used only for the names
        change_box = Gtk.VBox() # used only for the changing

        for name in communication:
            # Label_Box
            lab = Gtk.Label(name.capitalize() + str('        ')) # The long space is made that GUI looks better
            label_box.pack_start(lab, 0, 0, 8)

            #Change_Box
            entry_list, nbr = self.get_entry_list(name)

            #Start each Changing box here
            communication_entry = Gtk.ComboBox.new_with_model(entry_list)
            communication_entry.set_title(name) #Give the boxes names to separate them
            communication_entry.connect('changed', self.main_com_changed)

            # Necessary for Combobox but not importent for program
            renderer_text = Gtk.CellRendererText()
            communication_entry.pack_start(renderer_text, True)
            communication_entry.add_attribute(renderer_text, "text", 0)

            communication_entry.set_active(nbr)
            change_box.pack_start(communication_entry, 0, 0, 0)


        main_box.pack_start(label_box, 0, 0, 0)
        main_box.pack_start(change_box, 0, 0, 0)

        return main_box

    def get_entry_list(self, app):
        """
        Returns a List of all active instance of the given application in this main_instance (project)

        :param app: application name (poolviewer,...)
        :return: liststore and listplace
        """
        count = 0   # Counts the amount of entries
        ret = 0     # Saves the place of the main_communication application
        liststore = Gtk.ListStore(str)
        liststore.append(['-']) # Append an empty entry to each list, so that all Boxes show something
        for service in self.our_con:
            if service[:-1] == cfg['ccs-dbus_names'][app]:  # Filer all instance of given application
                if service == self.parent.my_bus_name:  # If it is the calling application dbus cannot be used, do this
                    liststore.append([service[-1]])
                    count += 1  # The list just got longer
                    if str(communication[app]) == str(service[-1]):     # Check if it is the main_communication
                        ret = count
                    continue
                conn = dbus_connection(app, service[-1])
                if conn.Variables('main_instance') == self.parent.main_instance: # Check if both are in the same instance
                    liststore.append([service[-1]])
                    count += 1  # The list just got longer
                    if str(communication[app]) == str(service[-1]):     # Check if it is the main_communication
                        ret = count

        return liststore, ret

    def main_com_changed(self, widget):
        """
        Is called when some connection is changed

        :param widget:
        :return:
        """
        entry = widget.get_model()[widget.get_active()][0]
        # Put in the selected entry, if connection should be switched of use 0
        try:
            self.new_communication[widget.get_title()] = int(entry)
        except:
            self.new_communication[widget.get_title()] = 0

        return

    def get_tick_box(self):
        """
        Creates the Check Button

        :return: a Gtk.Box
        """
        main_box = Gtk.VBox()
        self.change_button = Gtk.CheckButton.new_with_label('Entire Project')
        self.change_button.set_tooltip_text('Change for the entire {} project (recommended) or only for the {} {}'.format(self.parent.main_instance, self.parent.my_bus_name.split('.')[1].capitalize(), self.parent.my_bus_name[-1]))
        self.change_button.set_active(True)
        #self.change_button.connect('toggled', self.tick_changed)

        main_box.pack_start(self.change_button, 0, 0, 0)

        return main_box


class ProjectDialog(Gtk.Dialog):
    """
    Dialog that optionally pops up at CCS/TST start-up to allow for project and IDB configuration
    """

    def __init__(self):
        super(ProjectDialog, self).__init__()

        self.set_title('Project configuration')
        self.set_default_size(300, 100)

        self.project_selection = self._create_project_selection()
        self.idb_selection = self._create_idb_selection()

        self.cfg = cfg
        ca = self.get_content_area()
        ca.set_spacing(2)

        project_label = Gtk.Label('Project')
        project_label.set_size_request(80, -1)
        project_label.set_xalign(0)
        project_box = Gtk.Box(Gtk.Orientation.HORIZONTAL)
        project_box.pack_start(project_label, 0, 0, 10)
        project_box.pack_start(self.project_selection, 1, 1, 0)
        ca.add(project_box)

        idb_label = Gtk.Label('IDB schema')
        idb_label.set_size_request(80, -1)
        idb_label.set_xalign(0)
        idb_box = Gtk.Box(Gtk.Orientation.HORIZONTAL)
        idb_box.pack_start(idb_label, 0, 0, 10)
        idb_box.pack_start(self.idb_selection, 1, 1, 0)
        ca.add(idb_box)

        self.add_buttons('OK', 1, 'Cancel', 2)

        self.connect('response', self._write_config)
        self.connect('delete-event', Gtk.main_quit)

        self.show_all()

        self.action_area.get_children()[0].grab_focus()  # set focus to OK button

    @staticmethod
    def _create_project_selection():
        project_selection = Gtk.ComboBoxText()

        ccs_path = cfg.get('paths', 'ccs')
        ccs_path += '/' if not ccs_path.endswith('/') else ''
        projects = glob.glob(ccs_path + PCPREFIX + '*')

        projects = [p.replace(ccs_path + PCPREFIX, '').replace('.py', '') for p in projects]

        for p in projects:
            project_selection.append(p, p)

        set_as = cfg.get('ccs-database', 'project')
        project_selection.set_active_id(set_as)

        return project_selection

    @staticmethod
    def _create_idb_selection():
        idb_selection = Gtk.ComboBoxText()

        mibs = scoped_session_idb.execute('show databases').fetchall()
        mibs = [mib for mib, in mibs if mib.count('mib')]

        for m in mibs:
            idb_selection.append(m, m)

        set_as = cfg.get('database', 'mib-schema')
        idb_selection.set_active_id(set_as)

        return idb_selection

    def _write_config(self, widget, data):
        if data == 1:

            self.cfg.save_option_to_file('project', 'name', self.project_selection.get_active_text())
            self.cfg.save_option_to_file('database', 'mib-schema', self.idb_selection.get_active_text())

            self.destroy()
            Gtk.main_quit()

        else:
            self.close()
            sys.exit()


# some default variable definitions that require functions defined above

# create local look-up tables for data pool items from MIB
try:
    DP_ITEMS_SRC_FILE = cfg.get('database', 'datapool-items')
    if DP_ITEMS_SRC_FILE:
        # get DP from file
        _dp_items = get_data_pool_items(src_file=DP_ITEMS_SRC_FILE, as_dict=True)
    else:
        raise ValueError
except (FileNotFoundError, ValueError, confignator.config.configparser.NoOptionError):
    if 'DP_ITEMS_SRC_FILE' not in locals():
        DP_ITEMS_SRC_FILE = None
    logger.warning('Could not load data pool from file: {}. Using MIB instead.'.format(DP_ITEMS_SRC_FILE))
    _dp_items = get_data_pool_items(as_dict=True)
finally:
    # DP_IDS_TO_ITEMS = {int(k[0]): k[1] for k in _dp_items}
    DP_IDS_TO_ITEMS = {k: _dp_items[k]['descr'] for k in _dp_items}
    DP_ITEMS_TO_IDS = {_dp_items[k]['descr']: k for k in _dp_items}

# S13 header/offset info
try:
    _s13_info = get_tm_parameter_sizes(13, 1)
    SDU_PAR_LENGTH = _s13_info[0][-1]
    # length of PUS + source header in S13 packets (i.e. data to be removed when collecting S13)
    S13_HEADER_LEN_TOTAL = TM_HEADER_LEN + sum([p[-1] for p in _s13_info])
except (SQLOperationalError, NotImplementedError, IndexError):
    logger.warning('Could not get S13 info from MIB, using default values')
    SDU_PAR_LENGTH = 1
    S13_HEADER_LEN_TOTAL = 21