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

import java.io.*;
import java.lang.reflect.*;
import java.security.*;
import java.util.*;
import java.util.concurrent.*;

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

import org.jitsi.service.configuration.*;
import org.jitsi.util.OSUtils;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smack.provider.*;
import org.jivesoftware.smack.util.Base64;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.*;
import org.jivesoftware.smackx.packet.*;
import org.osgi.framework.*;
import org.xmlpull.mxp1.*;
import org.xmlpull.v1.*;

/**
 * Keeps track of entity capabilities.
 *
 * This work is based on Jonas Adahl's smack fork.
 *
 * @author Emil Ivov
 * @author Lyubomir Marinov
 */
public class EntityCapsManager
{
    /**
     * The <tt>Logger</tt> used by the <tt>EntityCapsManager</tt> class and its
     * instances for logging output.
     */
    private static final Logger logger
        = Logger.getLogger(EntityCapsManager.class);

    /**
     * Static OSGi bundle context used by this class.
     */
    private static BundleContext bundleContext;

    /**
     * Configuration service instance used by this class.
     */
    private static ConfigurationService configService;

    /**
     * The prefix of the <tt>ConfigurationService</tt> properties which persist
     * {@link #caps2discoverInfo}.
     */
    private static final String CAPS_PROPERTY_NAME_PREFIX
        = "net.java.sip.communicator.impl.protocol.jabber.extensions.caps."
            + "EntityCapsManager.CAPS.";

    /**
     * An empty array of <tt>UserCapsNodeListener</tt> elements explicitly
     * defined in order to reduce unnecessary allocations.
     */
    private static final UserCapsNodeListener[] NO_USER_CAPS_NODE_LISTENERS
        = new UserCapsNodeListener[0];

    /**
     * The node value to advertise.
     */
    private static String entityNode
        = OSUtils.IS_ANDROID ? "http://android.jitsi.org" : "http://jitsi.org";

    /**
     * The <tt>Map</tt> of <tt>Caps</tt> to <tt>DiscoverInfo</tt> which
     * associates a node#ver with the entity capabilities so that they don't
     * have to be retrieved every time their necessary. Because ver is
     * constructed from the entity capabilities using a specific hash method,
     * the hash method is also associated with the entity capabilities along
     * with the node and the ver in order to disambiguate cases of equal ver
     * values for different entity capabilities constructed using different hash
     * methods.
     */
    private static final Map<Caps, DiscoverInfo> caps2discoverInfo
        = new ConcurrentHashMap<Caps, DiscoverInfo>();

    /**
     * Map of Full JID -&gt; DiscoverInfo/null. In case of c2s connection the
     * key is formed as user@server/resource (resource is required) In case of
     * link-local connection the key is formed as user@host (no resource)
     */
    private final Map<String, Caps> userCaps
        = new ConcurrentHashMap<String, Caps>();

    /**
     * CapsVerListeners gets notified when the version string is changed.
     */
    private final Set<CapsVerListener> capsVerListeners
        = new CopyOnWriteArraySet<CapsVerListener>();

    /**
     * The current hash of our version and supported features.
     */
    private String currentCapsVersion = null;

    /**
     * The list of <tt>UserCapsNodeListener</tt>s interested in events notifying
     * about changes in the list of user caps nodes of this
     * <tt>EntityCapsManager</tt>.
     */
    private final List<UserCapsNodeListener> userCapsNodeListeners
        = new LinkedList<UserCapsNodeListener>();

    static
    {
        ProviderManager.getInstance().addExtensionProvider(
                CapsPacketExtension.ELEMENT_NAME,
                CapsPacketExtension.NAMESPACE,
                new CapsProvider());
    }

