from copy import deepcopy
from functools import wraps
from gettext import gettext
import traceback
from typing import Iterable
from gi.repository import GdkPixbuf, Gdk, Gio, GLib, Gtk, GObject
from itertools import zip_longest
import os
import shutil
import threading

try:
    from xdg.Config import icon_size
except ImportError:
    icon_size = 48


from . import logger, eprint, APPID, INFO, PACKAGE
from .alternative import AltDB, AltGroup, AltOption, CommandResult
from .appdata import LOGO_PATH, get_data_path
from .description import altname_desc, query_package, describe_cmds
try:
    from .log import setColoredLogger
except ImportError:
    setColoredLogger = None


_ = gettext


GObject.threads_init()


def hide_on_delete(window, *args):
    """
    Warpper for Gtk.Widget.hide_on_delete, but allow superfluous arguments.
    Used for signal callback.
    """
    return Gtk.Widget.hide_on_delete(window)


def reset_dialog(dialog, *args):
    """
    Select cancel button as default when reshow the dialog. Used for signal
    callback.
    """
    btn_cancel = \
        dialog.get_widget_for_response(Gtk.ResponseType.CANCEL) or \
        dialog.get_widget_for_response(Gtk.ResponseType.CLOSE)
    btn_cancel.grab_focus()
    btn_cancel.grab_default()


@Gtk.Template.from_file(get_data_path('glade/file_entry.glade'))
class FileEntry(Gtk.Box):
    __gtype_name__ = 'FileEntry'
    entry = Gtk.Template.Child('entry')

    @Gtk.Template.Callback('open_file')
    def open_file(self, button):
        """Select a file and fill the entry with the path."""
        file_chooser = Gtk.FileChooserDialog(
            title=_('Select File'), action=Gtk.FileChooserAction.OPEN,
            parent=button.get_toplevel(), buttons=(
                Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
                Gtk.STOCK_OPEN, Gtk.ResponseType.OK))
        if file_chooser.run() == Gtk.ResponseType.OK:
            self.entry.set_text(file_chooser.get_filename())
        file_chooser.destroy()

    def set_text(self, text):
        return self.entry.set_text(text)

    def get_text(self):
        return self.entry.get_text()

    def set_icon_from_icon_name(self, icon_pos, icon_name):
        return self.entry.set_icon_from_icon_name(icon_pos, icon_name)

    def connect(self, detailed_signal, handler):
        return self.entry.connect(
            detailed_signal, lambda widget, *args: handler(self, *args))


