#!/usr/bin/env python3
from gi.repository import Gtk, Pango
import getpass
import os
import subprocess
import signal
import sys

"""
Take a Break
Copyright (C) 2015  Jacob Vlijm
contact: vlijm@planet.nl
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, either version 3 of the License, or any later version. This
program is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details. You
should have received a copy of the GNU General Public License along with this
program.  If not, see <http://www.gnu.org/licenses/>.
"""

class InterFace(Gtk.Window):

    def __init__(self):

        doc = "/usr/share/takeabreak/docs/about"
        self.user = getpass.getuser()
        dirlist = setup_dirs()
        prefs = dirlist[0]; self.config_dir = dirlist[1]
        self.curr_set = prefs+"/takeabreak_prefs"
        self.settings = self.read_settings()
        self.new_settings = list(self.settings)
        self.effect_options = [
            ["rotate", "Screen upside down"],
            ["dim", "Dim screen"],
            ["screensaver", "Screen saver & lock"],
            ["message", "Countdown message"],
            ["lock" , "Lock screen"],
            ]

        Gtk.Window.__init__(self, title="Take a Break settings")
        # maingrid
        maingrid = Gtk.Grid()
        maingrid.set_column_homogeneous(False)
        maingrid.set_border_width(10)
        self.add(maingrid)
        # icon
        image = Gtk.Image(xalign=0)
        image.set_from_file("/usr/share/takeabreak/icon/break_64.png")
        maingrid.attach(image, 0, 0, 1, 1)
        # grid for checkbox/h separator/switch
        controlsgrid = Gtk.Grid()
        maingrid.attach(controlsgrid, 2, 0, 1, 1)
        # autostart checkbox
        self.check_autostart = Gtk.CheckButton("Start on login")
        self.check_autostart.connect(
            "toggled", self.toggle_autostart, "autostart:",
            )
        auto_set = self.strip_setting("autostart:")                                             
        auto_set = eval(auto_set) if auto_set != None else False 
        self.check_autostart.set_active(auto_set)
        if "show_tooltips:True" in self.settings:
            self.check_autostart.set_tooltip_text(
                "Add to Startup Applications"
                )
        controlsgrid.attach(self.check_autostart, 0, 0, 1, 1)
        # separator grid between switch and autostart checkbox
        switchsep = Gtk.Grid()
        switchsep.set_border_width(5)
        controlsgrid.attach(switchsep, 0, 1, 1, 1)
        # grid for switch (to prevent it from stretching horizontally)
        switchgrid = Gtk.Grid()
        controlsgrid.attach(switchgrid, 0, 2, 1, 1)     
        # switch
        switch = Gtk.Switch()
        switch.connect("notify::active", self.toggle_run)
        checkruns = self.test_ifruns()
        if checkruns != []:
            switch.set_active(True)
        else:
            switch.set_active(False)
        if "show_tooltips:True" in self.settings:
            switch.set_tooltip_text("Start/Stop Take a Break")  
        switchgrid.attach(switch, 0, 1, 1, 1)
        # separator grid below topsection
        topsep = Gtk.Grid()
        topsep.set_border_width(10)
        maingrid.attach(topsep, 0, 1, 4, 1)
        # settings header
        self.preferences_label = Gtk.Label("Settings", xalign=0)
        self.preferences_label.modify_font(Pango.FontDescription('bold'))
        self.preferences_label.set_justify(Gtk.Justification.LEFT)
        maingrid.attach(self.preferences_label, 0, 2, 4, 1)
        # separator grid below settings header
        settings_sep = Gtk.Grid()
        settings_sep.set_border_width(5)
        maingrid.attach(settings_sep, 0, 3, 4, 1)
        # uptime section
        self.uptime_label = Gtk.Label("Work time", xalign=0)
        maingrid.attach(self.uptime_label, 0, 4, 2, 1)
        uptime_grid = Gtk.Grid()
        maingrid.attach(uptime_grid, 2, 4, 1, 1)
        self.uptime = Gtk.Entry(xalign=0)
        self.uptime.set_width_chars(5)
        uptime_set = self.strip_setting("awaketime:")                                             
        uptime_set = uptime_set if uptime_set != None else "50"
        self.uptime.set_text(uptime_set)
        self.uptime.connect('changed', self.get_uptime)
        uptime_grid.attach(self.uptime, 0, 0, 1, 1)
        self.minutes_label = Gtk.Label(" (in minutes)")
        if "show_tooltips:True" in self.settings:
            self.uptime.set_tooltip_text("Work time (in minutes)")
        uptime_grid.attach(self.minutes_label, 1, 0, 1, 1)
        # breaktime section
        self.breaktime_label = Gtk.Label("Break time", xalign=0)
        maingrid.attach(self.breaktime_label, 0, 5, 2, 1)
        breaktime_grid = Gtk.Grid()
        maingrid.attach(breaktime_grid, 2, 5, 1, 1)
        self.breaktime = Gtk.Entry(xalign=0)
        self.breaktime.set_width_chars(5)
        breaktime_set = self.strip_setting("sleeptime:")                                             
        breaktime_set = breaktime_set if breaktime_set != None else "10"
        self.breaktime.set_text(breaktime_set)
        self.breaktime.connect('changed', self.get_breaktime)
        breaktime_grid.attach(self.breaktime, 0, 0, 1, 1)
        self.minutes_label = Gtk.Label(" (in minutes)")
        if "show_tooltips:True" in self.settings:
            self.breaktime.set_tooltip_text("Break (in whole minutes)") 
        breaktime_grid.attach(self.minutes_label, 1, 0, 1, 1)
        # sep below time section
        cb_sectionsep = Gtk.Grid()
        maingrid.attach(cb_sectionsep, 0, 6, 4, 1)
        cb_sectionsep.set_border_width(5)
        # show nitifications checkbox
        self.shownotify = Gtk.CheckButton("Show notifications")
        notify_set = self.strip_setting("notify_msg:")
        notify_set = eval(notify_set) if notify_set != None else True
        self.shownotify.set_active(notify_set)
        self.shownotify.connect("toggled", self.toggle_notify, "notify_msg:")
        if "show_tooltips:True" in self.settings:
            self.shownotify.set_tooltip_text(
                "Show notifications for start/stop, restart and upcoming break"
                )
        maingrid.attach(self.shownotify, 0, 7, 4, 1)
        # smart resume
        self.smartres = Gtk.CheckButton("Smart resume")
        self.smartres.connect(
            "toggled", self.toggle_smartresume, "from_unidle:"
            )
        if "show_tooltips:True" in self.settings:
            self.smartres.set_tooltip_text(
                "After a break, start counting time from first un- idle\n"+\
                "(when user actually starts working again)")
        maingrid.attach(self.smartres, 0, 8, 4, 1)
        smartres_set = self.strip_setting("from_unidle:")
        smartres_set = eval(smartres_set) if smartres_set != None else False
        self.smartres.set_active(smartres_set)
        self.showtooltips= Gtk.CheckButton("Show tooltips")
        self.showtooltips.connect(
            "toggled", self.toggle_tooltips, "show_tooltips:"
            )
        tooltips_set = self.strip_setting("show_tooltips:")
        tooltips_set = eval(tooltips_set) if tooltips_set != None else True
        self.showtooltips.set_active(tooltips_set)
        if "show_tooltips:True" in self.settings:
            self.showtooltips.set_tooltip_text(
                "Show tooltips on this user interface\n"+\
                "(changes will be applied on next run)")
        maingrid.attach(self.showtooltips, 0, 9, 4, 1)
        # sep below checkboxes 
        lb_sectionsep = Gtk.Grid() 
        lb_sectionsep.set_border_width(5)
        maingrid.attach(lb_sectionsep, 0, 10, 4, 1)
        # listbox label
        label = Gtk.Label("Effect:", xalign=0)
        maingrid.attach(label, 0, 11, 1, 1)
        # listbox
        listbox = Gtk.ListBox()
        listbox.set_selection_mode(Gtk.SelectionMode.SINGLE)
        if "show_tooltips:True" in self.settings:
            listbox.set_tooltip_text(
                "Screen effect during the break\n"+\
                "Options may depend on your system.")
        row = Gtk.ListBoxRow()
        hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=50)
        row.add(hbox)
        combo = Gtk.ComboBoxText()
        combolist = list(enumerate(self.effect_options))
        for i, arg in combolist:
            combo.insert(i, str(i), arg[1])
        try:
            relevant = [item.split(":")[1] \
                        for item in self.settings if "mode:" in item][0]
            combo_index = [item[0] for item in combolist \
                           if item[1][0] == relevant][0]
            combo.set_active(combo_index)
        except IndexError:
            combo.set_active(1)
        maingrid.attach(listbox, 1, 11, 4, 1)
        combo.connect("changed", self.combo_change)
        listbox.add(row)
        hbox.pack_start(combo, False, True, 0)  
        aboutsep1 = Gtk.Grid()
        maingrid.attach(aboutsep1, 0, 12, 4, 1)
        aboutsep1.set_border_width(5)
        # About header
        self.about_label = Gtk.Label("About & legal notice", xalign=0)
        self.about_label.modify_font(Pango.FontDescription('bold'))
        self.about_label.set_justify(Gtk.Justification.LEFT)
        maingrid.attach(self.about_label, 0, 13, 4, 1)
        # separator
        aboutsep2 = Gtk.Grid()
        aboutsep2.set_border_width(5)
        maingrid.attach(aboutsep2, 0, 14, 4, 1)
        # text window
        scrolledwindow = Gtk.ScrolledWindow()
        scrolledwindow.set_hexpand(True)
        scrolledwindow.set_vexpand(True)
        scrolledwindow.set_min_content_height(100)
        maingrid.attach(scrolledwindow, 0, 15, 4, 1)
        # text field
        self.textfield = Gtk.TextView()
        self.textbuffer = self.textfield.get_buffer()
        self.textbuffer.set_text(open(doc).read())
        self.textfield.set_wrap_mode(Gtk.WrapMode.WORD)
        self.textfield.set_editable(False)
        self.textfield.modify_font(Pango.FontDescription('Ubuntu Mono 12'))
        scrolledwindow.add(self.textfield)          
        # separator
        bottomsep = Gtk.Grid()
        maingrid.attach(bottomsep, 0, 16, 4, 1)
        bottomsep.set_border_width(5)
        # grid
        bottombutton_grid = Gtk.Grid()
        bottombutton_grid.set_column_spacing(180)
        maingrid.attach(bottombutton_grid, 0, 18, 4, 1)
        # placeholder, to be able to use set_column_spacing()
        fakebutton = Gtk.Grid()
        fakebutton.set_border_width(5)
        bottombutton_grid.attach(fakebutton, 0, 0, 1, 1)
        # quitbutton
        quitbutton = Gtk.Button("_Close", use_underline=True)
        quitbutton.set_label("Apply & close")
        quitbutton.connect("clicked", self.on_close_clicked)
        quitbutton.set_size_request(120,20)
        if "show_tooltips:True" in self.settings:
            quitbutton.set_tooltip_text(
                "Apply new settings & quit\n"+\
                "If the Take a Break backgound process is running, "+\
                "it will be restarted with the new settings")
        bottombutton_grid.attach(quitbutton, 3, 1, 1, 1)      
    def on_close_clicked(self, button):
        """
        function to take care of the actions when the preferences window is
        closed. It checks if changes were made to settings. If so, if the
        background script needs to be restarted. If so, it restarts the back-
        ground script with the newly applied settings. Furthermore, the
        function checks the integrity of newly chosen time settings before
        writing them to the preferences file, gives a warning if not.        
        """
        if self.settings != self.new_settings:       
            try:
                new_t = [int(item.split(":")[-1]) \
                         for item in self.new_settings \
                         if any(["awaketime" in item, "sleeptime" in item])]
                self.apply_newsettings()
                condition = [self.filter_settingslist(self.settings) == \
                    self.filter_settingslist(self.new_settings)][0]
                if condition == False:
                    check = self.test_ifruns()
                    if check:
                        self.stop_run(check)
                        self.start_run(start_msg = "Take a Break restarted")
                Gtk.main_quit()   
            except ValueError:
                subprocess.Popen(["zenity", "--warning",
                    "--text=Work- and Breaktime should be integers"])
        else:
            Gtk.main_quit()

    def filter_settingslist(self, l):
        """
        function to filter out settings, which are irrelevant to the decision
        to restart the background script or not.
        """
        return sorted([item for item in l if not any([
            "show_tooltips" in item, "autostart" in item])])

    def toggle_run(self, switch, gparam):
        """
        like the name sais, either stops the bacground script or starts it
        """
        runs = switch.get_active()
        pids = self.test_ifruns()
        if all([runs == False, len(pids) != 0]):   
            self.stop_run(pids)
        elif all([runs == True, len(pids) == 0]):
            # todo: make dependent on changed settings
            self.apply_newsettings() 
            self.start_run()

    def stop_run(self, pids):
        """
        stops the bacground script (and possible duplicate runs if user did a
        bad job from command line :), sends a message.
        """
        for pid in pids:
            os.kill(int(pid), signal.SIGQUIT)
        subprocess.Popen(["notify-send", "Take a Break is stopped"])

    def start_run(self, start_msg = "Take a Break is running" ):
        """
        starts the background script, shows message
        """
        settings = self.read_settings()
        cmd = "/usr/share/takeabreak/takeabreak/takeabreak_run "+(" ").join(settings)
        subprocess.Popen(["/bin/bash", "-c", cmd])
        subprocess.Popen(["notify-send", start_msg])

    def test_ifruns(self):
        """
        lists possible occurrence(s) of a running background script
        """
        return [l.split()[0].strip() for l in subprocess.check_output(
            ["ps", "-u", self.user, "ww"]
            ).decode("utf-8").splitlines() if "takeabreak_run" in l]

    def get_uptime(self, widget):
        """
        fetches & prepares data for settings change
        """
        line = "awaketime:"
        val = self.uptime.get_text()
        self.update_settings(line, val)

    def get_breaktime(self, widget):
        """
        fetches & prepares data for settings change
        """
        line = "sleeptime:"
        val = self.breaktime.get_text()
        self.update_settings(line, val)
    
    def toggle_notify(self, widget, line):
        """
        fetches & prepares data for settings change
        """
        val = "True" if self.shownotify.get_active() == True else "False"
        self.update_settings(line, val)

    def toggle_smartresume(self, widget, line):
        """
        fetches & prepares data for settings change
        """
        val = "True" if self.smartres.get_active() == True else "False"
        self.update_settings(line, val)

    def toggle_tooltips(self, widget, line):
        """
        fetches & prepares data for settings change
        """
        val = "True" if self.showtooltips.get_active() == True else "False"
        self.update_settings(line, val)

    def toggle_tostart(self, widget, line):
        """
        fetches & prepares data for settings change
        """
        val = "True" if self.showtooltips.get_active() == True else "False"
        self.update_settings(line, val)

    def toggle_autostart(self, widget, line):
        """
        toggles autostart; either create or remove starter to/from
        ~/.config/autostart
        """
        dtfile = self.config_dir+"/autostart/takeabreak.desktop"
        newstate = self.check_autostart.get_active()
        if newstate == False:
            val = "False"
            try:
                os.remove(dtfile)
            except FileNotFoundEror:
                pass
        elif newstate == True:
            val = "True"
            content = [
                "[Desktop Entry]",
                "Name=Take a Break",
                'Exec=/bin/bash -c '+\
                '"sleep 10&&/usr/share/takeabreak/takeabreak/takeabreak -toggle"',
                "Type=Application",
                "X-GNOME-Autostart-enabled=true",
                ]
            with open(dtfile, "wt") as out:
                for l in content:
                    out.write(l+"\n")
        self.update_settings(line, val)

    def update_settings(self, line, val):
        newline = line+val
        try:
            current = [item for item in list(
                enumerate(self.new_settings
                          )) if line in item[1]][0]
            self.new_settings[current[0]] = newline
        except IndexError:
            self.new_settings.append(newline)

    def combo_change(self, combo):
        """
        apply changes in listbox selection to settings- list
        """
        text = combo.get_active_text().strip()
        line = "mode:"
        val = [item[0] for item in self.effect_options if item[1] == text][0]
        self.update_settings(line, val)

    def read_settings(self):
        """
        read the current settings (-file), create if it doesn't exist
        """
        try:
            return [l for l in open(self.curr_set).read().splitlines()\
                    if ":" in l]
        except FileNotFoundError:
            open(self.curr_set, "wt")
            return []

    def apply_newsettings(self):
        """
        write altered settings to preferences file
        """
        with open(self.curr_set, "wt") as prefs:
            prefs.write(("\n").join(self.new_settings))

    def strip_setting(self, line):
        """
        retrieve relevant section of the specific settings- line in order to
        set the "old" values in the widgets correctly
        """
        try:
            return [item.split(":")[-1] for item in self.settings \
                    if item.startswith(line)][0] 
        except IndexError:
            pass
        
def toggle_cli():
    setup_dirs()
    runs = InterFace().test_ifruns()
    if runs:
        InterFace().stop_run(runs)
        print("Terminated Take a Break with pid "+str(runs[0])+"")
    else:
        InterFace().start_run()
        print("Take a Break running with pid "+str(InterFace().test_ifruns()[0]))  
               
def setup_dirs():
    home = os.environ["HOME"]
    config_dir = home+"/.config"
    autostart_dir = config_dir+"/autostart"
    takeabreak_prefs = config_dir+"/takeabreak"
    for dr in [config_dir, autostart_dir, takeabreak_prefs]:
        if not os.path.exists(dr):
            os.makedirs(dr)
    return [takeabreak_prefs, config_dir]

def run_gui():
    window = InterFace()
    window.connect("delete-event", Gtk.main_quit)
    window.set_default_size(330, 500)
    window.set_resizable(True)
    window.show_all()
    Gtk.main()

try:
    arg = sys.argv[1]
    if arg == "-toggle":
        toggle_cli()
    else:
        print("wrong argument; must be '-toggle', or run without arguments")
except IndexError:
    run_gui()



