    /**
     * Add {@link DiscoverInfo} to our caps database.
     * <p>
     * <b>Warning</b>: The specified <tt>DiscoverInfo</tt> is trusted to be
     * valid with respect to the specified <tt>Caps</tt> for performance reasons
     * because the <tt>DiscoverInfo</tt> should have already been validated in
     * order to be used elsewhere anyway.
     * </p>
     *
     * @param caps the <tt>Caps<tt/> i.e. the node, the hash and the ver for
     * which a <tt>DiscoverInfo</tt> is to be added to our caps database.
     * @param info {@link DiscoverInfo} for the specified <tt>Caps</tt>.
     */
    public static void addDiscoverInfoByCaps(Caps caps, DiscoverInfo info)
    {
        cleanupDiscoverInfo(info);
        /*
         * DiscoverInfo carries the node we're now associating it with a
         * specific node so we'd better keep them in sync.
         */
        info.setNode(caps.getNodeVer());

        synchronized (caps2discoverInfo)
        {
            DiscoverInfo oldInfo = caps2discoverInfo.put(caps, info);

            /*
             * If the specified info is a new association for the specified
             * node, remember it across application instances in order to not
             * query for it over the network.
             */
            if ((oldInfo == null) || !oldInfo.equals(info))
            {
                String xml = info.getChildElementXML();

                if ((xml != null) && (xml.length() != 0))
                {
                    getConfigService()
                        .setProperty(getCapsPropertyName(caps), xml);
                }
            }
        }
    }

    /**
     * Gets the name of the property in the <tt>ConfigurationService</tt> which
     * is or is to be associated with a specific <tt>Caps</tt> value.
     *
     * @param caps the <tt>Caps</tt> value for which the associated
     * <tt>ConfigurationService</tt> property name is to be returned
     * @return the name of the property in the <tt>ConfigurationService</tt>
     * which is or is to be associated with a specific <tt>Caps</tt> value
     */
    private static String getCapsPropertyName(Caps caps)
    {
        return
            CAPS_PROPERTY_NAME_PREFIX
                + caps.node + '#' + caps.hash + '#' + caps.ver;
    }

    /**
     * Returns cached instance of {@link ConfigurationService}.
     */
    private static ConfigurationService getConfigService()
    {
        if (configService == null)
        {
            configService = ServiceUtils.getService(
                bundleContext, ConfigurationService.class);
        }
        return configService;
    }

    /**
     * Sets OSGi bundle context instance that will be used by this class.
     * @param bundleContext the <tt>BundleContext</tt> instance to be used by
     *                      this class or <tt>null</tt> to clear the reference.
     */
    public static void setBundleContext(BundleContext bundleContext)
    {
        if (bundleContext == null)
        {
            configService = null;
        }
        EntityCapsManager.bundleContext = bundleContext;
    }

    /**
     * Add a record telling what entity caps node a user has.
     *
     * @param user the user (Full JID)
     * @param node the node (of the caps packet extension)
     * @param hash the hashing algorithm used to calculate <tt>ver</tt>
     * @param ver the version (of the caps packet extension)
     * @param ext the ext (of the caps packet extension)
     * @param online indicates if the user is online
     */
    private void addUserCapsNode(   String user,
                                    String node,
                                    String hash,
                                    String ver,
                                    String ext,
                                    boolean online)
    {
        if ((user != null) && (node != null) && (hash != null) && (ver != null))
        {
            Caps caps = userCaps.get(user);
            String bareJid=StringUtils.parseBareAddress(user);

            if ((caps == null)
                    || !caps.node.equals(node)
                    || !caps.hash.equals(hash)
                    || !caps.ver.equals(ver))
            {
                caps = new Caps(node, hash, ver, ext);

                userCaps.put(user, caps);
            }
            else
                return;

            // Fire userCapsNodeAdded.
            UserCapsNodeListener[] listeners;

            synchronized (userCapsNodeListeners)
            {
                listeners
                    = userCapsNodeListeners.toArray(
                            NO_USER_CAPS_NODE_LISTENERS);
            }
            if (listeners.length != 0)
            {
                String nodeVer = caps.getNodeVer();

                for (UserCapsNodeListener listener : listeners)
                    listener.userCapsNodeAdded(user,
                        getFullJidsByBareJid(bareJid),
                        nodeVer, online);
            }
        }
    }

