#!/usr/bin/python3
import argparse
import gi
import os
import re
import shlex
import setproctitle
import subprocess
import sys
import xapp.threading as xt
import xapp.util
import xapp.widgets
gi.require_version("Gtk", "3.0")
gi.require_version('GtkSource', '3.0')
gi.require_version('XApp', '1.0')
from gi.repository import Gtk, Gdk, GtkSource, Gio, XApp
from page_kernels import KernelsWidget
from page_users import UsersWidget

setproctitle.setproctitle("mintsysadm")

_ = xapp.util.l10n("mintsysadm")

GRUB_FILE = "/etc/default/grub.d/98_mintsysadm.cfg"
ENVIRONMENT_FILE = "/etc/X11/Xsession.d/90_mintsysadm"
ENVIRONMENT_REFERENCE_RE = re.compile(r"\$(?:[A-Za-z_][A-Za-z0-9_]*|\{[A-Za-z_][A-Za-z0-9_]*\})")


def quote_environment_value(value):
    """Quote a value while preserving simple environment variable references."""
    parts = []
    position = 0

    for match in ENVIRONMENT_REFERENCE_RE.finditer(value):
        if match.start() > position:
            parts.append(shlex.quote(value[position:match.start()]))

        reference = match.group(0)
        if reference.startswith("${"):
            name = reference[2:-1]
        else:
            name = reference[1:]
        parts.append('"${%s}"' % name)
        position = match.end()

    if position < len(value):
        parts.append(shlex.quote(value[position:]))

    return "".join(parts) if parts else shlex.quote(value)

class MyApplication(Gtk.Application):
    # Main initialization routine
    def __init__(self, application_id, flags):
        Gtk.Application.__init__(self, application_id=application_id, flags=flags)
        self.connect("activate", self.activate)
        self.connect("command-line", self.on_command_line)
        self.app_window = None
        self.page = "users"

    def on_command_line(self, app, command_line):
        # Parse fresh args each time
        parser = argparse.ArgumentParser(add_help=False)
        group = parser.add_mutually_exclusive_group()
        group.add_argument("--boot", action="store_const", dest="page", const="boot")
        group.add_argument("--environment", action="store_const", dest="page", const="environment")
        group.add_argument("--kernels", action="store_const", dest="page", const="kernels")
        group.add_argument("--users", action="store_const", dest="page", const="users")
        argv = command_line.get_arguments()[1:]
        args, _ = parser.parse_known_args(argv)
        if args.page is None:
            args.page = "users"
        self.page = args.page
        self.activate(app)
        return 0

    def activate(self, application):
        if self.app_window is None:
            self.app_window = MintSysadmWindow(self)
            self.add_window(self.app_window.window)
        window = self.app_window.window
        window = self.get_windows()[0]
        window.present()
        window.show_all()
        self.app_window.show_page(self.page)

