#!/usr/bin/env python3

# GPL-2+ ("and any later version")
# Kai Lüke 2020

import gi # Debian package: python3-gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gio, GLib # Debian package: gir1.2-glib-2.0, gir1.2-gtk-3.0
import os, sys, signal, psutil # Debian package: python3-psutil
from pathlib import Path

from subprocess import check_output

os.chdir(os.path.dirname(sys.argv[0]))
user_folder = str(Path.home()) + "/.config/systemd/user"

UI_FILE = "/usr/share/wake-mobile/wake-mobile.ui"

dummy_time = "0000-01-01 00:00:00"
startmsg = "Wake your device and issue an alarm\n(You need to stay logged in)"

class WakeMobile(Gtk.Application):
  alarm_set = False
  alarm_pid = 0
  prev_alarm = None

  def __init__(self, application_id):
    Gtk.Application.__init__(self, application_id=application_id)

  def do_startup(self):
    Gtk.Application.do_startup(self)

  def do_activate(self):
    list = Gtk.Application.get_windows(self)
    if list:
      list[0].present()
      return
    self.builder = Gtk.Builder()
    self.builder.add_from_file(UI_FILE)
    self.builder.connect_signals(self)
    self.lefttime = self.builder.get_object("lefttime")
    self.hour = self.builder.get_object("hour")
    self.minute = self.builder.get_object("minute")
    self.second = self.builder.get_object("second")
    self.volumescale = self.builder.get_object("volume")
    self.volumeoption = self.builder.get_object("volumecheck")
    self.soundvolume = self.builder.get_object("soundvolume")
    self.window = self.builder.get_object("appwindow")
    self.window.set_application(self)
    self.errorlabel = self.builder.get_object("error")
    self.errorlabel.set_label(startmsg)
    self.setwakebutton = self.builder.get_object("setwake")
    self.cancelbutton = self.builder.get_object("cancel")
    self.snoozebutton = self.builder.get_object("snooze")
    self.window.show_all()
    try:
      with open(user_folder + "/wake-up.timer", "r") as f:
        content = f.read()
      time_var = content.split("OnCalendar=")[1].split("\n")[0]
      if time_var != dummy_time:
        self.alarmtime = GLib.DateTime.new_from_iso8601(time_var, default_tz=GLib.TimeZone.new_local())
        self.prev_alarm = self.alarmtime
        self.hour.set_value(self.alarmtime.get_hour())
        self.minute.set_value(self.alarmtime.get_minute())
        self.second.set_value(self.alarmtime.get_second())
        self.set_alarm(rewrite=False)
      else:
        try:
          prev_alarm_found = content.split("Previous-Alarm=")[1].split("\n")[0]
          self.prev_alarm = GLib.DateTime.new_from_iso8601(prev_alarm_found, default_tz=GLib.TimeZone.new_local())
          self.hour.set_value(self.prev_alarm.get_hour())
          self.minute.set_value(self.prev_alarm.get_minute())
          self.second.set_value(self.prev_alarm.get_second())
        except Exception as e:
          print(e)
      volume_var = int(content.split("Wake-Mobile-Volume=")[1].split("\n")[0])
      if volume_var == -1:
        self.volumeoption.set_active(False)
      else:
        self.soundvolume.set_value(volume_var)
    except Exception as e:
      print(e)
    self.update()
    GLib.timeout_add_seconds(1, self.updater)

  def updater(self):
    self.update()
    if not self.alarm_set or (self.alarmtime.difference(GLib.DateTime.new_now_local()) > 0):
        return True
    self.alarm_set = False
    self.snoozebutton.set_sensitive(True)
    try:
      if self.volumeoption.get_active():
        try:
          default_sink = str(check_output(["pactl", "get-default-sink"]), "utf-8").split("\n")[0]
          check_output(["pactl", "set-sink-mute", default_sink, "0"])
          check_output(["pactl", "set-sink-volume", default_sink, str(int(self.volumescale.get_value()))+ "%"])
        except Exception as e:
          print(e)
      self.alarm_pid, _, _, _ = GLib.spawn_async(["/usr/bin/gnome-session-inhibit", "--inhibit", "suspend", "canberra-gtk-play", "-i", "alarm-clock-elapsed", "-l", "999999999"], standard_output=-1, standard_input=-1, standard_error=-1)
      self.errorlabel.set_label("<span color='orange'>Wake up!</span>")
      self.lefttime.set_label("No time left")
    except Exception as e:
      print(e)
      self.errorlabel.set_label("<span color='red'>Error issuing alarm</span>")
    return True

  def set_alarm(self, rewrite=True):
    self.alarm_set = True
    self.window.set_sensitive(False)
    self.disable_elements(True)
    try:
      if rewrite:
        time_var = self.alarmtime.format("%Y-%m-%d %H:%M:%S")
        check_output(["set-user-alarm", time_var])
        self.gen_user_service()
        self.gen_user_timer(time_var)
        self.load_user_timer()
      self.errorlabel.set_label("Alarm is set")
    except Exception as e:
      print(e)
      self.errorlabel.set_markup("<span color='red'>Error setting wake time</span>")
    self.window.set_sensitive(True)
    self.update()

  def snooze_clicked(self, button):
    self.alarmtime = GLib.DateTime.new_now_local().add_minutes(3)
    self.hour.set_value(self.alarmtime.get_hour())
    self.minute.set_value(self.alarmtime.get_minute())
    self.second.set_value(self.alarmtime.get_second())
    self.end_alarm()
    self.set_alarm()

  def end_alarm(self):
    if self.alarm_pid != 0:
      children = psutil.Process(pid=self.alarm_pid).children(recursive=True)
      for child in children:
        os.kill(child.pid, signal.SIGTERM)
      os.kill(self.alarm_pid, signal.SIGTERM)
      self.alarm_pid = 0

  def stop_clicked(self, button):
    self.disable_elements(False)
    self.alarm_set = False
    self.errorlabel.set_label(startmsg)
    self.end_alarm()
    self.gen_user_timer(dummy_time)
    check_output(["systemctl", "--user", "daemon-reload"]) # Debian package: systemd
    check_output(["systemctl", "--user", "disable", "--now", "wake-up.timer"])
    check_output(["set-user-alarm"])
    self.update()

  def set_wake_clicked(self, button):
    self.alarmtime = self.calc_wakeup_time()
    self.prev_alarm = self.alarmtime
    self.set_alarm()

  def disable_elements(self, val):
    self.setwakebutton.set_sensitive(not val)
    self.cancelbutton.set_sensitive(val)
    self.snoozebutton.set_sensitive(False)
    self.volumeoption.set_sensitive(not val)
    self.volumescale.set_sensitive(not val)
    self.hour.set_sensitive(not val)
    self.minute.set_sensitive(not val)
    self.second.set_sensitive(not val)

  def on_volumecheck_toggled(self, volumecheckelem):
    self.volumescale.set_sensitive(volumecheckelem.get_active())

  def on_volume_changed(self, volumeadjustment):
    self.volumescale.set_fill_level(volumeadjustment.get_value())

  def on_window_destroy(self, window):
    self.end_alarm()
    self.quit()

  def quit_cb(self, action, parameter):
    self.on_window_destroy(self.window)

  def calc_wakeup_time(self):
    hour = int(self.hour.get_value())
    minute = int(self.minute.get_value())
    second = int(self.second.get_value())
    tn = GLib.DateTime.new_now_local()
    wtime = GLib.DateTime.new_local(tn.get_year(), tn.get_month(), tn.get_day_of_month(), hour, minute, second)
    if wtime.difference(tn) < 0:
      wtime = wtime.add_days(1)
    if wtime.difference(tn) < 0:
      raise Exception("Something went wrong with the time calculation: This should never happen!")
    return wtime

  def update(self):
    tn = GLib.DateTime.new_now_local()
    wtime = self.calc_wakeup_time()
    total_sec_left = int(wtime.difference(tn)/1000/1000)
    sec_left = str(int(total_sec_left % 60))
    min_left = str((int(total_sec_left/60) % 60))
    hours_left = str(int(total_sec_left/60/60))
    label = hours_left + ":" + min_left + ":" + sec_left + " left"
    if self.alarm_pid != 0:
      pass
    elif self.alarm_set:
      self.lefttime.set_label("<span color='orange'>" + label + "</span>")
    else:
      self.lefttime.set_label(label)

  def gen_user_service(self):
    p = os.path.realpath(__file__)
    content = f"""[Unit]
Description=User Wake Up Action
[Service]
ExecStart=gapplication launch org.gnome.gitlab.kailueke.WakeMobile
"""
    with open(user_folder + "/wake-up.service", "w") as f:
      f.write(content)

  def gen_user_timer(self, time_arg):
    # TODO: Is "Persistent=true" really needed for race safety?
    volume_setting = int(self.volumescale.get_value()) if self.volumeoption.get_active() else -1
    if self.prev_alarm:
      prev_alarm_found = self.prev_alarm.format("%Y-%m-%d %H:%M:%S")
    else:
      prev_alarm_found = dummy_time
    content = f"""[Unit]
Description=User Wake Up Timer
[Timer]
Persistent=true
AccuracySec=1us
OnCalendar={time_arg}
# Previous-Alarm={prev_alarm_found}
# Wake-Mobile-Volume={volume_setting}
[Install]
WantedBy=timers.target
"""
    with open(user_folder + "/wake-up.timer", "w") as f:
      f.write(content)

  def load_user_timer(self):
    check_output(["systemctl", "--user", "daemon-reload"])
    check_output(["systemctl", "--user", "enable", "wake-up.timer"])
    check_output(["systemctl", "--user", "restart", "wake-up.timer"])


if __name__ == "__main__":
  check_output(["mkdir", "-p", user_folder])
  check_output(["gapplication"]) # Debian package: libglib2.0-bin
  check_output(["pactl", "--version"]) # Debian package: pulseaudio-utils
  check_output(["canberra-gtk-play", "--version"]) # Debian package: gnome-session-canberra
  check_output(["gnome-session-inhibit", "--version"]) # Debian package: gnome-session-bin
  app = WakeMobile("org.gnome.gitlab.kailueke.WakeMobile")
  exit_status = app.run(sys.argv)
  sys.exit(exit_status)