@Gtk.Template.from_file(get_data_path('glade/edit_dialog.glade'))
class EditDialog(Gtk.Dialog):
    __gtype_name__ = 'EditDialog'
    slave_it = None

    requires = Gtk.Template.Child('requires')
    slaves_tv = Gtk.Template.Child('slaves_tv')
    slave_fields = Gtk.Template.Child('slave_fields')
    slaves_edit = Gtk.Template.Child('slaves_edit')
    new_group_warning = Gtk.Template.Child('new_group_warning')

    def _init_edit_dialog(self):
        for i, (field_name, label_name, widget_class) in \
                enumerate(self.REQUIRES):
            widget = widget_class()
            widget.set_hexpand(True)
            setattr(self, field_name.lower(), widget)
            self.requires.attach(Gtk.Label(label=label_name), 0, i, 1, 1)
            self.requires.attach(widget, 1, i, 1, 1)
        self.requires.show_all()

        self.slaves_entries = []
        for i, (column_name, label_name, widget_class) in \
                enumerate(self.SLAVES):
            # slaves_tv
            column = Gtk.TreeViewColumn(
                label_name, Gtk.CellRendererText(),
                text=2 * i, background=2 * i + 1)
            column.set_resizable(True)
            self.slaves_tv.append_column(column)
            # slave_fields
            widget = widget_class()
            widget.i_column = i
            widget.connect('changed', self.on_slave_fields_changed)
            self.slaves_entries.append(widget)
            box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL)
            box.pack_start(Gtk.Label(label=label_name), False, False, 0)
            box.pack_start(widget, True, True, 0)
            self.slave_fields.pack_start(box, False, False, 0)
        self.slave_fields.show_all()
        self.slaves_model = Gtk.ListStore(*[str] * len(self.SLAVES) * 2)
        self.slaves_tv.set_model(self.slaves_model)

    def __new__(cls, *args, **kwargs):
        if cls != EditDialog:
            self = EditDialog(**kwargs)
            self.__class__ = cls
            EditDialog._init_edit_dialog(self)
            return self
        else:
            return super().__new__(cls, *args, **kwargs)

    @Gtk.Template.Callback('add_row')
    def add_row(self, button):
        new_row: list[str | None] = [None] * len(self.SLAVES) * 2
        new_row[0] = '<new>'
        self.slaves_model.append(new_row)

    @Gtk.Template.Callback('remove_row')
    def remove_row(self, button):
        model, it = self.slaves_tv.get_selection().get_selected()
        del model[it]
        for widget in self.slaves_entries:
            widget.set_text('')

    @Gtk.Template.Callback('on_click_slave')
    def on_click_slave(self, widget):
        model, it = widget.get_selected()
        self.slave_it = it
        if it is None:
            return
        for widget, text in zip_longest(
                self.slaves_entries, model[it][::2] if it else ()):
            widget.set_text(text or '')

    def on_slave_fields_changed(self, widget):
        if self.slave_it is None:
            return
        self.slaves_model[self.slave_it][2 * widget.i_column] = \
            widget.get_text()

    @Gtk.Template.Callback('on_response')
    def on_response(self, window, response_id):
        # only bind to cancel button
        # dialog response can not be cancelled, thus not suitable for validation
        if response_id == Gtk.ResponseType.CANCEL:
            # do not emit delete-event
            self.destroy()

    @Gtk.Template.Callback('close')
    def close(self, *args):
        return super().close()

    @Gtk.Template.Callback('on_delete_event')
    def on_delete_event(self, window, event):
        validated = True
        empty = True
        for (field_name, _, widget_class) in self.REQUIRES:
            widget = getattr(self, field_name.lower())
            if not widget.get_text():
                self.requires_set_vaild(widget, False)
                validated = False
            else:
                self.requires_set_vaild(widget, True)
                empty = False
        for row in self.slaves_model:
            if not row[0]:
                row[1] = 'red'
                validated = False
            elif row[1]:
                row[1] = None
                empty = False
        if self.is_creating and empty:
            return
        if not validated:
            return True
        return self.on_close(window, event)

    @staticmethod
    def requires_set_vaild(widget, status):
        widget.set_icon_from_icon_name(
            Gtk.EntryIconPosition.SECONDARY, None if status else 'dialog-error')


class GroupDialog(EditDialog):
    group: AltGroup | None

    REQUIRES = (
        ('Name', _('Name'), Gtk.Entry),
        ('Link', _('Link'), FileEntry),
    )
    SLAVES = REQUIRES

    def __init__(self, group: AltGroup | None = None, *args, **kwargs):
        self.group = group

        if self.group is None:
            self.set_title(_('Add group'))
            self.new_group_warning.show()
        else:
            self.set_title(_('Edit group - {}').format(self.group.name))
            self.name.set_text(self.group.name)
            self.link.set_text(self.group.link)
            for slave_name in self.group[1:]:
                self.slaves_model.append(
                    (slave_name, None, self.group[slave_name], None))

    @property
    def is_creating(self):
        return self.group is None

    def on_close(self, window, event):
        main_instance = self.get_transient_for().main_instance
        name = self.name.get_text()
        reload_groups_p = False
        slaves_diff = {}

        if self.group is None:
            self.group = AltGroup(name, create=True)
            main_instance.alt_db.add(self.group)
            reload_groups_p = True
        elif name != self.group.name:
            slaves_diff[self.group.name] = name
            main_instance.alt_db.move(self.group.name, name)
            reload_groups_p = True
        self.group.link = self.link.get_text()

        slaves = set()
        for i, row in enumerate(self.slaves_model):
            new_slave = row[0]
            if self.group[i + 1] != new_slave:
                slaves_diff[self.group[i + 1]] = new_slave
            self.group[new_slave] = row[2]
            slaves.add(new_slave)
        for old_slave in self.group[1:]:
            if old_slave not in slaves:
                del self.group[old_slave]
        for option in self.group.options:
            option.update({
                slaves_diff[k]: v for k, v in option.items() if k in slaves_diff
            })

        if reload_groups_p:
            main_instance.load_groups()
        else:
            main_instance.load_options()
        main_instance.on_change()