class MintSysadmWindow():

    def __init__(self, application):

        self.application = application

        gladefile = "/usr/share/mintsysadm/mintsysadm.ui"
        self.builder = Gtk.Builder()
        self.builder.set_translation_domain("mintsysadm")
        self.builder.add_from_file(gladefile)
        self.window = self.builder.get_object("main_window")
        self.window.set_title(_("System Administration"))
        self.window.set_icon_name("mintsysadm")


        self.stack = self.builder.get_object("main_stack")
        self.kernels_widget = KernelsWidget(
            self.window,
            self.builder,
        )
        self.builder.get_object("grub_switch").connect("notify::active", self.grub_switch_toggled)

        # Fill in the boot page
        cmdline_args = subprocess.check_output("LANG=C cat /proc/cmdline", shell=True).decode("utf-8", errors='replace').split()
        args = []
        for arg in cmdline_args:
            if arg in ["ro"]:
                continue
            if arg.lower().startswith("boot_image=") or arg.lower().startswith("root="):
                continue
            args.append(arg)
        self.builder.get_object("label_cmdline").set_label(" ".join(args))

        self.boot_args_editor = xapp.widgets.ListEditor()
        self.boot_args_editor.set_allow_duplicates(False)
        self.boot_args_editor.set_allow_add(True, _("Add a boot argument"), _("If the argument is not recognized by the kernel it will be ignored."))
        self.boot_args_editor.set_allow_edit(True)
        self.boot_args_editor.set_allow_remove(True)
        self.boot_args_editor.set_sort_function(False) # Don't sort the list
        self.boot_args_editor.set_allow_ordering(True)
        self.boot_args_editor.set_validation_function(self.validate_boot_argument)
        self.builder.get_object("arguments_editor").add(self.boot_args_editor)
        self.get_boot_config()
        # self.load_boot()
        self.builder.get_object("button_boot_save").connect("clicked", self.save_boot_config)
        self.builder.get_object("grub_button_close").connect("clicked", self.close_grub_dialog)

        # Fill in the environment variables page
        self.environment_editor = xapp.widgets.ListEditor()
        self.environment_editor.set_allow_duplicates(False)
        self.environment_editor.set_allow_add(True, _("Add an environment variable"), _("Enter the variable in NAME=VALUE format."))
        self.environment_editor.set_allow_edit(True)
        self.environment_editor.set_allow_remove(True)
        self.environment_editor.set_sort_function(None)
        self.environment_editor.set_allow_ordering(False)
        self.environment_editor.set_validation_function(self.validate_environment_variable)
        self.builder.get_object("environment_editor").add(self.environment_editor)
        self.load_environment_variables()
        self.environment_editor.connect("list-changed", self.save_environment_variables)

        accel_group = Gtk.AccelGroup()
        self.window.add_accel_group(accel_group)

        # Hide users page in desktop environments that have their own user management
        desktop_env = os.environ.get('XDG_CURRENT_DESKTOP', '').lower()
        if desktop_env not in ['gnome', 'kde', 'xfce', 'mate']:
            # Fill in the users page
            self.users_widget = UsersWidget(self.window)
            self.builder.get_object("page_users").add(self.users_widget)
        else:
            page_users = self.builder.get_object("page_users")
            self.stack.remove(page_users)

        # Menubar
        menu = self.builder.get_object("main_menu")

        item = Gtk.ImageMenuItem(label=_("Quit"))
        image = Gtk.Image.new_from_icon_name("xsi-exit-symbolic", Gtk.IconSize.MENU)
        item.set_image(image)
        item.connect('activate', self.on_menu_quit)
        key, mod = Gtk.accelerator_parse("<Control>Q")
        item.add_accelerator("activate", accel_group, key, mod, Gtk.AccelFlags.VISIBLE)
        key, mod = Gtk.accelerator_parse("<Control>W")
        item.add_accelerator("activate", accel_group, key, mod, Gtk.AccelFlags.VISIBLE)
        menu.append(item)

        item = Gtk.ImageMenuItem()
        item.set_image(Gtk.Image.new_from_icon_name("xsi-help-about-symbolic", Gtk.IconSize.MENU))
        item.set_label(_("About"))
        item.connect("activate", self.open_about)
        menu.append(item)

        menu.show_all()

    def show_page(self, page_name):
        page_name = f"page_{page_name}"
        self.stack.set_visible_child_name(page_name)

    def grub_switch_toggled(self, switch, gparam):
        # Disable "wait indefinitely" if the menu is hidden.
        if switch.get_active():
            self.builder.get_object("grub_timeout_spinner").get_adjustment().set_lower(-1.0)
        else:
            self.builder.get_object("grub_timeout_spinner").get_adjustment().set_lower(0.0)
        self.builder.get_object("grub_timeout_spinner").update()

    def get_boot_config(self):
        # Read the boot arguments and "remember last" setting from our grub config file
        # We don't want to expose all the settings in system grub conf here because
        # we can't easily revert them in our file without knowing their value ranges and we only want to let
        # the user change the list of custom boot args, not the ones set by packages.
        # GRUB_DEFAULT and GRUB_SAVEDEFAULT are both required for the 'remember last' option
        # but since we're making this file we can assume if one is here the other is.
        savedefault_present = False
        boot_args = []
        if os.path.exists(GRUB_FILE):
            with open(GRUB_FILE, "r", errors="replace") as grub_file:
                for line in grub_file:
                    line = line.strip()
                    if line.startswith("GRUB_CMDLINE_LINUX_DEFAULT="):
                        match = re.search(r'GRUB_CMDLINE_LINUX_DEFAULT="\$GRUB_CMDLINE_LINUX_DEFAULT\s*([^"]*)"', line)
                        if match:
                            boot_args = match.group(1).strip().split()
                    elif line.startswith("GRUB_SAVEDEFAULT="):
                        savedefault_present = True

        self.builder.get_object("grub_remember_last_switch").set_active(savedefault_present)
        self.boot_args_editor.set_strings(boot_args)

        # Read menu style and timeout from actual grub config (including our own file)
        # because these can be changed directly during installation
        menu_visible = False
        menu_timeout = 0
        grub_files = ["/etc/default/grub"]
        grub_d = "/etc/default/grub.d"
        if os.path.isdir(grub_d):
            for fname in sorted(os.listdir(grub_d)):
                grub_files.append(os.path.join(grub_d, fname))
        for path in grub_files:
            with open(path, "r", errors="replace") as grub_file:
                for line in grub_file:
                    line = line.strip()
                    if line.startswith("#"):
                        continue
                    if line.startswith("GRUB_TIMEOUT="):
                        try:
                            menu_timeout = float(line.split("=")[-1])
                        except ValueError:
                            pass
                    elif line.startswith("GRUB_TIMEOUT_STYLE=menu"):
                        menu_visible = True
                    elif line.startswith("GRUB_TIMEOUT_STYLE="):
                        menu_visible = False

        self.builder.get_object("grub_switch").set_active(menu_visible)
        self.builder.get_object("grub_timeout_spinner").set_value(menu_timeout)

    def save_boot_config(self, button):
        menu = "hidden"
        if self.builder.get_object("grub_switch").get_active():
            menu = "menu"

        menu_timeout = self.builder.get_object("grub_timeout_spinner").get_value_as_int()
        boot_args = " ".join(self.boot_args_editor.get_strings())

        grub_text = f"""# Do not edit this file. It is generated by mintsysadm.
GRUB_TIMEOUT_STYLE={menu}
GRUB_TIMEOUT={menu_timeout}
GRUB_CMDLINE_LINUX_DEFAULT="$GRUB_CMDLINE_LINUX_DEFAULT {boot_args}"
"""
        if self.builder.get_object("grub_remember_last_switch").get_active():
            grub_text += "GRUB_DEFAULT=saved\nGRUB_SAVEDEFAULT=true\n"

        with open(GRUB_FILE, "w") as grub_file:
            print(grub_text, file=grub_file)

        buf = self.builder.get_object("textview_grub_output").get_buffer()
        buf.set_text(_("Running update-grub...\n\n"))
        self.builder.get_object("grub_result_label").set_text(_("Processing..."))
        self.update_grub()
        self.grub_dialog = self.builder.get_object("grub_dialog")
        self.grub_dialog.show()
        self.grub_dialog.run()
        self.grub_dialog.hide()

    def load_environment_variables(self):
        variables = []

        if os.path.exists(ENVIRONMENT_FILE):
            with open(ENVIRONMENT_FILE, "r", errors="replace") as environment_file:
                for line in environment_file:
                    line = line.strip()
                    if not line or line.startswith("#"):
                        continue
                    try:
                        fields = shlex.split(line)
                    except ValueError:
                        continue
                    if len(fields) == 2 and fields[0] == "export" and "=" in fields[1]:
                        variables.append(fields[1])

        self.environment_editor.set_strings(variables)

    def save_environment_variables(self, editor, variables):
        if not variables:
            if os.path.exists(ENVIRONMENT_FILE):
                os.remove(ENVIRONMENT_FILE)
            return

        with open(ENVIRONMENT_FILE, "w") as environment_file:
            print("# Do not edit this file. It is generated by mintsysadm.", file=environment_file)
            for variable in variables:
                name, value = variable.split("=", 1)
                print("export %s=%s" % (name, quote_environment_value(value)), file=environment_file)

    def close_grub_dialog(self, button):
        self.grub_dialog.hide()

    @xt.run_async
    def update_grub(self):
        proc = subprocess.Popen(["/usr/sbin/update-grub"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)
        for line in proc.stdout:
            self.update_grub_output(line)
        proc.wait()
        rc = proc.returncode
        if rc == 0:
            self.update_grub_success(True)
        else:
            self.update_grub_success(False)

    @xt.run_idle
    def update_grub_success(self, success):
        label = self.builder.get_object("grub_result_label")
        if success:
            label.set_markup("<span foreground='green'>%s</span>" % _("Success"))
        else:
            label.set_markup("<span foreground='red'>%s</span>" % _("Error"))

    @xt.run_idle
    def update_grub_output(self, line):
        buffer = self.builder.get_object("textview_grub_output").get_buffer()
        end_iter = buffer.get_end_iter()
        buffer.insert(end_iter, line)
        textview = self.builder.get_object("textview_grub_output")
        textview.scroll_to_iter(end_iter, 0.0, False, 0.0, 1.0)

    def validate_boot_argument(self, text):
        if " " in text:
            return _("Boot arguments cannot include space characters.")
        # None means no error
        return None

    def validate_environment_variable(self, text):
        if not re.match(r"^[A-Za-z_][A-Za-z0-9_]*=", text):
            return _("Environment variables must use the NAME=VALUE format.")
        # None means no error
        return None

    def open_about(self, widget):
        dlg = Gtk.AboutDialog()
        dlg.set_transient_for(self.window)
        dlg.set_title(_("About"))
        dlg.set_program_name("mintsysadm")
        dlg.set_comments(_("System Administration"))
        try:
            h = open('/usr/share/common-licenses/GPL', encoding="utf-8")
            s = h.readlines()
            gpl = ""
            for line in s:
                gpl += line
            h.close()
            dlg.set_license(gpl)
        except Exception as e:
            print (e)

        dlg.set_version("__DEB_VERSION__")
        dlg.set_icon_name("mintsysadm")
        dlg.set_logo_icon_name("mintsysadm")
        def close(w, res):
            if res == Gtk.ResponseType.CANCEL or res == Gtk.ResponseType.DELETE_EVENT:
                w.destroy()
        dlg.connect("response", close)
        dlg.show()

    def on_menu_quit(self, widget):
        self.application.quit()

if __name__ == "__main__":
    application = MyApplication("com.linuxmint.sysadm", Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
    application.run(sys.argv)
