/*
 * 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.contactlist;

import java.util.*;

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

/**
 * A default implementation of the <code>MetaContact</code> interface.
 *
 * @author Emil Ivov
 * @author Lubomir Marinov
 */
public class MetaContactImpl
    extends DataObject
    implements MetaContact
{
    /**
     * Logger for <tt>MetaContactImpl</tt>.
     */
    private static final Logger logger
        = Logger.getLogger(MetaContactImpl.class);

    /**
     * A vector containing all protocol specific contacts merged in this
     * MetaContact.
     */
    private final List<Contact> protoContacts = new Vector<Contact>();

    /**
     * The list of capabilities of the meta contact.
     */
    private final Map<String, List<Contact>> capabilities
        = new HashMap<String, List<Contact>>();

    /**
     * The number of contacts online in this meta contact.
     */
    private int contactsOnline = 0;

    /**
     * An id uniquely identifying the meta contact in this contact list.
     */
    private final String uid;

    /**
     * Returns a human readable string used by the UI to display the contact.
     */
    private String displayName = "";

    /**
     * The contact that should be chosen by default when communicating with this
     * meta contact.
     */
    private Contact defaultContact = null;

    /**
     * A locally cached copy of an avatar that we should return for lazy calls
     * to the getAvatarMethod() in order to speed up display.
     */
    private byte[] cachedAvatar = null;

    /**
     * A flag that tells us whether or not we have already tried to restore
     * an avatar from cache. We need this to know whether a <tt>null</tt> cached
     * avatar implies that there is no locally stored avatar or that we simply
     * haven't tried to retrieve it. This should allow us to only interrogate
     * the file system if haven't done so before.
     */
    private boolean avatarFileCacheAlreadyQueried = false;

    /**
     * A callback to the meta contact group that is currently our parent. If
     * this is an orphan meta contact that has not yet been added or has been
     * removed from a group this callback is going to be null.
     */
    private MetaContactGroupImpl parentGroup = null;

    /**
     * Hashtable containing the contact details.
     * Name -> Value or Name -> (List of values).
     */
    private Map<String, List<String>> details;

    /**
     * Whether user has renamed this meta contact.
     */
    private boolean isDisplayNameUserDefined = false;

    /**
     * Creates new meta contact with a newly generated meta contact UID.
     */
    MetaContactImpl()
    {
        //create the uid
        this.uid = String.valueOf(System.currentTimeMillis())
                   + String.valueOf(hashCode());
        this.details = null;
    }

    /**
     * Creates a new meta contact with the specified UID. This constructor
     * MUST ONLY be used when restoring contacts stored in the contactlist.xml.
     *
     * @param metaUID the meta uid that this meta contact should have.
     * @param details the already stored details for the contact.
     */
    MetaContactImpl(String metaUID, Map<String, List<String>> details)
    {
        this.uid = metaUID;
        this.details = details;
    }

    /**
     * Returns the number of protocol specific <tt>Contact</tt>s that this
     * <tt>MetaContact</tt> contains.
     *
     * @return an int indicating the number of protocol specific contacts
     *   merged in this <tt>MetaContact</tt>
     */
    public int getContactCount()
    {
        return protoContacts.size();
    }

    /**
     * Returns a Contact, encapsulated by this MetaContact and coming from
     * the specified ProtocolProviderService.
     * <p>
     * In order to prevent problems with concurrency, the <tt>Iterator</tt>
     * returned by this method is not be over the actual list of contacts but
     * over a copy of that list.
     *
     * @param provider a reference to the <tt>ProtocolProviderService</tt>
     *   that we'd like to get a <tt>Contact</tt> for.
     * @return a <tt>Contact</tt> encapsulated in this <tt>MetaContact</tt>
     *   and originating from the specified provider.
     */
    public Iterator<Contact> getContactsForProvider(
                                    ProtocolProviderService provider)
    {
        LinkedList<Contact> providerContacts = new LinkedList<Contact>();

        for (Contact contact : protoContacts)
        {
            if(contact.getProtocolProvider() == provider)
                providerContacts.add( contact );
        }

        return providerContacts.iterator();
    }

    /**
     * Returns all protocol specific Contacts, encapsulated by this MetaContact
     * and supporting the given <tt>opSetClass</tt>. If none of the
     * contacts encapsulated by this MetaContact is supporting the specified
     * <tt>OperationSet</tt> class then an empty iterator is returned.
     * <p>
     * @param opSetClass the operation for which the default contact is needed
     * @return a <tt>List</tt> over all contacts encapsulated in this
     * <tt>MetaContact</tt> and supporting the specified <tt>OperationSet</tt>
     */
    public List<Contact> getContactsForOperationSet(
                                    Class<? extends OperationSet> opSetClass)
    {
        LinkedList<Contact> opSetContacts = new LinkedList<Contact>();

        for (Contact contact : protoContacts)
        {
            ProtocolProviderService contactProvider
                = contact.getProtocolProvider();
            // First try to ask the capabilities operation set if such is
            // available.
            OperationSetContactCapabilities capOpSet
                = contactProvider.getOperationSet(
                        OperationSetContactCapabilities.class);

            if (capOpSet != null)
            {
                synchronized (capabilities)
                {
                    List<Contact> capContacts
                        = capabilities.get(opSetClass.getName());
                    if (capContacts != null && capContacts.contains(contact))
                    {
                        opSetContacts.add(contact);
                    }
                }
            }
            else if (contactProvider.getOperationSet(opSetClass) != null)
            {
                opSetContacts.add(contact);
            }
        }

        return opSetContacts;
    }

    /**
     * Returns contacts, encapsulated by this MetaContact and belonging to
     * the specified protocol ContactGroup.
     * <p>
     * In order to prevent problems with concurrency, the <tt>Iterator</tt>
     * returned by this method is not be over the actual list of contacts but
     * over a copy of that list.
     *
     * @param parentProtoGroup a reference to the <tt>ContactGroup</tt>
     *   whose children we'd like removed..
     * @return an Iterator over all <tt>Contact</tt>s encapsulated in this
     * <tt>MetaContact</tt> and belonging to the specified proto ContactGroup.
     */
    public Iterator<Contact> getContactsForContactGroup(
                                            ContactGroup parentProtoGroup)
    {
        List<Contact> providerContacts = new LinkedList<Contact>();

        for (Contact contact : protoContacts)
        {
            if(contact.getParentContactGroup() == parentProtoGroup)
                providerContacts.add( contact );
        }

        return providerContacts.iterator();
    }


    /**
     * Returns a contact encapsulated by this meta contact, having the specified
     * contactAddress and coming from the indicated ownerProvider.
     * <p>
     * @param contactAddress the address of the contact who we're looking for.
     * @param ownerProvider a reference to the ProtocolProviderService that
     * the contact we're looking for belongs to.
     * @return a reference to a <tt>Contact</tt>, encapsulated by this
     * MetaContact, carrying the specified address and originating from the
     * specified ownerProvider or null if no such contact exists..
     */
    public Contact getContact(String contactAddress,
                              ProtocolProviderService ownerProvider)
    {
        for (Contact contact : protoContacts)
        {
            if(contact.getProtocolProvider() == ownerProvider
               && (contact.getAddress().equals(contactAddress)
                    || contact.equals(contactAddress)))
                return contact;
        }

        return null;
    }

    /**
     * Returns a contact encapsulated by this meta contact, having the specified
     * contactAddress and coming from a provider with a mathing
     * <tt>accountID</tt>. The method returns null if no such contact exists.
     * <p>
     * @param contactAddress the address of the contact who we're looking for.
     * @param accountID the identifier of the provider that the contact we're
     * looking for must belong to.
     * @return a reference to a <tt>Contact</tt>, encapsulated by this
     * MetaContact, carrying the specified address and originating from the
     * ownerProvider carryign <tt>accountID</tt>.
     */
    public Contact getContact(String contactAddress,
                              String accountID)
    {
        for (Contact contact : protoContacts)
        {
            if(  contact.getProtocolProvider().getAccountID()
                    .getAccountUniqueID().equals(accountID)
               && contact.getAddress().equals(contactAddress))
                return contact;
        }

        return null;
    }

    /**
     * Returns <tt>true</tt> if the given <tt>protocolContact</tt> is contained
     * in this <tt>MetaContact</tt>, otherwise - returns <tt>false</tt>.
     * @param protocolContact the <tt>Contact</tt> we're looking for
     * @return <tt>true</tt> if the given <tt>protocolContact</tt> is contained
     * in this <tt>MetaContact</tt>, otherwise - returns <tt>false</tt>
     */
    public boolean containsContact(Contact protocolContact)
    {
        return protoContacts.contains(protocolContact);
    }

    /**
     * Returns a <tt>java.util.Iterator</tt> over all protocol specific
     * <tt>Contacts</tt> encapsulated by this <tt>MetaContact</tt>.
     * <p>
     * In order to prevent problems with concurrency, the <tt>Iterator</tt>
     * returned by this method is not over the actual list of contacts but over
     * a copy of that list.
     * <p>
     * @return a <tt>java.util.Iterator</tt> over all protocol specific
     * <tt>Contact</tt>s that were registered as subcontacts for this
     * <tt>MetaContact</tt>
     */
    public Iterator<Contact> getContacts()
    {
        return new LinkedList<Contact>(protoContacts).iterator();
    }

    /**
     * Currently simply returns the most connected protocol contact. We should
     * add the possibility to choose it also according to preconfigured
     * preferences.
     *
     * @return the default <tt>Contact</tt> to use when communicating with
     *   this <tt>MetaContact</tt>
     */
    public Contact getDefaultContact()
    {
        if(defaultContact == null)
        {
            PresenceStatus currentStatus = null;
            for (Contact protoContact : protoContacts)
            {
                PresenceStatus contactStatus = protoContact.getPresenceStatus();

                if (currentStatus != null)
                {
                    if (currentStatus.getStatus() < contactStatus.getStatus())
                    {
                        currentStatus = contactStatus;
                        defaultContact = protoContact;
                    }
                }
                else
                {
                    currentStatus = contactStatus;
                    defaultContact = protoContact;
                }
            }
        }
        return defaultContact;
    }

    /**
     * Returns a default contact for a specific operation (call,
     * file transfer, IM ...)
     *
     * @param operationSet the operation for which the default contact is needed
     * @return the default contact for the specified operation.
     */
    public Contact getDefaultContact(Class<? extends OperationSet> operationSet)
    {
        Contact defaultOpSetContact = null;

        Contact defaultContact = getDefaultContact();

        // if the current default contact supports the requested operationSet
        // we use it
        if (defaultContact != null)
        {
            ProtocolProviderService contactProvider
                = defaultContact.getProtocolProvider();

            // First try to ask the capabilities operation set if such is
            // available.
            OperationSetContactCapabilities capOpSet = contactProvider
                .getOperationSet(OperationSetContactCapabilities.class);

            if (capOpSet != null)
            {
                synchronized (capabilities)
                {
                    List<Contact> capContacts
                        = capabilities.get(operationSet.getName());
                    if (capContacts != null
                        && capContacts.contains(defaultContact))
                    {
                        defaultOpSetContact = defaultContact;
                    }
                }
            }
            else if (contactProvider.getOperationSet(operationSet) != null)
            {
                defaultOpSetContact = defaultContact;
            }
        }

        if (defaultOpSetContact == null)
        {
            PresenceStatus currentStatus = null;

            for (Contact protoContact : protoContacts)
            {
                ProtocolProviderService contactProvider
                    = protoContact.getProtocolProvider();

                // First try to ask the capabilities operation set if such is
                // available.
                OperationSetContactCapabilities capOpSet = contactProvider
                    .getOperationSet(OperationSetContactCapabilities.class);

                // We filter to care only about contact which support
                // the needed opset.
                if (capOpSet != null)
                {
                    synchronized (capabilities)
                    {
                        List<Contact> capContacts
                            = capabilities.get(operationSet.getName());
    
                        if (capContacts == null
                                || !capContacts.contains(protoContact))
                        {
                            continue;
                        }
                    }
                }
                else if (contactProvider.getOperationSet(operationSet) == null)
                    continue;

                PresenceStatus contactStatus
                        = protoContact.getPresenceStatus();

                if (currentStatus != null)
                {
                    if (currentStatus.getStatus()
                            < contactStatus.getStatus())
                    {
                        currentStatus = contactStatus;
                        defaultOpSetContact = protoContact;
                    }
                }
                else
                {
                    currentStatus = contactStatus;
                    defaultOpSetContact = protoContact;
                }
            }
        }
        return defaultOpSetContact;
    }

    /**
     * Returns a String identifier (the actual contents is left to
     * implementations) that uniquely represents this <tt>MetaContact</tt> in
     * the containing <tt>MetaContactList</tt>
     *
     * @return a String uniquely identifying this meta contact.
     */
    public String getMetaUID()
    {
        return uid;
    }

    /**
     * Compares this meta contact with the specified object for order.  Returns
     * a negative integer, zero, or a positive integer as this meta contact is
     * less than, equal to, or greater than the specified object.
     * <p>
     * The result of this method is calculated the following way:
     * <p>
     * (contactsOnline - o.contactsOnline) * 1 000 000  <br>
     * + getDisplayName().compareTo(o.getDisplayName()) * 100 000
     * + getMetaUID().compareTo(o.getMetaUID())<br>
     * <p>
     * Or in other words ordering of meta accounts would be first done by
     * presence status, then display name, and finally (in order to avoid
     * equalities) be the fairly random meta contact metaUID.
     * <p>
     * @param   o the <code>MetaContact</code> to be compared.
     * @return  a negative integer, zero, or a positive integer as this object
     *      is less than, equal to, or greater than the specified object.
     *
     * @throws ClassCastException if the specified object is not
     *          a MetaContactListImpl
     */
    public int compareTo(MetaContact o)
    {
        MetaContactImpl target = (MetaContactImpl) o;

        int isOnline
            = (contactsOnline > 0)
            ? 1
            : 0;
        int targetIsOnline
            = (target.contactsOnline > 0)
            ? 1
            : 0;

        return ( (10 - isOnline) - (10 - targetIsOnline)) * 100000000
            + getDisplayName().compareToIgnoreCase(target.getDisplayName())
            * 10000
            + getMetaUID().compareTo(target.getMetaUID());
    }

    /**
     * Returns a string representation of this contact, containing most of its
     * representative details.
     *
     * @return  a string representation of this contact.
     */
    @Override
    public String toString()
    {
        StringBuffer buff = new StringBuffer("MetaContact[ DisplayName=")
            .append(getDisplayName()).append("]");

        return buff.toString();
    }


    /**
     * Returns a characteristic display name that can be used when including
     * this <tt>MetaContact</tt> in user interface.
     * @return a human readable String that represents this meta contact.
     */
    public String getDisplayName()
    {
        return displayName;
    }

    /**
     * Determines if display name was changed for
     * this <tt>MetaContact</tt> in user interface.
     * @return whether display name was changed by user.
     */
    boolean isDisplayNameUserDefined()
    {
        return isDisplayNameUserDefined;
    }

    /**
     * Changes that display name was changed for
     * this <tt>MetaContact</tt> in user interface.
     * @param value control whether display name is user defined
     */
    void setDisplayNameUserDefined(boolean value)
    {
        this.isDisplayNameUserDefined = value;
    }

    /**
     * Queries a specific protocol <tt>Contact</tt> for its avatar. Beware that
     * this method could cause multiple network operations. Use with caution.
     *
     * @param contact the protocol <tt>Contact</tt> to query for its avatar
     * @return an array of <tt>byte</tt>s representing the avatar returned by
     * the specified <tt>Contact</tt> or <tt>null</tt> if the specified
     * <tt>Contact</tt> did not or failed to return an avatar
     */
    private byte[] queryProtoContactAvatar(Contact contact)
    {
        try
        {
            byte[] contactImage = contact.getImage();

            if ((contactImage != null) && (contactImage.length > 0))
                return contactImage;
        }
        catch (Exception ex)
        {
            logger.error("Failed to get the photo of contact " + contact, ex);
        }
        return null;
    }

    /**
     * Returns the avatar of this contact, that can be used when including this
     * <tt>MetaContact</tt> in user interface. The isLazy parameter would tell
     * the implementation if it could return the locally stored avatar or it
     * should obtain the avatar right from the server.
     *
     * @param isLazy Indicates if this method should return the locally stored
     * avatar or it should obtain the avatar right from the server.
     * @return an avatar (e.g. user photo) of this contact.
     */
    public byte[] getAvatar(boolean isLazy)
    {
        byte[] result = null;
        if (!isLazy)
        {
            // the caller is willing to perform a lengthy operation so let's
            // query the proto contacts for their avatars.
            Iterator<Contact> protoContacts = getContacts();

            while( protoContacts.hasNext())
            {
                Contact contact = protoContacts.next();
                result = queryProtoContactAvatar(contact);

                // if we got a result from the above, then let's cache and
                // return it.
                if ((result != null) && (result.length > 0))
                {
                    cacheAvatar(contact, result);
                    return result;
                }
            }
        }

        //if we get here then the caller is probably not willing to perform
        //network operations and opted for a lazy retrieve (... or the
        //queryAvatar method returned null because we are calling it too often)
        if((cachedAvatar != null) && (cachedAvatar.length > 0))
        {
            //we already have a cached avatar, so let's return it
            return cachedAvatar;
        }

        //no cached avatar. let's try the file system for previously stored
        //ones. (unless we already did this)
        if ( avatarFileCacheAlreadyQueried )
            return null;
        avatarFileCacheAlreadyQueried = true;

        Iterator<Contact> iter = this.getContacts();

        while (iter.hasNext())
        {
            Contact protoContact = iter.next();

            cachedAvatar = AvatarCacheUtils.getCachedAvatar(protoContact);
            /*
             * Caching a zero-length avatar happens but such an avatar isn't
             * very useful.
             */
            if ((cachedAvatar != null) && (cachedAvatar.length > 0))
                return cachedAvatar;
        }

        return null;
    }

    /**
     * Returns an avatar that can be used when presenting this
     * <tt>MetaContact</tt> in user interface. The method would also make sure
     * that we try the network for new versions of avatars.
     *
     * @return an avatar (e.g. user photo) of this contact.
     */
    public byte[] getAvatar()
    {
        return getAvatar(false);
    }

    /**
     * Sets a name that can be used when displaying this contact in user
     * interface components.
     * @param displayName a human readable String representing this
     * <tt>MetaContact</tt>
     */
    void setDisplayName(String displayName)
    {
        synchronized (getParentGroupModLock())
        {
            if (parentGroup != null)
                parentGroup.lightRemoveMetaContact(this);

            this.displayName = (displayName == null) ? "" : displayName;

            if (parentGroup != null)
                parentGroup.lightAddMetaContact(this);
        }
    }

    /**
     * Adds the specified protocol specific contact to the list of contacts
     * merged in this meta contact. The method also keeps up to date the
     * contactsOnline field which is used in the compareTo() method.
     *
     * @param contact the protocol specific Contact to add.
     */
    void addProtoContact(Contact contact)
    {
        synchronized (getParentGroupModLock())
        {
            if (parentGroup != null)
                parentGroup.lightRemoveMetaContact(this);
            contactsOnline += contact.getPresenceStatus().isOnline() ? 1 : 0;

            this.protoContacts.add(contact);

            // Re-init the default contact.
            defaultContact = null;

            // if this is our first contact and we don't already have a display
            // name, use theirs.
            if (this.protoContacts.size() == 1
                && (this.displayName == null || this.displayName.trim()
                    .length() == 0))
            {
                // be careful not to use setDisplayName() here cause this will
                // bring us into a deadlock.
                this.displayName = contact.getDisplayName();
            }

            if (parentGroup != null)
                parentGroup.lightAddMetaContact(this);

            ProtocolProviderService contactProvider
                = contact.getProtocolProvider();

            // Check if the capabilities operation set is available for this
            // contact and add a listener to it in order to track capabilities'
            // changes for all contained protocol contacts.
            OperationSetContactCapabilities capOpSet
                = contactProvider
                    .getOperationSet(OperationSetContactCapabilities.class);

            if (capOpSet != null)
            {
                addCapabilities(contact,
                    capOpSet.getSupportedOperationSets(contact));
            }
        }
    }

    /**
     * Called by MetaContactListServiceImpl after a contact has changed its
     * status, so that ordering in the parent group is updated. The method also
     * elects the most connected contact as default contact.
     *
     * @return the new index at which the contact was added.
     */
    int reevalContact()
    {
        synchronized (getParentGroupModLock())
        {
            //first lightremove or otherwise we won't be able to get hold of the
            //contact
            if (parentGroup != null)
            {
                parentGroup.lightRemoveMetaContact(this);
            }

            this.contactsOnline = 0;
            int maxContactStatus = 0;

            for (Contact contact : protoContacts)
            {
                int contactStatus = contact.getPresenceStatus()
                        .getStatus();

                if(maxContactStatus < contactStatus)
                {
                    maxContactStatus = contactStatus;
                    this.defaultContact = contact;
                }
                if (contact.getPresenceStatus().isOnline())
                    contactsOnline++;
            }
            //now read it and the contact would be automatically placed
            //properly by the containing group
            if (parentGroup != null)
            {
                return parentGroup.lightAddMetaContact(this);
            }
        }

        return -1;
    }

    /**
     * Removes the specified protocol specific contact from the contacts
     * encapsulated in this <code>MetaContact</code>. The method also updates
     * the total status field accordingly. And updates its ordered position
     * in its parent group. If the display name of this <code>MetaContact</code>
     * was the one of the removed contact, we update it.
     *
     * @param contact the contact to remove
     */
    void removeProtoContact(Contact contact)
    {
        synchronized (getParentGroupModLock())
        {
            if (parentGroup != null)
                parentGroup.lightRemoveMetaContact(this);
            contactsOnline -= contact.getPresenceStatus().isOnline() ? 1 : 0;
            this.protoContacts.remove(contact);

            if (defaultContact == contact)
                defaultContact = null;

            if ((protoContacts.size() > 0)
                    && displayName.equals(contact.getDisplayName()))
            {
                displayName = getDefaultContact().getDisplayName();
            }

            if (parentGroup != null)
                parentGroup.lightAddMetaContact(this);

            ProtocolProviderService contactProvider
                = contact.getProtocolProvider();

            // Check if the capabilities operation set is available for this
            // contact and add a listener to it in order to track capabilities'
            // changes for all contained protocol contacts.
            OperationSetContactCapabilities capOpSet
                = contactProvider
                    .getOperationSet(OperationSetContactCapabilities.class);

            if (capOpSet != null)
            {
                removeCapabilities(contact,
                    capOpSet.getSupportedOperationSets(contact));
            }
        }
    }

    /**
     * Removes all proto contacts that belong to the specified provider.
     *
     * @param provider the provider whose contacts we want removed.
     *
     * @return true if this <tt>MetaContact</tt> was modified and false
     * otherwise.
     */
    boolean removeContactsForProvider(ProtocolProviderService provider)
    {
        boolean modified = false;
        Iterator<Contact> contactsIter = protoContacts.iterator();

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

            if (contact.getProtocolProvider() == provider)
            {
                contactsIter.remove();
                modified = true;
            }
        }

        // if the default contact has been modified, set it to null
        if (modified && !protoContacts.contains(defaultContact))
        {
            defaultContact = null;
        }

        return modified;
    }

    /**
     * Removes all proto contacts that belong to the specified protocol group.
     *
     * @param protoGroup the group whose children we want removed.
     *
     * @return true if this <tt>MetaContact</tt> was modified and false
     * otherwise.
     */
    boolean removeContactsForGroup(ContactGroup protoGroup)
    {
        boolean modified = false;
        Iterator<Contact> contactsIter = protoContacts.iterator();

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

            if (contact.getParentContactGroup() == protoGroup)
            {
                contactsIter.remove();
                modified = true;
            }
        }

        // if the default contact has been modified, set it to null
        if (modified && !protoContacts.contains(defaultContact))
        {
            defaultContact = null;
        }

        return modified;
    }


    /**
     * Sets <tt>parentGroup</tt> as a parent of this meta contact. Do not
     * call this method with a null argument even if a group is removing
     * this contact from itself as this could lead to race conditions (imagine
     * another group setting itself as the new parent and you removing it).
     * Use unsetParentGroup instead.
     *
     * @param parentGroup the <tt>MetaContactGroupImpl</tt> that is currently a
     * parent of this meta contact.
     * @throws NullPointerException if <tt>parentGroup</tt> is null.
     */
    void setParentGroup(MetaContactGroupImpl parentGroup)
    {
        if (parentGroup == null)
            throw new NullPointerException("Do not call this method with a "
                + "null argument even if a group is removing this contact "
                + "from itself as this could lead to race conditions "
                + "(imagine another group setting itself as the new "
                + "parent and you  removing it). Use unsetParentGroup "
                + "instead.");

        synchronized (getParentGroupModLock())
        {
            this.parentGroup = parentGroup;
        }
    }

    /**
     * If <tt>parentGroup</tt> was the parent of this meta contact then it
     * sets it to null. Call this method when removing this contact from a
     * meta contact group.
     * @param parentGrp the <tt>MetaContactGroupImpl</tt> that we don't want
     * considered as a parent of this contact any more.
     */
    void unsetParentGroup(MetaContactGroupImpl parentGrp)
    {
        synchronized(getParentGroupModLock())
        {
            if (parentGroup == parentGrp)
                parentGroup = null;
        }
    }

    /**
     * Returns the group that is currently holding this meta contact.
     *
     * @return the group that is currently holding this meta contact.
     */
    MetaContactGroupImpl getParentGroup()
    {
        return parentGroup;
    }

    /**
     * Returns the MetaContactGroup currently containing this meta contact
     * @return a reference to the MetaContactGroup currently containing this
     * meta contact.
     */
    public MetaContactGroup getParentMetaContactGroup()
    {
        return getParentGroup();
    }

    /**
     * Adds a custom detail to this contact.
     * @param name name of the detail.
     * @param value the value of the detail.
     */
    public void addDetail(String name, String value)
    {
        if (details == null)
            details = new Hashtable<String, List<String>>();

        List<String> values = details.get(name);

        if (values == null)
        {
            values = new ArrayList<String>();
            details.put(name, values);
        }

        values.add(value);

        fireMetaContactModified(name, null, value);
    }

    /**
     * Remove the given detail.
     * @param name of the detail to be removed.
     * @param value value of the detail to be removed.
     */
    public void removeDetail(String name, String value)
    {
        if (details == null)
            return;

        List<String> values = details.get(name);
        if (values == null)
            return;

        values.remove(value);

        fireMetaContactModified(name, value, null);
    }

    /**
     * Remove all details with given name.
     * @param name of the details to be removed.
     */
    public void removeDetails(String name)
    {
        if (details == null)
            return;

        Object removed = details.remove(name);

        fireMetaContactModified(name, removed, null);
    }

    /**
     * Change the detail.
     * @param name of the detail to be changed.
     * @param oldValue the old value of the detail.
     * @param newValue the new value of the detail.
     */
    public void changeDetail(String name, String oldValue, String newValue)
    {
        if (details == null)
            return;

        List<String> values = details.get(name);
        if(values == null)
            return;

        int changedIx = values.indexOf(oldValue);
        if(changedIx == -1)
            return;

        values.set(changedIx, newValue);

        fireMetaContactModified(name, oldValue, newValue);
    }

    /**
     * Fires a new <tt>MetaContactModifiedEvent</tt> which is to notify about a
     * modification with a specific name of this <tt>MetaContact</tt> which has
     * caused a property value change from a specific <tt>oldValue</tt> to a
     * specific <tt>newValue</tt>.
     *
     * @param modificationName the name of the modification which has caused
     * a new <tt>MetaContactModifiedEvent</tt> to be fired
     * @param oldValue the value of the property before the modification
     * @param newValue the value of the property after the modification
     */
    private void fireMetaContactModified(
            String modificationName,
            Object oldValue,
            Object newValue)
    {
        MetaContactGroupImpl parentGroup = getParentGroup();

        if (parentGroup != null)
            parentGroup
                .getMclServiceImpl()
                    .fireMetaContactEvent(
                        new MetaContactModifiedEvent(
                                this,
                                modificationName,
                                oldValue,
                                newValue));
    }

    /**
     * Gets all details with a given name.
     *
     * @param name the name of the details we are searching for
     * @return a <tt>List</tt> of <tt>String</tt>s which represent the details
     * with the specified <tt>name</tt>
     */
    public List<String> getDetails(String name)
    {
        List<String> values = (details == null) ? null : details.get(name);

        if(values == null)
            values = new ArrayList<String>();
        else
            values = new ArrayList<String>(values);
        return values;
    }

    /**
     * Stores avatar bytes in the given <tt>Contact</tt>.
     *
     * @param protoContact The contact in which we store the avatar.
     * @param avatarBytes The avatar image bytes.
     */
    public void cacheAvatar( Contact protoContact,
                             byte[] avatarBytes)
    {
        this.cachedAvatar = avatarBytes;
        this.avatarFileCacheAlreadyQueried = true;

        AvatarCacheUtils.cacheAvatar(protoContact, avatarBytes);
    }

    /**
     * Updates the capabilities for the given contact.
     *
     * @param contact the <tt>Contact</tt>, which capabilities have changed
     * @param opSets the new updated set of operation sets
     */
    public void updateCapabilities( Contact contact,
                                    Map<String, ? extends OperationSet> opSets)
    {
        OperationSetContactCapabilities capOpSet
            = contact.getProtocolProvider().getOperationSet(
                OperationSetContactCapabilities.class);

        // This should not happen, because this method is called explicitly for
        // events coming from the capabilities operation set.
        if (capOpSet == null)
            return;

        removeCapabilities(contact, opSets);
        addCapabilities(contact, opSets);
    }

    /**
     * Remove capabilities for the given contacts.
     *
     * @param contact the <tt>Contact</tt>, which capabilities we remove
     * @param opSets the new updated set of operation sets
     */
    private void removeCapabilities(Contact contact,
                                    Map<String, ? extends OperationSet> opSets)
    {
        Set<String> contactNewCaps = opSets.keySet();
        synchronized(capabilities)
        {
            // remove contact from opSet to contact mapping
            capabilities.entrySet()
                .stream()
                .filter(e -> !contactNewCaps.contains(e.getKey()))
                .forEach(e -> e.getValue().remove(contact));
    
            // remove opSets that have no associated contact
            capabilities.entrySet().removeIf(e -> e.getValue().size() == 0);
        }
    }

    /**
     * Adds the capabilities of the given contact.
     *
     * @param contact the <tt>Contact</tt>, which capabilities we add
     * @param opSets the map of operation sets supported by the contact
     */
    private void addCapabilities(   Contact contact,
                                    Map<String, ? extends OperationSet> opSets)
    {
        synchronized (capabilities)
        {
            for (String newCap : opSets.keySet())
            {
                List<Contact> capContacts = capabilities.get(newCap);
                if (capContacts == null)
                {
                    capContacts = new LinkedList<>();
                    capabilities.put(newCap, capContacts);
                }

                capContacts.add(contact);
            }
        }
    }

    /**
     * Gets the sync lock for use when modifying {@link #parentGroup}.
     *
     * @return the sync lock for use when modifying {@link #parentGroup}
     */
    private Object getParentGroupModLock()
    {
        /*
         * XXX The use of uid as parentGroupModLock is a bit unusual but a
         * dedicated lock enlarges the shallow runtime size of this instance and
         * having hundreds of MetaContactImpl instances is not unusual for a
         * multi-protocol application. With respect to parentGroupModLock being
         * unique among the MetaContactImpl instances, uid is fine because it is
         * also supposed to be unique in the same way.
         */
        return uid;
    }
}