    /**
     * Adds a specific <tt>UserCapsNodeListener</tt> to the list of
     * <tt>UserCapsNodeListener</tt>s interested in events notifying about
     * changes in the list of user caps nodes of this
     * <tt>EntityCapsManager</tt>.
     *
     * @param listener the <tt>UserCapsNodeListener</tt> which is interested in
     * events notifying about changes in the list of user caps nodes of this
     * <tt>EntityCapsManager</tt>
     */
    public void addUserCapsNodeListener(UserCapsNodeListener listener)
    {
        if (listener == null)
            throw new NullPointerException("listener");
        synchronized (userCapsNodeListeners)
        {
            if (!userCapsNodeListeners.contains(listener))
                userCapsNodeListeners.add(listener);
        }
    }

    /**
     * Remove records telling what entity caps node a contact has.
     *
     * @param contact the contact
     */
    public void removeContactCapsNode(Contact contact)
    {
        Caps caps = null;
        String lastRemovedJid = null;
        String bareJid=StringUtils.parseBareAddress(
            contact.getAddress());

        Iterator<String> iter = userCaps.keySet().iterator();
        while(iter.hasNext())
        {
            String jid = iter.next();

            if(StringUtils.parseBareAddress(jid).equals(
                contact.getAddress()))
            {
                caps = userCaps.get(jid);
                lastRemovedJid = jid;
                iter.remove();
            }
        }

        // fire only for the last one, at the end the event out
        // of the protocol will be one and for the contact
        if(caps != null)
        {
            UserCapsNodeListener[] listeners;
            synchronized (userCapsNodeListeners)
            {
                listeners
                    = userCapsNodeListeners.toArray(
                            NO_USER_CAPS_NODE_LISTENERS);
            }
            if (listeners.length != 0)
            {
                String nodeVer = caps.getNodeVer();

                for (UserCapsNodeListener listener : listeners)
                    listener.userCapsNodeRemoved(
                        lastRemovedJid,
                        getFullJidsByBareJid(bareJid),
                        nodeVer, false);
            }
        }
    }

    /**
     * Remove a record telling what entity caps node a user has.
     *
     * @param user the user (Full JID)
     */
    public void removeUserCapsNode(String user)
    {
        Caps caps = userCaps.remove(user);
        String bareJid=StringUtils.parseBareAddress(user);

        // Fire userCapsNodeRemoved.
        if (caps != null)
        {
            UserCapsNodeListener[] listeners;

            synchronized (userCapsNodeListeners)
            {
                listeners
                    = userCapsNodeListeners.toArray(
                            NO_USER_CAPS_NODE_LISTENERS);
            }
            if (listeners.length != 0)
            {
                String nodeVer = caps.getNodeVer();

                for (UserCapsNodeListener listener : listeners)
                    listener.userCapsNodeRemoved(user,
                    getFullJidsByBareJid(bareJid),
                    nodeVer, false);
            }
        }
    }

    /**
     * Removes a specific <tt>UserCapsNodeListener</tt> from the list of
     * <tt>UserCapsNodeListener</tt>s interested in events notifying about
     * changes in the list of user caps nodes of this
     * <tt>EntityCapsManager</tt>.
     *
     * @param listener the <tt>UserCapsNodeListener</tt> which is no longer
     * interested in events notifying about changes in the list of user caps
     * nodes of this <tt>EntityCapsManager</tt>
     */
    public void removeUserCapsNodeListener(UserCapsNodeListener listener)
    {
        if (listener != null)
        {
            synchronized (userCapsNodeListeners)
            {
                userCapsNodeListeners.remove(listener);
            }
        }
    }

    /**
     * Gets the <tt>Caps</tt> i.e. the node, the hash and the ver of a user.
     *
     * @param user the user (Full JID)
     * @return the <tt>Caps</tt> i.e. the node, the hash and the ver of
     * <tt>user</tt>
     */
    public Caps getCapsByUser(String user)
    {
        return userCaps.get(user);
    }
    
