#!/usr/bin/env python3
# -*- Mode: Python; indent-tabs-mode: nil; tab-width: 4; coding: utf-8 -*-
# (c) 2005-2012 Canonical, GPL
# (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de>

from gi.repository import Gtk

from aptdaemon import client, errors
from defer import inline_callbacks
from aptdaemon.gtk3widgets import (
    AptCancelButton,
    AptConfigFileConflictDialog,
    AptDetailsExpander,
    AptMediumRequiredDialog,
    AptProgressBar,
)
from aptdaemon.enums import (
    EXIT_SUCCESS,
    EXIT_FAILED,
    STATUS_COMMITTING,
    STATUS_DOWNLOADING,
    STATUS_DOWNLOADING_REPO,
    STATUS_FINISHED,
    get_error_description_from_enum,
    get_error_string_from_enum,
    get_status_string_from_enum,
)

from UpdateManager.backend import InstallBackend
from UpdateManager.UnitySupport import UnitySupport
from UpdateManager.Dialogs import BuilderDialog

from gettext import gettext as _

import dbus
import os


class UpdateManagerExpander(AptDetailsExpander):
    """An AptDetailsExpander which can be used with multiple terminals.

    The default AptDetailsExpander will shrink/hide when its transaction
    finishes. But here we want to support "chaining" transactions. So we
    override the status-changed handler to only do that when we are
    running the final transaction."""

    def __init__(self, transaction, terminal=True, final=False):
        super().__init__(transaction, terminal)
        self.final = final

    def _on_status_changed(self, trans, status):
        if status in (STATUS_DOWNLOADING, STATUS_DOWNLOADING_REPO):
            self.set_sensitive(True)
            self.download_scrolled.show()
            if self.terminal:
                self.terminal.hide()
        elif status == STATUS_COMMITTING:
            self.download_scrolled.hide()
            if self.terminal:
                self.terminal.show()
                self.set_sensitive(True)
            elif self.final:
                self.set_expanded(False)
                self.set_sensitive(False)
        elif self.final and status == STATUS_FINISHED:
            self.download_scrolled.hide()
            if self.terminal:
                self.terminal.hide()
            self.set_sensitive(False)
            self.set_expanded(False)


class AptStackedProgressBar(Gtk.ProgressBar):
    """A GtkProgressBar which represents the state of many aptdaemon
    transactions.

    aptdaemon provides AptProgressBar for the state of *one* transaction to
    be represented in a progress bar. This widget creates one of those per
    containing transaction, and scales its progress to the given ratio, so
    one progress bar can show the state of many transactions."""

    def __init__(self, unity):
        self.current_max_progress = 0
        self.progress_bars = []
        self.unity = unity

        super().__init__()

    def add_transaction(self, trans, max_progress):
        assert 0 <= max_progress <= 1

        progress = AptProgressBar(trans)
        self.progress_bars.append(progress)
        progress.min = self.current_max_progress
        self.current_max_progress += max_progress

        if self.current_max_progress > 1:
            self.current_max_progress = 1

        progress.max = self.current_max_progress
        progress.connect("notify::fraction", self._update_progress)
        progress.connect("notify::text", self._update_text)

    def _update_progress(self, inner_progress, data):
        delta = inner_progress.max - inner_progress.min
        position_in_delta = delta * inner_progress.get_fraction()
        new_progress = inner_progress.min + position_in_delta
        self.set_fraction(new_progress)
        self.unity.set_progress(new_progress * 100)

    def _update_text(self, inner_progress, data):
        self.set_text(inner_progress.get_text())


