#!/usr/bin/python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
import os
from os.path import expanduser
import getpass
import pwd
import re
import resource
import shutil
import subprocess
import glob
import dbus.glib
import dbus
import time

class SysInfo:
  """Get information about the system"""
  
  # get info about if rtaccess is setup right
  def user_audio(self):
    """Checks if current user is in the audio group, or not"""
    # create a list of users who are members of audio group:
    with open("/etc/group", "r") as groups_file:
      for line in groups_file:
        if re.match("^audio:", line):
          audio_users = line.split(':')[3].rstrip().split(',')

    user = getpass.getuser()
    audio_group = False
    if user in audio_users:
      audio_group = True

    return audio_group

  def check_pam_files(self):
    '''Checks for the existence of two files'''
    jack_file_exists = False
    if os.path.isfile("/etc/security/limits.d/audio.conf"):
      jack_file_exists = True
    return jack_file_exists

  def check_rlimits(self):
    """returns hard rlimit values for RTPRIO and MEMLOCK"""
    return resource.getrlimit(resource.RLIMIT_RTPRIO)[1], resource.getrlimit(resource.RLIMIT_MEMLOCK)[1]

  # System tweaks
  def get_performance(self):
    '''Checks for current cpu governor'''
    in_performance = False
    if os.path.isfile("/sys/devices/system/cpu/cpufreq/policy0/scaling_governor"):
      with open("/sys/devices/system/cpu/cpufreq/policy0/scaling_governor", "r") as perform_file_test:
        for line in perform_file_test:
          if re.match("performance", line.rstrip()):
            in_performance = True
    return in_performance

  def get_boosted(self):
    '''Checks for Intel boost state'''
    boosted = False
    boost_path = "/sys/devices/system/cpu/intel_pstate/no_turbo"
    if os.path.exists(boost_path):
      with open(boost_path, "r") as boost_test:
        for line in boost_test:
          if re.match("0", line.rstrip()):
            boosted = True
    return boosted

  # Audio stuff

class RTSetup:
  # defs for doing things
  def __init__(self):
    self.enabled_path = "/etc/security/limits.d/audio.conf"
    self.disabled_path = "/etc/security/limits.d/audio.conf.disabled"
    self.backup_file = "/usr/share/ubuntustudio-controls/audio.conf"

  def set_governor(self, enable):
    if enable == True:
      gov = "performance"
    else:
      if os.path.exists("/sys/devices/system/cpu/intel_pstate"):
        gov = "powersave"
      else:
        gov = "ondemand"
    cmd = "pkexec /usr/sbin/ubuntustudio-system "+gov
    subprocess.call(cmd, shell = True)

  def set_boost(self, enable):
    boost_path = "/sys/devices/system/cpu/intel_pstate/no_turbo"
    if os.path.exists(boost_path):
      if enable == True:
        cmd = "pkexec /usr/sbin/ubuntustudio-system boost"
        subprocess.call(cmd, shell = True)
      else:
        cmd = "pkexec /usr/sbin/ubuntustudio-system noboost"
        subprocess.call(cmd, shell = True)


  def set_user_audio_group(self, users):
    for user in users:
      if user[1] == True:
        subprocess.call(["/usr/sbin/adduser", user[0], "audio"])
      elif user[1] == False:
        subprocess.call(["/usr/sbin/deluser", user[0], "audio"])