    /**
     * Gets the full Jids (with resources) as Strings.
     *
     * @param the bare Jid
     * @return the full Jids as an ArrayList <tt>user</tt>
     */
    public ArrayList<String> getFullJidsByBareJid(String bareJid)
    {
        ArrayList<String> jids = new ArrayList<String>();
        for(String jid: userCaps.keySet())
        {
            if(bareJid.equals(StringUtils.parseBareAddress(jid))){
                jids.add(jid);
            }
        }
        return jids;
    }

    /**
     * Get the discover info given a user name. The discover info is returned if
     * the user has a node#ver associated with it and the node#ver has a
     * discover info associated with it.
     *
     * @param user user name (Full JID)
     * @return the discovered info
     */
    public DiscoverInfo getDiscoverInfoByUser(String user)
    {
        Caps caps = userCaps.get(user);

        return (caps == null) ? null : getDiscoverInfoByCaps(caps);
    }

    /**
     * Get our own caps version.
     *
     * @return our own caps version
     */
    public String getCapsVersion()
    {
        return currentCapsVersion;
    }

    /**
     * Get our own entity node.
     *
     * @return our own entity node.
     */
    public String getNode()
    {
        return entityNode;
    }

    /**
     * Set our own entity node.
     *
     * @param node the new node
     */
    public void setNode(String node)
    {
        entityNode = node;
    }

    /**
     * Retrieve DiscoverInfo for a specific node.
     *
     * @param caps the <tt>Caps</tt> i.e. the node, the hash and the ver
     * @return The corresponding DiscoverInfo or null if none is known.
     */
    public static DiscoverInfo getDiscoverInfoByCaps(Caps caps)
    {
        synchronized (caps2discoverInfo)
        {
            DiscoverInfo discoverInfo = caps2discoverInfo.get(caps);

            /*
             * If we don't have the discoverInfo in the runtime cache yet, we
             * may have it remembered in a previous application instance.
             */
            if (discoverInfo == null)
            {
                ConfigurationService configurationService
                    = getConfigService();
                String capsPropertyName = getCapsPropertyName(caps);
                String xml = configurationService.getString(capsPropertyName);

                if ((xml != null) && (xml.length() != 0))
                {
                    IQProvider discoverInfoProvider
                        = (IQProvider)
                            ProviderManager.getInstance().getIQProvider(
                                    "query",
                                    "http://jabber.org/protocol/disco#info");

                    if (discoverInfoProvider != null)
                    {
                        XmlPullParser parser = new MXParser();

                        try
                        {
                            parser.setFeature(
                                    XmlPullParser.FEATURE_PROCESS_NAMESPACES,
                                    true);
                            parser.setInput(new StringReader(xml));
                            // Start the parser.
                            parser.next();
                        }
                        catch (XmlPullParserException xppex)
                        {
                            parser = null;
                        }
                        catch (IOException ioex)
                        {
                            parser = null;
                        }

                        if (parser != null)
                        {
                            try
                            {
                                discoverInfo
                                    = (DiscoverInfo)
                                        discoverInfoProvider.parseIQ(parser);
                            }
                            catch (Exception ex)
                            {
                            }

                            if (discoverInfo != null)
                            {
                                if (caps.isValid(discoverInfo))
                                    caps2discoverInfo.put(caps, discoverInfo);
                                else
                                {
                                    logger.error(
                                            "Invalid DiscoverInfo for "
                                                + caps.getNodeVer()
                                                + ": "
                                                + discoverInfo);
                                    /*
                                     * The discoverInfo doesn't seem valid
                                     * according to the caps which means that we
                                     * must have stored invalid information.
                                     * Delete the invalid information in order
                                     * to not try to validate it again.
                                     */
                                    configurationService.removeProperty(
                                            capsPropertyName);
                                }
                            }
                        }
                    }
                }
            }
            return discoverInfo;
        }
    }

