# telepathy-pinocchio - dummy Telepathy connection manager for instrumentation
#
# Copyright (C) 2008 Nokia Corporation
# Copyright (C) 2008 Collabora Ltd.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public License
# version 2.1 as published by the Free Software Foundation.
#
# 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 St, Fifth Floor, Boston, MA
# 02110-1301 USA

import os.path
from xml.dom import minidom

import telepathy as tp

import pinocchio as pin
from aliasing import *
from avatars import *
from capabilities import *
from presence import *

class Connection(tp.server.Connection,
                 aliasing.Aliasing,
                 avatars.Avatars,
                 capabilities.Capabilities,
                 presence.Presence):
    """Representation of a virtual connection."""

    _CONTACT_LIST_NAMES = ('subscribe', 'publish', 'hide', 'allow', 'deny')

    _optional_parameters = {'password': 's', 'register': 'b'}
    _mandatory_parameters = {'account': 's'}
    _parameter_defaults = {'account': pin.common.ACCOUNT_DEFAULT}

    def __init__(self, manager, parameters):
        self.check_parameters(parameters)

        tp.server.Connection.__init__(self, pin.common.PROTO_DEFAULT,
                                      unicode(parameters['account']),
                                      'pinocchio')
        aliasing.Aliasing.__init__(self)
        avatars.Avatars.__init__(self)
        capabilities.Capabilities.__init__(self)
        presence.Presence.__init__(self)

        # accept the default alias, etc. (including the required, though empty,
        # avatar)
        account_id = self.get_account_id()
        extended_attrs = {}
        self_handle = pin.server.HandleContact(self.get_handle_id(),
                                               account_id, self, extended_attrs)
        self.set_self_handle(self_handle)

        self._manager = manager
        self._contacts_file = pin.common.get_contacts_file(
                                            account_id,
                                            pin.common.PREFIX_SAVED_PREFERRED)

    def Connect(self):
        """Request connection establishment."""

        self._manager.connection_connect(self)
        self.StatusChanged(tp.constants.CONNECTION_STATUS_CONNECTED,
                           tp.constants.CONNECTION_STATUS_REASON_REQUESTED)

        # create the standard ContactList channels (emitting the NewChannel
        # signal in the process), as described in the Channel specification
        for list_name in self._CONTACT_LIST_NAMES:
            handle = tp.server.Handle(self.get_handle_id(),
                                      tp.constants.HANDLE_TYPE_LIST, list_name)
            self._handles[handle.get_type(), handle.get_id()] = handle

            self._channel_get_or_create(tp.interfaces.CHANNEL_TYPE_CONTACT_LIST,
                                        tp.constants.HANDLE_TYPE_LIST,
                                        handle.get_id(), True)

        # XXX: this is kinda hacky, since we have to re-parse the file later
        # create all Groups listed in the contacts file
        group_names = pin.server.contacts_file_get_groups(self)
        for group_name in group_names:
            handle = tp.server.Handle(self.get_handle_id(),
                                      tp.constants.HANDLE_TYPE_GROUP,
                                      group_name)
            self._handles[handle.get_type(), handle.get_id()] = handle

            self._channel_get_or_create(tp.interfaces.CHANNEL_TYPE_CONTACT_LIST,
                                        tp.constants.HANDLE_TYPE_GROUP,
                                        handle.get_id(), True)

    def Disconnect(self):
        """Request connection tear-down."""

        self._manager.connection_disconnect(self)
        self.StatusChanged(tp.constants.CONNECTION_STATUS_DISCONNECTED,
                           tp.constants.CONNECTION_STATUS_REASON_REQUESTED)

    def RequestChannel(self, channel_type, handle_type, handle,
                       suppress_handler):
        """
        Returns a new channel to the given handle.
        
        Arguments:
        channel_type -- DBus interface name for the type of channel requested
        handle_type -- the Handle_Type of handle, or Handle_Type_None if no
                       handle is specified
        handle -- nonzero handle to open a channel to, or zero for an
                  anonymous channel (handle_type must be Handle_Type_None)
        suppress_handler -- true if requesting client will display the channel
                            itself (and no handler should take responsibility
                            for it)

        Returns:
        object_path -- DBus object path for the channel (new or retrieved)

        Exceptions:
        telepathy.errors.NotImplemented
        telepathy.errors.Disconnected
        """
        self.check_connected()

        channel = self._channel_get_or_create (channel_type, handle_type,
                                               handle, suppress_handler)

        # TODO: optimize here by only saving if a channel was created
        self.save()

        return channel._object_path

    @dbus.service.method(tp.interfaces.CONNECTION, in_signature='uas',
                         out_signature='au', sender_keyword='sender')
    def RequestHandles(self, handle_type, names, sender):
        """Returns a list of handles for the given type and names. Creates new
        handle IDs only as necessary (as required in the specification and as
        violated in telepathy-python 0.14.0's default implementation).
        
        Arguments:
        handle_type -- the Handle_Type of handle handles
        names -- iterable of names for the handles
        sender -- magic DBus name for the caller (not passed explicitly)

        Returns:
        ids -- list of corresponding handle IDs
        """
        self.check_connected()
        self.check_handle_type(handle_type)

        ids = []
        for name in names:
            id, id_is_new = self.get_handle_id_idempotent(handle_type, name)
            if id_is_new:
                if handle_type == tp.constants.HANDLE_TYPE_CONTACT:
                    handle = pin.server.HandleContact(id, name, self,
                                                      extended_attrs={})
                else:
                    handle = tp.server.Handle(id, handle_type, name)

                self._handles[handle_type, id] = handle
            else:
                handle = self._handles[handle_type, id]

            # self._client_handles is a set, so this won't make dupes
            self.add_client_handle(handle, sender)
            ids.append(id)

        return ids

    @dbus.service.method(tp.interfaces.CONNECTION, in_signature='',
                         out_signature='')
    def reset_to_default_contacts_file(self):
        """Reverts to the default contact list for this account and disconnects
        this Connection. The next instantiation of a Connection for this account
        will use the default contact list.

        Note that disconnecting the Connection will affect any other clients
        using this Connection.
        """
        self.Disconnect()

        # clear out any modified version of the contact list (upon next
        # Connect(), the default contact list will be read in)
        account_id = self.get_account_id()
        filename = pin.common.get_contacts_file(account_id,
                                                pin.common.PREFIX_SAVED)
        if os.path.isfile(filename):
            os.remove(filename)

    def _channel_get_or_create (self, channel_type, handle_type, handle,
                                suppress_handler):
        """
        Returns a new channel to the given handle.
        
        Arguments:
        channel_type -- DBus interface name for the type of channel requested
        handle_type -- the Handle_Type of handle, or Handle_Type_None if no
                       handle is specified
        handle -- nonzero handle to open a channel to, or zero for an
                  anonymous channel (handle_type must be Handle_Type_None)
        suppress_handler -- true if requesting client will display the channel
                            itself (and no handler should take responsibility
                            for it)

        Returns:
        channel -- telepathy.server.Channel object (new or retrieved)

        Exceptions:
        telepathy.errors.NotImplemented
        telepathy.errors.Disconnected
        """
        self.check_connected()

        channel = None

        # TODO: support handle_type == 0 && handle == 0

        for channel_existing in self._channels:
            if channel_type == channel_existing.GetChannelType():
                handle_obj = None
                channel_handle_obj = None

                channel_handle_type, channel_handle_obj = \
                                                    channel_existing.GetHandle()

                # this would be a bit simpler, but we have to factor in the
                # given handle and handle_type
                if (handle_type, handle) in self._handles.keys():
                    handle_obj = self._handles[handle_type, handle]

                if handle_obj and channel_handle_obj:
                    handle_name =  handle_obj.get_name()
                    channel_name = channel_handle_obj.get_name()

                    if     handle_name == channel_name \
                       and handle_type == channel_handle_type:
                        channel = channel_existing

        if not channel:
            channel = self._manager.channel_new(self, channel_type, handle_type,
                                                handle, suppress_handler)

        return channel

    def get_handle_obj(self, type, id):
        """Returns the Handle object for a given handle (type, ID) pair.

        Arguments:
        type -- the Telepathy Handle_Type
        id -- the integer handle value
        """
        self.check_handle(type, id)

        return self._handles[type, id]

    def get_handle_id_idempotent(self, handle_type, name):
        """Returns a handle ID for the given type and name, creating a new
        handle ID only as necessary (similar to RequestHandles' definition in
        the specification).

        Arguments:
        handle_type -- Telepathy Handle_Type for all the handles
        name -- username for the contact

        Returns:
        handle_id -- ID for the given username
        is_new -- True if the ID was created (did not exist)
        """
        is_new = False
        handle_id = 0
        for handle in self._handles.values():
            if handle.get_name() == name:
                handle_id = handle.get_id()
                break

        # if the handle doesn't already exist, create a new one
        if handle_id <= 0:
            handle_id = self.get_handle_id()
            is_new = True
        
        return handle_id, is_new

    def get_account_id(self):
        """Returns the sanitized account name for the given connection."""

        return self._name.get_name().split('.')[-1]

    def get_contact_channel_membership_info(self):
        """Returns a map of contacts to their contact lists and groups.

        Returns:
        mapping -- dict of handle IDs to [contact list names, group names]
        """
        MAPPING_CONTACT_LISTS = 0
        MAPPING_GROUPS = 1
        mapping = {}

        for channel in self._channels:
            if channel.GetChannelType() == \
                                        tp.interfaces.CHANNEL_TYPE_CONTACT_LIST:
                channel_handle_type, ignore = channel.GetHandle()

                if   channel_handle_type == tp.constants.HANDLE_TYPE_LIST:
                    mapping_pos = MAPPING_CONTACT_LISTS
                elif channel_handle_type == tp.constants.HANDLE_TYPE_GROUP:
                    mapping_pos = MAPPING_GROUPS

                # TODO: also factor in local_pending and remote_pending
                members = channel.GetMembers()

                for member_id in members:
                    # make space for the lists if we don't already have it
                    if member_id not in mapping:
                        mapping[member_id] = [[], []]

                    channel_name = channel._handle.get_name()
                    mapping[member_id][mapping_pos].append(channel_name)

        return mapping

    def save(self):
        """Writes the current contact list, group, and contact state out to a
        new contacts file.
        """
        dom_impl = minidom.getDOMImplementation()
        xml_doc = dom_impl.createDocument(None, 'roster', None)
        roster_xml = xml_doc.documentElement

        # add newline for human-readability
        newline_value = xml_doc.createTextNode('\n')
        roster_xml.appendChild(newline_value)

        contact_channels_map = self.get_contact_channel_membership_info()
        for handle_obj, lists_groups in contact_channels_map.items():
            contact_lists, groups = lists_groups

            contact_xml = handle_obj.get_xml(contact_lists, groups)
            roster_xml.appendChild(contact_xml)

            # add newline for human-readability
            newline_value = xml_doc.createTextNode('\n\n')
            roster_xml.appendChild(newline_value)

        
        account_id = self.get_account_id()
        pin.common.save_roster(xml_doc, account_id)