class UbuntuStudioControls:

  def __init__(self):
    '''Activate the SysInfo class'''
    # this is a long chunk of code that initializes every thing
    # it should probably be split into tabs at least
    self.sysinfo = SysInfo()
    '''Create the GUI'''
    builder = Gtk.Builder()
    builder.add_from_file("/usr/share/ubuntustudio-controls/ubuntustudio-controls.glade")
    '''Get windows'''
    self.window_main = builder.get_object('window_main')
    self.window_help = builder.get_object('window_help')
    self.message_dialog_changes_info = builder.get_object('message_dialog_changes_info')
    self.message_dialog_rt_info = builder.get_object('message_dialog_rt_info')
    self.message_dialog_changes_info.set_transient_for(self.window_main)
    self.message_dialog_rt_info.set_transient_for(self.window_main)
    '''Get buttons'''
    self.rt_button = builder.get_object('rt_button')
    self.rt_warning = builder.get_object('rt_warning')
    self.combo_governor = builder.get_object('combo_governor')
    self.combo_boost = builder.get_object('combo_boost')
    self.button_msg_ok = builder.get_object('button_msg_ok')
    '''audio tab stuff'''
    self.jack_device_combo = builder.get_object('jack_device_combo')
    self.jack_usb_dev_combo = builder.get_object('jack_usb_dev_combo')
    self.jack_rate_combo = builder.get_object('jack_rate_combo')
    self.combobox_late = builder.get_object('combobox_late')
    self.combo_periods = builder.get_object('combo_periods')
    self.usb_plug_check = builder.get_object('usb_plug_check')
    self.combo_zita_add = builder.get_object('combo_zita_add')
    self.combo_zita_remove = builder.get_object('combo_zita_remove')
    self.combo_output = builder.get_object('combo_output')
    self.combo_out_ports = builder.get_object('combo_out_ports')
    self.jack_autostart_check = builder.get_object('jack_autostart_check')
    self.jack_midi_check = builder.get_object('jack_midi_check')
    self.pulse_check = builder.get_object('pulse_check')

    user_bus = dbus.SessionBus()
    user_bus.add_signal_receiver(self.db_ses_cb, dbus_interface='org.ubuntustudio.control.event', signal_name='pong_signal')

    '''Check if audio.conf and/or audio.conf.disabled exists, returns are true or false'''
    self.rt_file = False
    self.jack_file_exists = self.sysinfo.check_pam_files()
    if self.jack_file_exists and self.sysinfo.user_audio():
      rtprio, memlock = self.sysinfo.check_rlimits()
      if rtprio == 0:
        self.rt_button.set_label("Logout required")
        self.rt_button.set_sensitive(False)
        self.message_dialog_rt_info.show()
        self.rt_warning.set_text("Session restart required for Real Time Permissions")
      else:
        # turn off warning text, check on, deactivate
        self.rt_warning.set_text("")
        self.rt_button.set_label("Real Time Permissions Enabled")
        self.rt_button.set_sensitive(False)

    # show current CPU Governor
    self.combo_governor.append_text("Performance")
    if os.path.exists("/sys/devices/system/cpu/intel_pstate/"):
      self.combo_governor.append_text("Powersave")
    else:
      self.combo_governor.append_text("Ondemand")
    self.in_performance = self.sysinfo.get_performance()
    if self.in_performance:
      self.combo_governor.set_active(0)
    else:
      self.combo_governor.set_active(1)

    # show boost state
    if os.path.exists("/sys/devices/system/cpu/intel_pstate/no_turbo"):
      self.boosted = self.sysinfo.get_boosted()
      if self.boosted:
        self.combo_boost.set_active(1)
      else:
        self.combo_boost.set_active(0)
    else:
      self.combo_boost.set_sensitive(False)

    # Audio stuff

    # first set defaults
    self.jack = False
    self.driver = "alsa"
    self.sr = "48000"
    self.late = "1024"
    self.period = "2"
    self.zframe = "512"
    self.zdev = ""
    self.pulse = True
    self.a2j = True
    self.dev = "default"
    self.dev_desc = "default"
    self.d_out = "default"
    self.o_port = "1"
    self.usb = "slave"
    self.usbdev = ""

    global autojack
    autojack = False

    # read in autojack config file
    home = expanduser("~")
    if os.path.isfile(home+"/.config/autojackrc"):
      with open(home+"/.config/autojackrc", "r") as rc_file:
        for line in rc_file:
          if re.match("^#", line):
            continue
          lsplit = line.rstrip().split("=", 1)
          if lsplit[0] == "JACK":
            self.jack = lsplit[1]
          elif lsplit[0] == "DRIVER":
            self.driver = lsplit[1]
          elif lsplit[0] == "DEV":
            self.dev = self.dev_desc = lsplit[1]
          elif lsplit[0] == "RATE":
            self.sr = lsplit[1]
          elif lsplit[0] == "FRAME":
            self.late = lsplit[1]
          elif lsplit[0] == "ZFRAME":
            self.zframe = lsplit[1]
          elif lsplit[0] == "PERIOD":
            self.period = lsplit[1]
          elif lsplit[0] == "PULSE":
            self.pulse = lsplit[1]
          elif lsplit[0] == "A2J":
            self.a2j = lsplit[1]
          elif lsplit[0] == "OUTPUT":
            self.d_out = lsplit[1]
          elif lsplit[0] == "PORTS":
            self.o_port = lsplit[1]
          elif lsplit[0] == "XDEV":
            self.zdev = lsplit[1]
          elif lsplit[0] == "USBAUTO":
            self.usb = lsplit[1]
          elif lsplit[0] == "USBDEV":
            self.usbdev = lsplit[1]

    self.jack_rate_combo.set_active_id(self.sr)
    self.combobox_late.set_active_id(self.late)
    self.combo_periods.set_active_id(self.period)
    self.usb_plug_check.set_active(self.usb == "True")
    self.combo_out_ports.set_active_id(self.o_port)
    self.jack_autostart_check.set_active(self.jack == "True")
    self.jack_midi_check.set_active(self.a2j == "True")
    self.pulse_check.set_active(self.pulse == "True")

    self.jack_device_combo.append("default", "default")
    self.jack_device_combo.set_active_id("default")
    self.combo_output.append("system", "Jack Master")
    self.combo_output.set_active_id("system")
    self.jack_usb_dev_combo.append("", "No USB Master")
    if self.usbdev == "":
      self.jack_usb_dev_combo.set_active_id("")
    else:
      self.jack_usb_dev_combo.append(self.usbdev, self.usbdev)
      self.jack_usb_dev_combo.set_active_id(self.usbdev)

    for x in range(0, 10):
      #card loop
      cname = ""
      next_id = ""
      next_d = ""
      is_usb = False
      if os.path.exists("/proc/asound/card"+str(x)):
        with open("/proc/asound/card"+str(x)+"/id", "r") as card_file:
          for line in card_file:
            #only need one line
            cname = line.rstrip()
      if os.path.exists("/proc/asound/card"+str(x)+"/usbbus"):
        is_usb = True
      for y in range(0, 10):
        d_type = ""
        d_desc = ""
        if os.path.exists("/proc/asound/card"+str(x)+"/pcm"+str(y)+"p"):
          d_type = "playback"
        if os.path.exists("/proc/asound/card"+str(x)+"/pcm"+str(y)+"c"):
          if d_type == "":
            d_type = "capture"
          else:
            d_type = d_type+" and capture"
        if d_type != "":
          for z in range(0, 10):
            if os.path.exists("/proc/asound/card"+str(x)+"/pcm"+str(y)+d_type[0]+"/sub"+str(z)):
              with open("/proc/asound/card"+str(x)+"/pcm"+str(y)+d_type[0]+"/sub"+str(z)+"/info", "r") as info_file:
                for line in info_file:
                  if re.match("^name:", line.rstrip()):
                    dname = line.rstrip().split(": ", 1)[1]
                    next_id = cname+","+str(y)+","+str(z)
                    next_d = next_id+" "+d_type+" ("+dname+")"
              #   Have everything we need now

              # this block will fit where ever we know sub-ids and description
              if is_usb:
                if next_id != self.usbdev:
                  self.jack_usb_dev_combo.append(next_id, next_d)
              else:
                self.jack_device_combo.append(next_id, next_d)
                # opps need to check for playback able
                if next_d.find("playback") >= 0:
                  self.combo_output.append(next_id, next_d)
                if x == 0 and self.dev == "default":
                  self.dev = next_id
                if x == 0 and self.dev_desc == "default":
                  self.dev_desc = next_d
                if x == 0 and self.d_out == "default":
                  self.d_out = next_id 
                if self.dev == next_id:
                  self.jack_device_combo.set_active_id(next_id)
                  self.dev_desc = next_d
                else:
                  if self.zdev.find(next_id) >= 0:
                    self.combo_zita_remove.append(next_id, next_d)
                  else:
                    self.combo_zita_add.append(next_id, next_d)
                if self.d_out == next_id:
                  self.combo_output.set_active_id(next_id)


    handlers = {
      "on_window_main_delete_event": self.on_window_main_delete_event,
      "on_window_help_delete_event": self.on_window_help_delete_event,
      "on_main_button_cancel_clicked": self.on_main_button_cancel_clicked,
      "on_main_button_help_clicked": self.on_main_button_help_clicked,
      "combo_governor_changed_cb": self.combo_governor_changed_cb,
      "combo_boost_changed_cb": self.combo_boost_changed_cb,
      "rt_button_hit": self.rt_button_hit,
      "on_button_msg_ok_clicked": self.on_button_msg_ok_clicked,
      "on_button_rt_info_ok_clicked": self.on_button_rt_info_ok_clicked,
      "on_button_help_ok_clicked": self.on_button_help_ok_clicked,
      "cb_zita_add": self.cb_zita_add,
      "cb_zita_remove": self.cb_zita_remove,
      "cb_jack_start": self.cb_jack_start,
      "cb_jack_stop": self.cb_jack_stop,
      "cb_audio_apply": self.cb_audio_apply,
      "jack_device_changed": self.jack_device_changed,
      "usb_master_changed": self.usb_master_changed,
      "usb_master_entered": self.usb_master_entered,
    }
    builder.connect_signals(handlers)

    self.rtsetup = RTSetup()

  def db_ses_cb(*args, **kwargs):
    ''' this means we got a responce from autojack, Good'''
    global autojack
    autojack = True
    print ("autojack is running")

  def usb_master_entered(self, button, user):
    '''Before the user drops down refill this as it may have changed'''
    self.jack_usb_dev_combo.get_model().clear()
    self.jack_usb_dev_combo.append("", "No USB Master")
    if self.usbdev == "":
      self.jack_usb_dev_combo.set_active_id("")
    else:
      self.jack_usb_dev_combo.append(self.usbdev, self.usbdev)
      self.jack_usb_dev_combo.set_active_id(self.usbdev)
    for x in range(0, 10):
      #card loop
      cname = ""
      next_id = ""
      next_d = ""
      if os.path.exists("/proc/asound/card"+str(x)):
        with open("/proc/asound/card"+str(x)+"/id", "r") as card_file:
          for line in card_file:
            #only need one line
            cname = line.rstrip()
      if not os.path.exists("/proc/asound/card"+str(x)+"/usbbus"):
        continue
      for y in range(0, 10):
        d_type = ""
        d_desc = ""
        if os.path.exists("/proc/asound/card"+str(x)+"/pcm"+str(y)+"p"):
          d_type = "playback"
        if os.path.exists("/proc/asound/card"+str(x)+"/pcm"+str(y)+"c"):
          if d_type == "":
            d_type = "capture"
          else:
            d_type = d_type+" and capture"
        if d_type != "":
          for z in range(0, 10):
            if os.path.exists("/proc/asound/card"+str(x)+"/pcm"+str(y)+d_type[0]+"/sub"+str(z)):
              with open("/proc/asound/card"+str(x)+"/pcm"+str(y)+d_type[0]+"/sub"+str(z)+"/info", "r") as info_file:
                for line in info_file:
                  if re.match("^name:", line.rstrip()):
                    dname = line.rstrip().split(": ", 1)[1]
                    next_id = cname+","+str(y)+","+str(z)
                    next_d = next_id+" "+d_type+" ("+dname+")"
              #   Have everything we need now
              if next_id != self.usbdev:
                self.jack_usb_dev_combo.append(next_id, next_d)
    


  '''Functions for all the gui controls'''
  def on_window_main_delete_event(self, *args):
    Gtk.main_quit(*args)

  def on_window_help_delete_event(self, window, event):
    self.window_help.hide_on_delete()
    return True

  def on_main_button_cancel_clicked(self, button):
    Gtk.main_quit()

  def on_main_button_help_clicked(self, button):
    self.window_help.show()

  def rt_button_hit(self, button):
    cmd = "pkexec /usr/sbin/ubuntustudio-system fix"
    subprocess.call(cmd, shell = True)
    self.rt_button.set_label("Logout required")
    self.rt_button.set_sensitive(False)
    self.message_dialog_rt_info.show()
    self.rt_warning.set_text("Session restart required for Real Time Permissions")

  # system tweaks
  def combo_governor_changed_cb(self, button):
    if button.get_active_text() == "Performance":
      self.rtsetup.set_governor(True)
    else:
      self.rtsetup.set_governor(False)

  def combo_boost_changed_cb(self, button):
    if button.get_active_text() == "on":
      self.rtsetup.set_boost(True)
    else:
      self.rtsetup.set_boost(False)
    self.boosted = self.sysinfo.get_boosted()
    if self.boosted:
      self.combo_boost.set_active(1)
    else:
      self.combo_boost.set_active(0)

  # Audio setup call backs
  def cb_zita_add(self, button):
    a_id = str(button.get_active_id())
    a_desc = str(button.get_active_text())
    if a_id != "None" and a_id != "label":
      self.combo_zita_remove.append(a_id, a_desc)
      self.combo_zita_add.remove(button.get_active())
      self.combo_zita_add.set_active(0)

  def cb_zita_remove(self, button):
    a_id = str(button.get_active_id())
    a_desc = str(button.get_active_text())
    if a_id != "None" and a_id != "label":
      self.combo_zita_add.append(a_id, a_desc)
      self.combo_zita_remove.remove(button.get_active())
      self.combo_zita_remove.set_active(0)

  # this belongs to jack_device_changed
  def dev_ser(self, model, path, d_iter, dev_id):
    m_id = str(model.get(d_iter, 1)[0])
    if m_id == dev_id:
      del model[d_iter]
      return 1
    else:
      return 0
  
  def jack_device_changed(self, button):
    a_id = str(button.get_active_id())
    a_desc = str(button.get_active_text())
    if a_id != "None":
      self.combo_zita_add.append(self.dev, self.dev_desc)
      self.dev = a_id
      self.dev_desc = a_desc
      model = self.combo_zita_add.get_model()
      model.foreach(self.dev_ser, a_id)
      model = self.combo_zita_remove.get_model()
      model.foreach(self.dev_ser, a_id)
        
  def usb_master_changed(self, button):
    a_id = str(button.get_active_id())
    a_desc = str(button.get_active_text())
    if a_id != "None":
      # this needs to change can be none
      # none = desc: None id: "" (can that work?)
      self.usbdev = a_id

  def cb_jack_start(self, button):
    ''' call back for Jack (re)start button'''
    global autojack
    self.jack_autostart_check.set_active(True)
    # Write audio setting to ~/.config/autojack
    self.config_save()
    cmd = "dbus-send --type=signal / org.ubuntustudio.control.event.start_signal"
    if autojack:
      subprocess.call(cmd, shell = True)
    else:
      time.sleep(5)
      if autojack:
        subprocess.call(cmd, shell = True)
      else:
        self.start_autojack()

  def cb_jack_stop(self, button):
    global autojack
    self.jack_autostart_check.set_active(False)
    # Write audio setting to ~/.config/autojack
    self.config_save()
    cmd = "dbus-send --type=signal / org.ubuntustudio.control.event.stop_signal"
    if autojack:
      subprocess.call(cmd, shell = True)
    else:
      time.sleep(5)
      if autojack:
        subprocess.call(cmd, shell = True)
      else:
        self.start_autojack()

  # this belongs to config_save()
  def z_save(self, model, path, d_iter, save_file):
    m_id = str(model.get(d_iter, 1)[0])
    if m_id != "label" and m_id !="none":
      save_file.write(m_id+" ")
    return 0

  def cb_audio_apply(self, button):
    '''callback for audio tab apply button'''
    global autojack
    # Write audio setting to ~/.config/autojack
    self.config_save()
    cmd = "dbus-send --type=signal / org.ubuntustudio.control.event.config_signal"
    if autojack:
      # we should send a message to autojack "config"
      subprocess.call(cmd, shell = True)
    else:
      time.sleep(5)
      if autojack:
        subprocess.call(cmd, shell = True)
      else:
        self.start_autojack()

  def config_save(self):
    # Write audio setting to ~/.config/autojack
    home = expanduser("~")
    with open(home+"/.config/autojackrc", "w") as rc_file:
      rc_file.write("# file generated by ubuntustudio-controls\n\n")
      rc_file.write("\nJACK="+str(self.jack_autostart_check.get_active()))
      rc_file.write("\nDRIVER=alsa")
      rc_file.write("\nDEV="+str(self.jack_device_combo.get_active_id()))
      rc_file.write("\nRATE="+str(self.jack_rate_combo.get_active_id()))
      rc_file.write("\nFRAME="+str(self.combobox_late.get_active_id()))
      self.zframe = str(int(int(self.combobox_late.get_active_id()) / 2))
      rc_file.write("\nZFRAME="+self.zframe)
      rc_file.write("\nPERIOD="+str(self.combo_periods.get_active_id()))
      rc_file.write("\nPULSE="+str(self.pulse_check.get_active()))
      rc_file.write("\nA2J="+str(self.jack_midi_check.get_active()))
      rc_file.write("\nOUTPUT="+str(self.combo_output.get_active_id()))
      rc_file.write("\nPORTS="+str(self.combo_out_ports.get_active_id()))
      rc_file.write("\nUSBAUTO="+str(self.usb_plug_check.get_active()))
      rc_file.write("\nUSBDEV="+self.usbdev)
      rc_file.write("\nXDEV=\"")
      model = self.combo_zita_remove.get_model()
      model.foreach(self.z_save, rc_file)
      rc_file.write("\"\n# end of auto generated parameters\n")

  def start_autojack(self):
    '''start autojack as it has been detected as not running'''
    print("Starting Autojack...")
    cmd = "dbus-send --type=signal / org.ubuntustudio.control.event.quit_signal"
    subprocess.call(cmd, shell = True)
    #do it
    home = expanduser("~")
    if not os.path.exists(home+"/.log"):
      os.makedirs(home+"/.log")
    if os.path.isfile(home+"/.log/autojack.log"):
      cmd = "mv -f "+home+"/.log/autojack.log "+home+"/.log/autojack.log.old"
      subprocess.call(cmd, shell = True)
    cmd = "/usr/bin/autojack >"+home+"/.log/autojack.log 2>&1 < /dev/null &"
    subprocess.call(cmd, shell = True)

    
  def on_button_help_ok_clicked(self, button):
    self.window_help.hide()

  def on_button_msg_ok_clicked(self, button):
    Gtk.main_quit()

  def on_button_rt_info_ok_clicked(self, button):
    self.message_dialog_rt_info.hide()

  cmd = "dbus-send --type=signal / org.ubuntustudio.control.event.ping_signal"
  subprocess.call(cmd, shell = True)


us = UbuntuStudioControls()
us.window_main.show_all()

Gtk.main()