    /**
     * Removes from, to and packet-id from <tt>info</tt>.
     *
     * @param info the {@link DiscoverInfo} that we'd like to cleanup.
     */
    private static void cleanupDiscoverInfo(DiscoverInfo info)
    {
        info.setFrom(null);
        info.setTo(null);
        info.setPacketID(null);
    }

    /**
     * Gets the features of a specific <tt>DiscoverInfo</tt> in the form of a
     * read-only <tt>Feature</tt> <tt>Iterator<tt/> by calling the internal
     * method {@link DiscoverInfo#getFeatures()}.
     *
     * @param discoverInfo the <tt>DiscoverInfo</tt> the features of which are
     * to be retrieved
     * @return a read-only <tt>Feature</tt> <tt>Iterator</tt> which lists the
     * features of the specified <tt>discoverInfo</tt>
     */
    @SuppressWarnings("unchecked")
    private static Iterator<DiscoverInfo.Feature> getDiscoverInfoFeatures(
            DiscoverInfo discoverInfo)
    {
        Method getFeaturesMethod;

        try
        {
            getFeaturesMethod
                = DiscoverInfo.class.getDeclaredMethod("getFeatures");
        }
        catch (NoSuchMethodException nsmex)
        {
            throw new UndeclaredThrowableException(nsmex);
        }
        getFeaturesMethod.setAccessible(true);
        try
        {
            return
                (Iterator<DiscoverInfo.Feature>)
                    getFeaturesMethod.invoke(discoverInfo);
        }
        catch (IllegalAccessException iaex)
        {
            throw new UndeclaredThrowableException(iaex);
        }
        catch (InvocationTargetException itex)
        {
            throw new UndeclaredThrowableException(itex);
        }
    }

    /**
     * Registers this Manager's listener with <tt>connection</tt>.
     *
     * @param connection the connection that we'd like this manager to register
     * with.
     */
    public void addPacketListener(Connection connection)
    {
        PacketFilter filter = new PacketTypeFilter(Presence.class);

        connection.addPacketListener(new CapsPacketListener(), filter);
    }

    /**
     * Adds <tt>listener</tt> to the list of {@link CapsVerListener}s that we
     * notify when new features occur and the version hash needs to be
     * regenerated. The method would also notify <tt>listener</tt> if our
     * current caps version has been generated and is different than
     * <tt>null</tt>.
     *
     * @param listener the {@link CapsVerListener} we'd like to register.
     */
    public void addCapsVerListener(CapsVerListener listener)
    {
        synchronized (capsVerListeners)
        {
            if (capsVerListeners.contains(listener))
                return;

            capsVerListeners.add(listener);

            if (currentCapsVersion != null)
                listener.capsVerUpdated(currentCapsVersion);
        }
    }

    /**
     * Removes <tt>listener</tt> from the list of currently registered
     * {@link CapsVerListener}s.
     *
     * @param listener the {@link CapsVerListener} we'd like to unregister.
     */
    public void removeCapsVerListener(CapsVerListener listener)
    {
        synchronized(capsVerListeners)
        {
            capsVerListeners.remove(listener);
        }
    }

    /**
     * Notifies all currently registered {@link CapsVerListener}s that the
     * version hash has changed.
     */
    private void fireCapsVerChanged()
    {
        List<CapsVerListener> listenersCopy = null;

        synchronized(capsVerListeners)
        {
            listenersCopy = new ArrayList<CapsVerListener>(capsVerListeners);
        }

        for (CapsVerListener listener : listenersCopy)
            listener.capsVerUpdated(currentCapsVersion);
    }

    /**
     * Computes and returns the hash of the specified <tt>capsString</tt> using
     * the specified <tt>hashAlgorithm</tt>.
     *
     * @param hashAlgorithm the name of the algorithm to be used to generate the
     * hash
     * @param capsString the capabilities string that we'd like to compute a
     * hash for.
     *
     * @return the hash of <tt>capsString</tt> computed by the specified
     * <tt>hashAlgorithm</tt> or <tt>null</tt> if generating the hash has failed
     */
    private static String capsToHash(String hashAlgorithm, String capsString)
    {
        try
        {
            MessageDigest md = MessageDigest.getInstance(hashAlgorithm);
            byte[] digest = md.digest(capsString.getBytes());

            return Base64.encodeBytes(digest);
        }
        catch (NoSuchAlgorithmException nsae)
        {
            logger.error(
                    "Unsupported XEP-0115: Entity Capabilities hash algorithm: "
                        + hashAlgorithm);
            return null;
        }
    }