class InstallBackendAptdaemon(InstallBackend, BuilderDialog):
    """Makes use of aptdaemon to refresh the cache and to install updates."""

    def __init__(self, window_main, action):
        InstallBackend.__init__(self, window_main, action)
        ui_path = os.path.join(
            window_main.datadir, "gtkbuilder/UpdateProgress.ui"
        )
        BuilderDialog.__init__(
            self, window_main, ui_path, "pane_update_progress"
        )

        self.client = client.AptClient()
        self.unity = UnitySupport()
        self._expanded_size = None
        self.button_cancel = None
        self.trans_failed_msg = None
        self.progressbar = None
        self._active_transaction = None
        self._expander = None

    def close(self):
        if self.button_cancel and self.button_cancel.get_sensitive():
            try:
                self.button_cancel.clicked()
            except Exception:
                # there is not much left to do if the transaction can't be
                # canceled
                pass
            return True
        else:
            return False

    @inline_callbacks
    def update(self):
        """Refresh the package list"""
        try:
            trans = yield self.client.update_cache(defer=True)
            yield self._show_transaction(
                trans, self.ACTION_UPDATE, _("Checking for updates…"), False
            )
        except errors.NotAuthorizedError:
            self._action_done(
                self.ACTION_UPDATE,
                authorized=False,
                success=False,
                error_string=None,
                error_desc=None,
            )
        except Exception:
            self._action_done(
                self.ACTION_UPDATE,
                authorized=True,
                success=False,
                error_string=None,
                error_desc=None,
            )
            raise

    def _show_transaction_error(self, trans, action):
        error_string = get_error_string_from_enum(trans.error.code)
        error_desc = get_error_description_from_enum(trans.error.code)
        if self.trans_failed_msg:
            trans_failed = True
            error_desc = error_desc + "\n" + self.trans_failed_msg
        else:
            trans_failed = None
        self._action_done(
            action,
            authorized=True,
            success=False,
            error_string=error_string,
            error_desc=error_desc,
            trans_failed=trans_failed,
        )

    def _update_next_package(self, trans, status, action):
        if status == EXIT_FAILED:
            self._show_transaction_error(trans, action)
            return
        self._apt_update_oem()

    @inline_callbacks
    def _apt_update_oem(self):
        assert self._oem_packages_to_update
        elem = self._oem_packages_to_update.pop()
        sources_list_file = f"/etc/apt/sources.list.d/{elem}.list"

        try:
            if os.path.exists(sources_list_file):
                trans = yield self.client.update_cache(
                    sources_list=sources_list_file
                )
                if self._oem_packages_to_update:
                    finished_handler = self._update_next_package
                else:
                    finished_handler = self._on_finished

                yield self._show_transaction(
                    trans,
                    self.ACTION_PRE_INSTALL,
                    _("Installing updates…"),
                    True,
                    on_finished_handler=finished_handler,
                    progress_bar_max=0.1 / self._len_oem_updates,
                )
        except errors.NotAuthorizedError:
            self._action_done(
                self.ACTION_PRE_INSTALL,
                authorized=False,
                success=False,
                error_string=None,
                error_desc=None,
            )
        except errors.TransactionFailed as e:
            self.trans_failed_msg = str(e)
        except dbus.DBusException as e:
            if e.get_dbus_name() != "org.freedesktop.DBus.Error.NoReply":
                raise
            self._action_done(
                self.ACTION_PRE_INSTALL,
                authorized=False,
                success=False,
                error_string=None,
                error_desc=None,
            )
        except Exception:
            self._action_done(
                self.ACTION_PRE_INSTALL,
                authorized=True,
                success=False,
                error_string=None,
                error_desc=None,
            )
            raise

    def _update_oem(self, trans, status, action):
        # This is the "finished" handler of installing an oem metapackage
        # What we do now is:
        #  1. update_cache() for the new sources.lists only

        if status == EXIT_FAILED:
            self._show_transaction_error(trans, action)
            return

        (install, _, _, _, _, _) = trans.packages

        self._oem_packages_to_update = set(install)
        self._len_oem_updates = len(install)

        self._apt_update_oem()

    @inline_callbacks
    def commit_oem(self, pkgs_install_oem, pkgs_upgrade_oem):
        self.all_oem_packages = set(pkgs_install_oem) | set(pkgs_upgrade_oem)
        # Nothing to do? Go to the regular updates.
        try:
            if not pkgs_install_oem and not pkgs_upgrade_oem:
                self._action_done(
                    self.ACTION_PRE_INSTALL,
                    authorized=True,
                    success=True,
                    error_string=None,
                    error_desc=None,
                    trans_failed=None,
                )
                return

            if pkgs_install_oem:
                trans = yield self.client.install_packages(
                    pkgs_install_oem, defer=True
                )
                yield self._show_transaction(
                    trans,
                    self.ACTION_PRE_INSTALL,
                    _("Installing updates…"),
                    True,
                    on_finished_handler=self._update_oem,
                    progress_bar_max=0.1,
                )
        except errors.NotAuthorizedError:
            self._action_done(
                self.ACTION_PRE_INSTALL,
                authorized=False,
                success=False,
                error_string=None,
                error_desc=None,
            )
        except errors.TransactionFailed as e:
            self.trans_failed_msg = str(e)
        except dbus.DBusException as e:
            if e.get_dbus_name() != "org.freedesktop.DBus.Error.NoReply":
                raise
            self._action_done(
                self.ACTION_PRE_INSTALL,
                authorized=False,
                success=False,
                error_string=None,
                error_desc=None,
            )
        except Exception:
            self._action_done(
                self.ACTION_PRE_INSTALL,
                authorized=True,
                success=False,
                error_string=None,
                error_desc=None,
            )
            raise

    @inline_callbacks
    def commit(self, pkgs_install, pkgs_upgrade, pkgs_remove):
        """Commit a list of package adds and removes"""
        try:
            reinstall = purge = downgrade = []
            trans = yield self.client.commit_packages(
                pkgs_install,
                reinstall,
                pkgs_remove,
                purge,
                pkgs_upgrade,
                downgrade,
                defer=True,
            )
            yield self._show_transaction(
                trans, self.ACTION_INSTALL, _("Installing updates…"), True
            )
        except errors.NotAuthorizedError:
            self._action_done(
                self.ACTION_INSTALL,
                authorized=False,
                success=False,
                error_string=None,
                error_desc=None,
            )
        except errors.TransactionFailed as e:
            self.trans_failed_msg = str(e)
        except dbus.DBusException as e:
            # print(e, e.get_dbus_name())
            if e.get_dbus_name() != "org.freedesktop.DBus.Error.NoReply":
                raise
            self._action_done(
                self.ACTION_INSTALL,
                authorized=False,
                success=False,
                error_string=None,
                error_desc=None,
            )
        except Exception:
            self._action_done(
                self.ACTION_INSTALL,
                authorized=True,
                success=False,
                error_string=None,
                error_desc=None,
            )
            raise

    def _on_details_changed(self, trans, details, label_details):
        label_details.set_label(details)

    def _on_status_changed(self, trans, status, label_details):
        label_details.set_label(get_status_string_from_enum(status))
        # Also resize the window if we switch from download details to
        # the terminal window
        if (
            status == STATUS_COMMITTING
            and self._expander
            and self._expander.terminal.get_visible()
        ):
            self._resize_to_show_details(self._expander)

    @inline_callbacks
    def _show_transaction(
        self,
        trans,
        action,
        header,
        show_details,
        progress_bar_max=1,
        on_finished_handler=None,
    ):

        if on_finished_handler is None:
            on_finished_handler = self._on_finished

        self.label_header.set_label(header)

        if not self.progressbar:
            self.progressbar = AptStackedProgressBar(self.unity)
            self.progressbar.show()
            self.progressbar_slot.add(self.progressbar)

        self.progressbar.add_transaction(trans, progress_bar_max)

        if self.button_cancel:
            self.button_cancel.set_transaction(trans)
        else:
            self.button_cancel = AptCancelButton(trans)
            self.button_cancel.show()
            self.button_cancel_slot.add(self.button_cancel)

        if action == self.ACTION_UPDATE:
            self.button_cancel.set_label(Gtk.STOCK_STOP)

        if show_details:
            if not self._expander:
                self._expander = UpdateManagerExpander(trans)
                self._expander.set_vexpand(True)
                self._expander.set_hexpand(True)
                self._expander.show_all()
                self._expander.connect("notify::expanded", self._on_expanded)
                self.expander_slot.add(self._expander)
                self.expander_slot.show()
            else:
                self._expander.set_transaction(trans)
            self._expander.final = action != self.ACTION_PRE_INSTALL
        elif self._expander:
            self._expander_slot.hide()

        trans.connect(
            "status-details-changed",
            self._on_details_changed,
            self.label_details,
        )
        trans.connect(
            "status-changed", self._on_status_changed, self.label_details
        )
        trans.connect("finished", on_finished_handler, action)
        trans.connect("medium-required", self._on_medium_required)
        trans.connect("config-file-conflict", self._on_config_file_conflict)

        yield trans.set_debconf_frontend("gnome")
        yield trans.run()

    def _on_expanded(self, expander, param):
        # Make the dialog resizable if the expander is expanded
        # try to restore a previous size
        if not expander.get_expanded():
            self._expanded_size = (
                expander.terminal.get_visible(),
                self.window_main.get_size(),
            )
            self.window_main.end_user_resizable()
        elif self._expanded_size:
            term_visible, (stored_width, stored_height) = self._expanded_size
            # Check if the stored size was for the download details or
            # the terminal widget
            if term_visible != expander.terminal.get_visible():
                # The stored size was for the download details, so we need
                # get a new size for the terminal widget
                self._resize_to_show_details(expander)
            else:
                self.window_main.begin_user_resizable(
                    stored_width, stored_height
                )
        else:
            self._resize_to_show_details(expander)

    def _resize_to_show_details(self, expander):
        """Resize the window to show the expanded details.

        Unfortunately the expander only expands to the preferred size of the
        child widget (e.g showing all 80x24 chars of the Vte terminal) if
        the window is rendered the first time and the terminal is also visible.
        If the expander is expanded afterwards the window won't change its
        size anymore. So we have to do this manually. See LP#840942
        """
        if expander.get_expanded():
            win_width, win_height = self.window_main.get_size()
            exp_width = expander.get_allocation().width
            exp_height = expander.get_allocation().height
            if expander.terminal.get_visible():
                terminal_width = expander.terminal.get_char_width() * 80
                terminal_height = expander.terminal.get_char_height() * 24
                new_width = terminal_width - exp_width + win_width
                new_height = terminal_height - exp_height + win_height
            else:
                new_width = win_width + 100
                new_height = win_height + 200
            self.window_main.begin_user_resizable(new_width, new_height)

    def _on_medium_required(self, transaction, medium, drive):
        dialog = AptMediumRequiredDialog(medium, drive, self.window_main)
        res = dialog.run()
        dialog.hide()
        if res == Gtk.ResponseType.OK:
            transaction.provide_medium(medium)
        else:
            transaction.cancel()

    def _on_config_file_conflict(self, transaction, old, new):
        dialog = AptConfigFileConflictDialog(old, new, self.window_main)
        res = dialog.run()
        dialog.hide()
        if res == Gtk.ResponseType.YES:
            transaction.resolve_config_file_conflict(old, "replace")
        else:
            transaction.resolve_config_file_conflict(old, "keep")

    def _on_finished(self, trans, status, action):
        error_string = None
        error_desc = None
        trans_failed = False
        if status == EXIT_FAILED:
            error_string = get_error_string_from_enum(trans.error.code)
            error_desc = get_error_description_from_enum(trans.error.code)
            if self.trans_failed_msg:
                trans_failed = True
                error_desc = error_desc + "\n" + self.trans_failed_msg
        # tell unity to hide the progress again
        self.unity.set_progress(-1)
        is_success = status == EXIT_SUCCESS
        try:
            self._action_done(
                action,
                authorized=True,
                success=is_success,
                error_string=error_string,
                error_desc=error_desc,
                trans_failed=trans_failed,
            )
        except TypeError:
            # this module used to be be lazily imported and in older code
            # trans_failed= is not accepted
            # TODO: this workaround can be dropped in Ubuntu 20.10
            self._action_done(
                action,
                authorized=True,
                success=is_success,
                error_string=error_string,
                error_desc=error_desc,
            )


if __name__ == "__main__":
    import mock

    options = mock.Mock()
    data_dir = "/usr/share/update-manager"

    from UpdateManager.UpdateManager import UpdateManager

    app = UpdateManager(data_dir, options)

    b = InstallBackendAptdaemon(app, None)
    b.commit(["2vcard"], [], [])
    Gtk.main()
