#!/usr/bin/python
# demo D-Bus server using GDBus
# (C) 2010 Martin Pitt <martin@piware.de>

# manual tests (when greatly increasing the last g_timeout_add):
# gdbus introspect -e -d de.piware.Demo -o / -x
# gdbus call -e -d de.piware.Demo -o / -m de.piware.Demo.Hello.hello Martin
# gdbus call -e -d de.piware.Demo -o / -m org.freedesktop.DBus.Properties.GetAll de.piware.Demo.Hello

from gi.repository import GObject
from gi.repository import Gio
from gi.repository import GLib
import subprocess
import inspect
from IPython.external.path._path import path


class _Gio_DBusAnnotationInfo(object):
    def __init__(self):
        key = None
        value = None
        annotations = []

class _Gio_DBusArgInfo(object):
    def __init__(self, name="", signature=""):
        self.name = name
        self.signature = signature


class _Gio_DBusMethodInfo(object):
    def __init__(self, name="", interface=None, in_args=[], out_args=[], annotations=[]):
        self.name = name
        self.interface = interface
        self.in_args = in_args
        self.out_args = out_args
        self.annotations = annotations
    
    def _generate_xml(self, indent=0):
        indentation = '  ' * indent
        xml = indentation + '<method name="{name}">\n'.format(name=self.name)
        for arg in self.in_args:
            xml += indentation + '  <arg type="{type}" name="{name}" direction="in">\n'.format(name=arg.name,
                                                                                              type=arg.signature)
            xml += indentation + '  </arg>\n'
        for arg in self.out_args:
            xml += indentation + '  <arg type="{type}" name="{name}" direction="out">\n'.format(name=arg.name,
                                                                                               type=arg.signature)
            xml += indentation + '  </arg>\n'
        xml += indentation + '</method>\n'
        return xml

class _Gio_DBusSignalInfo(object):
    def __init__(self, name="", args=[], annotations=[]):
        self.name = name
        self.args = args
        self.annotations = annotations

class _Gio_DBusPropertyInfo(object):
    def __init__(self, name="", interface=None, signature=[], flags=Gio.DBusPropertyInfoFlags.NONE, annotations=[]):
        self.name = name
        self.interface = interface
        self.signature = signature
        self.flags = flags
        self.annotations = annotations
    
    def _generate_xml(self, indent=0):
        indentation = '  ' * indent
        access = ''
        if self.flags & Gio.DBusPropertyInfoFlags.READABLE:
            access += 'read'
        if self.flags & Gio.DBusPropertyInfoFlags.WRITABLE:
            access += 'write'
        xml = indentation + '<property type="{type}" name="{name}" access="{access}">\n'.format(name=self.name,
                                                                                                type=self.signature,
                                                                                                access=access)
        xml += indentation + '</property>\n'
        return xml


class _Gio_DBusInterfaceInfo(object):
    def __init__(self, name=""):
        self.name = name
        self.methods = []
        self.signals = []
        self.properties = []
        self.annotations = []
    
    def generate_xml(self, indent=0):
        indentation = '  ' * indent
        xml = indentation + '<interface name="{name}">\n'.format(name=self.name)
        print(self.methods)
        for member in self.methods + self.properties + self.annotations:
            xml += member._generate_xml(indent + 1)
        xml += indentation + '</interface>\n'
        return xml
    
    

class _Gio_DBusNodeInfo(object):
    def __init__(self, path=""):
        self.path = path
        self.interfaces = {}
        self.nodes = []
        self.annotations = []
    
    def generate_xml(self, indent=0):
        indentation = '  ' * indent
        xml = indentation + '<node name="{name}">\n'.format(name=self.path)
        for interface in self.interfaces.values():
            xml += interface.generate_xml(indent + 1)
        xml += indentation + '</node>\n'
        return xml


def dbus_method(interface, in_signature=None, out_signature=None):
    def decorator(func):
        in_signature_list = GLib.Variant.split_signature(in_signature)
        arg_names = inspect.getargspec(func).args
        arg_names.pop(0) # eat "self" argument
        if len(in_signature_list) != len(arg_names):
            raise TypeError, 'specified signature %s for method %s does not match length of arguments' % (str(in_signature_list), func.func_name)
        args = []
        for arg_signature, arg_name in zip(in_signature_list, arg_names):
            args.append(_Gio_DBusArgInfo(name=arg_name, signature=arg_signature))
        
        out_arg = _Gio_DBusArgInfo(name="return", signature=out_signature or "")
        func.__dbusinfo__ = _Gio_DBusMethodInfo(name=func.__name__,
                                                interface=interface,
                                                in_args=args,
                                                out_args=[out_arg])
        return func

    return decorator