class OptionDialog(EditDialog):
    group: AltGroup
    option: AltOption | None

    REQUIRES = (
        ('Path', _('Path'), FileEntry),
        ('Priority', _('Priority'), Gtk.SpinButton),
    )
    SLAVES = (
        ('Name', _('Name'), Gtk.Entry),
        ('Path', _('Path'), FileEntry),
    )

    def __init__(
            self, group: AltGroup, option: AltOption | None = None,
            *args, **kwargs):
        self.group = group
        self.option = option

        self.slaves_edit.hide()
        self.priority.set_adjustment(
            Gtk.Adjustment(0, -(1 << 31), 1 << 31, 1, 10, 0))
        if self.option is None:
            self.set_title(_('Add option'))
            self.priority.set_value(0)
            for slave_name in self.group[1:]:
                self.slaves_model.append([slave_name, None, None, None])
        else:
            self.set_title(
                _('Edit option - {}').format(self.option[self.group.name]))
            self.path.set_text(self.option[self.group.name])
            self.priority.set_value(self.option.priority)
            for slave_name in self.group[1:]:
                self.slaves_model.append([
                    slave_name, None, self.option[slave_name], None])

    @property
    def is_creating(self):
        return self.option is None

    def on_close(self, window, event):
        main_instance = self.get_transient_for().main_instance
        if self.option is None:
            self.option = AltOption()
            self.group.options.append(self.option)
        for row in self.slaves_model:
            if row[2]:
                self.option[row[0]] = row[2]
            elif row[0] in self.option:
                del self.option[row[0]]
        path = self.path.get_text()
        self.option[self.group.name] = path
        self.option.priority = self.priority.get_value_as_int()
        main_instance.load_options()
        main_instance.on_change()


def advanced(f):
    @wraps(f)
    def wrapper(self, *args, **kwargs):
        if not self.edit_warning_show_check.get_active() or \
                self.edit_warning.run() != Gtk.ResponseType.CANCEL:
            return f(self, *args, **kwargs)
    return wrapper


icontheme = Gtk.IconTheme.get_default()
STATUS_ICONS = []
for icon_name in ('dialog-ok', 'dialog-error'):
    try:
        icon = icontheme.load_icon(icon_name, 8, 0)
    except GLib.Error:
        icon = None
    STATUS_ICONS.append(icon)
STATUS_ICONS.append(None)
del icon
del icontheme


