#!/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

class _Gio_DBusMethodInfo:
    interface = None
    in_args = None
    out_signature = None

def Gio_bus_method(dbus_interface, in_signature=None, out_signature=None):
    def decorator(func):
        func._dbus_method = _Gio_DBusMethodInfo()
        func._dbus_method.interface = dbus_interface
        #func._dbus_method.out_signature = '(' + (out_signature or '') + ')'
        func._dbus_method.out_signature = out_signature or ''

        func._dbus_method.in_args = []
        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) != len(arg_names):
            raise TypeError, 'specified signature %s for method %s does not match length of arguments' % (str(in_signature_list), func.func_name)
        for pair in zip(in_signature_list, arg_names):
            func._dbus_method.in_args.append(pair)

        return func

    return decorator

class GioDBusServiceObject:
    class _DBusInfo:
        object_path = None
        connection = None
        reg_id = None
        methods = None # interface -> method_name -> info_map
                       # info_map keys: method_name, in_signature, out_signature

    def __init__(self, object_path):
        self.__dbus_info = GioDBusServiceObject._DBusInfo()
        self.__dbus_info.object_path = object_path
    
        # set up the vtable maps, for more efficient lookups at runtime
        self.__dbus_info.methods = {}
        for id in dir(self):
            attr = getattr(self, id)
            if hasattr(attr, '_dbus_method'):
                self.__dbus_info.methods.setdefault(attr._dbus_method.interface, {})[id] = {
                    'in_args': attr._dbus_method.in_args,
                    'out_signature': attr._dbus_method.out_signature,
                }

    def _add_to_connection(self, connection):
        self.__dbus_info.connection = connection
        print 'add_to_connection', connection

        node_info = Gio.DBusNodeInfo.new_for_xml(self.__dbus_introspection_xml())
        print '--- XML: ---\n%s\n-------' % node_info.generate_xml(4).str

        for interface in self.__dbus_info.methods:
            self.__dbus_info.reg_id = connection.register_object(
                    self.__dbus_info.object_path,
                    node_info.lookup_interface(interface),
                    self.__dbus_method_call,
                    self.__dbus_get_property,
                    self.__dbus_set_property
                    )
            print 'registered object id %i for interface %s' % (self.__dbus_info.reg_id, interface)

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

    def __dbus_introspection_xml(self):
        '''Generate introspection XML'''

        xml = '<node>\n'

        for interface in self.__dbus_info.methods:
            xml += '  <interface name="%s">\n' % interface

            for method, data in self.__dbus_info.methods[interface].items():
                xml += '    <method name="%s">\n' % method
                for (sig, name) in data['in_args']:
                    xml += '      <arg type="%s" name="%s" direction="in"/>' % (sig, name)
                xml += '      <arg type="%s" name="return" direction="out"/>' % data['out_signature']
                xml += '    </method>\n'

            xml += '  </interface>\n'

        xml += '</node>\n'
        return xml

    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:
            info = self.__dbus_info.methods[iface_name][method_name]
        except KeyError:
            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 = getattr(self, method_name)(*parameters.unpack())
            print 'method_call: got ret', ret
            invocation.return_value(GLib.Variant('(' + info['out_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):
        error = GLib.Error.new_literal(GLib.io_channel_error_quark(), 1, 
                'Not implemented yet')
        return None

    def __dbus_set_property(self, conn, sender, object_path, iface_name, prop_name):
        error = GLib.Error.new_literal(GLib.io_channel_error_quark(), 1, 
                'Not implemented yet')
        return False

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

    @Gio_bus_method(dbus_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

#
# 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_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:number',
            'variant:int32:99'])
    return False

#
# bus_own_name() callbacks
#

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

#
# main
#

service = PiwareDemo('/')

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(5000, send_property_set)
#GObject.timeout_add(6000, send_property_get)
GObject.timeout_add(3000, ml.quit)
ml.run()

print 'unowning bus name...'
Gio.bus_unown_name(bus_id)
GObject.timeout_add(500, ml.quit)
ml.run()