class dbus_property(object):
    def __init__(self, interface, signature, fget=None, fset=None):
        # check if fget is a data descriptor => a property
        if hasattr(fget, '__set__') and hasattr(fget, '__get__'):
            self.fget = None
            self.wrapped = fget
            prop = self.wrapped
        else:
            self.fget = fget
            self.wrapped = None
            prop = self
        self.fset = fset

        flags = Gio.DBusPropertyInfoFlags.NONE
        if prop.fget is not None:
            flags |= Gio.DBusPropertyInfoFlags.READABLE
        if prop.fset is not None:
            flags |= Gio.DBusPropertyInfoFlags.WRITABLE
        self.__dbusinfo__ = _Gio_DBusPropertyInfo(interface=interface,
                                                  signature=signature,
                                                  flags=flags)
    
    def __call__(self, arg):
        # check if we're decorating a data descriptor => a property
        if hasattr(arg, '__set__') and hasattr(arg, '__get__'):
            self.wrapped = arg
            return self
        return self.getter(arg)
    
    def __get__(self, obj, type):
        if obj is None:
            return self
        if self.wrapped is not None:
            return self.wrapped.__get__(obj, type)
        if self.fget is None:
            raise AttributeError('unreadable attribute')
        return self.fget(obj)
    
    def __set__(self, obj, value):
        if self.wrapped is not None:
            return self.wrapped.__set__(obj, value)
        if self.fset is None:
            raise AttributeError('can''t set attribute')
        return self.fset(obj, value)
    
    def __getattr__(self, name):
        return getattr(self.wrapped, name)
    
    def getter(self, fget):
        if self.wrapped is not None:
            return type(self)(interface=self.__dbusinfo__.interface,
                              signature=self.__dbusinfo__.signature,
                              fget=self.wrapped.getter(fget))
        else:
            return type(self)(interface=self.__dbusinfo__.interface,
                              signature=self.__dbusinfo__.signature,
                              fget=fget,
                              fset=self.fset)
    
    def setter(self, fset):
        if self.wrapped is not None:
            return type(self)(interface=self.__dbusinfo__.interface,
                              signature=self.__dbusinfo__.signature,
                              fget=self.wrapped.setter(fset))
        else:
            return type(self)(interface=self.__dbusinfo__.interface,
                              signature=self.__dbusinfo__.signature,
                              fget=self.fget,
                              fset=fset)


class GioDBusServiceObject(GObject.GObject):
    def __init__(self, object_path):
        super(GioDBusServiceObject, self).__init__()
        self.__dbusinfo__ = _Gio_DBusNodeInfo(path=object_path)
        self.__dbus_connection = None
        self.__dbus_regids = []
        
        for id in dir(self):
            # don't use getattr(self, id) as default to avoid calling
            # __get__ of properties (which may not have been initialized)
            attr = getattr(type(self), id, None)
            if attr is None:
                getattr(self, id)
            try:
                info = attr.__dbusinfo__
            except:
                continue
            interface = self.__dbusinfo__.interfaces.setdefault(info.interface,
                                                                _Gio_DBusInterfaceInfo(name=info.interface))
            if isinstance(info, _Gio_DBusMethodInfo):
                interface.methods.append(info)
            elif isinstance(info, _Gio_DBusPropertyInfo):
                # the name of properties is determined by its attribute name
                info.name = info.name or id
                interface.properties.append(info)
            elif isinstance(info, _Gio_DBusSignalInfo):
                interface.signals.append(info)
            
    
    def _add_to_connection(self, connection):
        self.__dbus_connection = connection
        print 'add_to_connection', connection
        
        print '--- Generated XML: ---\n%s\n-------' % self.__dbusinfo__.generate_xml()
        node_info = Gio.DBusNodeInfo.new_for_xml(self.__dbusinfo__.generate_xml())
        print '--- XML: ---\n%s\n-------' % node_info.generate_xml(0).str

        for interface in self.__dbusinfo__.interfaces:
            print(interface, )
            regid = connection.register_object(
                    self.__dbusinfo__.path,
                    node_info.lookup_interface(interface),
                    self.__dbus_method_call,
                    self.__dbus_get_property,
                    self.__dbus_set_property
                    )
            self.__dbus_regids.append(regid)
            print 'registered object id %i for interface %s' % (regid, interface)

    def _remove_from_connection(self):
        print 'remove_from_connection', self.__dbus_connection
        self.__dbus_info.connection.unregister_object(self.__dbus_info.reg_id)
        self.__dbus_info.reg_id = None
        self.__dbus_info.connection = None


    def __dbus_method_call(self, conn, sender, object_path, iface_name, method_name,
            parameters, invocation):

        print 'method call: %s %s.%s(%s)' % (object_path, iface_name, method_name, str(parameters))

        try:
            method = getattr(self, method_name)
            info = method.__dbusinfo__
        except AttributeError:
            invocation.return_error_literal(Gio.dbus_error_quark(), 
                    Gio.DBusError.UNKNOWN_METHOD,
                    'No such interface or method: %s.%s' % (iface_name, method_name))
            return

        print 'method_call: got info', info
        print 'calling with', parameters.unpack()
        try:
            ret = method(*parameters.unpack())
            print 'method_call: got ret', ret
            invocation.return_value(GLib.Variant('(' + info.out_args[0].signature + ')', (ret,)))
        except Exception as e:
            print 'method_call: got exception', e
            invocation.return_error_literal(Gio.dbus_error_quark(), 
                    Gio.DBusError.IO_ERROR,
                    'Method %s.%s failed with: %s' % (iface_name, method_name, str(e)))

    def __dbus_get_property(self, conn, sender, object_path, iface_name, prop_name):
        try:
            info = getattr(type(self), prop_name).__dbusinfo__
            ret = getattr(self, prop_name)
        except:
            return None
        return GLib.Variant(info.signature, ret)

    def __dbus_set_property(self, conn, sender, object_path, iface_name, prop_name, value):
        try:
            info = getattr(type(self), prop_name).__dbusinfo__
            ret = setattr(self, prop_name, value.unpack())
        except:
            return False
        return True