class MainWindow:
    altdb_paths: dict[str, str]

    db: AltDB
    db_orig: AltDB
    group: AltGroup | None

    debug: int
    use_polkit: bool
    delay_mode: bool
    group_cleaning: bool
    group_filter_pattern: str

    _query_package: bool

    def __init__(
            self, app: 'GAlternativesApp',
            paths: Iterable[tuple[str, str]] | None = None,
            group: str | None = None):
        """Load alternative database and fetch objects from the builder."""
        self.altdb_paths = dict(paths)
        self.group = AltGroup(group, create=True) if group else None
        self.debug = app.debug
        self.use_polkit = app.use_polkit if app else \
            os.getuid() and bool(shutil.which('pkexec'))
        self.delay_mode = False
        self.group_cleaning = False
        self.group_filter_pattern = ''
        self._query_package = False

        # glade XML
        self.builder = Gtk.Builder.new_from_file(
            get_data_path('glade/galternatives.glade'))
        for widget_id in {
            # main window
            'main_window', 'main_accelgroup',
            'pending_box', 'groups_tv',
            'group_find_btn', 'group_find_entry', 'groups_tv_filter',
            'group_icon', 'alternative_label',
            'link_label', 'description_label',
            'status_switch', 'options_tv', 'options_column_package',
            'options_menu',
            # dialogs and messages
            'preferences_dialog',
            'edit_warning', 'edit_warning_show_check',
            'confirm_closing', 'commit_failed', 'results_tv'
        }:
            setattr(self, widget_id, self.builder.get_object(widget_id))
        self.main_window.set_application(app)
        self.main_window.set_icon_from_file(LOGO_PATH)
        self.main_window.main_instance = self

        # save placeholder text strings
        self.empty_group = \
            AltGroup(self.alternative_label.get_text(), create=True)
        self.empty_group[self.empty_group.name] = self.link_label.get_text()
        self.empty_group.description = self.description_label.get_text()
        self.empty_group._current = False

        # signals
        self.builder.connect_signals(self)
        # actions
        self.options_menu.insert_action_group('win', self.main_window)
        self.actions = {}
        for name, activate in {
            ('preferences', lambda *args: self.preferences_dialog.show()),
            ('quit', self.on_quit),
            ('group.add', self.add_group),
            ('group.edit', self.edit_group),
            ('group.remove', self.remove_group),
            ('group.find', self.find_group),
            ('option.add', self.add_option),
            ('option.edit', self.edit_option),
            ('option.remove', self.remove_option),
            ('change.save', self.on_save),
            ('change.reload', self.load_db),
        }:
            action = Gio.SimpleAction(name=name)
            action.connect('activate', activate)
            self.main_window.add_action(action)
            self.actions[name] = action
        for name in {'delay_mode', 'query_package', 'use_polkit'}:
            action = Gio.SimpleAction.new_stateful(
                name, None, GLib.Variant('b', getattr(self, name)))

            def on_action_toggle_func(name):
                def on_action_toggle(action, value):
                    action.set_state(value)
                    setattr(self, name, value.get_boolean())

                return on_action_toggle

            action.connect('change-state', on_action_toggle_func(name))
            self.main_window.add_action(action)
        # workaround https://stackoverflow.com/questions/19657017
        self.builder.get_object('group_add_btn').get_child().add_accelerator(
            'clicked', self.main_accelgroup,
            *Gtk.accelerator_parse('Insert'), Gtk.AccelFlags.VISIBLE)
        self.builder.get_object('group_edit_btn').get_child().add_accelerator(
            'clicked', self.main_accelgroup,
            *Gtk.accelerator_parse('Return'), Gtk.AccelFlags.VISIBLE)
        self.builder.get_object('group_remove_btn').get_child().add_accelerator(
            'clicked', self.main_accelgroup,
            *Gtk.accelerator_parse('Delete'), Gtk.AccelFlags.VISIBLE)
        self.group_find_btn.get_child().add_accelerator(
            'clicked', self.main_accelgroup,
            *Gtk.accelerator_parse('<Control>f'), Gtk.AccelFlags.VISIBLE)
        # model filter
        self.groups_tv_filter.set_visible_func(self.group_filter)

    hide_on_delete = staticmethod(hide_on_delete)
    reset_dialog = staticmethod(reset_dialog)

    def show(self):
        """Show the main window. Pretend itself as Gtk.Window."""
        # Correct the display name.
        # Ref: https://stackoverflow.com/questions/9324163/
        # how-to-set-application-title-in-gnome-shell
        self.main_window.set_wmclass(INFO['program_name'], PACKAGE)
        return self.main_window.show()

    def destroy(self):
        return self.main_window.destroy()

    @property
    def has_unsaved(self):
        """Whether there are unsaved changes"""
        return self.pending_box.get_visible()

    # config actions begin #

    @property
    def query_package(self):
        return self._query_package

    @query_package.setter
    def query_package(self, value):
        if value == self._query_package:
            return value
        self._query_package = value

        self.options_column_package.set_visible(value)
        if value:
            self.load_options_pkgname()
        return value

    def load_config(self, widget=None):
        for option in AltDB.PATH_NAMES:
            self.builder.get_object(option + '_chooser').set_filename(
                getattr(self.db, option))

    def on_preferences_dialog_response(self, widget, response_id):
        self.altdb_paths = {
            option: self.builder.get_object(option + '_chooser').get_filename()
            for option in AltDB.PATH_NAMES
        }
        if any(getattr(self.db, option) != path
               for option, path in self.altdb_paths.items()):
            self.load_db()

    # config actions end #

    # commit actions begin #

    def on_quit(self, *args):
        """Check for unsaved changes before quitting."""
        if self.has_unsaved:
            response_id = self.confirm_closing.run()
            if response_id == Gtk.ResponseType.CANCEL:
                return True
            if response_id == Gtk.ResponseType.NO:
                return self.do_quit()
            if response_id == Gtk.ResponseType.YES:
                self.do_save()
                return self.has_unsaved
        self.do_quit()

    def do_quit(self, *args):
        """Close the main window."""
        self.main_window.destroy()

    def on_save(self, *args):
        self.do_save()

    def do_save(self, diff_cmds: list[list[str]] | None = None, autosave=False):
        """Save changes."""
        if diff_cmds is None:
            diff_cmds = self.db.compare(self.db_orig)
        self.main_window.set_sensitive(False)
        if self.debug > 1:
            logger.debug(
                'submit commands %s%s', diff_cmds,
                ' via polkit' if self.use_polkit else '')
        threading.Thread(target=lambda: GObject.idle_add(
            self.do_save_callback, diff_cmds, autosave, *self.db.commit(
                diff_cmds, 'pkexec' if self.use_polkit else None))).start()

    def do_save_callback(
            self, diff_cmds: list[list[str]], autosave: bool, returncode: int,
            results: list[CommandResult]):
        self.main_window.set_sensitive(True)
        if returncode:
            # failed
            model = self.results_tv.get_model()
            model.clear()
            for cmd, result in zip_longest(
                    describe_cmds(diff_cmds), results, fillvalue=None):
                it = model.append(None, (
                    STATUS_ICONS[result.returncode != 0 if result else 2],
                    cmd[0]))
                for info in cmd[1:]:
                    model.append(it, (
                        None, '    ' + info))
                if result:
                    model.append(it, (
                        None, '  ' + _('Run command: ') + ' '.join(result.cmd)))
                    out = result.out.rstrip()
                    if out:
                        model.append(it, (None, '  ' + out))
                    err = result.err.rstrip()
                    if err:
                        model.append(it, (None, '  ' + err))
            self.commit_failed.show_all()

        if not returncode and not autosave:
            # succeeded and other fields may also be changed, flush GUI
            self.load_db()
        else:
            # succeeded and only `current' changed, do a quick save
            # or failed, keep everything
            self.db_orig = AltDB(**self.altdb_paths)
            self.on_change()

    def save_and_quit(self, *args, **kwargs):
        self.do_save()
        self.do_quit()

    def load_db(self, *args):
        self.db = AltDB(**self.altdb_paths)
        self.db_orig = deepcopy(self.db)
        self.pending_box.hide()
        self.load_groups()

    # commit actions end #

    # detail windows actions begin #
    # TODO: windows need a good way for handling (group destruction)

    @advanced
    def add_group(self, widget, data):
        self.show_group_window(None)

    @advanced
    def edit_group(self, widget, data):
        self.show_group_window(self.group)

    @advanced
    def remove_group(self, widget, data):
        if not self.group:
            logger.warning('no group selected')
            return

        del self.db[self.group.name]
        self.load_groups()
        self.on_change()

    def show_group_window(self, group: AltGroup | None):
        window = GroupDialog(group, transient_for=self.main_window)
        window.present()

    def find_group(self, widget, data):
        if self.group_find_btn.get_active():
            self.group_find_entry.show()
            self.group_find_entry.grab_focus()
        else:
            self.group_find_entry.hide()
            self.group_find_entry.set_text('')

    def on_group_find_entry_changed(self, widget):
        self.group_filter_pattern = widget.get_text()
        self.group_cleaning = True
        self.groups_tv_filter.refilter()
        self.group_cleaning = False
        self.click_group()

    def on_group_find_entry_key_release_event(self, widget, event):
        if event.keyval == Gdk.KEY_Escape:
            self.group_find_btn.set_active(False)
            self.find_group(None, None)

    def group_filter(self, model, iter, data):
        return self.group_filter_pattern in model[iter][0]

    @advanced
    def add_option(self, widget, data):
        if not self.group:
            logger.warning('no group selected')
            return

        self.show_option_window(self.group, None)

    @advanced
    def edit_option(self, widget, data):
        if not self.group:
            logger.warning('no group selected')
            return

        self.show_option_window(self.group, self.option)

    @advanced
    def remove_option(self, widget, data):
        self.load_options()
        self.on_change()

    def show_option_window(self, group: AltGroup, option: AltOption | None):
        window = OptionDialog(group, option, transient_for=self.main_window)
        window.present()

    # detail windows actions end #

    # main window actions begin #

    def load_groups(self):
        # load alternative group into groups_tv
        treeview = self.groups_tv
        selection = treeview.get_selection()
        modelfilter = treeview.get_model()
        model = modelfilter.get_model()

        self.group_cleaning = True
        model.clear()
        if self.group_find_btn.get_active():
            self.group_find_btn.set_active(False)
        self.group_cleaning = False

        next_group = None
        for group_name in sorted(self.db):
            it = model.append((group_name, ))
            if self.group and self.group.name == group_name:
                # clear self.group first to prevent same-tab reloading problem
                self.group = None
                path = model.get_path(it)
                treeview.set_cursor(path)
                # by this time self.group has been changed
                # disable this code block
                next_group = self.group
                self.group = None
        self.group = next_group

        if self.group is None:
            # disable edit/remove buttons
            self.actions['group.edit'].set_enabled(False)
            self.actions['group.remove'].set_enabled(False)
            # clear options_tv
            self.group = self.empty_group
            self.load_options()
            self.description_label.set_text(self.empty_group.description)
            self.group = None

    def click_group(self, widget=None):
        """Load options for selected alternative group into options_tv."""
        if self.group_cleaning:
            return
        if widget is None:
            widget = self.groups_tv.get_selection()
        model, it = widget.get_selected()
        if it is None:
            if self.group:
                for row in model:
                    if row[0] == self.group.name:
                        it = row.iter
                        path = model.get_path(it)
                        self.group_cleaning = True
                        self.groups_tv.set_cursor(path)
                        self.group_cleaning = False
                        break
            return
        group = self.db[model.get_value(it, 0)]
        if group == self.group:
            return

        # enable buttons
        if not self.group:
            self.actions['group.edit'].set_enabled(True)
            self.actions['group.remove'].set_enabled(True)
        # save current group
        self.group = group
        self.load_options()

    def load_options(self):
        if self.group is None:
            return

        # set the name of the alternative to the information area
        name, description, icon = altname_desc(self.group.name)
        self.alternative_label.set_text(name)
        self.link_label.set_text(self.group.link)
        self.description_label.set_text(description)
        self.group_icon.set_from_icon_name(
            icon, self.group_icon.get_icon_name()[1])
        self.status_switch.set_active(self.group.status)

        # set columns
        self.options_tv.get_column(1).set_title(self.group.name)
        for i in range(4, self.options_tv.get_n_columns()):
            self.options_tv.remove_column(self.options_tv.get_column(4))
        for i in range(1, len(self.group)):
            column = Gtk.TreeViewColumn(
                self.group[i], Gtk.CellRendererText(), text=i + 3)
            column.set_resizable(True)
            self.options_tv.append_column(column)

        # load options_liststore
        options_model = Gtk.ListStore(
            bool, int, str, *(str, ) * len(self.group))
        self.options_tv.set_model(options_model)
        for option in self.group.options:
            options_model.append((
                option == self.group.current,
                option.priority,
                None,
            ) + option.paths(self.group))
        if self.query_package:
            self.load_options_pkgname()

    def load_options_pkgname(self):
        for record in self.options_tv.get_model():
            if record[2]:
                break
            record[2] = query_package(record[3])

    def load_options_current(self):
        self.options_tv.get_model().foreach(
            lambda model, path, it: model.set(
                it, 0, self.group.current == self.group.options[path[0]]))

    def change_status(self, widget, gparam):
        """
        Handle click on status_switch.

        When current status is auto, it will block the click action.
        """
        widget.set_sensitive(not widget.get_active())
        if widget.get_active():
            self.select_option()

    def click_option(self, widget, path):
        """Handle click on radio buttons of options."""
        self.select_option(int(path))

    def select_option(self, index=None):
        if index is None:
            if self.group.status:
                return
        else:
            if not self.group.status and \
                    self.group.current == self.group.options[index]:
                return
        self.group.select(index)
        self.load_options_current()
        self.status_switch.set_active(self.group.status)
        self.on_change(not self.delay_mode)

    def on_options_tv_button_press_event(self, treeview, event):
        if event.button == 3 and self.group:
            pthinfo = treeview.get_path_at_pos(int(event.x), int(event.y))
            self.option = pthinfo and self.group.options[pthinfo[0][0]]
            pthinfo = bool(pthinfo)
            self.actions['option.edit'].set_enabled(pthinfo)
            self.actions['option.remove'].set_enabled(pthinfo)
            self.options_menu.popup(None, None, None, None,
                                    event.button, event.time)

    def on_change(self, autosave=False):
        diff_cmds = self.db.compare(self.db_orig)
        if autosave and len(diff_cmds) == 1:
            self.do_save(diff_cmds, autosave=True)
            return
        if diff_cmds:
            self.pending_box.show()
        else:
            self.pending_box.hide()

    # main window actions end #


