#
# Client support for DBus observers
#
# Copyright (C) 2019  Red Hat, Inc.  All rights reserved.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library 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
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301
# USA
#
import logging
from functools import partial

from dasbus.constants import DBUS_FLAG_NONE
from dasbus.signal import Signal

import gi
gi.require_version("Gio", "2.0")
from gi.repository import Gio

log = logging.getLogger(__name__)

__all__ = [
    "DBusObserverError",
    "DBusObserver",
    "GLibMonitoring"
]


class DBusObserverError(Exception):
    """Exception class for the DBus observers."""
    pass


class GLibMonitoring(object):
    """The low-level DBus monitoring library based on GLib."""

    @classmethod
    def watch_name(cls, connection, name, flags=DBUS_FLAG_NONE,
                   name_appeared=None, name_vanished=None):
        """Watch a service name on the DBus connection."""
        name_appeared_closure = None
        name_vanished_closure = None

        if name_appeared:
            name_appeared_closure = partial(
                cls._name_appeared_callback,
                user_data=(name_appeared, ())
            )

        if name_vanished:
            name_vanished_closure = partial(
                cls._name_vanished_callback,
                user_data=(name_vanished, ())
            )

        registration_id = Gio.bus_watch_name_on_connection(
            connection,
            name,
            flags,
            name_appeared_closure,
            name_vanished_closure
        )

        return partial(
            cls._unwatch_name,
            connection,
            registration_id
        )

    @classmethod
    def _name_appeared_callback(cls, connection, name, name_owner, user_data):
        """Callback for watch_name.."""
        # Prepare the user's callback.
        callback, callback_args = user_data

        # Call user's callback.
        callback(name_owner, *callback_args)

    @classmethod
    def _name_vanished_callback(cls, connection, name, user_data):
        """Callback for watch_name."""
        # Prepare the user's callback.
        callback, callback_args = user_data

        # Call user's callback.
        callback(*callback_args)

    @classmethod
    def _unwatch_name(cls, connection, registration_id):
        """Stops watching a service name on the DBus connection."""
        Gio.bus_unwatch_name(registration_id)


class DBusObserver(object):
    """Base class for DBus observers.

    This class is recommended to use only to watch the availability
    of a service on DBus. It doesn't provide any support for accessing
    objects provided by the service.

    Usage:

    .. code-block:: python

        # Create the observer and connect to its signals.
        observer = DBusObserver(SystemBus, "org.freedesktop.NetworkManager")

        def callback1(observer):
            print("Service is available!")

        def callback2(observer):
            print("Service is unavailable!")

        observer.service_available.connect(callback1)
        observer.service_unavailable.connect(callback2)

        # Connect to the service once it is available.
        observer.connect_once_available()

        # Disconnect the observer.
        observer.disconnect()

    """

    def __init__(self, message_bus, service_name, monitoring=GLibMonitoring):
        """Creates a DBus service observer.

        :param message_bus: a message bus
        :param service_name: a DBus name of a service
        """
        self._message_bus = message_bus
        self._service_name = service_name
        self._is_service_available = False

        self._service_available = Signal()
        self._service_unavailable = Signal()

        self._monitoring = monitoring
        self._subscriptions = []

    @property
    def service_name(self):
        """Returns a DBus name."""
        return self._service_name

    @property
    def is_service_available(self):
        """The proxy can be accessed."""
        return self._is_service_available

    @property
    def service_available(self):
        """Signal that emits when the service is available.

        Signal emits this class as an argument. You have to
        call the watch method to activate the signals.
        """
        return self._service_available

    @property
    def service_unavailable(self):
        """Signal that emits when the service is unavailable.

        Signal emits this class as an argument. You have to
        call the watch method to activate the signals.
        """
        return self._service_unavailable

    def connect_once_available(self):
        """Connect to the service once it is available.

        The observer is not connected to the service until it
        emits the service_available signal.
        """
        self._watch()

    def disconnect(self):
        """Disconnect from the service.

        Disconnect from the service if it is connected and stop
        watching its availability.
        """
        self._unwatch()

        if self.is_service_available:
            self._disable_service()

    def _watch(self):
        """Watch the service name on DBus."""
        subscription = self._monitoring.watch_name(
            self._message_bus.connection,
            self.service_name,
            DBUS_FLAG_NONE,
            self._service_name_appeared_callback,
            self._service_name_vanished_callback
        )

        self._subscriptions.append(subscription)

    def _unwatch(self):
        """Stop to watch the service name on DBus."""
        while self._subscriptions:
            callback = self._subscriptions.pop()
            callback()

    def _enable_service(self):
        """Enable the service."""
        self._is_service_available = True
        self._service_available.emit(self)

    def _disable_service(self):
        """Disable the service."""
        self._is_service_available = False
        self._service_unavailable.emit(self)

    def _service_name_appeared_callback(self, *args):
        """Callback for the watch method."""
        if not self.is_service_available:
            self._enable_service()

    def _service_name_vanished_callback(self, *args):
        """Callback for the watch method."""
        if self.is_service_available:
            self._disable_service()

    def __str__(self):
        """Returns a string version of this object."""
        return self._service_name

    def __repr__(self):
        """Returns a string representation."""
        return "{}({})".format(
            self.__class__.__name__,
            self._service_name
        )