class PiwareDemo(GioDBusServiceObject):
    def __init__(self, object_path):
        super(PiwareDemo, self).__init__(object_path)
        GioDBusServiceObject.__init__(self, object_path)
        self.number = 42

    @dbus_method(interface='de.piware.Demo.Hello', in_signature='s',
            out_signature='s')
    def hello(self, message):
        if message == 'error':
            raise ValueError, 'requested error'
        return 'Good day %s' % message

    @dbus_property(interface='de.piware.Demo.Hello', signature='i')
    def number(self):
        print("number getter")
        return self._number
    @number.setter
    def number(self, value):
        print("number setter")
        self._number = value
    
    @dbus_property(interface="de.piware.Demo.Hello", signature="i")
    @property
    def pnumber(self):
        print("pnumber getter")
        return self._number
    @pnumber.setter
    def pnumber(self, value):
        print("pnumber setter")
        self._number = value
        
    @dbus_property(interface="de.piware.Demo.Hello", signature="i")
    @GObject.Property(type=int)
    def gnumber(self):
        print("gnumber getter")
        return self._number
    @gnumber.setter
    def gnumber(self, value):
        print("gnumber setter")
        self._number = value


class Derived(PiwareDemo):
    @PiwareDemo.pnumber.setter
    def pnumber(self, value):
        print("pnumber override")



#
# These spawn a separate process for doing queries (cheesy hack), so our main
# loop can keep running
#

def send_request(arg):
    print '\n----- sending request hello("%s") -------' % arg
    subprocess.Popen(['gdbus', 'call', '-e', '-d', 'de.piware.Demo', '-o', '/',
        '-m', 'de.piware.Demo.Hello.hello', arg])
    return False

def send_property_getall():
    print '\n----- sending property getall -------'
    subprocess.Popen(['gdbus', 'call', '-e', '-d', 'de.piware.Demo', '-o', '/',
        '-m', 'org.freedesktop.DBus.Properties.GetAll', 'de.piware.Demo.Hello'])
    return False

def send_property_get():
    print '\n----- sending property get -------'
    subprocess.Popen(['gdbus', 'call', '-e', '-d', 'de.piware.Demo', '-o', '/',
        '-m', 'org.freedesktop.DBus.Properties.Get', 'de.piware.Demo.Hello', 'number'])
    return False

def send_property_set():
    print '\n----- sending property set -------'
    subprocess.Popen(['dbus-send', '--print-reply', '--dest=de.piware.Demo',
            '/', 'org.freedesktop.DBus.Properties.Set',
            'string:de.piware.Demo.Hello', 'string:pnumber',
            'variant:int32:99'])
    return False

def send_property_set_derived():
    print '\n----- sending property set -------'
    subprocess.Popen(['dbus-send', '--print-reply', '--dest=de.piware.Demo',
            '/derived', 'org.freedesktop.DBus.Properties.Set',
            'string:de.piware.Demo.Hello', 'string:pnumber',
            'variant:int32:99'])
    return False

#
# bus_own_name() callbacks
#

def bus_acquired(conn, name):
    service._add_to_connection(conn)
    service2._add_to_connection(conn)

#
# main
#
if __name__ == '__main__':
    service = PiwareDemo('/')
    service2 = Derived("/derived")
    
    print("created")
    bus_id = Gio.bus_own_name(Gio.BusType.SESSION, 'de.piware.Demo',
            Gio.BusNameOwnerFlags.NONE, bus_acquired, None, None)
    
    # now send some requests to check the result
    ml = GObject.MainLoop()
    GObject.timeout_add(1000, send_request, 'Martin')
    GObject.timeout_add(2000, send_request, 'error')
    GObject.timeout_add(3000, send_property_get)
    GObject.timeout_add(4000, send_property_set)
    GObject.timeout_add(5000, send_property_getall)
    GObject.timeout_add(6000, send_property_set_derived)
    GObject.timeout_add(70000, ml.quit)
    ml.run()
    
    print 'unowning bus name...'
    Gio.bus_unown_name(bus_id)
    GObject.timeout_add(500, ml.quit)
    ml.run()