    /**
     * Converts the form field values in the <tt>ffValuesIter</tt> into a
     * caps string.
     *
     * @param ffValuesIter the {@link Iterator} containing the form field
     * values.
     * @param capsBldr a <tt>StringBuilder</tt> to which the caps string
     * representing the form field values is to be appended
     */
    private static void formFieldValuesToCaps(
            Iterator<String> ffValuesIter,
            StringBuilder capsBldr)
    {
        SortedSet<String> fvs = new TreeSet<String>();

        while( ffValuesIter.hasNext())
            fvs.add(ffValuesIter.next());

        for (String fv : fvs)
            capsBldr.append(fv).append('<');
    }

    /**
     * Calculates the <tt>String</tt> for a specific <tt>DiscoverInfo</tt> which
     * is to be hashed in order to compute the ver string for that
     * <tt>DiscoverInfo</tt>.
     *
     * @param discoverInfo the <tt>DiscoverInfo</tt> for which the
     * <tt>String</tt> to be hashed in order to compute its ver string is to be
     * calculated
     * @return the <tt>String</tt> for <tt>discoverInfo</tt> which is to be
     * hashed in order to compute its ver string
     */
    private static String calculateEntityCapsString(DiscoverInfo discoverInfo)
    {
        StringBuilder bldr = new StringBuilder();

        // Add identities
        {
            Iterator<DiscoverInfo.Identity> identities = discoverInfo.getIdentities();
            SortedSet<DiscoverInfo.Identity> is
                = new TreeSet<DiscoverInfo.Identity>(
                        new Comparator<DiscoverInfo.Identity>()
                        {
                            public int compare(
                                    DiscoverInfo.Identity i1,
                                    DiscoverInfo.Identity i2)
                            {
                                int category
                                    = i1.getCategory().compareTo(
                                            i2.getCategory());

                                if (category != 0)
                                    return category;

                                int type = i1.getType().compareTo(i2.getType());

                                if (type != 0)
                                    return type;

                                /*
                                 * TODO Sort by xml:lang.
                                 *
                                 * Since sort by xml:lang is currently missing,
                                 * use the last supported sort criterion i.e.
                                 * type.
                                 */
                                return type;
                            }
                        });

            if (identities != null)
                while (identities.hasNext())
                    is.add(identities.next());

            for (DiscoverInfo.Identity i : is)
            {
                bldr
                    .append(i.getCategory())
                        .append('/')
                            .append(i.getType())
                                .append("//")
                                    .append(i.getName())
                                        .append('<');
            }
        }

        // Add features
        {
            Iterator<DiscoverInfo.Feature> features
                = getDiscoverInfoFeatures(discoverInfo);
            SortedSet<String> fs = new TreeSet<String>();

            if (features != null)
                while (features.hasNext())
                    fs.add(features.next().getVar());

            for (String f : fs)
                bldr.append(f).append('<');
        }

        DataForm extendedInfo
            = (DataForm) discoverInfo.getExtension("x", "jabber:x:data");

        if (extendedInfo != null)
        {
            synchronized (extendedInfo)
            {
                SortedSet<FormField> fs
                    = new TreeSet<FormField>(
                            new Comparator<FormField>()
                            {
                                public int compare(FormField f1,
                                                FormField f2)
                                {
                                    return
                                        f1.getVariable().compareTo(
                                                f2.getVariable());
                                }
                            });

                FormField formType = null;

                for (Iterator<FormField> fieldsIter = extendedInfo.getFields();
                     fieldsIter.hasNext();)
                {
                    FormField f = fieldsIter.next();
                    if (!f.getVariable().equals("FORM_TYPE"))
                        fs.add(f);
                    else
                        formType = f;
                }

                // Add FORM_TYPE values
                if (formType != null)
                    formFieldValuesToCaps(formType.getValues(), bldr);

                // Add the other values
                for (FormField f : fs)
                {
                    bldr.append(f.getVariable()).append('<');
                    formFieldValuesToCaps(f.getValues(), bldr);
                }
            }
        }

        return bldr.toString();
    }

