#!/usr/bin/python3
import gi, os, re, jwmkit_utils
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GdkPixbuf
import xml.etree.ElementTree as ET

# JWM Kit - A set of Graphical Apps to simplify use of JWM (Joe's Window Manager) <https://codeberg.org/JWMKit/JWM_Kit>
# Copyright © 2020-2022 Calvin Kent McNabb <apps.jwmkit@gmail.com>
#
# This file is part of JWM Kit.
#
# JWM Kit is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2,
# as published by the Free Software Foundation.
#
# JWM Kit 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 JWM Kit.  If not, see <https://www.gnu.org/licenses/>.


class MainWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title="JWM Kit Startups")
        self.set_border_width(15)
        self.status_message = Gtk.Label()
        self.status_message.set_markup('<big>Select or add an item to edit</big>')
        try:
            self.set_icon_from_file('/usr/share/pixmaps/jwmkit/executegray.svg')
        except gi.repository.GLib.Error:
            self.set_icon_name(Gtk.STOCK_EXECUTE)
        self.home = os.path.expanduser('~')
        self.rc_file = self.get_paths()
        if self.rc_file == 'warning_settings':
            jwmkit_utils.warning_settings(self)
            return
        self.user_terminal = 'xterm'
        self.file_selected = ''
        self.commands = self.read_rc_file()
        self.set_hexpand(False)
        main_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5)
        self.add(main_box)
        scrolledwindow = Gtk.ScrolledWindow()
        scrolledwindow.set_min_content_width(640)
        scrolledwindow.set_min_content_height(220)
        header_box = Gtk.Box(spacing=4)
        top_box = Gtk.Box(spacing=4)
        main_box.add(header_box)
        main_box.add(top_box)
        main_box.pack_start(scrolledwindow, True, True, 0)
        self.listbox = Gtk.ListBox()
        self.listbox.set_selection_mode(Gtk.SelectionMode.SINGLE)
        center_box = Gtk.Box(spacing=4)
        bottom_box = Gtk.Box(spacing=4)
        scrolledwindow.add(self.listbox)
        main_box.add(center_box)
        main_box.add(bottom_box)

        self.add_button = Gtk.Button(label="Add", image=Gtk.Image(stock=Gtk.STOCK_ADD))
        self.remove_button = Gtk.Button(label="Remove", image=Gtk.Image(stock=Gtk.STOCK_DELETE))
        self.save_button = Gtk.Button(label="Save", image=Gtk.Image(stock=Gtk.STOCK_SAVE))
        self.close_button = Gtk.Button(label="Close", image=Gtk.Image(stock=Gtk.STOCK_CANCEL))
        self.browse_button = Gtk.Button(label="Browse", image=Gtk.Image(stock=Gtk.STOCK_FIND))
        self.video_button = Gtk.Button(label="Display", image=Gtk.Image(stock=Gtk.STOCK_ZOOM_FIT))
        about_button = Gtk.Button(image=Gtk.Image(stock=Gtk.STOCK_ABOUT))
        about_button.set_always_show_image(True)
        about_button.connect('clicked', jwmkit_utils.get_about, self)
        about_button.set_property("width-request", 40)
        top_box.pack_end(about_button, False, False, 0)
        self.cmd_entry = Gtk.Entry()
        command_count = len(self.commands)
        for i in range(command_count):
            cmd = self.commands[i]
            self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
            vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
            self.hbox.add(vbox)
            self.listbox.add(self.hbox)
            cmd_label = Gtk.Label(label=cmd[3], xalign=0)

            start_check = Gtk.CheckButton()
            restart_check = Gtk.CheckButton()
            shutdown_check = Gtk.CheckButton()

            if cmd[0] == 1: start_check.set_active(True)
            start_check.connect("toggled", self.start_toggled, i)
            restart_check.connect("toggled", self.restart_toggled, i)
            shutdown_check.connect("toggled", self.shut_toggled, i)
            if cmd[1] == 1: restart_check.set_active(True)
            if cmd[2] == 1: shutdown_check.set_active(True)
            self.hbox.pack_start(start_check, False, False, 0)
            self.hbox.pack_start(restart_check, False, False, 45)
            self.hbox.pack_start(shutdown_check, False, False, 0)
            self.hbox.pack_start(cmd_label, False, False, 35)
        if command_count == 0:
            self.add_command('')

        header_label = Gtk.Label()
        header_label.set_markup('\n  <big><b>Startups</b></big>'
                                '\n  start an application or script when JWM starts, restarts, or shutdowns\n')
        header_box.add(jwmkit_utils.create_image('/usr/share/pixmaps/jwmkit/executegray.svg', 48, 48, True))
        header_box.add(header_label)

        start_label = Gtk.Label(label="Startup")
        restart_label = Gtk.Label(label="Restart")
        shut_label = Gtk.Label(label="Shutdown")
        cmd_label = Gtk.Label(label="Command")

        top_box.pack_start(start_label, False, False, 0)
        top_box.pack_start(restart_label, False, False, 18)
        top_box.pack_start(shut_label, False, False, 0)
        top_box.pack_start(cmd_label, False, False, 15)

        self.add_button.set_always_show_image(True)
        self.remove_button.set_always_show_image(True)
        self.save_button.set_always_show_image(True)
        self.close_button.set_always_show_image(True)
        self.browse_button.set_always_show_image(True)
        self.video_button.set_always_show_image(True)

        self.add_button.set_tooltip_text('Add a new startup item')
        self.remove_button.set_tooltip_text('Remove the item from startups\n2nd click confirms')
        self.save_button.set_tooltip_text('Saving will overwrite existing startup list')
        self.browse_button.set_tooltip_text('Browse the file system for executables or .desktop files')
        self.video_button.set_tooltip_text('Generate a command that restores your monitor(s) current configuration.')

        self.add_button.set_property("width-request", 100)
        self.remove_button.set_property("width-request", 100)
        self.save_button.set_property("width-request", 100)
        self.close_button.set_property("width-request", 100)
        self.browse_button.set_property("width-request", 100)
        self.video_button.set_property("width-request", 100)
        self.cmd_entry.set_property("width-request", 300)

        center_box.add(self.add_button)
        center_box.pack_start(self.cmd_entry, True, True, 0)
        bottom_box.pack_start(self.remove_button, False, False, 0)
        bottom_box.pack_start(self.status_message, True, True, 0)
        center_box.pack_end(self.save_button, False, False, 0)
        bottom_box.pack_end(self.close_button, False, False, 0)
        bottom_box.pack_end(self.video_button, False, False, 0)
        center_box.pack_end(self.browse_button, False, False, 0)

        self.close_button.connect("clicked", Gtk.main_quit)
        self.add_button.connect("clicked", self.add_command)
        self.remove_button.connect("clicked", self.remove_command)
        self.cmd_entry.connect("changed", self.on_entry_change)
        self.save_button.connect("clicked", self.save_command)
        self.browse_button.connect("clicked", self.on_browse)
        self.listbox.connect("row-selected", self.on_listbox_change)
        self.video_button.connect("clicked", self.video_cmd)

    def video_cmd(self, button):
        current_mode = []
        rate = []
        primary = []
        rotate = []
        reflect = []
        # read output of xrandr for gathering current video mode
        xrandr = os.popen('xrandr').read()
        # get display names, resolution, and position
        temp = re.findall('\n([^ ]+?\d+) connected.+?([\dx]+)[\+\-]([\d\+\-]+)', xrandr)
        # convert tuples to list
        for item in temp: current_mode.append(list(item))
        # get refresh rate, rotation, reflection, and primary flag of each display
        for i in range(len(current_mode)):
            data = re.findall(re.escape(current_mode[i][0]) + ' connected.+?' + re.escape(current_mode[i][1]) + '[^\(]+(left|normal|right|inverted)', xrandr)
            try:
                rotate.append(data[0])
            except IndexError:
                rotate.append(None)
            data = re.findall(re.escape(current_mode[i][0]) + ' connected.+' + re.escape(current_mode[i][1]) +  '[^\(]+?([XY] axis|X and Y axis)', xrandr)
            try:
                reflect.append(data[0])
            except IndexError:
                reflect.append(None)
            data = re.findall(re.escape(current_mode[i][0]) + '.+? (primary)', xrandr)
            try:
                primary.append(data[0])
            except IndexError:
                primary.append(None)
            data = re.findall('(?s)' + re.escape(current_mode[i][0]) + ' .+?([\d\.]+)\*', xrandr, re.MULTILINE)
            try:
                rate.append(data[0])
            except IndexError:
                rate.append(None)
            # fix instances where the x and y resolutions are flipped,
            if rotate[i] in ['left', 'right']:
                xy = current_mode[i][1].split('x')
                current_mode[i][1] = xy[1] + 'x' + xy[0]
        # build a xrandr command capable of restore the current video mode
        cmd = 'xrandr'
        for i in range(len(current_mode)):
            cmd += ' --output "' + current_mode[i][0] + '"'
            cmd += ' --mode ' + current_mode[i][1]
            cmd += ' --rate ' + rate[i]
            cmd += ' --pos ' + current_mode[i][2].replace('+', 'x').replace('-', 'x')
            if rotate[i] != None: cmd += ' --rotate ' + rotate[i]
            if reflect[i] != None: cmd += ' --reflect ' + reflect[i]
            if primary[i] == 'primary': cmd += ' --primary'

        self.cmd_entry.set_text('{} && sleep 1'.format(cmd))

    def start_toggled(self, check, i):
        self.commands[i][0] = check.get_active()
        self.confirm_cleanup()

    def restart_toggled(self, check, i):
        self.commands[i][1] = check.get_active()
        self.confirm_cleanup()

    def shut_toggled(self, check, i):
        self.commands[i][2] = check.get_active()
        self.confirm_cleanup()

    def add_command(self, widget):
        if self.remove_button.get_label() == "Remove":
            self.commands.append([False, False, False, ''])
            self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
            vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
            self.hbox.add(vbox)
            self.listbox.add(self.hbox)
            cmd_label = Gtk.Label(label='', xalign=0)
            start_check = Gtk.CheckButton()
            restart_check = Gtk.CheckButton()
            shutdown_check = Gtk.CheckButton()
            self.hbox.pack_start(start_check, False, False, 0)
            self.hbox.pack_start(restart_check, False, False, 45)
            self.hbox.pack_start(shutdown_check, False, False, 0)
            self.hbox.pack_start(cmd_label, False, False, 35)
            self.listbox.show_all()
            self.listbox.select_row(self.listbox.get_row_at_index(len(self.commands)-1))
            self.on_entry_change(self.cmd_entry)
        else:
            self.confirm_cleanup()

    def on_listbox_change(self, listbox, row):
        self.status_message.set_markup('<big>Status:  OK</big>')
        self.confirm_cleanup()
        try:
            i = self.listbox.get_selected_rows()[0].get_index()
            self.cmd_entry.set_text(self.commands[i][3])
        except IndexError:
            print('data already updated')

    def remove_command(self, widget):
        if self.remove_button.get_label() == "Remove":
            self.status_message.set_markup('<big>Delete entry?</big>')
            self.remove_button.set_label("OK")
            self.add_button.set_label("Cancel")
        else:
            i = self.listbox.get_selected_rows()[0].get_index() - 1
            if i >= 0:
                i = self.listbox.get_row_at_index(i)
            else:
                i = self.listbox.get_row_at_index(1)
            del self.commands[self.listbox.get_selected_rows()[0].get_index()]
            self.listbox.remove(self.listbox.get_selected_rows()[0])
            self.listbox.select_row(i)
            self.confirm_cleanup()

    def confirm_cleanup(self):
        self.remove_button.set_label("Remove")
        self.add_button.set_label("Add")
        self.status_message.set_markup('<big>Status:  OK</big>')

    def get_paths(self):
        settings = self.home + '/.config/jwmkit/settings'
        path_ok = False
        if not os.path.isfile(settings):
            return 'warning_settings'
        with open(settings) as f:
            f = f.read()
        f = '\n{}'.format(f)
        try:
            path = re.findall('\nstart.*=(.+)', f)[0]
            if path.startswith('$HOME'):
                path = self.home + path[5:]
                if path.startswith('$HOME'):
                    path = self.home + path[5:]
                path_ok = True
        except IndexError:
            pass
        if not path_ok:
            return 'warning_settings'
        return path

    def on_entry_change(self, widget):
        try:
            i = self.listbox.get_selected_rows()[0].get_index()
        except IndexError:
            self.status_message.set_markup('<big>Nothing Selected!</big>')
            return
        if self.commands[i][3] != self.cmd_entry.get_text():
            self.commands[i][3] = self.cmd_entry.get_text()
            cmd = self.commands[i]
            self.hbox = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
            vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
            self.hbox.add(vbox)
            cmd_label = Gtk.Label(label=cmd[3], xalign=0)
            start_check = Gtk.CheckButton()
            restart_check = Gtk.CheckButton()
            shutdown_check = Gtk.CheckButton()
            if cmd[0] == 1: start_check.set_active(True)
            start_check.connect("toggled", self.start_toggled, i)
            restart_check.connect("toggled", self.restart_toggled, i)
            shutdown_check.connect("toggled", self.shut_toggled, i)
            if cmd[1] == 1: restart_check.set_active(True)
            if cmd[2] == 1: shutdown_check.set_active(True)
            self.hbox.pack_start(start_check, False, False, 0)
            self.hbox.pack_start(restart_check, False, False, 45)
            self.hbox.pack_start(shutdown_check, False, False, 0)
            self.hbox.pack_start(cmd_label, False, False, 35)
            self.listbox.remove(self.listbox.get_selected_rows()[0])
            self.listbox.insert(self.hbox, i)
            self.listbox.select_row(self.listbox.get_row_at_index(i))
            self.listbox.show_all()

    def save_command(self, widget):
        self.status_message.set_markup('<big>Status:  OK</big>')
        self.confirm_cleanup()

        def xml_escape(bad):
            if type(bad) == str:
                bad = re.sub('(&)(?!#\d\d|amp;)', '&amp;', bad)
                return bad.replace('<', '&lt;').replace('>', '&gt;').replace('"', '&quot;').replace("'", '&apos;')

        data = '<JWM>\n'
        for cmd in self.commands:
            check_total = 0
            if cmd[0] is True:
                check_total += 1
                data += '     <StartupCommand>' + xml_escape(cmd[3]) + '</StartupCommand>\n'
            if cmd[1] is True:
                check_total += 1
                data += '     <RestartCommand>' + xml_escape(cmd[3]) + '</RestartCommand>\n'
            if cmd[2] is True:
                check_total += 1
                data += '     <ShutdownCommand>' + xml_escape(cmd[3]) + '</ShutdownCommand>\n'
            if check_total == 0:
                self.status_message.set_markup('<b>Warning :</b> Items without an option\nselected were not be saved')
        data += '</JWM>'
        with open(self.rc_file, "w") as f:
            f.write(data)

    def on_browse(self, button):
        self.status_message.set_markup('<big>Status:  OK</big>')
        self.confirm_cleanup()
        file_dialog = Gtk.FileChooserDialog(title="Select a file", parent=self, action=Gtk.FileChooserAction.OPEN)
        file_dialog.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OPEN, Gtk.ResponseType.OK)
        file_dialog.set_current_folder('/usr/share/applications')
        file_selected = file_dialog.run()
        if file_selected == Gtk.ResponseType.OK:
            file_selected = file_dialog.get_filename()
            if file_selected.lower().endswith('.desktop'):
                file_selected = self.read_desktop(file_selected)
            if file_selected not in [None, '']:
                self.cmd_entry.set_text(file_selected)
                self.on_entry_change(self.cmd_entry)
        file_dialog.destroy()

    def read_desktop(self, file_selected):
        is_terminal = 'false'
        with open(file_selected) as f:
            items = f.read()
            try:
                name_selected = re.findall('Name=(.+)', items)[0]
            except IndexError:
                name_selected = 'Terminal'
            try:
                exec_selected = re.findall('Exec=(.+)', items)[0]
                try:
                    is_terminal = re.findall('Terminal=(.+)', items)[0]
                    if is_terminal.lower() == 'true':
                        exec_selected = self.user_terminal + ' -T "' + name_selected + '" -e "' + exec_selected + '"'
                except IndexError:
                    print('desktop file does not contain "Terminal" entry')
            except IndexError:
                return ''
        return exec_selected

    def read_rc_file(self):
        startsups, starts, restarts, = [], [], []
        try:
            tree = ET.parse(self.rc_file)
            root = tree.getroot()
            for node in root:
                start = node.text
                if node.tag == "StartupCommand":
                    starts.append(node.text)
                    startsups.append([True, False, False, start])
                if node.tag == "RestartCommand":
                    data = node.text
                    if data in starts:
                        startsups[starts.index(data)][1] = True
                    else:
                        starts.append(node.text)
                        startsups.append([False, True, False, data])
                elif node.tag == "ShutdownCommand":
                    data = node.text
                    if data in starts:
                        startsups[starts.index(data)][2] = True
                    else:
                        startsups.append([False, False, True, data])
        except (ET.ParseError, FileNotFoundError):
            # file is missing or corrupt
            # icon paths list will be empty
            self.status_message.set_markup \
                ('<b>Icons config is missing or corrupt</b>\n Save function will create a new file')
            icon_path = []
        return startsups


win = MainWindow()
win.connect("delete-event", Gtk.main_quit)
win.set_position(Gtk.WindowPosition.CENTER)
win.show_all()
Gtk.main()


# TODO
#  Change display button to a Pop-up list of common startups. Check to see if require binaries are installed before
#      adding them to the list. Some of these ( Verse, fortune ) will require code for a YAD window/splash.
#  For example:
#  - polkit-1-gnome
#  - verse of the day
#  - fortune
#  - policykit-1-gnome
#  -  conky
#  -  spacefm --desktop
#  -  pcmanfm --desktop
#  -  connman-gkt
#  -  clipman
