import gi

import dbus
gi.require_version('TelepathyGLib', '0.12')
from gi.repository import GObject
from gi.repository import TelepathyGLib

from sugar3.activity import activity
from sugar3.presence import presenceservice
from sugar3.presence.sugartubeconn import SugarTubeConnection

import logging
_logger = logging.getLogger('ShareableActivity')

IFACE = 'org.laptop.ShareableActivity'


class ShareableObject(dbus.service.Object):

    def __init__(self, tube, path):
        dbus.service.Object.__init__(self, tube, path)

    @dbus.service.signal(dbus_interface=IFACE, signature='sv')
    def SendMessage(self, msg, kwargs):
        pass

    @dbus.service.signal(dbus_interface=IFACE, signature='ssv')
    def SendMessageTo(self, busname, msg, kwargs):
        pass


class ShareableActivity(activity.Activity):

    '''
    A shareable activity.

    Signals to connect to for more notifications:
        self.get_shared_activity().connect('buddy-joined', ...)
        self.get_shared_activity().connect('buddy-left', ...)
    '''

    def __init__(self, handle, *args, **kwargs):
        '''
        Initialize the ShareableActivity class.

        Kwargs:
            service_path
        '''

        activity.Activity.__init__(self, handle, *args, **kwargs)

        self._sync_hid = None
        self._message_cbs = {}

        self._connection = None
        self._tube_conn = None

        self._pservice = presenceservice.get_instance()
        self._owner = self._pservice.get_owner()
        self._owner_id = str(self._owner.props.nick)

        self._service_path = kwargs.get('service_path',
                                        self._generate_service_path())
        self._dbus_object = None

        _logger.debug('Setting service name %s, service path %s',
                      IFACE, self._service_path)

        self._connect_to_ps()

    def get_shared_activity(self):
        '''Get shared_activity object; works for different API versions.'''
        try:
            return self.shared_activity
        except:
            return self._shared_activity

    def get_owner(self):
        '''Return buddy object of the owner.'''
        return self._owner

    def get_owner_id(self):
        '''Return id (nickname) of the owner.'''
        return self._owner_id

    def get_bus_name(self):
        '''
        Return the DBus bus name for the tube we're using, or None if there
        is no tube yet.
        '''
        if self._tube_conn is not None:
            return self._tube_conn.get_unique_name()
        else:
            return None

    def _generate_service_path(self):
        bundle_id = self.get_bundle_id()
        last = bundle_id.split('.')[-1]
        instance_id = self.get_id()
        return '/org/laptop/ShareableActivity/%s/%s' % (last, instance_id)

    def _connect_to_ps(self):
        '''
        Connect to the presence service.
        '''
        if self.get_shared_activity():
            self.connect('joined', self._sa_joined_cb)
            if self.get_shared():
                self._sa_joined_cb()
        else:
            self.connect('shared', self._sa_shared_cb)

    def _setup_shared_activity(self):
        '''
        Setup sharing stuff: get channels etc.
        '''

        sa = self.get_shared_activity()
        if sa is None:
            _logger.error('_setup_shared_activity(): no shared_activity yet!')
            return False

        self._connection = sa.telepathy_conn
        self._tubes_chan = sa.telepathy_tubes_chan
        self._text_chan = sa.telepathy_text_chan

        self._tubes_chan[
            TelepathyGLib.IFACE_CHANNEL_TYPE_TUBES].connect_to_signal(
            'NewTube', self._new_tube_cb)

    def _sa_shared_cb(self, activity):
        self._setup_shared_activity()
        self._tubes_chan[TelepathyGLib.IFACE_CHANNEL_TYPE_TUBES].OfferDBusTube(
            IFACE, {})

    def _sa_joined_cb(self, activity):
        """Callback for when we join an existing activity."""

        _logger.info('Joined existing activity')
        self._request_sync = True
        self._setup_shared_activity()

        self._tubes_chan[TelepathyGLib.IFACE_CHANNEL_TYPE_TUBES].ListTubes(
            reply_handler=self._list_tubes_reply_cb,
            error_handler=self._list_tubes_error_cb)

    def _list_tubes_reply_cb(self, tubes):
        """Callback for when requesting an existing tube"""
        _logger.debug('_list_tubes_reply_cb(): %r', tubes)
        for tube_info in tubes:
            self._new_tube_cb(*tube_info)

    def _list_tubes_error_cb(self, e):
        _logger.error('ListTubes() failed: %s', e)

    def _new_tube_cb(self, id, initiator, type, service, params, state):
        _logger.debug('New tube: ID=%d initator=%d type=%d service=%s '
                      'params=%r state=%d', id, initiator, type, service,
                      params, state)

        if (type == TelepathyGLib.TubeType.DBUS and service == IFACE):
            if state == TelepathyGLib.TubeState.LOCAL_PENDING:
                self._tubes_chan[
                    TelepathyGLib.IFACE_CHANNEL_TYPE_TUBES].AcceptDBusTube(id)

            self._tube_conn = SugarTubeConnection(
                self._connection, self._tubes_chan[
                    TelepathyGLib.IFACE_CHANNEL_TYPE_TUBES],
                id, group_iface=self._text_chan[
                    TelepathyGLib.IFACE_CHANNEL_INTERFACE_GROUP])

            self._tube_conn.add_signal_receiver(
                self._send_message_cb,
                'SendMessage', sender_keyword='sender')

            self._dbus_object = ShareableObject(self._tube_conn,
                                                self._service_path)

    def buddy_joined(self, activity, buddy):
        '''
        Override to take action when a buddy joins.
        '''
        _logger.debug('Buddy joined: %s', buddy)

    def buddy_left(self, activity, buddy):
        '''
        Override to take action when a buddy left.
        '''
        _logger.debug('Buddy left: %s', buddy)

    def connect_message(self, msg, func):
        '''
        Connect function 'func' so that it's called when message <msg>
        is received. The function will receive keyword arguments sent
        with the message.
        '''
        self._message_cbs[msg] = func

    def message_received(self, msg, **kwargs):
        '''
        Override to take action when a message is received.
        This function will not be called for message handlers already
        registered with connect_message().
        '''
        _logger.debug('Received message: %s(%r)', msg, kwargs)

    def send_message(self, msg, **kwargs):
        '''
        Send a message to all connected buddies.
        '''
        if self._dbus_object is not None:
            _logger.debug('Sending message: %s(%r)', msg, kwargs)
            self._dbus_object.SendMessage(msg, kwargs)
        else:
            _logger.debug('Not shared, not sending message %s(%r)',
                          msg, kwargs)

    def send_message_to(self, buddy, msg, **kwargs):
        '''
        Send a message to one particular buddy.
        '''
        if self._dbus_object is not None:
            _logger.debug('Sending message to %s: %s(%r)', buddy, msg, kwargs)
            # FIXME: convert to busname
            self._dbus_object.SendMessageTo(buddy, msg, kwargs)
        else:
            _logger.debug('Not shared, not sending message %s(%r) to %s',
                          msg, kwargs, buddy)

    def _dispatch_message(self, msg, kwargs):
        passkwargs = {}
        for k, v in kwargs.iteritems():
            passkwargs[str(k)] = v

        if msg in self._message_cbs:
            func = self._message_cbs[msg]
            func(**passkwargs)
        else:
            self.message_received(msg, **passkwargs)

    def _send_message_cb(self, msg, kwargs, sender=None):
        '''Callback to filter message signals.'''
        _logger.debug(
            'Sender: %s, owner: %s, owner_id: %s, busname: %s', sender,
            self.get_owner(), self.get_owner_id(), self.get_bus_name())
        if sender == self.get_bus_name():
            return
        kwargs['sender'] = sender
        self._dispatch_message(msg, kwargs)

    def _send_message_to_cb(self, to, msg, kwargs, sender=None):
        '''Callback to filter message signals.'''
        if to != self.get_bus_name():
            return
        kwargs['sender'] = sender
        kwargs['to'] = to
        self._dispatch_message(msg, kwargs)

    # FIXME: build a standard system to sync state from a single buddy
    def request_sync(self):
        if self._sync_hid is not None:
            return

        self._syncreq_buddy = 0
        self._sync_hid = GObject.timeout_add(2000, self._request_sync_cb)
        self._request_sync_cb()

    def _request_sync_cb(self):
        if self._syncreq_buddy <= len(self._connected_buddies):
            self._sync_hid = None
            return False

        self._syncreq_buddy += 1