    /**
     * Calculates the ver string for the specified <tt>discoverInfo</tt>,
     * identity type, name features, and extendedInfo.
     *
     * @param discoverInfo the {@link DiscoverInfo} we'd be creating a ver
     * <tt>String</tt> for
     */
    public void calculateEntityCapsVersion(DiscoverInfo discoverInfo)
    {
        setCurrentCapsVersion(
            discoverInfo,
            capsToHash(
                CapsPacketExtension.HASH_METHOD,
                calculateEntityCapsString(discoverInfo)));
    }

    /**
     * Set our own caps version.
     *
     * @param discoverInfo the {@link DiscoverInfo} that we'd like to map to the
     * <tt>capsVersion</tt>.
     * @param capsVersion the new caps version
     */
    public void setCurrentCapsVersion(DiscoverInfo discoverInfo,
                                      String capsVersion)
    {
        Caps caps
            = new Caps(getNode(), CapsPacketExtension.HASH_METHOD, capsVersion,
                    null);

        /*
         * DiscoverInfo carries the node and the ver and we're now setting a new
         * ver so we should update the DiscoveryInfo.
         */
        discoverInfo.setNode(caps.getNodeVer());

        if (!caps.isValid(discoverInfo))
        {
            throw
                new IllegalArgumentException(
                        "The specified discoverInfo must be valid with respect"
                            + " to the specified capsVersion");
        }

        currentCapsVersion = capsVersion;
        addDiscoverInfoByCaps(caps, discoverInfo);
        fireCapsVerChanged();
    }

    /**
     * The {@link PacketListener} that will be registering incoming caps.
     */
    private class CapsPacketListener
        implements PacketListener
    {
        /**
         * Handles incoming presence packets and maps jids to node#ver strings.
         *
         * @param packet the incoming presence <tt>Packet</tt> to be handled
         * @see PacketListener#processPacket(Packet)
         */
        public void processPacket(Packet packet)
        {
            // Check it the packet indicates  that the user is online. We
            // will use this information to decide if we're going to send
            // the discover info request.
            boolean online
                = (packet instanceof Presence)
                        && ((Presence) packet).isAvailable();

            CapsPacketExtension ext
                = (CapsPacketExtension)
                    packet.getExtension(
                            CapsPacketExtension.ELEMENT_NAME,
                            CapsPacketExtension.NAMESPACE);

            if(ext != null && online)
            {
                /*
                 * Before Version 1.4 of XEP-0115: Entity Capabilities,
                 * the 'ver' attribute was generated differently and the 'hash'
                 * attribute was absent. The 'ver' attribute in Version 1.3
                 * represents the specific version of the client and thus does
                 * not provide a way to validate the DiscoverInfo sent by
                 * the client. If EntityCapsManager receives no 'hash'
                 * attribute, it will assume the legacy format and will not
                 * cache it because the DiscoverInfo to be received from
                 * the client later on will not be trustworthy.
                 */
                String hash = ext.getHash();

                /* Google Talk web does not set hash, but we need it to
                 * be cached
                 */
                if (hash == null)
                    hash = "";

                addUserCapsNode(
                        packet.getFrom(),
                        ext.getNode(), hash, ext.getVersion(),
                        ext.getExtensions(), online);
            }
            else if (!online)
            {
                removeUserCapsNode(packet.getFrom());
            }
        }
    }

