#!/usr/bin/python3 -u

# autojack - Monitors dbus for added audio devices (hot plugged USB audio intefaces)
# on detect it does one of three things:
# makes it the jack device
# makes it a jack client (via zita-ajbridge)
# nothing
#
# The mode is chosen from either the USBAUTO setting or if the device
# name is present in XDEV then slave mode is assumed. If DEV has the
# device name, then master is chosen.
#
# autojack also monitors dbus for messages from studio-controls to:
# - stop jack
# - re/start jack
# - remove a USB device as jack master to allow safe device removal
# - reread ~/.config/autojackrc and apply any changes

import os
from os.path import expanduser
import re
import time
from gi.repository import GLib
import dbus
import dbus.mainloop.glib
import subprocess
import signal
import configparser
import logging

def get_dev_info(x):
    '''uses audio device number to make a device structure with device
    info. The layout is:
        device[0] = device name as a string
        device[1] = bool true is USB device
        device[2] = number of sub devices (0 means MIDI only)
        device[3 to n] = subdevice info as a list:
            device[n][0] = sub device number (some devices skip...)
            device[n][1] = bool has playback
            device[n][2] = PID of playback user
            device[n][3] = bool has capture
            device[n][4] = PID of capture user
    This structure is returned. If the device does not
    exist, a minimal structure is still returned ["", False, 0]'''
    device = []
    cname = ""
    l_usb = False
    sub = 0
    if os.path.exists(f"/proc/asound/card{str(x)}"):
        if os.path.isfile(f"/proc/asound/card{str(x)}/id"):
            with open(f"/proc/asound/card{str(x)}/id", "r") as card_file:
                for line in card_file:
                    # only need one line
                    cname = line.rstrip()
        else:
            cname = str(x)

    if os.path.exists(f"/proc/asound/card{str(x)}/usbbus"):
        l_usb = True

    device.append(cname)
    device.append(l_usb)
    device.append(sub)

    for y in range(0, 10):
        subdevice = []
        cap = False
        cap_pid = 0
        play = False
        play_pid = 0
        if os.path.exists(f"/proc/asound/card{str(x)}/pcm{str(y)}p"):
            play = True
            if os.path.exists(f"/proc/asound/card{str(x)}/pcm{str(y)}p/sub0"):
                with open(f"/proc/asound/card{str(x)}/pcm{str(y)}p/sub0/status", "r") as info_file:
                    for line in info_file:
                        if re.match("^owner_pid", line.rstrip()):
                            play_pid = int(line.rstrip().split(": ", 1)[1])

        if os.path.exists(f"/proc/asound/card{str(x)}/pcm{str(y)}c"):
            cap = True
            if os.path.exists(f"/proc/asound/card{str(x)}/pcm{str(y)}c/sub0"):
                with open(f"/proc/asound/card{str(x)}/pcm{str(y)}c/sub0/status", "r") as info_file:
                    for line in info_file:
                        if re.match("^owner_pid", line.rstrip()):
                            cap_pid = int(line.rstrip().split(": ", 1)[1])

        if play or cap:
            device[2] = device[2] + 1
            subdevice.append(y)
            subdevice.append(play)
            subdevice.append(play_pid)
            subdevice.append(cap)
            subdevice.append(cap_pid)
            device.append(subdevice)
    # change this to create a list, USB cards may have devices and subdevices
    logging.debug(f"Device list: {str(device)}")
    return device


def import_device_array():
    '''creates an array of device structures as per get_dev_info(), from
    device 0 to the highest number device currently on the system. Missing
    device numbers will still save a space in the case a USB device has
    been removed and there is still a higher number so that array index
    can be the same as the device number.'''
    global devices
    devices = []
    ndevs = 0

    if os.path.exists("/proc/asound/cards"):
        with open("/proc/asound/cards", "r") as cards_file:
            for line in cards_file:
                # need to find lines with:space/int/space[
                # ndevs = int from above
                # last one is highest dev number
                sub = line.rstrip()[1:]
                sub2 = sub.split(" ")
                if sub2[0].isdigit():
                    ndevs = int(sub2[0])
    ndevs += 1
    for x in range(0, ndevs):
        # card loop
        device = []
        device = get_dev_info(x)
        devices.append(device)
        del device