class AboutDialog(Gtk.AboutDialog):
    """About dialog of the application."""
    __slots__ = ()

    def __init__(self, **kwargs):
        kwargs.update(INFO)
        if 'license_type' in kwargs and isinstance(kwargs['license_type'], str):
            if hasattr(Gtk.License, kwargs['license_type']):
                kwargs['license_type'] = \
                    getattr(Gtk.License, kwargs['license_type'])
            else:
                logger.warning("`license_type' incorrect!")

        super().__init__(
            logo=LOGO_PATH and GdkPixbuf.Pixbuf.new_from_file_at_scale(
                LOGO_PATH, icon_size, -1, True),
            translator_credits=_('translator_credits'),
            **kwargs)

        self.connect('response', hide_on_delete)
        self.connect('delete-event', hide_on_delete)


class GAlternativesApp(Gtk.Application):
    use_polkit: bool
    target_group: str | None
    debug: int
    altdb_paths: dict[str, str]

    _window: MainWindow | None
    _about_dialog: AboutDialog | None

    __slots__ = tuple(__annotations__)

    def __init__(self, *args, **kwargs):
        super().__init__(
            *args, application_id=APPID,
            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, **kwargs)
        self.add_main_option(
            'debug', ord('d'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
            _('Enable debug output'), None)
        self.add_main_option(
            'normal', ord('n'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE,
            _('Do not try to acquire root (as normal user)'), None)
        self.add_main_option(
            'altdir', 0, GLib.OptionFlags.NONE, GLib.OptionArg.FILENAME,
            _('Specify the alternatives directory'), None)
        self.add_main_option(
            'admindir', 0, GLib.OptionFlags.NONE, GLib.OptionArg.FILENAME,
            _('Specify the administrative directory'), None)
        self.add_main_option(
            'log', 0, GLib.OptionFlags.NONE, GLib.OptionArg.FILENAME,
            _('Specify the log file'), None)
        # BUG: Gtk.Application.add_option_group() not working

        self.use_polkit = False
        self.target_group = None
        self.debug = 0
        self.altdb_paths = {}

        self._window = None
        self._about_dialog = None

    def do_handle_local_options(self, options: GLib.VariantDict):
        if not self.debug:
            self.debug = int(options.contains('debug'))
        if setColoredLogger:
            setColoredLogger(logger, self.debug)
            logger.debug(_('Testing galternatives...'))

        # GtkBuilder will error out if the version requirements are not met.
        # No need to check it again.

        if os.getuid():
            # not root
            if options.contains('normal'):
                logger.warning(_(
                    'No root privileges detected, but continuing anyway'))
            elif shutil.which('pkexec'):
                self.use_polkit = True
            else:
                dialog = Gtk.MessageDialog(
                    None, Gtk.DialogFlags.DESTROY_WITH_PARENT,
                    Gtk.MessageType.WARNING, Gtk.ButtonsType.OK_CANCEL,
                    _('<b><tt>pkexec</tt> required for privileged '
                      'operations.</b>'),
                    use_markup=True,
                    secondary_text=_(
                        'The program needs pkexec to perform privileged '
                        'alternatives system modifications under normal user. '
                        'Unless you have modified your system to explicitly '
                        'allow your normal user to do so, GAlternatives will '
                        'not work.'))
                if dialog.run() != Gtk.ResponseType.OK:
                    return 1
                dialog.destroy()

        for option in AltDB.PATH_NAMES:
            value = options.lookup_value(option)
            if value:
                self.altdb_paths[option] = bytes(value).rstrip(b'\0').decode()

        return -1

    def do_local_command_line(self, arguments: list[str]):
        self.debug = 0
        for arg in arguments:
            if arg.startswith('-dd'):
                self.debug = 2
                break
        return Gtk.Application.do_local_command_line(self, arguments)

    def do_command_line(self, command_line: Gio.ApplicationCommandLine):
        args = command_line.get_arguments()
        if len(args) > 2:
            eprint(_('Error: Specifying more than one group is not allowed'))
            return 2

        self._window = MainWindow(self, self.altdb_paths, self.target_group)
        try:
            self._window.load_db()
        except:
            print(traceback.format_exc())
            self._window.destroy()
            return 1

        if len(args) > 1:
            self.target_group = args[1]
            if self.target_group not in self._window.db:
                eprint(_('Error: No such group "{}"').format(self.target_group))
                self._window.destroy()
                return 1

        self._about_dialog = AboutDialog(
            transient_for=self._window.main_window)
        self.activate()
        return 0

    def do_startup(self):
        Gtk.Application.do_startup(self)
        self.set_app_menu(
            Gtk.Builder.new_from_file(get_data_path('glade/menubar.ui'))
            .get_object('menu'))

        # Cannot use add_action_entries()
        # see https://bugzilla.gnome.org/show_bug.cgi?id=678655
        for name, activate in {
            'about': self.on_about,
        }.items():
            action = Gio.SimpleAction(name=name)
            action.connect('activate', activate)
            self.add_action(action)

    def do_activate(self):
        if self.debug > 1:
            self._window.edit_warning_show_check.set_active(False)
        self._window.show()

    def on_about(self, action, param):
        self._about_dialog.present()
