#
# Copyright (C) 2013-2015  Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties 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 this program; if not, write to the
# Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#
# Red Hat Author(s): Vratislav Podzimek <vpodzime@redhat.com>
#
# This module was taken from the sources of the Anaconda installer.
#

"""Module providing thread-safe and mainloop-safe DBus operations."""

import gi
gi.require_version("GLib", "2.0")
gi.require_version("Gio", "2.0")

from gi.repository import GLib, Gio


DEFAULT_DBUS_TIMEOUT = 100 * 1000
DBUS_INTRO_IFACE = "org.freedesktop.DBus.Introspectable"


class SafeDBusError(Exception):
    """Class for exceptions defined in this module."""


class DBusCallError(SafeDBusError):
    """Class for the errors related to calling methods over DBus."""


def get_new_system_connection():
    """Return a new connection to the system bus."""

    return Gio.DBusConnection.new_for_address_sync(
        Gio.dbus_address_get_for_bus_sync(Gio.BusType.SYSTEM, None),
        Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT |
        Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION,
        None, None)


def call_sync(service, obj_path, iface, method, args,
              connection=None, fds=None, timeout=DEFAULT_DBUS_TIMEOUT):
    """
    Safely call a given method on a given object of a given service over DBus
    passing given arguments. If a connection is given, it is used, otherwise a
    new connection is established. Safely means that it is a synchronous,
    thread-safe call not using any main loop.

    :param service: DBus service to use
    :type service: str
    :param obj_path: object path of the object to call method on
    :type obj_path: str
    :param iface: interface to use
    :type iface: str
    :param method: name of the method to call
    :type method: str
    :param args: arguments to pass to the method
    :type args: GVariant
    :param connection: connection to use (if None, a new connection is
                       established)
    :type connection: Gio.DBusConnection
    :param fds: list of file descriptors for the call
    :type: Gio.UnixFDList
    :param timeout: timeout in milliseconds for the call (-1 for default timeout)
    :type timeout: int
    :return: unpacked value returned by the method
    :rtype: tuple with elements that depend on the method
    :raise DBusCallError: if some DBus related error appears

    """

    if not connection:
        try:
            connection = get_new_system_connection()
        except GLib.GError as gerr:
            raise DBusCallError("Unable to connect to system bus: %s" % gerr)

    if connection.is_closed():
        raise DBusCallError("Connection is closed")

    try:
        ret = connection.call_with_unix_fd_list_sync(service, obj_path, iface, method, args,
                                                     None, Gio.DBusCallFlags.NONE,  # pylint: disable=no-member
                                                     timeout, fds, None)
    except GLib.GError as gerr:
        msg = "Failed to call %s method on %s with %s arguments: %s" % \
              (method, obj_path, args, gerr.message)  # pylint: disable=no-member
        raise DBusCallError(msg)

    if ret is None:
        msg = "No return from %s method on %s with %s arguments" % (method, obj_path, args)
        raise DBusCallError(msg)

    return ret[0].unpack()


def check_object_available(service, obj_path, iface=None):
    intro_data = call_sync(service, obj_path, DBUS_INTRO_IFACE, "Introspect", None)
    node_info = Gio.DBusNodeInfo.new_for_xml(intro_data[0])
    if not iface:
        # just check if any interface is available (there are none for
        # non-existing objects)
        return bool(node_info.interfaces)
    else:
        return any(intface.name == iface for intface in node_info.interfaces)
