
# Slixmpp: The Slick XMPP Library
# Copyright (C) 2010  Nathanael C. Fritz
# This file is part of Slixmpp.
# See the file LICENSE for copying permission.
import threading

from slixmpp.xmlstream import JID
from slixmpp.roster import RosterItem


class RosterNode(object):

    """
    A roster node is a roster for a single JID.

    Attributes:
        xmpp           -- The main Slixmpp instance.
        jid            -- The JID that owns the roster node.
        db             -- Optional interface to an external datastore.
        auto_authorize -- Determines how authorizations are handled:
                            True  -- Accept all subscriptions.
                            False -- Reject all subscriptions.
                            None  -- Subscriptions must be
                                     manually authorized.
                          Defaults to True.
        auto_subscribe -- Determines if bi-directional subscriptions
                          are created after automatically authrorizing
                          a subscription request.
                          Defaults to True
        last_status    -- The last sent presence status that was broadcast
                          to all contact JIDs.

    Methods:
        add           -- Add a JID to the roster.
        update        -- Update a JID's subscription information.
        subscribe     -- Subscribe to a JID.
        unsubscribe   -- Unsubscribe from a JID.
        remove        -- Remove a JID from the roster.
        presence      -- Return presence information for a JID's resources.
        send_presence -- Shortcut for sending a presence stanza.
    """

    def __init__(self, xmpp, jid, db=None):
        """
        Create a roster node for a JID.

        Arguments:
            xmpp -- The main Slixmpp instance.
            jid  -- The JID that owns the roster.
            db   -- Optional interface to an external datastore.
        """
        self.xmpp = xmpp
        self.jid = jid
        self.db = db
        self.ignore_updates = False
        self.auto_authorize = True
        self.auto_subscribe = True
        self.last_status = None
        self._version = ''
        self._jids = {}
        self._last_status_lock = threading.Lock()

        if self.db:
            if hasattr(self.db, 'version'):
                self._version = self.db.version(self.jid)
            for jid in self.db.entries(self.jid):
                self.add(jid)

    @property
    def version(self):
        """Retrieve the roster's version ID."""
        if self.db and hasattr(self.db, 'version'):
            self._version = self.db.version(self.jid)
        return self._version

    @version.setter
    def version(self, version):
        """Set the roster's version ID."""
        self._version = version
        if self.db and hasattr(self.db, 'set_version'):
            self.db.set_version(self.jid, version)

    def __getitem__(self, key):
        """
        Return the roster item for a subscribed JID.

        A new item entry will be created if one does not already exist.
        """
        if key is None:
            key = JID('')
        if not isinstance(key, JID):
            key = JID(key)
        key = key.bare
        if key not in self._jids:
            self.add(key, save=True)
        return self._jids[key]

    def __delitem__(self, key):
        """
        Remove a roster item from the local storage.

        To remove an item from the server, use the remove() method.
        """
        if key is None:
            key = JID('')
        if not isinstance(key, JID):
            key = JID(key)
        key = key.bare
        if key in self._jids:
            del self._jids[key]

    def __len__(self):
        """Return the number of JIDs referenced by the roster."""
        return len(self._jids)

    def keys(self):
        """Return a list of all subscribed JIDs."""
        return self._jids.keys()

    def has_jid(self, jid):
        """Returns whether the roster has a JID."""
        return jid in self._jids

    def groups(self):
        """Return a dictionary mapping group names to JIDs."""
        result = {}
        for jid in self._jids:
            groups = self._jids[jid]['groups']
            if not groups:
                if '' not in result:
                    result[''] = []
                result[''].append(jid)
            for group in groups:
                if group not in result:
                    result[group] = []
                result[group].append(jid)
        return result

    def __iter__(self):
        """Iterate over the roster items."""
        return self._jids.__iter__()

    def set_backend(self, db=None, save=True):
        """
        Set the datastore interface object for the roster node.

        Arguments:
            db -- The new datastore interface.
            save -- If True, save the existing state to the new
                    backend datastore. Defaults to True.
        """
        self.db = db
        existing_entries = set(self._jids)
        new_entries = set(self.db.entries(self.jid, {}))

        for jid in existing_entries:
            self._jids[jid].set_backend(db, save)
        for jid in new_entries - existing_entries:
            self.add(jid)

    def add(self, jid, name='', groups=None, afrom=False, ato=False,
            pending_in=False, pending_out=False, whitelisted=False,
            save=False):
        """
        Add a new roster item entry.

        Arguments:
            jid         -- The JID for the roster item.
            name        -- An alias for the JID.
            groups      -- A list of group names.
            afrom       -- Indicates if the JID has a subscription state
                           of 'from'. Defaults to False.
            ato         -- Indicates if the JID has a subscription state
                           of 'to'. Defaults to False.
            pending_in  -- Indicates if the JID has sent a subscription
                           request to this connection's JID.
                           Defaults to False.
            pending_out -- Indicates if a subscription request has been sent
                           to this JID.
                           Defaults to False.
            whitelisted -- Indicates if a subscription request from this JID
                           should be automatically authorized.
                           Defaults to False.
            save        -- Indicates if the item should be persisted
                           immediately to an external datastore,
                           if one is used.
                           Defaults to False.
        """
        if isinstance(jid, JID):
            key = jid.bare
        else:
            key = jid

        state = {'name': name,
                 'groups': groups or [],
                 'from': afrom,
                 'to': ato,
                 'pending_in': pending_in,
                 'pending_out': pending_out,
                 'whitelisted': whitelisted,
                 'subscription': 'none'}
        self._jids[key] = RosterItem(self.xmpp, jid, self.jid,
                                     state=state, db=self.db,
                                     roster=self)
        if save:
            self._jids[key].save()

    def subscribe(self, jid):
        """
        Subscribe to the given JID.

        Arguments:
            jid -- The JID to subscribe to.
        """
        self[jid].subscribe()

    def unsubscribe(self, jid):
        """
        Unsubscribe from the given JID.

        Arguments:
            jid -- The JID to unsubscribe from.
        """
        self[jid].unsubscribe()

    def remove(self, jid):
        """
        Remove a JID from the roster.

        Arguments:
            jid -- The JID to remove.
        """
        self[jid].remove()
        if not self.xmpp.is_component:
            return self.update(jid, subscription='remove')

    def update(self, jid, name=None, subscription=None, groups=[],
                     timeout=None, callback=None, timeout_callback=None):
        """
        Update a JID's subscription information.

        Arguments:
            jid          -- The JID to update.
            name         -- Optional alias for the JID.
            subscription -- The subscription state. May be one of: 'to',
                            'from', 'both', 'none', or 'remove'.
            groups       -- A list of group names.
            timeout      -- The length of time (in seconds) to wait
                            for a response before continuing if blocking
                            is used. Defaults to self.response_timeout.
            callback     -- Optional reference to a stream handler function.
                            Will be executed when the roster is received.
        """
        if not groups:
            groups = []

        self[jid]['name'] = name
        self[jid]['groups'] = groups
        self[jid].save()

        if not self.xmpp.is_component:
            iq = self.xmpp.Iq()
            iq['type'] = 'set'
            iq['roster']['items'] = {jid: {'name': name,
                                           'subscription': subscription,
                                           'groups': groups}}

            return iq.send(timeout=timeout, callback=callback,
                           timeout_callback=timeout_callback)

    def presence(self, jid, resource=None):
        """
        Retrieve the presence information of a JID.

        May return either all online resources' status, or
        a single resource's status.

        Arguments:
            jid      -- The JID to lookup.
            resource -- Optional resource for returning
                        only the status of a single connection.
        """
        if resource is None:
            return self[jid].resources

        default_presence = {'status': '',
                            'priority': 0,
                            'show': ''}
        return self[jid].resources.get(resource,
                                       default_presence)

    def reset(self):
        """
        Reset the state of the roster to forget any current
        presence information. Useful after a disconnection occurs.
        """
        for jid in self:
            self[jid].reset()

    def send_presence(self, **kwargs):
        """
        Create, initialize, and send a Presence stanza.

        If no recipient is specified, send the presence immediately.
        Otherwise, forward the send request to the recipient's roster
        entry for processing.

        Arguments:
            pshow     -- The presence's show value.
            pstatus   -- The presence's status message.
            ppriority -- This connections' priority.
            pto       -- The recipient of a directed presence.
            pfrom     -- The sender of a directed presence, which should
                         be the owner JID plus resource.
            ptype     -- The type of presence, such as 'subscribe'.
            pnick     -- Optional nickname of the presence's sender.
        """
        if self.xmpp.is_component and not kwargs.get('pfrom', ''):
            kwargs['pfrom'] = self.jid
        self.xmpp.send_presence(**kwargs)

    def send_last_presence(self):
        if self.last_status is None:
            self.send_presence()
        else:
            pres = self.last_status
            if self.xmpp.is_component:
                pres['from'] = self.jid
            else:
                del pres['from']
            pres.send()

    def __repr__(self):
        return repr(self._jids)