def import_config():
    ''' sets default parmeters, then reads values from configuration file'''
    global config
    global def_config
    global config_path
    global config_file
    global zdev
    global pulse_in
    global pulse_out
    global p_in_con
    global p_out_con
    zdev = []
    pulse_in = []
    pulse_out = []
    p_in_con = []
    p_out_con = []

    config = configparser.ConfigParser()
    def_config = config['DEFAULT']
    config_path = "~/.config/autojack"
    config_file = "{path}/autojackrc".format(path=config_path)
    old_config_file = "~/.config/autojackrc"

    # first set defaults
    config['DEFAULT'] = {
        'JACK': "False",
        'DRIVER': "alsa",
        'CHAN-IN': "0",
        'CHAN-OUT': "0",
        'RATE': "48000",
        'FRAME': "1024",
        'PERIOD': "2",
        'ZFRAME': "512",
        'XDEV': "",
        'PULSE-IN': "pulse_in",
        'PULSE-OUT': "pulse_out",
        'PJ-IN-CON': '1',
        'PJ-OUT-CON': '1',
        'A2J': "True",
        'DEV': "0,0,0",
        'USBAUTO': "True",
        'USB-SINGLE': "False",
        'USBDEV': "",
        "PULSE": "True",
        "LOG-LEVEL": "15",
    }

    # read in autojack config file
    c_file = expanduser(config_file)
    if os.path.isfile(c_file):
        # New config file exists, read it in
        config.read(c_file)
    else:
        # New file did not exist, let's check for a legacy file
        c_file = expanduser(old_config_file)
        if os.path.isfile(c_file):
            # Found the legacy config, let's load it
            with open(c_file, "r") as rc_file:
                for line in rc_file:
                    if re.match("^#", line):
                        continue
                    lsplit = line.rstrip().split("=", 1)
                    if lsplit[0] == "JACK":
                        def_config['JACK'] = lsplit[1]
                    elif lsplit[0] == "DRIVER":
                        def_config['DRIVER'] = lsplit[1]
                    elif lsplit[0] == "DEV":
                        def_config['DEV'] = lsplit[1]
                    elif lsplit[0] == "CHAN-IN":
                        def_config['CHAN-IN'] = lsplit[1]
                    elif lsplit[0] == "CHAN-OUT":
                        def_config['CHAN-OUT'] = lsplit[1]
                    elif lsplit[0] == "RATE":
                        def_config['RATE'] = lsplit[1]
                    elif lsplit[0] == "FRAME":
                        def_config['FRAME'] = lsplit[1]
                    elif lsplit[0] == "ZFRAME":
                        def_config['ZFRAME'] = lsplit[1]
                    elif lsplit[0] == "PERIOD":
                        def_config['PERIOD'] = lsplit[1]
                    elif lsplit[0] == "PULSE":
                        pulse = lsplit[1]
                        if pulse == 'True':
                            def_config['PULSE-IN'] = 'pulse_in'
                            def_config['PULSE-OUT'] = 'pulse_out'
                        else:
                            def_config['PULSE-IN'] = ''
                            def_config['PULSE-OUT'] = ''
                    elif lsplit[0] == "PULSE-IN":
                        pin = lsplit[1]
                        if pin:
                            def_config['PULSE-IN'] = 'pulse_in'
                    elif lsplit[0] == "PULSE-OUT":
                        pout = lsplit[1]
                        if pout:
                            def_config['PULSE-OUT'] = 'pulse_out'
                    elif lsplit[0] == "A2J":
                        def_config['A2J'] = lsplit[1]
                    elif lsplit[0] == "OUTPUT":
                        def_config['PJ-IN-CON'] = "1"
                        def_config['PJ-OUT-CON'] = "1"
                    elif lsplit[0] == "PORTS":
                        def_config['PJ-OUT-CON'] = f"{lsplit[1]}"
                    elif lsplit[0] == "XDEV":
                        def_config['XDEV'] = lsplit[1].strip('"')
                        zdev = def_config['XDEV'].strip('"').split()
                    elif lsplit[0] == "USBAUTO":
                        def_config['USBAUTO'] = lsplit[1]
                    elif lsplit[0] == "USB-SINGLE":
                        def_config['USB-SINGLE'] = lsplit[1]
                    elif lsplit[0] == "USBDEV":
                        def_config['USBDEV'] = lsplit[1]

    if def_config['DEV'] == "default":
        def_config['DEV'] = "0,0,0"
    zdev = def_config['XDEV'].strip('"').split()
    pulse_in = def_config['PULSE-IN'].strip('"').split()
    pulse_out = def_config['PULSE-OUT'].strip('"').split()
    p_in_con = def_config['PJ-IN-CON'].strip('"').split()
    p_out_con = def_config['PJ-OUT-CON'].strip('"').split()
    if pulse_in == [] and pulse_out == []:
        def_config['PULSE'] = "False"