    /**
     * Implements an immutable value which stands for a specific node, a
     * specific hash (algorithm) and a specific ver.
     *
     * @author Lyubomir Marinov
     */
    public static class Caps
    {
        /** The hash (algorithm) of this <tt>Caps</tt> value. */
        public final String hash;

        /** The node of this <tt>Caps</tt> value. */
        public final String node;

        /** The ext info of this <tt>Caps</tt> value. */
        public String ext;

        /**
         * The String which is the concatenation of {@link #node} and the
         * {@link #ver} separated by the character '#'. Cached for the sake of
         * efficiency.
         */
        private final String nodeVer;

        /** The ver of this <tt>Caps</tt> value. */
        public final String ver;

        /**
         * Initializes a new <tt>Caps</tt> instance which is to represent a
         * specific node, a specific hash (algorithm) and a specific ver.
         *
         * @param node the node to be represented by the new instance
         * @param hash the hash (algorithm) to be represented by the new
         * instance
         * @param ver the ver to be represented by the new instance
         * @param ext the ext to be represented by the new instance
         */
        public Caps(String node, String hash, String ver, String ext)
        {
            if (node == null)
                throw new NullPointerException("node");
            if (hash == null)
                throw new NullPointerException("hash");
            if (ver == null)
                throw new NullPointerException("ver");

            this.node = node;
            this.hash = hash;
            this.ver = ver;
            this.ext = ext;

            this.nodeVer = this.node + '#' + this.ver;
        }

        /**
         * Gets a <tt>String</tt> which represents the concatenation of the
         * <tt>node</tt> property of this instance, the character '#' and the
         * <tt>ver</tt> property of this instance.
         *
         * @return a <tt>String</tt> which represents the concatenation of the
         * <tt>node</tt> property of this instance, the character '#' and the
         * <tt>ver</tt> property of this instance
         */
        public final String getNodeVer()
        {
            return nodeVer;
        }

        /**
         * Determines whether a specific <tt>DiscoverInfo</tt> is valid
         * according to this <tt>Caps</tt> i.e. whether the
         * <tt>discoverInfo</tt> has the node and the ver of this <tt>Caps</tt>
         * and the ver calculated from the <tt>discoverInfo</tt> using the hash
         * (algorithm) of this <tt>Caps</tt> is equal to the ver of this
         * <tt>Caps</tt>.
         *
         * @param discoverInfo the <tt>DiscoverInfo</tt> to be validated by this
         * <tt>Caps</tt>
         * @return <tt>true</tt> if the specified <tt>DiscoverInfo</tt> has the
         * node and the ver of this <tt>Caps</tt> and the ver calculated from
         * the <tt>discoverInfo</tt> using the hash (algorithm) of this
         * <tt>Caps</tt> is equal to the ver of this <tt>Caps</tt>; otherwise,
         * <tt>false</tt>
         */
        public boolean isValid(DiscoverInfo discoverInfo)
        {
            if(discoverInfo != null)
            {
                // The "node" attribute is not necessary in the query element.
                // For example, Swift does not send back the "node" attribute in
                // the Disco#info response. Thus, if the node of the IQ response
                // is null, then we set it to the request one.
                if(discoverInfo.getNode() == null)
                {
                    discoverInfo.setNode(getNodeVer());
                }

                if(getNodeVer().equals(discoverInfo.getNode())
                    && !hash.equals("")
                    && ver.equals(
                            capsToHash(
                                hash,
                                calculateEntityCapsString(discoverInfo))))
                {
                    return true;
                }
            }
            return false;
        }

        @Override
        public boolean equals(Object o)
        {
            if(this == o) return true;
            if(o == null || getClass() != o.getClass()) return false;

            Caps caps = (Caps) o;

            if(!hash.equals(caps.hash)) return false;
            if(!node.equals(caps.node)) return false;
            if(!ver.equals(caps.ver)) return false;

            return true;
        }

        @Override
        public int hashCode()
        {
            int result = hash.hashCode();
            result = 31 * result + node.hashCode();
            result = 31 * result + ver.hashCode();
            return result;
        }
    }
}
