/*
 * Jitsi, the OpenSource Java VoIP and Instant Messaging client.
 *
 * Copyright @ 2015 Atlassian Pty Ltd
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.java.sip.communicator.impl.protocol.jabber;

import java.util.*;

import net.java.sip.communicator.service.protocol.*;
import net.java.sip.communicator.service.protocol.event.*;
import net.java.sip.communicator.service.protocol.jabberconstants.*;
import net.java.sip.communicator.util.*;

import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smack.util.*;
import org.jivesoftware.smackx.packet.*;

/**
 * The Jabber implementation of a Persistent Presence Operation set. This class
 * manages our own presence status as well as subscriptions for the presence
 * status of our buddies. It also offers methods for retrieving and modifying
 * the buddy contact list and adding listeners for changes in its layout.
 *
 * @author Damian Minkov
 * @author Lyubomir Marinov
 * @author Hristo Terezov
 */
public class OperationSetPersistentPresenceJabberImpl
    extends AbstractOperationSetPersistentPresence<
                ProtocolProviderServiceJabberImpl>
{
    /**
     * The logger.
     */
    private static final Logger logger =
        Logger.getLogger(OperationSetPersistentPresenceJabberImpl.class);

    /**
     * Contains our current status message. Note that this field would only
     * be changed once the server has confirmed the new status message and
     * not immediately upon setting a new one..
     */
    private String currentStatusMessage = "";

    /**
     * The presence status that we were last notified of entering.
     * The initial one is OFFLINE
     */
    private PresenceStatus currentStatus;

    /**
     * A map containing bindings between Jitsi's jabber presence
     * status instances and Jabber status codes
     */
    private static Map<String, Presence.Mode> scToJabberModesMappings
        = new Hashtable<String, Presence.Mode>();

    static
    {
        scToJabberModesMappings.put(JabberStatusEnum.AWAY,
                                  Presence.Mode.away);
        scToJabberModesMappings.put(JabberStatusEnum.ON_THE_PHONE,
                                  Presence.Mode.away);
        scToJabberModesMappings.put(JabberStatusEnum.IN_A_MEETING,
            Presence.Mode.away);
        scToJabberModesMappings.put(JabberStatusEnum.EXTENDED_AWAY,
                                  Presence.Mode.xa);
        scToJabberModesMappings.put(JabberStatusEnum.DO_NOT_DISTURB,
                                  Presence.Mode.dnd);
        scToJabberModesMappings.put(JabberStatusEnum.FREE_FOR_CHAT,
                                  Presence.Mode.chat);
        scToJabberModesMappings.put(JabberStatusEnum.AVAILABLE,
                                  Presence.Mode.available);
    }

    /**
     * A map containing bindings between Jitsi's xmpp presence
     * status instances and priorities to use for statuses.
     */
    private static Map<String, Integer> statusToPriorityMappings
        = new Hashtable<String, Integer>();

    /**
     * The server stored contact list that will be encapsulating smack's
     * buddy list.
     */
    private final ServerStoredContactListJabberImpl ssContactList;

    /**
     * Listens for subscriptions.
     */
    private JabberSubscriptionListener subscribtionPacketListener = null;

    /**
     * Current resource priority.
     */
    private int resourcePriorityAvailable = 30;

    /**
     * Manages statuses and different user resources.
     */
    private ContactChangesListener contactChangesListener = null;

    /**
     * Manages the presence extension to advertise the SHA-1 hash of this
     * account avatar as defined in XEP-0153.
     */
    private VCardTempXUpdatePresenceExtension vCardTempXUpdatePresenceExtension
        = null;

    /**
     * Handles all the logic about mobile indicator for contacts.
     */
    private final MobileIndicator mobileIndicator;

    /**
     * The last sent presence to server, contains the status, the resource
     * and its priority.
     */
    private Presence currentPresence = null;

    /**
     * The local contact presented by the provider.
     */
    private ContactJabberImpl localContact = null;

    /**
     * Creates the OperationSet.
     * @param provider the parent provider.
     * @param infoRetreiver retrieve contact information.
     */
    public OperationSetPersistentPresenceJabberImpl(
        ProtocolProviderServiceJabberImpl provider,
        InfoRetreiver infoRetreiver)
    {
        super(provider);

        currentStatus =
            parentProvider.getJabberStatusEnum().getStatus(
                JabberStatusEnum.OFFLINE);

        initializePriorities();

        ssContactList = new ServerStoredContactListJabberImpl(
            this , provider, infoRetreiver);

        parentProvider.addRegistrationStateChangeListener(
            new RegistrationStateListener());

        mobileIndicator = new MobileIndicator(parentProvider, ssContactList);
    }

    /**
     * Registers a listener that would receive events upon changes in server
     * stored groups.
     *
     * @param listener a ServerStoredGroupChangeListener impl that would
     *   receive events upon group changes.
     */
    @Override
    public void addServerStoredGroupChangeListener(ServerStoredGroupListener
        listener)
    {
        ssContactList.addGroupListener(listener);
    }

    /**
     * Creates a group with the specified name and parent in the server
     * stored contact list.
     *
     * @param parent the group where the new group should be created
     * @param groupName the name of the new group to create.
     * @throws OperationFailedException if such group already exists
     */
    public void createServerStoredContactGroup(ContactGroup parent,
                                               String groupName)
        throws OperationFailedException
    {
        assertConnected();

        if (!parent.canContainSubgroups())
           throw new IllegalArgumentException(
               "The specified contact group cannot contain child groups. Group:"
               + parent );

        ssContactList.createGroup(groupName);
    }

    /**
     * Creates a non persistent contact for the specified address. This would
     * also create (if necessary) a group for volatile contacts that would not
     * be added to the server stored contact list. The volatile contact would
     * remain in the list until it is really added to the contact list or
     * until the application is terminated.
     * @param id the address of the contact to create.
     * @return the newly created volatile <tt>ContactImpl</tt>
     */
    public synchronized ContactJabberImpl createVolatileContact(String id)
    {
        return createVolatileContact(id, null);
    }

    /**
     * Creates a non persistent contact for the specified address. This would
     * also create (if necessary) a group for volatile contacts that would not
     * be added to the server stored contact list. The volatile contact would
     * remain in the list until it is really added to the contact list or
     * until the application is terminated.
     * @param id the address of the contact to create.
     * @param displayName the display name of the contact.
     * @return the newly created volatile <tt>ContactImpl</tt>
     */
    public synchronized ContactJabberImpl createVolatileContact(String id,
        String displayName)
    {
        return createVolatileContact(id, false, displayName);
    }

    /**
     * Creates a non persistent contact for the specified address. This would
     * also create (if necessary) a group for volatile contacts that would not
     * be added to the server stored contact list. The volatile contact would
     * remain in the list until it is really added to the contact list or
     * until the application is terminated.
     * @param id the address of the contact to create.
     * @param isPrivateMessagingContact indicates whether the contact should be private
     * messaging contact or not.
     * @return the newly created volatile <tt>ContactImpl</tt>
     */
    public synchronized ContactJabberImpl createVolatileContact(String id,
        boolean isPrivateMessagingContact)
    {
        return createVolatileContact(id, isPrivateMessagingContact, null);
    }

    /**
     * Creates a non persistent contact for the specified address. This would
     * also create (if necessary) a group for volatile contacts that would not
     * be added to the server stored contact list. The volatile contact would
     * remain in the list until it is really added to the contact list or
     * until the application is terminated.
     * @param id the address of the contact to create.
     * @param isPrivateMessagingContact indicates whether the contact should be private
     * messaging contact or not.
     * @param displayName the display name of the contact.
     * @return the newly created volatile <tt>ContactImpl</tt>
     */
    public synchronized ContactJabberImpl createVolatileContact(String id,
        boolean isPrivateMessagingContact, String displayName)
    {
        // first check for already created one.
        ContactGroupJabberImpl notInContactListGroup =
            ssContactList.getNonPersistentGroup();
        ContactJabberImpl sourceContact;
        if(notInContactListGroup != null
            && (sourceContact = notInContactListGroup.findContact(
                                    StringUtils.parseBareAddress(id)))
                != null)
            return sourceContact;
        else
        {
            sourceContact = ssContactList.createVolatileContact(
                id, isPrivateMessagingContact, displayName);
            if(isPrivateMessagingContact
                && StringUtils.parseResource(id) != null)
            {
                updateResources(sourceContact, false);
            }
            return sourceContact;
        }
    }

    /**
     * Creates and returns a unresolved contact from the specified
     * <tt>address</tt> and <tt>persistentData</tt>.
     *
     * @param address an identifier of the contact that we'll be creating.
     * @param persistentData a String returned Contact's getPersistentData()
     *   method during a previous run and that has been persistently stored
     *   locally.
     * @param parentGroup the group where the unresolved contact is supposed
     *   to belong to.
     * @return the unresolved <tt>Contact</tt> created from the specified
     *   <tt>address</tt> and <tt>persistentData</tt>
     */
    public Contact createUnresolvedContact(String address,
                                           String persistentData,
                                           ContactGroup parentGroup)
    {
        if(! (parentGroup instanceof ContactGroupJabberImpl ||
              parentGroup instanceof RootContactGroupJabberImpl) )
            throw new IllegalArgumentException(
                "Argument is not an jabber contact group (group="
                + parentGroup + ")");

        ContactJabberImpl contact =
            ssContactList.createUnresolvedContact(parentGroup, address);

        contact.setPersistentData(persistentData);

        return contact;
    }

    /**
     * Creates and returns a unresolved contact from the specified
     * <tt>address</tt> and <tt>persistentData</tt>.
     *
     * @param address an identifier of the contact that we'll be creating.
     * @param persistentData a String returned Contact's getPersistentData()
     *   method during a previous run and that has been persistently stored
     *   locally.
     * @return the unresolved <tt>Contact</tt> created from the specified
     *   <tt>address</tt> and <tt>persistentData</tt>
     */
    public Contact createUnresolvedContact(String address,
                                           String persistentData)
    {
        return createUnresolvedContact(  address
                                       , persistentData
                                       , getServerStoredContactListRoot());
    }

    /**
     * Creates and returns a unresolved contact group from the specified
     * <tt>address</tt> and <tt>persistentData</tt>.
     *
     * @param groupUID an identifier, returned by ContactGroup's
     *   getGroupUID, that the protocol provider may use in order to create
     *   the group.
     * @param persistentData a String returned ContactGroups's
     *   getPersistentData() method during a previous run and that has been
     *   persistently stored locally.
     * @param parentGroup the group under which the new group is to be
     *   created or null if this is group directly underneath the root.
     * @return the unresolved <tt>ContactGroup</tt> created from the
     *   specified <tt>uid</tt> and <tt>persistentData</tt>
     */
    public ContactGroup createUnresolvedContactGroup(String groupUID,
        String persistentData, ContactGroup parentGroup)
    {
        return ssContactList.createUnresolvedContactGroup(groupUID);
    }

    /**
     * Returns a reference to the contact with the specified ID in case we
     * have a subscription for it and null otherwise/
     *
     * @param contactID a String identifier of the contact which we're
     *   seeking a reference of.
     * @return a reference to the Contact with the specified
     *   <tt>contactID</tt> or null if we don't have a subscription for the
     *   that identifier.
     */
    public Contact findContactByID(String contactID)
    {
        return ssContactList.findContactById(contactID);
    }

    /**
     * Returns the status message that was confirmed by the serfver
     *
     * @return the last status message that we have requested and the aim
     *   server has confirmed.
     */
    public String getCurrentStatusMessage()
    {
        return currentStatusMessage;
    }

    /**
     * Returns the protocol specific contact instance representing the local
     * user.
     *
     * @return the Contact (address, phone number, or uin) that the Provider
     *   implementation is communicating on behalf of.
     */
    public Contact getLocalContact()
    {
        if(localContact != null)
            return localContact;

        final String id = parentProvider.getAccountID().getUserID();

        localContact
            = new ContactJabberImpl(null, ssContactList, false, true);
        localContact.setLocal(true);
        localContact.updatePresenceStatus(currentStatus);
        localContact.setJid(parentProvider.getOurJID());

        Map<String, ContactResourceJabberImpl> rs
            = localContact.getResourcesMap();

        if(currentPresence != null)
            rs.put(parentProvider.getOurJID(),
                createResource( currentPresence,
                                parentProvider.getOurJID(),
                                localContact));

        Iterator<Presence> presenceIterator = ssContactList.getPresences(id);
        while(presenceIterator.hasNext())
        {
            Presence p = presenceIterator.next();

            String fullJid = p.getFrom();
            rs.put(fullJid, createResource(p, p.getFrom(), localContact));
        }

        // adds xmpp listener for changes in the local contact resources
        PacketFilter presenceFilter = new PacketTypeFilter(Presence.class);
        parentProvider.getConnection()
            .addPacketListener(
                new PacketListener()
                {
                    @Override
                    public void processPacket(Packet packet)
                    {
                        Presence presence = (Presence) packet;
                        String from = presence.getFrom();

                        if(from == null
                            || !StringUtils.parseBareAddress(from).equals(id))
                            return;

                        // own resource update, let's process it
                        updateResource(localContact, null, presence);
                    }
                }, presenceFilter);

        return localContact;
    }

    /**
     * Creates ContactResource from the presence, full jid and contact.
     * @param presence the presence object.
     * @param fullJid the full jid for the resource.
     * @param contact the contact.
     * @return the newly created resource.
     */
    private ContactResourceJabberImpl createResource(
        Presence presence,
        String fullJid,
        Contact contact)
    {
        String resource = StringUtils.parseResource(fullJid);

        return new ContactResourceJabberImpl(
            fullJid,
            contact,
            resource,
            jabberStatusToPresenceStatus(presence, parentProvider),
            presence.getPriority(),
            mobileIndicator.isMobileResource(resource, fullJid));
    }

    /**
     * Clear resources used for local contact and before that update its
     * resources in order to fire the needed events.
     */
    private void clearLocalContactResources()
    {
        if(localContact != null)
            removeResource(localContact, localContact.getAddress());

        currentPresence = null;
        localContact = null;
    }

    /**
     * Returns a PresenceStatus instance representing the state this provider
     * is currently in.
     *
     * @return the PresenceStatus last published by this provider.
     */
    public PresenceStatus getPresenceStatus()
    {
        return currentStatus;
    }

    /**
     * Returns the root group of the server stored contact list.
     *
     * @return the root ContactGroup for the ContactList stored by this
     *   service.
     */
    public ContactGroup getServerStoredContactListRoot()
    {
        return ssContactList.getRootGroup();
    }

    /**
     * Returns the set of PresenceStatus objects that a user of this service
     * may request the provider to enter.
     *
     * @return Iterator a PresenceStatus array containing "enterable" status
     *   instances.
     */
    public Iterator<PresenceStatus> getSupportedStatusSet()
    {
        return parentProvider.getJabberStatusEnum().getSupportedStatusSet();
    }

    /**
     * Checks if the contact address is associated with private messaging
     * contact or not.
     * @param contactAddress the address of the contact.
     * @return <tt>true</tt> the contact address is associated with private
     * messaging contact and <tt>false</tt> if not.
     */
    public boolean isPrivateMessagingContact(String contactAddress)
    {
        return ssContactList.isPrivateMessagingContact(contactAddress);
    }

    /**
     * Removes the specified contact from its current parent and places it
     * under <tt>newParent</tt>.
     *
     * @param contactToMove the <tt>Contact</tt> to move
     * @param newParent the <tt>ContactGroup</tt> where <tt>Contact</tt>
     *   would be placed.
     */
    public void moveContactToGroup(Contact contactToMove,
                                   ContactGroup newParent)
        throws OperationFailedException
    {
        assertConnected();

        if( !(contactToMove instanceof ContactJabberImpl) )
            throw new IllegalArgumentException(
                "The specified contact is not an jabber contact." +
                contactToMove);
        if( !(newParent instanceof AbstractContactGroupJabberImpl) )
            throw new IllegalArgumentException(
                "The specified group is not an jabber contact group."
                + newParent);

        ssContactList.moveContact((ContactJabberImpl)contactToMove,
                                  (AbstractContactGroupJabberImpl)newParent);
    }

    /**
     * Requests the provider to enter into a status corresponding to the
     * specified parameters.
     *
     * @param status the PresenceStatus as returned by
     *   getRequestableStatusSet
     * @param statusMessage the message that should be set as the reason to
     *   enter that status
     * @throws IllegalArgumentException if the status requested is not a
     *   valid PresenceStatus supported by this provider.
     * @throws IllegalStateException if the provider is not currently
     *   registered.
     * @throws OperationFailedException with code NETWORK_FAILURE if
     *   publishing the status fails due to a network error.
     */
    public void publishPresenceStatus(PresenceStatus status,
                                      String statusMessage)
        throws IllegalArgumentException,
               IllegalStateException,
               OperationFailedException
    {
        assertConnected();

        JabberStatusEnum jabberStatusEnum =
            parentProvider.getJabberStatusEnum();
        boolean isValidStatus = false;
        for (Iterator<PresenceStatus> supportedStatusIter
                        = jabberStatusEnum.getSupportedStatusSet();
             supportedStatusIter.hasNext();)
        {
            if (supportedStatusIter.next().equals(status))
            {
                isValidStatus = true;
                break;
            }
        }
        if (!isValidStatus)
            throw new IllegalArgumentException(status
                + " is not a valid Jabber status");

        // if we got publish presence and we are still in a process of
        // initializing the roster, just save the status and we will dispatch
        // it when we are ready with the roster as sending initial presence
        // is recommended to be done after requesting the roster, but we want
        // to also dispatch it
        synchronized(ssContactList.getRosterInitLock())
        {
            if(!ssContactList.isRosterInitialized())
            {
                // store it
                ssContactList.setInitialStatus(status);
                ssContactList.setInitialStatusMessage(statusMessage);
                return;
            }
        }

        if (status.equals(jabberStatusEnum.getStatus(JabberStatusEnum.OFFLINE)))
        {
            parentProvider.unregister();
            clearLocalContactResources();
        }
        else
        {
            Presence presence = new Presence(Presence.Type.available);
            currentPresence = presence;
            presence.setMode(presenceStatusToJabberMode(status));
            presence.setPriority(
                getPriorityForPresenceStatus(status.getStatusName()));

            // on the phone is a special status which is away
            // with custom status message
            if(status.equals(jabberStatusEnum.getStatus(
                    JabberStatusEnum.ON_THE_PHONE)))
            {
                presence.setStatus(JabberStatusEnum.ON_THE_PHONE);
            }
            else if(status.equals(jabberStatusEnum.getStatus(
                JabberStatusEnum.IN_A_MEETING)))
            {
                presence.setStatus(JabberStatusEnum.IN_A_MEETING);
            }
            else
                presence.setStatus(statusMessage);
            //presence.addExtension(new Version());

            parentProvider.getConnection().sendPacket(presence);

            if(localContact != null)
                updateResource(localContact,
                    parentProvider.getOurJID(), presence);
        }

        fireProviderStatusChangeEvent(currentStatus, status);

        String oldStatusMessage = getCurrentStatusMessage();

        /*
         * XXX Use StringUtils.isEquals instead of String.equals to avoid a
         * NullPointerException.
         */
        if(!org.jitsi.util.StringUtils.isEquals(
                oldStatusMessage,
                statusMessage))
        {
            currentStatusMessage = statusMessage;
            fireProviderStatusMessageChangeEvent(
                    oldStatusMessage,
                    getCurrentStatusMessage());
        }
    }

    /**
     * Gets the <tt>PresenceStatus</tt> of a contact with a specific
     * <tt>String</tt> identifier.
     *
     * @param contactIdentifier the identifier of the contact whose status we're
     * interested in.
     * @return the <tt>PresenceStatus</tt> of the contact with the specified
     * <tt>contactIdentifier</tt>
     * @throws IllegalArgumentException if the specified
     * <tt>contactIdentifier</tt> does not identify a contact known to the
     * underlying protocol provider
     * @throws IllegalStateException if the underlying protocol provider is not
     * registered/signed on a public service
     * @throws OperationFailedException with code NETWORK_FAILURE if retrieving
     * the status fails due to errors experienced during network communication
     */
    public PresenceStatus queryContactStatus(String contactIdentifier)
        throws IllegalArgumentException,
               IllegalStateException,
               OperationFailedException
    {
        /*
         * As stated by the javadoc, IllegalStateException signals that the
         * ProtocolProviderService is not registered.
         */
        assertConnected();

        Connection xmppConnection = parentProvider.getConnection();

        if (xmppConnection == null)
        {
            throw
                new IllegalArgumentException(
                        "The provider/account must be signed on in order to"
                            + " query the status of a contact in its roster");
        }

        Presence presence
            = xmppConnection.getRoster().getPresence(contactIdentifier);

        if(presence != null)
            return jabberStatusToPresenceStatus(presence, parentProvider);
        else
        {
            return
                parentProvider.getJabberStatusEnum().getStatus(
                        JabberStatusEnum.OFFLINE);
        }
    }

    /**
     * Removes the specified group from the server stored contact list.
     *
     * @param group the group to remove.
     */
    public void removeServerStoredContactGroup(ContactGroup group)
        throws OperationFailedException
    {
        assertConnected();

        if( !(group instanceof ContactGroupJabberImpl) )
            throw new IllegalArgumentException(
                "The specified group is not an jabber contact group: " + group);

        ssContactList.removeGroup(((ContactGroupJabberImpl)group));
    }

    /**
     * Removes the specified group change listener so that it won't receive
     * any further events.
     *
     * @param listener the ServerStoredGroupChangeListener to remove
     */
    @Override
    public void removeServerStoredGroupChangeListener(ServerStoredGroupListener
        listener)
    {
        ssContactList.removeGroupListener(listener);
    }

    /**
     * Renames the specified group from the server stored contact list.
     *
     * @param group the group to rename.
     * @param newName the new name of the group.
     */
    public void renameServerStoredContactGroup(ContactGroup group,
                                               String newName)
    {
        assertConnected();

        if( !(group instanceof ContactGroupJabberImpl) )
            throw new IllegalArgumentException(
                "The specified group is not an jabber contact group: " + group);

        ssContactList.renameGroup((ContactGroupJabberImpl)group, newName);
    }

    /**
     * Handler for incoming authorization requests.
     *
     * @param handler an instance of an AuthorizationHandler for
     *   authorization requests coming from other users requesting
     *   permission add us to their contact list.
     */
    public void setAuthorizationHandler(AuthorizationHandler handler)
    {
        subscribtionPacketListener.setHandler(handler);
    }

    /**
     * Persistently adds a subscription for the presence status of the
     * contact corresponding to the specified contactIdentifier and indicates
     * that it should be added to the specified group of the server stored
     * contact list.
     *
     * @param parent the parent group of the server stored contact list
     *   where the contact should be added. <p>
     * @param contactIdentifier the contact whose status updates we are
     *   subscribing for.
     * @throws IllegalArgumentException if <tt>contact</tt> or
     *   <tt>parent</tt> are not a contact known to the underlying protocol
     *   provider.
     * @throws IllegalStateException if the underlying protocol provider is
     *   not registered/signed on a public service.
     * @throws OperationFailedException with code NETWORK_FAILURE if
     *   subscribing fails due to errors experienced during network
     *   communication
     */
    public void subscribe(ContactGroup parent, String contactIdentifier)
        throws IllegalArgumentException, IllegalStateException,
               OperationFailedException
    {
        assertConnected();

        if(! (parent instanceof ContactGroupJabberImpl) )
            throw new IllegalArgumentException(
                "Argument is not an jabber contact group (group="
                            + parent + ")");

        ssContactList.addContact(parent, contactIdentifier);
    }

    /**
     * Adds a subscription for the presence status of the contact
     * corresponding to the specified contactIdentifier.
     *
     * @param contactIdentifier the identifier of the contact whose status
     *   updates we are subscribing for. <p>
     * @throws IllegalArgumentException if <tt>contact</tt> is not a contact
     *   known to the underlying protocol provider
     * @throws IllegalStateException if the underlying protocol provider is
     *   not registered/signed on a public service.
     * @throws OperationFailedException with code NETWORK_FAILURE if
     *   subscribing fails due to errors experienced during network
     *   communication
     */
    public void subscribe(String contactIdentifier) throws
        IllegalArgumentException, IllegalStateException,
        OperationFailedException
    {
        assertConnected();

        ssContactList.addContact(contactIdentifier);
    }

    /**
     * Removes a subscription for the presence status of the specified
     * contact.
     *
     * @param contact the contact whose status updates we are unsubscribing
     *   from.
     * @throws IllegalArgumentException if <tt>contact</tt> is not a contact
     *   known to the underlying protocol provider
     * @throws IllegalStateException if the underlying protocol provider is
     *   not registered/signed on a public service.
     * @throws OperationFailedException with code NETWORK_FAILURE if
     *   unsubscribing fails due to errors experienced during network
     *   communication
     */
    public void unsubscribe(Contact contact) throws IllegalArgumentException,
        IllegalStateException, OperationFailedException
    {
        assertConnected();

        if(! (contact instanceof ContactJabberImpl) )
            throw new IllegalArgumentException(
                "Argument is not an jabber contact (contact=" + contact + ")");

        ssContactList.removeContact((ContactJabberImpl)contact);
    }

    /**
     * Converts the specified jabber status to one of the status fields of the
     * JabberStatusEnum class.
     *
     * @param presence the Jabber Status
     * @param jabberProvider the parent provider.
     * @return a PresenceStatus instance representation of the Jabber Status
     * parameter. The returned result is one of the JabberStatusEnum fields.
     */
    public static PresenceStatus jabberStatusToPresenceStatus(
        Presence presence, ProtocolProviderServiceJabberImpl jabberProvider)
    {
        JabberStatusEnum jabberStatusEnum =
            jabberProvider.getJabberStatusEnum();
        // fixing issue: 336
        // from the smack api :
        // A null presence mode value is interpreted to be the same thing
        // as Presence.Mode.available.
        if(presence.getMode() == null && presence.isAvailable())
            return jabberStatusEnum.getStatus(JabberStatusEnum.AVAILABLE);
        else if(presence.getMode() == null && !presence.isAvailable())
            return jabberStatusEnum.getStatus(JabberStatusEnum.OFFLINE);

        Presence.Mode mode = presence.getMode();

        if(mode.equals(Presence.Mode.available))
            return jabberStatusEnum.getStatus(JabberStatusEnum.AVAILABLE);
        else if(mode.equals(Presence.Mode.away))
        {
            // on the phone a special status
            // which is away with custom status message
            if(presence.getStatus() != null
                && presence.getStatus().contains(JabberStatusEnum.ON_THE_PHONE))
                return jabberStatusEnum.getStatus(JabberStatusEnum.ON_THE_PHONE);
            else if(presence.getStatus() != null
                && presence.getStatus().contains(JabberStatusEnum.IN_A_MEETING))
                return jabberStatusEnum.getStatus(JabberStatusEnum.IN_A_MEETING);
            else
                return jabberStatusEnum.getStatus(JabberStatusEnum.AWAY);
        }
        else if(mode.equals(Presence.Mode.chat))
            return jabberStatusEnum.getStatus(JabberStatusEnum.FREE_FOR_CHAT);
        else if(mode.equals(Presence.Mode.dnd))
            return jabberStatusEnum.getStatus(JabberStatusEnum.DO_NOT_DISTURB);
        else if(mode.equals(Presence.Mode.xa))
            return jabberStatusEnum.getStatus(JabberStatusEnum.EXTENDED_AWAY);
        else
        {
            //unknown status
            if(presence.isAway())
                return jabberStatusEnum.getStatus(JabberStatusEnum.AWAY);
            if(presence.isAvailable())
                return jabberStatusEnum.getStatus(JabberStatusEnum.AVAILABLE);

            return jabberStatusEnum.getStatus(JabberStatusEnum.OFFLINE);
        }
    }

    /**
     * Converts the specified JabberStatusEnum member to the corresponding
     * Jabber Mode
     *
     * @param status the jabberStatus
     * @return a PresenceStatus instance
     */
    public static Presence.Mode presenceStatusToJabberMode(
            PresenceStatus status)
    {
        return scToJabberModesMappings.get(status
            .getStatusName());
    }

    /**
     * Utility method throwing an exception if the stack is not properly
     * initialized.
     *
     * @throws IllegalStateException if the underlying stack is not registered
     * and initialized.
     */
    void assertConnected()
        throws IllegalStateException
    {
        if (parentProvider == null)
        {
            throw
                new IllegalStateException(
                        "The provider must be non-null and signed on the"
                            + " Jabber service before being able to"
                            + " communicate.");
        }
        if (!parentProvider.isRegistered())
        {
            // if we are not registered but the current status is online
            // change the current status
            if((currentStatus != null) && currentStatus.isOnline())
            {
                fireProviderStatusChangeEvent(
                    currentStatus,
                    parentProvider.getJabberStatusEnum().getStatus(
                            JabberStatusEnum.OFFLINE));
            }

            throw
                new IllegalStateException(
                        "The provider must be signed on the Jabber service"
                            + " before being able to communicate.");
        }
    }

    /**
     * Fires provider status change.
     *
     * @param oldStatus old status
     * @param newStatus new status
     */
    @Override
    public void fireProviderStatusChangeEvent(
        PresenceStatus oldStatus,
        PresenceStatus newStatus)
    {
        if (!oldStatus.equals(newStatus))
        {
            currentStatus = newStatus;

            super.fireProviderStatusChangeEvent(oldStatus, newStatus);

            PresenceStatus offlineStatus =
                    parentProvider.getJabberStatusEnum().getStatus(
                        JabberStatusEnum.OFFLINE);

            if(newStatus.equals(offlineStatus))
            {
                //send event notifications saying that all our buddies are
                //offline. The protocol does not implement top level buddies
                //nor subgroups for top level groups so a simple nested loop
                //would be enough.
                Iterator<ContactGroup> groupsIter =
                    getServerStoredContactListRoot().subgroups();
                while(groupsIter.hasNext())
                {
                    ContactGroup group = groupsIter.next();

                    Iterator<Contact> contactsIter = group.contacts();

                    while(contactsIter.hasNext())
                    {
                        ContactJabberImpl contact
                            = (ContactJabberImpl)contactsIter.next();

                        updateContactStatus(contact, offlineStatus);
                    }
                }

                //do the same for all contacts in the root group
                Iterator<Contact> contactsIter
                    = getServerStoredContactListRoot().contacts();

                while (contactsIter.hasNext())
                {
                    ContactJabberImpl contact
                        = (ContactJabberImpl) contactsIter.next();

                    updateContactStatus(contact, offlineStatus);
                }
            }
        }
    }

    /**
     * Sets the display name for <tt>contact</tt> to be <tt>newName</tt>.
     * <p>
     * @param contact the <tt>Contact</tt> that we are renaming
     * @param newName a <tt>String</tt> containing the new display name for
     * <tt>metaContact</tt>.
     * @throws IllegalArgumentException if <tt>contact</tt> is not an
     * instance that belongs to the underlying implementation.
     */
    @Override
    public void setDisplayName(Contact contact, String newName)
        throws IllegalArgumentException
    {
        assertConnected();

        if(! (contact instanceof ContactJabberImpl) )
            throw new IllegalArgumentException(
                "Argument is not an jabber contact (contact=" + contact + ")");

        RosterEntry entry = ((ContactJabberImpl)contact).getSourceEntry();
        if(entry != null)
            entry.setName(newName);
    }

    /**
     * Our listener that will tell us when we're registered to server
     * and is ready to accept us as a listener.
     */
    private class RegistrationStateListener
        implements RegistrationStateChangeListener
    {
        /**
         * The method is called by a ProtocolProvider implementation whenever
         * a change in the registration state of the corresponding provider had
         * occurred.
         * @param evt ProviderStatusChangeEvent the event describing the status
         * change.
         */
        public void registrationStateChanged(RegistrationStateChangeEvent evt)
        {
            if (logger.isDebugEnabled())
                logger.debug("The Jabber provider changed state from: "
                         + evt.getOldState()
                         + " to: " + evt.getNewState());

            if(evt.getNewState() == RegistrationState.REGISTERING)
            {
                // we will add listener for RosterPackets
                // as this will indicate when one is received
                // and we are ready to dispatch the contact list
                // note that our listener will be added just before the
                // one used in the Roster itself, but later we
                // will wait for it to be ready
                // (inside method XMPPConnection.getRoster())
                parentProvider.getConnection().addPacketListener(
                    new ServerStoredListInit(),
                    new PacketTypeFilter(RosterPacket.class)
                );

                // will be used to store presence events till roster is
                // initialized
                contactChangesListener = new ContactChangesListener();

                // Adds subscription listener as soon as connection is created
                // or we can miss some subscription requests
                if(subscribtionPacketListener == null)
                {
                    subscribtionPacketListener =
                        new JabberSubscriptionListener();
                    parentProvider
                        .getConnection()
                            .addPacketListener(
                                subscribtionPacketListener,
                                new PacketTypeFilter(Presence.class));
                }
            }
            else if(evt.getNewState() == RegistrationState.REGISTERED)
            {
                createContactPhotoPresenceListener();
                createAccountPhotoPresenceInterceptor();
            }
            else if(evt.getNewState() == RegistrationState.UNREGISTERED
                 || evt.getNewState() == RegistrationState.AUTHENTICATION_FAILED
                 || evt.getNewState() == RegistrationState.CONNECTION_FAILED)
            {
                //since we are disconnected, we won't receive any further status
                //updates so we need to change by ourselves our own status as
                //well as set to offline all contacts in our contact list that
                //were online
                PresenceStatus oldStatus = currentStatus;
                PresenceStatus offlineStatus =
                    parentProvider.getJabberStatusEnum().getStatus(
                        JabberStatusEnum.OFFLINE);
                currentStatus = offlineStatus;
                clearLocalContactResources();

                fireProviderStatusChangeEvent(oldStatus, currentStatus);

                ssContactList.cleanup();

                Connection connection = parentProvider.getConnection();
                if(connection != null)
                {
                    connection.removePacketListener(subscribtionPacketListener);

                    // the roster is guaranteed to be non-null
                    connection.getRoster()
                        .removeRosterListener(contactChangesListener);
                }

                subscribtionPacketListener = null;
                contactChangesListener = null;
            }
        }
    }

    /**
     * Updates the resources for the contact.
     * @param contact the contact which resources to update.
     * @param removeUnavailable whether to remove unavailable resources.
     * @return whether resource has been updated
     */
    private boolean updateResources(ContactJabberImpl contact,
                                 boolean removeUnavailable)
    {
        if (!contact.isResolved()
            || (contact instanceof VolatileContactJabberImpl
                && ((VolatileContactJabberImpl)contact)
                        .isPrivateMessagingContact()))
            return false;

        boolean eventFired = false;
        Map<String, ContactResourceJabberImpl> resources =
            contact.getResourcesMap();

        // Do not obtain getRoster if we are not connected, or new Roster
        // will be created, all the resources that will be returned will be
        // unavailable. As we are not connected if set remove all resources
        if( parentProvider.getConnection() == null
            || !parentProvider.getConnection().isConnected())
        {
            if(removeUnavailable)
            {
                Iterator<Map.Entry<String, ContactResourceJabberImpl>>
                    iter = resources.entrySet().iterator();
                while(iter.hasNext())
                {
                    Map.Entry<String, ContactResourceJabberImpl> entry
                        = iter.next();

                    iter.remove();

                    contact.fireContactResourceEvent(
                        new ContactResourceEvent(contact, entry.getValue(),
                            ContactResourceEvent.RESOURCE_REMOVED));
                    eventFired = true;
                }
            }
            return eventFired;
        }

        Iterator<Presence> it =
            parentProvider.getConnection().getRoster()
                .getPresences(contact.getAddress());

        // Choose the resource which has the highest priority AND supports
        // Jingle, if we have two resources with same priority take
        // the most available.
        while(it.hasNext())
        {
            Presence presence = it.next();

            eventFired = updateResource(contact, null, presence) || eventFired;
        }

        if(!removeUnavailable)
            return eventFired;

        Iterator<String> resourceIter = resources.keySet().iterator();
        while (resourceIter.hasNext())
        {
            String fullJid = resourceIter.next();

            if(!parentProvider.getConnection().getRoster()
                    .getPresenceResource(fullJid).isAvailable())
            {
                eventFired = removeResource(contact, fullJid) || eventFired;
            }
        }

        return eventFired;
    }

    /**
     * Update the resources for the contact for the received presence.
     * @param contact the contact which resources to update.
     * @param fullJid the full jid to use, if null will use those from the
     * presence packet
     * @param presence the presence packet to use to get info.
     * @return whether resource has been updated
     */
    private boolean updateResource(ContactJabberImpl contact,
                                   String fullJid,
                                   Presence presence)
    {

        if(fullJid == null)
            fullJid = presence.getFrom();

        String resource = StringUtils.parseResource(fullJid);

        if (resource != null && resource.length() > 0)
        {
            Map<String, ContactResourceJabberImpl> resources =
                contact.getResourcesMap();

            ContactResourceJabberImpl contactResource
                = resources.get(fullJid);

            PresenceStatus newPresenceStatus
                = OperationSetPersistentPresenceJabberImpl
                    .jabberStatusToPresenceStatus(presence, parentProvider);

            if (contactResource == null)
            {
                contactResource = createResource(presence, fullJid, contact);

                resources.put(fullJid, contactResource);

                contact.fireContactResourceEvent(
                    new ContactResourceEvent(contact, contactResource,
                        ContactResourceEvent.RESOURCE_ADDED));
                return true;
            }
            else
            {
                boolean oldIndicator = contactResource.isMobile();
                boolean newIndicator =
                    mobileIndicator.isMobileResource(resource, fullJid);
                int oldPriority = contactResource.getPriority();

                // update mobile indicator, as cabs maybe added after
                // creating the resource for the contact
                contactResource.setMobile(newIndicator);

                contactResource.setPriority(presence.getPriority());
                if(oldPriority != contactResource.getPriority())
                {
                    // priority has been updated so update and the
                    // mobile indicator before firing an event
                    mobileIndicator.resourcesUpdated(contact);
                }

                if (contactResource.getPresenceStatus().getStatus()
                        != newPresenceStatus.getStatus()
                    || (oldIndicator != newIndicator)
                    || (oldPriority != contactResource.getPriority()))
                {
                    contactResource.setPresenceStatus(newPresenceStatus);

                    contact.fireContactResourceEvent(
                        new ContactResourceEvent(contact, contactResource,
                            ContactResourceEvent.RESOURCE_MODIFIED));
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Removes the resource indicated by the fullJid from the list with
     * resources for the contact.
     * @param contact from its list of resources to remove
     * @param fullJid the full jid.
     * @return whether resource has been updated
     */
    private boolean removeResource(ContactJabberImpl contact, String fullJid)
    {
        Map<String, ContactResourceJabberImpl> resources =
            contact.getResourcesMap();

        if (resources.containsKey(fullJid))
        {
            ContactResource removedResource = resources.remove(fullJid);

            contact.fireContactResourceEvent(
                new ContactResourceEvent(contact, removedResource,
                    ContactResourceEvent.RESOURCE_REMOVED));
            return true;
        }

        return false;
    }

    /**
     * Fires the status change, respecting resource priorities.
     * @param presence the presence changed.
     */
    void firePresenceStatusChanged(Presence presence)
    {
        if(contactChangesListener != null)
            contactChangesListener.firePresenceStatusChanged(presence);
    }

    /**
     * Updates contact status and its resources, fires PresenceStatusChange
     * events.
     *
     * @param contact the contact which presence to update if needed.
     * @param newStatus the new status.
     */
    private void updateContactStatus(
        ContactJabberImpl contact, PresenceStatus newStatus)
    {
        // When status changes this may be related to a change in the
        // available resources.
        boolean oldMobileIndicator = contact.isMobile();
        boolean resourceUpdated = updateResources(contact, true);
        mobileIndicator.resourcesUpdated(contact);

        PresenceStatus oldStatus
            = contact.getPresenceStatus();

        // when old and new status are the same do nothing
        // no change
        if(oldStatus.equals(newStatus)
            && oldMobileIndicator == contact.isMobile())
        {
            return;
        }

        contact.updatePresenceStatus(newStatus);

        if (logger.isDebugEnabled())
            logger.debug("Will Dispatch the contact status event.");

        fireContactPresenceStatusChangeEvent(
            contact, contact.getParentContactGroup(),
            oldStatus, newStatus,
            resourceUpdated);
    }

    /**
     * Manage changes of statuses by resource.
     */
    class ContactChangesListener
        implements RosterListener
    {
        /**
         * Store events for later processing, used when
         * initializing contactlist.
         */
        private boolean storeEvents = false;

        /**
         * Stored presences for later processing.
         */
        private List<Presence> storedPresences = null;

        /**
         * Map containing all statuses for a userID.
         */
        private final Map<String, TreeSet<Presence>> statuses =
            new Hashtable<String, TreeSet<Presence>>();

        /**
         * Not used here.
         * @param addresses list of addresses added
         */
        public void entriesAdded(Collection<String> addresses)
        {}

        /**
         * Not used here.
         * @param addresses list of addresses updated
         */
        public void entriesUpdated(Collection<String> addresses)
        {}

        /**
         * Not used here.
         * @param addresses list of addresses deleted
         */
        public void entriesDeleted(Collection<String> addresses)
        {}

        /**
         * Not used here.
         */
        public void rosterError(XMPPError error, Packet packet)
        {}

        /**
         * Received on resource status change.
         * @param presence presence that has changed
         */
        public void presenceChanged(Presence presence)
        {
            firePresenceStatusChanged(presence);
        }

        /**
         * Whether listener is currently storing presence events.
         * @return
         */
        boolean isStoringPresenceEvents()
        {
            return storeEvents;
        }

        /**
         * Adds presence packet to the list.
         * @param presence presence packet
         */
        void addPresenceEvent(Presence presence)
        {
            storedPresences.add(presence);
        }

        /**
         * Sets store events to true.
         */
        void storeEvents()
        {
            this.storedPresences = new ArrayList<Presence>();
            this.storeEvents = true;
        }

        /**
         * Process stored presences.
         */
        void processStoredEvents()
        {
            storeEvents = false;
            for(Presence p : storedPresences)
            {
                firePresenceStatusChanged(p);
            }
            storedPresences.clear();
            storedPresences = null;
        }

        /**
         * Fires the status change, respecting resource priorities.
         *
         * @param presence the presence changed.
         */
        void firePresenceStatusChanged(Presence presence)
        {
            if(storeEvents && storedPresences != null)
            {
                storedPresences.add(presence);
                return;
            }

            try
            {
                String userID
                    = StringUtils.parseBareAddress(presence.getFrom());

                OperationSetMultiUserChat mucOpSet =
                    parentProvider.getOperationSet(
                        OperationSetMultiUserChat.class);
                if(mucOpSet != null)
                {
                    List<ChatRoom> chatRooms
                        = mucOpSet.getCurrentlyJoinedChatRooms();
                    for(ChatRoom chatRoom : chatRooms)
                    {
                        if(chatRoom.getName().equals(userID))
                        {
                            userID = presence.getFrom();
                            break;
                        }
                    }
                }

                if (logger.isDebugEnabled())
                    logger.debug("Received a status update for buddy=" + userID);

                // all contact statuses that are received from all its resources
                // ordered by priority(higher first) and those with equal
                // priorities order with the one that is most connected as
                // first
                TreeSet<Presence> userStats = statuses.get(userID);
                if(userStats == null)
                {
                    userStats = new TreeSet<Presence>(new Comparator<Presence>()
                     {
                        public int compare(Presence o1, Presence o2)
                        {
                            int res = o2.getPriority() - o1.getPriority();

                            // if statuses are with same priorities
                            // return which one is more available
                            // counts the JabberStatusEnum order
                            if(res == 0)
                            {
                                res = jabberStatusToPresenceStatus(
                                        o2, parentProvider).getStatus()
                                      - jabberStatusToPresenceStatus(
                                            o1, parentProvider).getStatus();
                                // We have run out of "logical" ways to order
                                // the presences inside the TreeSet. We have
                                // make sure we are consinstent with equals.
                                // We do this by comparing the unique resource
                                // names. If this evaluates to 0 again, then we
                                // can safely assume this presence object 
                                // represents the same resource and by that the
                                // same client.
                                if(res == 0)
                                {
                                    res = o1.getFrom().compareTo(
                                        o2.getFrom());
                                }
                            }

                            return res;
                        }
                    });
                    statuses.put(userID, userStats);
                }
                else
                {
                    String resource = StringUtils.parseResource(
                            presence.getFrom());

                    // remove the status for this resource
                    // if we are online we will update its value with the new
                    // status
                    for (Iterator<Presence> iter = userStats.iterator();
                            iter.hasNext();)
                    {
                        Presence p = iter.next();

                        if (StringUtils.parseResource(p.getFrom()).equals(
                                resource))
                            iter.remove();
                    }
                }

                if(!jabberStatusToPresenceStatus(presence, parentProvider)
                        .equals(
                            parentProvider
                                .getJabberStatusEnum()
                                    .getStatus(JabberStatusEnum.OFFLINE)))
                {
                    userStats.add(presence);
                }

                Presence currentPresence;
                if (userStats.size() == 0)
                {
                    currentPresence = presence;

                    /*
                     * We no longer have statuses for userID so it doesn't make
                     * sense to retain (1) the TreeSet and (2) its slot in the
                     * statuses Map.
                     */
                    statuses.remove(userID);
                }
                else
                    currentPresence = userStats.first();

                ContactJabberImpl sourceContact
                    = ssContactList.findContactById(userID);

                if (sourceContact == null)
                {
                    logger.warn("No source contact found for id=" + userID);
                    return;
                }

                // statuses may be the same and only change in status message
                sourceContact.setStatusMessage(currentPresence.getStatus());

                updateContactStatus(
                    sourceContact,
                    jabberStatusToPresenceStatus(
                        currentPresence, parentProvider));
            }
            catch (IllegalStateException ex)
            {
                logger.error("Failed changing status", ex);
            }
            catch (IllegalArgumentException ex)
            {
                logger.error("Failed changing status", ex);
            }
        }
    }

    /**
     * Listens for subscription events coming from stack.
     */
    private class JabberSubscriptionListener
        implements PacketListener
    {
        /**
         * The authorization handler.
         */
        private AuthorizationHandler handler = null;

        /**
         * List of early subscriptions.
         */
        private Map<String, String> earlySubscriptions
            = new HashMap<String, String>();

        /**
         * Adds auth handler.
         *
         * @param handler
         */
        private synchronized void setHandler(AuthorizationHandler handler)
        {
            this.handler = handler;

            handleEarlySubscribeReceived();
        }

        /**
         * Process packets.
         * @param packet packet received to be processed
         */
        public void processPacket(Packet packet)
        {
            Presence presence = (Presence) packet;

            if (presence == null)
                return;

            Presence.Type presenceType = presence.getType();
            final String fromID = presence.getFrom();

            if (presenceType == Presence.Type.subscribe)
            {
                String displayName = null;
                Nick ext = (Nick) presence.getExtension(Nick.NAMESPACE);
                if(ext != null)
                    displayName = ext.getName();

                synchronized(this)
                {
                    if(handler == null)
                    {
                        earlySubscriptions.put(fromID, displayName);

                        // nothing to handle
                        return;
                    }
                }


                handleSubscribeReceived(fromID, displayName);
            }
            else if (presenceType == Presence.Type.unsubscribed)
            {
                if (logger.isTraceEnabled())
                    logger.trace(fromID + " does not allow your subscription");

                if(handler == null)
                {
                    logger.warn(
                        "No to handle unsubscribed AuthorizationHandler for "
                            + fromID);
                    return;
                }

                ContactJabberImpl contact
                    = ssContactList.findContactById(fromID);

                if(contact != null)
                {
                    AuthorizationResponse response
                        = new AuthorizationResponse(
                                AuthorizationResponse.REJECT,
                                "");

                    handler.processAuthorizationResponse(response, contact);
                    try{
                        ssContactList.removeContact(contact);
                    }
                    catch(OperationFailedException e)
                    {
                        logger.error(
                                "Cannot remove contact that unsubscribed.");
                    }
                }
            }
            else if (presenceType == Presence.Type.subscribed)
            {
                if(handler == null)
                {
                    logger.warn(
                        "No AuthorizationHandler to handle subscribed for "
                            + fromID);
                    return;
                }

                ContactJabberImpl contact
                    = ssContactList.findContactById(fromID);
                AuthorizationResponse response
                    = new AuthorizationResponse(
                            AuthorizationResponse.ACCEPT,
                            "");

                handler.processAuthorizationResponse(response, contact);
            }
            else if (presenceType == Presence.Type.available
                    && contactChangesListener != null
                    && contactChangesListener.isStoringPresenceEvents())
            {
                contactChangesListener.addPresenceEvent(presence);
            }
        }

        /**
         * Handles early presence subscribe that were received.
         */
        private void handleEarlySubscribeReceived()
        {
            for(String from : earlySubscriptions.keySet())
            {
                handleSubscribeReceived(from, earlySubscriptions.get(from));
            }

            earlySubscriptions.clear();
        }

        /**
         * Handles receiving a presence subscribe
         * @param fromID sender
         */
        private void handleSubscribeReceived(final String fromID,
            final String displayName)
        {
            // run waiting for user response in different thread
            // as this seems to block the packet dispatch thread
            // and we don't receive anything till we unblock it
            new Thread(new Runnable() {
            public void run()
            {
                if (logger.isTraceEnabled())
                {
                    logger.trace(
                            fromID
                                + " wants to add you to its contact list");
                }

                // buddy want to add you to its roster
                ContactJabberImpl srcContact
                    = ssContactList.findContactById(fromID);

                Presence.Type responsePresenceType = null;

                if(srcContact == null)
                {
                    srcContact = createVolatileContact(fromID, displayName);
                }
                else
                {
                    if(srcContact.isPersistent())
                        responsePresenceType = Presence.Type.subscribed;
                }

                if(responsePresenceType == null)
                {
                    AuthorizationRequest req = new AuthorizationRequest();
                    AuthorizationResponse response
                        = handler.processAuthorisationRequest(
                                        req, srcContact);

                    if(response != null)
                    {
                        if(response.getResponseCode()
                               .equals(AuthorizationResponse.ACCEPT))
                        {
                            responsePresenceType
                                = Presence.Type.subscribed;
                            if (logger.isInfoEnabled())
                                logger.info(
                                    "Sending Accepted Subscription");
                        }
                        else if(response.getResponseCode()
                                .equals(AuthorizationResponse.REJECT))
                        {
                            responsePresenceType
                                = Presence.Type.unsubscribed;
                            if (logger.isInfoEnabled())
                                logger.info(
                                    "Sending Rejected Subscription");
                        }
                    }
                }

                // subscription ignored
                if(responsePresenceType == null)
                    return;

                Presence responsePacket = new Presence(
                        responsePresenceType);

                responsePacket.setTo(fromID);
                parentProvider.getConnection().sendPacket(responsePacket);

            }}).start();
        }
    }

    /**
     * Runnable that resolves our list against the server side roster.
     * This thread is the one which will call getRoster for the first time.
     * And if roster is currently processing will wait for it (the wait
     * is internal into XMPPConnection.getRoster method).
     */
    private class ServerStoredListInit
        implements Runnable,
                   PacketListener
    {
        public void run()
        {
            // we are already notified lets remove us from the packet
            // listener
            parentProvider.getConnection()
                .removePacketListener(this);

            // init ssList
            ssContactList.init(contactChangesListener);

            // as we have dispatched the contact list and Roster is ready
            // lets start the jingle nodes discovery
            parentProvider.startJingleNodesDiscovery();
        }

        /**
         * When roster packet with no error is received we are ready to
         * to dispatch the contact list, doing it in different thread
         * to avoid blocking xmpp packet receiving.
         * @param packet the roster packet
         */
        public void processPacket(Packet packet)
        {
            // don't process packets that are errors
            if(packet.getError() != null)
            {
                return;
            }

            new Thread(this, getClass().getName()).start();
        }
    }

    /**
     * Creates an interceptor which modifies presence packet in order to add the
     * the element name "x" and the namespace "vcard-temp:x:update" in order to
     * advertise the avatar SHA-1 hash.
     */
    public void createAccountPhotoPresenceInterceptor()
    {
        // Verifies that we creates only one interceptor of this type.
        if(this.vCardTempXUpdatePresenceExtension == null)
        {
            byte[] avatar = null;
            try
            {
                // Retrieves the current server avatar.
                VCard vCard = new VCard();
                vCard.load(parentProvider.getConnection());
                avatar = vCard.getAvatar();
            }
            catch(XMPPException ex)
            {
                logger.info("Can not retrieve account avatar for "
                    + parentProvider.getOurJID() + ": " + ex.getMessage());
            }

            // Creates the presence extension to generates the  the element
            // name "x" and the namespace "vcard-temp:x:update" containing
            // the avatar SHA-1 hash.
            this.vCardTempXUpdatePresenceExtension =
                new VCardTempXUpdatePresenceExtension(avatar);

            // Intercepts all sent presence packet in order to add the
            // photo tag.
            parentProvider.getConnection().addPacketInterceptor(
                    this.vCardTempXUpdatePresenceExtension,
                    new PacketTypeFilter(Presence.class));
        }
    }

    /**
     * Updates the presence extension to advertise a new photo SHA-1 hash
     * corresponding to the new avatar given in parameter.
     *
     * @param imageBytes The new avatar set for this account.
     */
    public void updateAccountPhotoPresenceExtension(byte[] imageBytes)
    {
        try
        {
            // If the image has changed, then updates the presence extension and
            // send immediately a presence packet to advertise the photo update.
            if(this.vCardTempXUpdatePresenceExtension.updateImage(imageBytes))
            {
                this.publishPresenceStatus(currentStatus, currentStatusMessage);
            }
        }
        catch(OperationFailedException ex)
        {
            logger.info(
                    "Can not send presence extension to broadcast photo update",
                    ex);
        }
    }

    /**
     * Creates a listener to call a parser which manages presence packets with
     * the element name "x" and the namespace "vcard-temp:x:update".
     */
    public void createContactPhotoPresenceListener()
    {
        // Registers the listener.
        parentProvider.getConnection().addPacketListener(
            new PacketListener()
            {
                public void processPacket(Packet packet)
                {
                    // Calls the parser to manages this presence packet.
                    parseContactPhotoPresence(packet);
                }
            },
            // Creates a filter to only listen to presence packet with the
            // element name "x" and the namespace "vcard-temp:x:update".
            new AndFilter(new PacketTypeFilter(Presence.class),
                new PacketExtensionFilter(
                    VCardTempXUpdatePresenceExtension.ELEMENT_NAME,
                    VCardTempXUpdatePresenceExtension.NAMESPACE)
                )
            );
    }

    /**
     * Parses a contact presence packet with the element name "x" and the
     * namespace "vcard-temp:x:update", in order to decide if the SHA-1 avatar
     * contained in the photo tag represents a new avatar for this contact.
     *
     * @param packet The packet received to parse.
     */
    public void parseContactPhotoPresence(Packet packet)
    {
        // Retrieves the contact ID and its avatar that Jitsi currently
        // managed concerning the peer that has send this presence packet.
        String userID
            = StringUtils.parseBareAddress(packet.getFrom());
        ContactJabberImpl sourceContact
            = ssContactList.findContactById(userID);

        /**
         * If this contact is not yet in our contact list, then there is no need
         * to manage this photo update.
         */
        if(sourceContact == null)
        {
            return;
        }

        byte[] currentAvatar = sourceContact.getImage(false);

        // Get the packet extension which contains the photo tag.
        DefaultPacketExtension defaultPacketExtension =
            (DefaultPacketExtension) packet.getExtension(
                    VCardTempXUpdatePresenceExtension.ELEMENT_NAME,
                    VCardTempXUpdatePresenceExtension.NAMESPACE);
        if(defaultPacketExtension != null)
        {
            try
            {
                String packetPhotoSHA1 =
                    defaultPacketExtension.getValue("photo");
                // If this presence packet has a photo tag with a SHA-1 hash
                // which differs from the current avatar SHA-1 hash, then Jitsi
                // retrieves the new avatar image and updates this contact image
                // in the contact list.
                if(packetPhotoSHA1 != null
                        && !packetPhotoSHA1.equals(
                            VCardTempXUpdatePresenceExtension.getImageSha1(
                                currentAvatar))
                  )
                {
                    byte[] newAvatar = null;

                    // If there is an avatar image, retrieves it.
                    if(packetPhotoSHA1.length() != 0)
                    {
                        // Retrieves the new contact avatar image.
                        VCard vCard = new VCard();
                        vCard.load(parentProvider.getConnection(), userID);
                        newAvatar = vCard.getAvatar();
                    }
                    // Else removes the current avatar image, since the contact
                    // has removed it from the server.
                    else
                    {
                        newAvatar = new byte[0];
                    }

                    // Sets the new avatar image to the Jitsi contact.
                    sourceContact.setImage(newAvatar);
                    // Fires a property change event to update the contact list.
                    this.fireContactPropertyChangeEvent(
                        ContactPropertyChangeEvent.PROPERTY_IMAGE,
                        sourceContact,
                        currentAvatar,
                        newAvatar);
                }
            }
            catch(XMPPException ex)
            {
                logger.info("Cannot retrieve vCard from: " + packet.getFrom());
                if(logger.isTraceEnabled())
                    logger.trace("vCard retrieval exception was: ", ex);
            }
        }
    }

    /**
     * Initializes the map with priorities and statuses which we will use when
     * changing statuses.
     */
    private void initializePriorities()
    {
        try
        {
            this.resourcePriorityAvailable =
                Integer.parseInt(parentProvider.getAccountID()
                    .getAccountPropertyString(
                        ProtocolProviderFactory.RESOURCE_PRIORITY));
        }
        catch(NumberFormatException ex)
        {
            logger.error("Wrong value for resource priority", ex);
        }

        addDefaultValue(JabberStatusEnum.AWAY, -5);
        addDefaultValue(JabberStatusEnum.EXTENDED_AWAY, -10);
        addDefaultValue(JabberStatusEnum.ON_THE_PHONE, -15);
        addDefaultValue(JabberStatusEnum.IN_A_MEETING, -16);
        addDefaultValue(JabberStatusEnum.DO_NOT_DISTURB, -20);
        addDefaultValue(JabberStatusEnum.FREE_FOR_CHAT, +5);
    }

    /**
     * Checks for account property that can override this status.
     * If missing use the shift value to create the priority to use, make sure
     * it is not zero or less than it.
     * @param statusName the status to check/create priority
     * @param availableShift the difference from available resource
     *                       value to use.
     */
    private void addDefaultValue(String statusName, int availableShift)
    {
        String resourcePriority = getAccountPriorityForStatus(statusName);
        if(resourcePriority != null)
        {
            try
            {
                addPresenceToPriorityMapping(
                    statusName,
                    Integer.parseInt(resourcePriority));
            }
            catch(NumberFormatException ex)
            {
                logger.error(
                    "Wrong value for resource priority for status: "
                        + statusName, ex);
            }
        }
        else
        {
            // if priority is less than zero, use the available priority
            int priority = resourcePriorityAvailable + availableShift;
            if(priority <= 0)
                priority = resourcePriorityAvailable;

            addPresenceToPriorityMapping(statusName, priority);
        }
    }

    /**
     * Adds the priority mapping for the <tt>statusName</tt>.
     * Make sure we replace ' ' with '_' and use upper case as this will be
     * and the property names used in account properties that can override
     * this values.
     * @param statusName the status name to use
     * @param value and its priority
     */
    private static void addPresenceToPriorityMapping(String statusName,
                                                     int value)
    {
        statusToPriorityMappings.put(
            statusName.replaceAll(" ", "_").toUpperCase(), value);
    }

    /**
     * Returns the priority which will be used for <tt>statusName</tt>.
     * Make sure we replace ' ' with '_' and use upper case as this will be
     * and the property names used in account properties that can override
     * this values.
     * @param statusName the status name
     * @return the priority which will be used for <tt>statusName</tt>.
     */
    private int getPriorityForPresenceStatus(String statusName)
    {
        Integer priority = statusToPriorityMappings.get(
                                statusName.replaceAll(" ", "_").toUpperCase());
        if(priority == null)
            return resourcePriorityAvailable;

        return priority;
    }

    /**
     * Returns the account property value for a status name, if missing return
     * null.
     * Make sure we replace ' ' with '_' and use upper case as this will be
     * and the property names used in account properties that can override
     * this values.
     * @param statusName
     * @return the account property value for a status name, if missing return
     * null.
     */
    private String getAccountPriorityForStatus(String statusName)
    {
        return parentProvider.getAccountID().getAccountPropertyString(
                    ProtocolProviderFactory.RESOURCE_PRIORITY + "_" +
                        statusName.replaceAll(" ", "_").toUpperCase());
    }

    /**
     * Returns the contactlist impl.
     * @return
     */
    public ServerStoredContactListJabberImpl getSsContactList()
    {
        return ssContactList;
    }
}