def reconfig():
    '''reads values from configuration file and changes run to match. This tries
    to do this without stopping jack if not needed'''
    global devices
    global zdev
    global pulse_in
    global pulse_out
    global config
    global def_config

    old_config = configparser.ConfigParser()
    old_def_config = old_config['DEFAULT']
    old_config.read_dict(config)
    oldzdev = zdev
    oldpulse_in = pulse_in
    oldpulse_out = pulse_out
    import_config()
    if def_config['LOG-LEVEL'] != old_def_config['LOG-LEVEL']:
        logger = logging.getLogger()
        logger.setLevel(int(def_config['LOG-LEVEL']))
        logging.debug(f"log level: {def_config['LOG-LEVEL']}")

    newlist = [def_config['JACK'], def_config['DRIVER'], def_config['CHAN-IN'], def_config['CHAN-OUT'],
               def_config['RATE'], def_config['FRAME'], def_config['PERIOD'], def_config['DEV'], def_config['USBDEV']]
    oldlist = [old_def_config['JACK'], old_def_config['DRIVER'], old_def_config['CHAN-IN'], old_def_config['CHAN-OUT'],
               old_def_config['RATE'], old_def_config['FRAME'], old_def_config['PERIOD'], old_def_config['DEV'],
               old_def_config['USBDEV']]
    if newlist != oldlist:
        config_start()
        return
    # if we got this far all the above params have not changed
    if def_config['JACK'] == "False":
        return
        # no use checking anything else

    pulse_dirty = False
    if def_config['PULSE-IN'] != old_def_config['PULSE-IN']:
        pulse_dirty = True
        disconnect_pa(old_def_config)
        cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-jack-source"],
                            universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"remove jackd_source: {cp.stdout.strip()}")
        for piname in pulse_in:
            def_config['PULSE'] = "True"
            cp = subprocess.run(["/usr/bin/pactl", "load-module", "module-jack-source", f"client_name={piname}",
                                "channels=2", "connect=no"],
                                universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
            logging.debug(f"Load jackd_source: {cp.stdout.strip()}")
            
    if def_config['PULSE-OUT'] != old_def_config['PULSE-OUT']:
        if not pulse_dirty:
            pulse_dirty = True
            disconnect_pa(old_def_config)
        cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-jack-sink"],
                            universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"PA unload jack-sink: {cp.stdout.strip()}")
        for poname in pulse_out:
            def_config['PULSE'] = "True"
            cp = subprocess.run(["/usr/bin/pactl", "load-module", "module-jack-sink", f"client_name={poname}", "channels=2",
                            "connect=no"], universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
            logging.debug(f"PA load jack-sink: {cp.stdout.strip()}")

    if [def_config['PJ-IN-CON'], def_config['PJ-OUT-CON']] != [old_def_config['PJ-IN-CON'], old_def_config['PJ-OUT-CON']]:
        # need to pass old_def_config
        if not pulse_dirty:
            pulse_dirty = True
            disconnect_pa(old_def_config)

    if pulse_dirty:
        connect_pa()

    if old_def_config['A2J'] != def_config['A2J']:
        cp = subprocess.run(["/usr/bin/killall", "-9", "a2jmidid"],
                            universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"kill old a2j: {cp.stdout.strip()}")
        if def_config['A2J'] == "True":
            # Can't add logging for background processes. Will show up in syslog
            subprocess.Popen(["/usr/bin/a2jmidid", "-e"], shell=False).pid

    if [def_config['USBAUTO'], def_config['USB-SINGLE'], zdev] != [old_def_config['USBAUTO'],
                                                                   old_def_config['USB-SINGLE'],
                                                                   oldzdev]:
        import_device_array()
        for device in devices:
            if device[3][2] and device[3][2] == device[3][4]:
                # in and out are same PID this is jack master skip or device in use
                continue
            if device[1]:
                # USB device
                if def_config['USBAUTO'] != old_def_config['USBAUTO'] or def_config['USB-SINGLE'] != old_def_config[
                    'USB-SINGLE']:
                    kill_slave(f"{device[0]},0,0")
                    if def_config['USBAUTO'] == "True":
                        start_slave(f"{device[0]},0,0")
            else:
                # not USB device
                if zdev != oldzdev:
                    tempname = "nothing"
                    dev_has_pid = False
                    for i in range(3, (device[2] + 3)):
                        tempname = f"{device[0]},{str(device[i][0])},0"
                        if device[i][2]:
                            dev_has_pid = True
                        if tempname in zdev:
                            if not dev_has_pid:
                                start_slave(tempname)
                        else:
                            kill_slave(tempname)


def config_start():
    ''' Pulls configuration and force restarts the world '''
    global last_master
    global zdev
    global pulse_in
    global pulse_out
    global def_config

    logging.info("Running: config_start()")

    import_config()
    logger = logging.getLogger()
    logger.setLevel(int(def_config['LOG-LEVEL']))
    logging.debug(f"log level: {def_config['LOG-LEVEL']}")

    # if at session start we should wait a few seconds for pulse
    # to be fully running
    time.sleep(2)
    # Stop jack if running
    cp = subprocess.run(["/usr/bin/killall", "-q", "-9", "jackdbus", "jackd", "a2jmidid"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"Kill old Procs: {cp.stdout.strip()}")
    if def_config['JACK'] == "False":
        # restart Pulse
        cp = subprocess.run(["/usr/bin/pulseaudio", "-k"],
                    universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"JACK == {def_config['JACK']} restart pulse: {cp.stdout.strip()}")
        return

    # Assume start of session where pulse may be fully loaded
    # get rid of anything that can automatically interfere
    cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-jackdbus-detect"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"remove jackdbus_detect: {cp.stdout.strip()}")
    cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-udev-detect"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"remove module-udev-detect: {cp.stdout.strip()}")
    cp = subprocess.run(["/usr/bin/pactl", "unload-module", "module-alsa-card"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"remove module-alsa-card: {cp.stdout.strip()}")
    if os.path.exists(f"/proc/asound/{def_config['USBDEV'].split(',')[0]}") and def_config['USBDEV'] != "":
        mdev = def_config['USBDEV']
    else:
        mdev = def_config['DEV']
    # Now start jackdbus with the configured device
    # the commands are all different so...
    if def_config['DRIVER'] == "alsa":
        cp = subprocess.run(["/usr/bin/jack_control", "ds", "alsa", "dps", "capture", "none", "dps", "playback", "none"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Jackdbus: {cp.stdout.strip()}")
        cp = subprocess.run(["/usr/bin/jack_control", "dps", "device", f"hw:{mdev}", "dps", "rate", def_config['RATE']],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Jackdbus: {cp.stdout.strip()}")
        cp = subprocess.run(["/usr/bin/jack_control", "dps", "period", def_config['FRAME'], "dps", "nperiods",
                        def_config['PERIOD'], "start"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Jackdbus: {cp.stdout.strip()}")
    elif def_config['DRIVER'] == "firewire":
        cp = subprocess.run(["/usr/bin/jack_control", "ds", "firewire"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Jackdbus: {cp.stdout.strip()}")
        cp = subprocess.run(["/usr/bin/jack_control", "dps", "device", f"hw:{mdev}", "dps", "rate", def_config['RATE']],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Jackdbus: {cp.stdout.strip()}")
        cp = subprocess.run(["/usr/bin/jack_control", "dps", "period", def_config['FRAME'], "dps", "nperiods",
                        def_config['PERIOD'], "start"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Jackdbus: {cp.stdout.strip()}")
    elif def_config['DRIVER'] == "dummy":
        cp = subprocess.run(["/usr/bin/jack_control", "ds", "dummy"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Jackdbus: {cp.stdout.strip()}")
        cp = subprocess.run(["/usr/bin/jack_control", "dps", "rate", def_config['RATE'], "dps" "period", def_config['FRAME']],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Jackdbus: {cp.stdout.strip()}")
        cp = subprocess.run(["/usr/bin/jack_control", "dps", "capture", def_config['CHAN-IN'], "dps", "playback",
                        def_config['CHAN-OUT'], "start"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Jackdbus: {cp.stdout.strip()}")
    else:
        # we should never get here
        logging.error("programming error! if you find this in your log file please file" +
              "a bug report about: Driver not found")
    last_master = mdev
    # maybe check for jack up (need function?)
    time.sleep(2)

    for piname in pulse_in:
        def_config['PULSE'] = "True"
        cp = subprocess.run(["/usr/bin/pactl", "load-module", "module-jack-source", f"client_name={piname}",
                        "channels=2", "connect=no"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Pulseaudio: {cp.stdout.strip()}")
    for poname in pulse_out:
        def_config['PULSE'] = "True"
        cp = subprocess.run(["/usr/bin/pactl", "load-module", "module-jack-sink", f"client_name={poname}", "channels=2",
                        "connect=no"], universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Pulseaudio: {cp.stdout.strip()}")

    for cname in zdev:
        if cname != "":
            start_slave(cname)

    # not sure all these delays need to be here. Was checking with old pulse.
    time.sleep(2)

    connect_pa()
    if def_config['USBAUTO'] == "True":
        import_device_array()
        for device in devices:
            if device[1]:
                # this is a USB device
                if f"{device[0]},0,0" != def_config['USBDEV']:
                    start_slave(f"{device[0]},0,0")

    if def_config['A2J'] == "True":
        # Can't set up logging
        subprocess.Popen(["/usr/bin/a2jmidid", "-e"], shell=False).pid


def connect_pa():
    '''connects pulse ports to the correct device ports. May have to
    use zita-ajbridge to first make the correct device available.'''
    global pulse_in
    global pulse_out
    global p_in_con
    global p_out_con

    if (p_in_con == []) and (p_out_con == []):
        return

    for i, pj_name in enumerate(pulse_in):
        port2 = ""
        port = p_in_con[i]
        if port == "none" or port == "0":
            continue
        elif "left" in port:
            port2 = f"{port[0-(len(port)-5)]}right"
        elif port.isnumeric():
            p = int(port)
            port = f"system:capture_{str(p)}"
            port2 = f"system:capture_{str(p + 1)}"
        else:
            p = int(port[(port.rfind("_") + 1):]) + 1
            port2 = f"{port[0:port.rfind('_') + 1]}{p}"
        cp = subprocess.run(["/usr/bin/jack_connect", port, f"{pj_name}:front-left"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Jack connect to pulse in left: {cp.stdout.strip()}")
        cp = subprocess.run(["/usr/bin/jack_connect", port2, f"{pj_name}:front-right"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Jack connect to pulse in right: {cp.stdout.strip()}")


    for i, pj_name in enumerate(pulse_out):
        port2 = ""
        port = p_out_con[i]
        if port == "none" or port == "0":
            continue
        elif "left" in port:
            port2 = f"{port[0:(len(port)-5)]}right"
        elif port.isnumeric():
            p = int(port)
            port = f"system:playback_{str(p)}"
            port2 = f"system:playback_{str(p + 1)}"
        else:
            p = int(port[(port.rfind("_") + 1):]) + 1
            port2 = f"{port[0:port.rfind('_') + 1]}{p}"
        cp = subprocess.run(["/usr/bin/jack_connect", port, f"{pj_name}:front-left"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Pulse out left connect to jack: {cp.stdout.strip()}")
        cp = subprocess.run(["/usr/bin/jack_connect", port2, f"{pj_name}:front-right"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
        logging.debug(f"Pulse out right connect to jack: {cp.stdout.strip()}")


def disconnect_pa(our_config):
    '''disconnect Pulse ports we know we have connected.
    The pa-jack bridge is left running.'''
    p_in_con = our_config['PJ-IN-CON'].strip('"').split()
    p_out_con = our_config['PJ-OUT-CON'].strip('"').split()
    for i, name in enumerate(our_config['PULSE-IN'].strip('"').split()):
        if int(p_in_con[i]):
            cp = subprocess.run(["/usr/bin/jack_disconnect", f"{name}:front-left",
                            f"system:capture_{p_in_con[i]}"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
            logging.debug(f"jack dissconnect PA: {cp.stdout.strip()}")
            cp = subprocess.run(["/usr/bin/jack_disconnect", f"{name}:front-right",
                            f"system:capture_{str(int(p_in_con[i]) + 1)}"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
            logging.debug(f"jack dissconnect PA: {cp.stdout.strip()}")

    for i, name in enumerate(our_config['PULSE-OUT'].strip('"').split()):
        if int(p_out_con[i]):
            cp = subprocess.run(["/usr/bin/jack_disconnect", f"{name}:front-left",
                           f"system:playback_{p_out_con[i]}"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
            logging.debug(f"jack dissconnect PA: {cp.stdout.strip()}")
            cp = subprocess.run(["/usr/bin/jack_disconnect", f"{name}:front-right",
                           f"system:playback_{str(int(p_out_con[i]) + 1)}"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
            logging.debug(f"jack dissconnect PA: {cp.stdout.strip()}")


def msg_cb_new(*args, **kwargs):
    '''call back for udev sensing new device. checks if device is audio.
    checks if device is USB. If both are true and configuration is to
    use this device, the device is either connected with zita-ajbridge
    or becomes jack's master device'''
    global devices
    global last_master
    global def_config

    if def_config['JACK'] == "False":
        # then we don't care
        return

    import_config()

    if args[0].find("sound-card") >= 0:
        # remake database
        import_device_array()
        a_if = args[0].split("sound-card", 1)
        audio_if = a_if[1].split(".", 1)[0]
        device = devices[int(audio_if)]
        # make sure device is USB and is not midi only
        if device[1] and device[2]:
            # tell gui devicelist has changed so it can up date device dropdowns
            subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/", "org.studio.control.event.usb_signal"],
                           shell=False)

            cid = f"{device[0]},0,0"
            logging.info(f" device = {cid} play:{str(device[3][1])} capture:{str(device[3][3])}")
            if cid == last_master:
                # spurious indication, ignore
                logging.debug(f" We already have that device - spurious signal, ignoring")
                return
            if def_config['JACK'] == "True" and def_config['DRIVER'] == "alsa":
                if def_config['USBDEV'] == cid:
                    # change_jack_master(cid, "sm")
                    logging.debug(f"Changing jack master to: {cid}")
                    config_start()
                    last_master = cid
                    time.sleep(1)
                    start_slave(def_config['DEV'])
                    time.sleep(1)
                    # connect_pa()
                elif def_config['USBAUTO'] == "True":
                    start_slave(cid)


def msg_cb_removed(*args, **kwargs):
    ''' dbus call back when a USB device removal has been detected by udev '''
    global devices
    global last_master
    global def_config

    if def_config['JACK'] == "False":
        # then we don't care
        return

    import_config()

    if args[0].find("sound-card") >= 0:
        # tell gui devicelist has changed
        subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/", "org.studio.control.event.usb_signal"],
                       shell=False)

        a_if = args[0].split("sound-card", 1)
        audio_if = a_if[1].split(".", 1)[0]
        logging.info(f"sound card: hw:{audio_if} removed")
        if os.path.exists(f"/proc/asound/card{str(audio_if)}"):
            # Hmm, I guess it hasn't really gone, false alarm
            logging.debug(f"Device is still here - signal ignored")
            return

        device = devices[int(audio_if)]
        cid = f"{device[0]},0,0"
        if def_config['JACK'] == "True":
            if not device[1]:
                # not a usb device
                return
            if def_config['USBDEV'] == cid:
                if last_master == def_config['USBDEV']:
                    kill_slave(def_config['DEV'])
                    time.sleep(1)
                    config_start()
            elif def_config['USBAUTO'] == "True":
                kill_slave(cid)
        import_device_array()


def get_card_rate(filename, l_usb):
    ''' Takes a filename and searches for lines that include the word:
    Rates or rates. From each of those lines any numbers are takes and
    compared to the list of rates already found and added to that list if
    it is not already there. The return is the closest rate to jack
    master sample rate'''
    global def_config

    dsr = def_config['RATE']
    full_list = []
    if os.path.isfile(filename):
        with open(filename, "r") as card_file:
            for line in card_file:
                srlist = []
                if 'Rates' in line:
                    srlist = line.rstrip().split()
                elif 'rates' in line:
                    srlist = line.rstrip().split()
                if srlist != []:
                    for word in srlist:
                        word = word.strip(',')
                        if word.isdigit():
                            if word not in full_list:
                                full_list.append(word)
        # full_list should have a list of all sample rates
        esr = lsr = 0
        hsr = 1000000
        if not l_usb:
            hsr = 48000
            lsr = 44100
        for word in full_list:
            if int(word) > hsr:
                continue
            if int(word) < lsr:
                continue
            if int(word) < int(def_config['RATE']):
                if int(word) > lsr:
                    lsr = int(word)
            elif int(word) > int(def_config['RATE']):
                if int(word) < hsr:
                    hsr = int(word)
            elif int(word) == int(def_config['RATE']):
                esr = int(word)
        if not esr:
            if hsr < int(def_config['RATE']):
                dsr = str(hsr)
            elif lsr > int(def_config['RATE']):
                dsr = str(lsr)
            elif (hsr - int(def_config['RATE'])) < (int(def_config['RATE']) - lsr):
                dsr = str(hsr)
            else:
                dsr = str(lsr)
    return dsr


def start_slave(ldev):
    ''' takes the audio device as a parameter and starts a bridge
    from that device to jack '''
    global devices
    global def_config
    global procs

    dsr = def_config['RATE']
    import_device_array()
    dname, l_dev, sub = ldev.split(",", 2)
    for device in devices:
        if device[0] == dname and device[2] > int(l_dev):
            if device[3 + int(l_dev)][1]:
                if device[1]:
                    # this is usb devices we should find /proc/dname/stream?
                    if os.path.isfile(f"/proc/asound/{dname}/stream0"):
                        dsr = get_card_rate(f"/proc/asound/{dname}/stream0", True)
                    logging.info(f"Got {dsr} as the closest rate to {def_config['RATE']}")
                    if def_config['USB-SINGLE'] == "False":
                        procout = subprocess.Popen(
                            ["/usr/bin/zita-j2a", "-j", f"{ldev}-out", "-d", f"hw:{ldev}", "-r", dsr, "-p",
                             def_config['ZFRAME'], "-n", def_config['PERIOD'], "-c", "100"], shell=False)
                        pidout = procout.pid
                        procs.append(procout)
                        device[3 + int(l_dev)][2] = pidout
                else:
                    # this is internal and so we look at /proc/dname/codec* if it is a file
                    if os.path.isfile(f"/proc/asound/{dname}/codec#0"):
                        dsr = get_card_rate(f"/proc/asound/{dname}/codec#0", False)
                    logging.info(f"Got {dsr} as the closest rate to {def_config['RATE']}")
                    procout = subprocess.Popen(
                        ["/usr/bin/zita-j2a", "-j", f"{ldev}-out", "-d", f"hw:{ldev}", "-r", dsr, "-p",
                            def_config['ZFRAME'], "-n", def_config['PERIOD'], "-c", "100"], shell=False)
                    pidout = procout.pid
                    procs.append(procout)
                    device[3 + int(l_dev)][2] = pidout
            if device[3 + int(l_dev)][3]:
                procin = subprocess.Popen(
                    ["/usr/bin/zita-a2j", "-j", f"{ldev}-in", "-d", f"hw:{ldev}", "-r", dsr, "-p", def_config['ZFRAME'],
                     "-n", def_config['PERIOD'], "-c", "100"], shell=False)
                pidin = procin.pid
                procs.append(procin)
                device[3 + int(l_dev)][4] = pidin


def kill_slave(ldev):
    ''' takes the device as a parameter and if the device exists
    and is bridged to jack, stops the bridge '''
    global devices
    global procs
    # XXXX need to separate out dev and subdev like above.
    dname, l_dev, sub = ldev.split(",", 2)
    for device in devices:
        if device[0] == dname and device[2]:
            if device[3 + int(l_dev)][2]:
                for i, pr in enumerate(procs):
                    if pr.pid == device[3 + int(l_dev)][2]:
                        logging.debug(f"kill {str(dname)} sub: {str(l_dev)} PID: {str(device[3 + int(l_dev)][2])}")
                        pr.send_signal(signal.SIGINT)
                        try:
                            rt = pr.wait(timeout=15)
                        except TimeoutExpired:
                            logging.debug(f"kill PID: {str(device[3 + int(l_dev)][2])} failed")
                            pr.terminate()
                            outs, errs = pr.communicate()
                        del procs[i]
            if device[3 + int(l_dev)][4]:
                for i, pr in enumerate(procs):
                    if pr.pid == device[3 + int(l_dev)][4]:
                        logging.info(f"kill {str(dname)} sub: {str(l_dev)} PID: {str(device[3 + int(l_dev)][4])}")
                        pr.send_signal(signal.SIGINT)
                        try:
                            rt = pr.wait(timeout=15)
                        except TimeoutExpired:
                            logging.debug(f"kill PID: {str(device[3 + int(l_dev)][4])} failed")
                            pr.terminate()
                            outs, errs = pr.communicate()
                        del procs[i]


def ses_cb_quit(*args, **kwargs):
    ''' dbus call back when quit signal caught. This is for use in
    testing and the GUI never sends this. '''
    logging.warning("Got quit signal.")
    cp = subprocess.run(["/usr/bin/killall", "-9", "jackdbus", "jackd", "a2jmidid"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"Kill jack and friends: {cp.stdout.strip()}")
    cp = subprocess.run(["/usr/bin/pulseaudio", "-k"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"Restart PA: {cp.stdout.strip()}")
    os._exit(0)


def handler(signum, frame):
    ''' a handler for system signals that may be sent by the system.
        we want to trap sigint, sigkill and sigterm and do the same as
        above. '''
    logging.warning(f"Got signal number: {str(signum)} - Dying.")
    cp = subprocess.run(["/usr/bin/killall", "-9", "jackdbus", "jackd", "a2jmidid"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"Kill jack and friends: {cp.stdout.strip()}")
    cp = subprocess.run(["/usr/bin/pulseaudio", "-k"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"Restart PA: {cp.stdout.strip()}")
    os._exit(0)


def ses_cb_stop(*args, **kwargs):
    ''' dbus call back when stop signal caught. This stops jack. '''
    logging.info("Got stop signal.")
    cp = subprocess.run(["/usr/bin/killall", "-9", "jackdbus", "jackd", "a2jmidid"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"Kill jack and friends: {cp.stdout.strip()}")
    cp = subprocess.run(["/usr/bin/pulseaudio", "-k"],
                        universal_newlines=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=False)
    logging.debug(f"Restart PA: {cp.stdout.strip()}")


def ses_cb_start(*args, **kwargs):
    ''' dbus call back when (re)start signal caught'''
    logging.info("Got start signal.")
    config_start()


def ses_cb_config(*args, **kwargs):
    ''' dbus call back when config signal caught '''
    logging.info("Got config signal.")
    reconfig()


def ses_cb_tablet(*args, **kwargs):
    ''' dbus call back when tablet signal caught
        This reads the config file and sets the tablet
        buttons, pen and size/position'''
    ''' not sure what we will do with this, if anything
		tablet plans changed/on hold for now; '''
    logging.info("Got tablet signal.")


def ses_cb_ping(*args, **kwargs):
    ''' dbus call back when config signal caught '''
    logging.info("Got ping signal.")
    time.sleep(3)
    subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/", "org.studio.control.event.V3_1_signal"],
                   shell=False)


def main():
    ''' Autojack runs at session start and manages audio for the session.
    this is the daemon for studio-controls'''
    global last_master
    global procs
    procs = []
    last_master = ""
    # set up logging
    logpath = expanduser("~/.log")
    # make sure the logfile directory exists
    if not os.path.exists(logpath):
        os.makedirs(logpath)
    logfile = expanduser("~/.log/autojack.log")
    if os.path.isfile(logfile):
        os.replace(logfile, f"{logfile}.old")

    logging.basicConfig(filename=logfile, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG)
    logging.info('logging online')
    dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
    signal.signal(signal.SIGHUP, handler)
    signal.signal(signal.SIGINT, handler)
    signal.signal(signal.SIGQUIT, handler)
    signal.signal(signal.SIGILL, handler)
    signal.signal(signal.SIGTRAP, handler)
    signal.signal(signal.SIGABRT, handler)
    signal.signal(signal.SIGBUS, handler)
    signal.signal(signal.SIGFPE, handler)
    #signal.signal(signal.SIGKILL, handler)
    signal.signal(signal.SIGUSR1, handler)
    signal.signal(signal.SIGSEGV, handler)
    signal.signal(signal.SIGUSR2, handler)
    signal.signal(signal.SIGPIPE, handler)
    signal.signal(signal.SIGALRM, handler)
    signal.signal(signal.SIGTERM, handler)
    subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/",
                    "org.studio.control.event.quit_signal"], shell=False)
    logging.info('kill other autojack intances')
    time.sleep(3)

    config_start()
    import_device_array()
    system_bus = dbus.SystemBus()
    system_bus.add_signal_receiver(msg_cb_new, dbus_interface='org.freedesktop.systemd1.Manager', signal_name='UnitNew')
    system_bus.add_signal_receiver(msg_cb_removed, dbus_interface='org.freedesktop.systemd1.Manager',
                                   signal_name='UnitRemoved')

    user_bus = dbus.SessionBus()
    user_bus.add_signal_receiver(ses_cb_stop, dbus_interface='org.studio.control.event',
                                 signal_name='stop_signal')
    user_bus.add_signal_receiver(ses_cb_start, dbus_interface='org.studio.control.event',
                                 signal_name='start_signal')
    user_bus.add_signal_receiver(ses_cb_config, dbus_interface='org.studio.control.event',
                                 signal_name='config_signal')
    user_bus.add_signal_receiver(ses_cb_ping, dbus_interface='org.studio.control.event',
                                 signal_name='ping_signal')
    user_bus.add_signal_receiver(ses_cb_tablet, dbus_interface='org.studio.control.event',
                                 signal_name='tablet_signal')
    user_bus.add_signal_receiver(ses_cb_quit, dbus_interface='org.studio.control.event',
                                 signal_name='quit_signal')

    subprocess.run(["/usr/bin/dbus-send", "--type=signal", "/",
                    "org.studio.control.event.V3_1_signal"], shell=False)

    loop = GLib.MainLoop()
    loop.run()


if __name__ == '__main__':
    main()
