/**
 * $Revision$
 * $Date$
 *
 * Copyright 2003-2007 Jive Software.
 *
 * All rights reserved. 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 org.jivesoftware.smackx.workgroup.agent;

import org.jivesoftware.smackx.workgroup.MetaData;
import org.jivesoftware.smackx.workgroup.WorkgroupInvitation;
import org.jivesoftware.smackx.workgroup.WorkgroupInvitationListener;
import org.jivesoftware.smackx.workgroup.ext.history.AgentChatHistory;
import org.jivesoftware.smackx.workgroup.ext.history.ChatMetadata;
import org.jivesoftware.smackx.workgroup.ext.macros.MacroGroup;
import org.jivesoftware.smackx.workgroup.ext.macros.Macros;
import org.jivesoftware.smackx.workgroup.ext.notes.ChatNotes;
import org.jivesoftware.smackx.workgroup.packet.*;
import org.jivesoftware.smackx.workgroup.settings.GenericSettings;
import org.jivesoftware.smackx.workgroup.settings.SearchSettings;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.filter.*;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.Form;
import org.jivesoftware.smackx.ReportedData;
import org.jivesoftware.smackx.packet.MUCUser;

import java.util.*;

/**
 * This class embodies the agent's active presence within a given workgroup. The application
 * should have N instances of this class, where N is the number of workgroups to which the
 * owning agent of the application belongs. This class provides all functionality that a
 * session within a given workgroup is expected to have from an agent's perspective -- setting
 * the status, tracking the status of queues to which the agent belongs within the workgroup, and
 * dequeuing customers.
 *
 * @author Matt Tucker
 * @author Derek DeMoro
 */
public class AgentSession {

    private Connection connection;

    private String workgroupJID;

    private boolean online = false;
    private Presence.Mode presenceMode;
    private int maxChats;
    private final Map<String, String> metaData;

    private Map<String, WorkgroupQueue> queues;

    private final List<OfferListener> offerListeners;
    private final List<WorkgroupInvitationListener> invitationListeners;
    private final List<QueueUsersListener> queueUsersListeners;

    private AgentRoster agentRoster = null;
    private TranscriptManager transcriptManager;
    private TranscriptSearchManager transcriptSearchManager;
    private Agent agent;
    private PacketListener packetListener;

    /**
     * Constructs a new agent session instance. Note, the {@link #setOnline(boolean)}
     * method must be called with an argument of <tt>true</tt> to mark the agent
     * as available to accept chat requests.
     *
     * @param connection   a connection instance which must have already gone through
     *                     authentication.
     * @param workgroupJID the fully qualified JID of the workgroup.
     */
    public AgentSession(String workgroupJID, Connection connection) {
        // Login must have been done before passing in connection.
        if (!connection.isAuthenticated()) {
            throw new IllegalStateException("Must login to server before creating workgroup.");
        }

        this.workgroupJID = workgroupJID;
        this.connection = connection;
        this.transcriptManager = new TranscriptManager(connection);
        this.transcriptSearchManager = new TranscriptSearchManager(connection);

        this.maxChats = -1;

        this.metaData = new HashMap<String, String>();

        this.queues = new HashMap<String, WorkgroupQueue>();

        offerListeners = new ArrayList<OfferListener>();
        invitationListeners = new ArrayList<WorkgroupInvitationListener>();
        queueUsersListeners = new ArrayList<QueueUsersListener>();

        // Create a filter to listen for packets we're interested in.
        OrFilter filter = new OrFilter();
        filter.addFilter(new PacketTypeFilter(OfferRequestProvider.OfferRequestPacket.class));
        filter.addFilter(new PacketTypeFilter(OfferRevokeProvider.OfferRevokePacket.class));
        filter.addFilter(new PacketTypeFilter(Presence.class));
        filter.addFilter(new PacketTypeFilter(Message.class));

        packetListener = new PacketListener() {
            public void processPacket(Packet packet) {
                try {
                    handlePacket(packet);
                }
                catch (Exception e) {
                    e.printStackTrace();
                }
            }
        };
        connection.addPacketListener(packetListener, filter);
        // Create the agent associated to this session
        agent = new Agent(connection, workgroupJID);
    }

    /**
     * Close the agent session. The underlying connection will remain opened but the
     * packet listeners that were added by this agent session will be removed.
     */
    public void close() {
        connection.removePacketListener(packetListener);
    }

    /**
     * Returns the agent roster for the workgroup, which contains
     *
     * @return the AgentRoster
     */
    public AgentRoster getAgentRoster() {
        if (agentRoster == null) {
            agentRoster = new AgentRoster(connection, workgroupJID);
        }

        // This might be the first time the user has asked for the roster. If so, we
        // want to wait up to 2 seconds for the server to send back the list of agents.
        // This behavior shields API users from having to worry about the fact that the
        // operation is asynchronous, although they'll still have to listen for changes
        // to the roster.
        int elapsed = 0;
        while (!agentRoster.rosterInitialized && elapsed <= 2000) {
            try {
                Thread.sleep(500);
            }
            catch (Exception e) {
                // Ignore
            }
            elapsed += 500;
        }
        return agentRoster;
    }

    /**
     * Returns the agent's current presence mode.
     *
     * @return the agent's current presence mode.
     */
    public Presence.Mode getPresenceMode() {
        return presenceMode;
    }

    /**
     * Returns the maximum number of chats the agent can participate in.
     *
     * @return the maximum number of chats the agent can participate in.
     */
    public int getMaxChats() {
        return maxChats;
    }

    /**
     * Returns true if the agent is online with the workgroup.
     *
     * @return true if the agent is online with the workgroup.
     */
    public boolean isOnline() {
        return online;
    }

    /**
     * Allows the addition of a new key-value pair to the agent's meta data, if the value is
     * new data, the revised meta data will be rebroadcast in an agent's presence broadcast.
     *
     * @param key the meta data key
     * @param val the non-null meta data value
     * @throws XMPPException if an exception occurs.
     */
    public void setMetaData(String key, String val) throws XMPPException {
        synchronized (this.metaData) {
            String oldVal = (String)this.metaData.get(key);

            if ((oldVal == null) || (!oldVal.equals(val))) {
                metaData.put(key, val);

                setStatus(presenceMode, maxChats);
            }
        }
    }

    /**
     * Allows the removal of data from the agent's meta data, if the key represents existing data,
     * the revised meta data will be rebroadcast in an agent's presence broadcast.
     *
     * @param key the meta data key.
     * @throws XMPPException if an exception occurs.
     */
    public void removeMetaData(String key) throws XMPPException {
        synchronized (this.metaData) {
            String oldVal = (String)metaData.remove(key);

            if (oldVal != null) {
                setStatus(presenceMode, maxChats);
            }
        }
    }

    /**
     * Allows the retrieval of meta data for a specified key.
     *
     * @param key the meta data key
     * @return the meta data value associated with the key or <tt>null</tt> if the meta-data
     *         doesn't exist..
     */
    public String getMetaData(String key) {
        return (String)metaData.get(key);
    }

    /**
     * Sets whether the agent is online with the workgroup. If the user tries to go online with
     * the workgroup but is not allowed to be an agent, an XMPPError with error code 401 will
     * be thrown.
     *
     * @param online true to set the agent as online with the workgroup.
     * @throws XMPPException if an error occurs setting the online status.
     */
    public void setOnline(boolean online) throws XMPPException {
        // If the online status hasn't changed, do nothing.
        if (this.online == online) {
            return;
        }

        Presence presence;

        // If the user is going online...
        if (online) {
            presence = new Presence(Presence.Type.available);
            presence.setTo(workgroupJID);
            presence.addExtension(new DefaultPacketExtension(AgentStatus.ELEMENT_NAME,
                    AgentStatus.NAMESPACE));

            PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class), new FromContainsFilter(workgroupJID)));

            connection.sendPacket(presence);

            presence = (Presence)collector.nextResult(5000);
            collector.cancel();
            if (!presence.isAvailable()) {
                throw new XMPPException("No response from server on status set.");
            }

            if (presence.getError() != null) {
                throw new XMPPException(presence.getError());
            }

            // We can safely update this iv since we didn't get any error
            this.online = online;
        }
        // Otherwise the user is going offline...
        else {
            // Update this iv now since we don't care at this point of any error
            this.online = online;

            presence = new Presence(Presence.Type.unavailable);
            presence.setTo(workgroupJID);
            presence.addExtension(new DefaultPacketExtension(AgentStatus.ELEMENT_NAME,
                    AgentStatus.NAMESPACE));
            connection.sendPacket(presence);
        }
    }

    /**
     * Sets the agent's current status with the workgroup. The presence mode affects
     * how offers are routed to the agent. The possible presence modes with their
     * meanings are as follows:<ul>
     * <p/>
     * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats
     * (equivalent to Presence.Mode.CHAT).
     * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.
     * However, special case, or extreme urgency chats may still be offered to the agent.
     * <li>Presence.Mode.AWAY -- the agent is not available and should not
     * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>
     * <p/>
     * The max chats value is the maximum number of chats the agent is willing to have
     * routed to them at once. Some servers may be configured to only accept max chat
     * values in a certain range; for example, between two and five. In that case, the
     * maxChats value the agent sends may be adjusted by the server to a value within that
     * range.
     *
     * @param presenceMode the presence mode of the agent.
     * @param maxChats     the maximum number of chats the agent is willing to accept.
     * @throws XMPPException         if an error occurs setting the agent status.
     * @throws IllegalStateException if the agent is not online with the workgroup.
     */
    public void setStatus(Presence.Mode presenceMode, int maxChats) throws XMPPException {
        setStatus(presenceMode, maxChats, null);
    }

    /**
     * Sets the agent's current status with the workgroup. The presence mode affects how offers
     * are routed to the agent. The possible presence modes with their meanings are as follows:<ul>
     * <p/>
     * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats
     * (equivalent to Presence.Mode.CHAT).
     * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.
     * However, special case, or extreme urgency chats may still be offered to the agent.
     * <li>Presence.Mode.AWAY -- the agent is not available and should not
     * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>
     * <p/>
     * The max chats value is the maximum number of chats the agent is willing to have routed to
     * them at once. Some servers may be configured to only accept max chat values in a certain
     * range; for example, between two and five. In that case, the maxChats value the agent sends
     * may be adjusted by the server to a value within that range.
     *
     * @param presenceMode the presence mode of the agent.
     * @param maxChats     the maximum number of chats the agent is willing to accept.
     * @param status       sets the status message of the presence update.
     * @throws XMPPException         if an error occurs setting the agent status.
     * @throws IllegalStateException if the agent is not online with the workgroup.
     */
    public void setStatus(Presence.Mode presenceMode, int maxChats, String status)
            throws XMPPException {
        if (!online) {
            throw new IllegalStateException("Cannot set status when the agent is not online.");
        }

        if (presenceMode == null) {
            presenceMode = Presence.Mode.available;
        }
        this.presenceMode = presenceMode;
        this.maxChats = maxChats;

        Presence presence = new Presence(Presence.Type.available);
        presence.setMode(presenceMode);
        presence.setTo(this.getWorkgroupJID());

        if (status != null) {
            presence.setStatus(status);
        }
        // Send information about max chats and current chats as a packet extension.
        DefaultPacketExtension agentStatus = new DefaultPacketExtension(AgentStatus.ELEMENT_NAME,
                AgentStatus.NAMESPACE);
        agentStatus.setValue("max-chats", "" + maxChats);
        presence.addExtension(agentStatus);
        presence.addExtension(new MetaData(this.metaData));

        PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class), new FromContainsFilter(workgroupJID)));

        this.connection.sendPacket(presence);

        presence = (Presence)collector.nextResult(5000);
        collector.cancel();
        if (!presence.isAvailable()) {
            throw new XMPPException("No response from server on status set.");
        }

        if (presence.getError() != null) {
            throw new XMPPException(presence.getError());
        }
    }

    /**
     * Sets the agent's current status with the workgroup. The presence mode affects how offers
     * are routed to the agent. The possible presence modes with their meanings are as follows:<ul>
     * <p/>
     * <li>Presence.Mode.AVAILABLE -- (Default) the agent is available for more chats
     * (equivalent to Presence.Mode.CHAT).
     * <li>Presence.Mode.DO_NOT_DISTURB -- the agent is busy and should not be disturbed.
     * However, special case, or extreme urgency chats may still be offered to the agent.
     * <li>Presence.Mode.AWAY -- the agent is not available and should not
     * have a chat routed to them (equivalent to Presence.Mode.EXTENDED_AWAY).</ul>
     *
     * @param presenceMode the presence mode of the agent.
     * @param status       sets the status message of the presence update.
     * @throws XMPPException         if an error occurs setting the agent status.
     * @throws IllegalStateException if the agent is not online with the workgroup.
     */
    public void setStatus(Presence.Mode presenceMode, String status) throws XMPPException {
        if (!online) {
            throw new IllegalStateException("Cannot set status when the agent is not online.");
        }

        if (presenceMode == null) {
            presenceMode = Presence.Mode.available;
        }
        this.presenceMode = presenceMode;

        Presence presence = new Presence(Presence.Type.available);
        presence.setMode(presenceMode);
        presence.setTo(this.getWorkgroupJID());

        if (status != null) {
            presence.setStatus(status);
        }
        presence.addExtension(new MetaData(this.metaData));

        PacketCollector collector = this.connection.createPacketCollector(new AndFilter(new PacketTypeFilter(Presence.class),
                new FromContainsFilter(workgroupJID)));

        this.connection.sendPacket(presence);

        presence = (Presence)collector.nextResult(5000);
        collector.cancel();
        if (!presence.isAvailable()) {
            throw new XMPPException("No response from server on status set.");
        }

        if (presence.getError() != null) {
            throw new XMPPException(presence.getError());
        }
    }

    /**
     * Removes a user from the workgroup queue. This is an administrative action that the
     * <p/>
     * The agent is not guaranteed of having privileges to perform this action; an exception
     * denying the request may be thrown.
     *
     * @param userID the ID of the user to remove.
     * @throws XMPPException if an exception occurs.
     */
    public void dequeueUser(String userID) throws XMPPException {
        // todo: this method simply won't work right now.
        DepartQueuePacket departPacket = new DepartQueuePacket(this.workgroupJID);

        // PENDING
        this.connection.sendPacket(departPacket);
    }

    /**
     * Returns the transcripts of a given user. The answer will contain the complete history of
     * conversations that a user had.
     *
     * @param userID the id of the user to get his conversations.
     * @return the transcripts of a given user.
     * @throws XMPPException if an error occurs while getting the information.
     */
    public Transcripts getTranscripts(String userID) throws XMPPException {
        return transcriptManager.getTranscripts(workgroupJID, userID);
    }

    /**
     * Returns the full conversation transcript of a given session.
     *
     * @param sessionID the id of the session to get the full transcript.
     * @return the full conversation transcript of a given session.
     * @throws XMPPException if an error occurs while getting the information.
     */
    public Transcript getTranscript(String sessionID) throws XMPPException {
        return transcriptManager.getTranscript(workgroupJID, sessionID);
    }

    /**
     * Returns the Form to use for searching transcripts. It is unlikely that the server
     * will change the form (without a restart) so it is safe to keep the returned form
     * for future submissions.
     *
     * @return the Form to use for searching transcripts.
     * @throws XMPPException if an error occurs while sending the request to the server.
     */
    public Form getTranscriptSearchForm() throws XMPPException {
        return transcriptSearchManager.getSearchForm(StringUtils.parseServer(workgroupJID));
    }

    /**
     * Submits the completed form and returns the result of the transcript search. The result
     * will include all the data returned from the server so be careful with the amount of
     * data that the search may return.
     *
     * @param completedForm the filled out search form.
     * @return the result of the transcript search.
     * @throws XMPPException if an error occurs while submiting the search to the server.
     */
    public ReportedData searchTranscripts(Form completedForm) throws XMPPException {
        return transcriptSearchManager.submitSearch(StringUtils.parseServer(workgroupJID),
                completedForm);
    }

    /**
     * Asks the workgroup for information about the occupants of the specified room. The returned
     * information will include the real JID of the occupants, the nickname of the user in the
     * room as well as the date when the user joined the room.
     *
     * @param roomID the room to get information about its occupants.
     * @return information about the occupants of the specified room.
     * @throws XMPPException if an error occurs while getting information from the server.
     */
    public OccupantsInfo getOccupantsInfo(String roomID) throws XMPPException {
        OccupantsInfo request = new OccupantsInfo(roomID);
        request.setType(IQ.Type.GET);
        request.setTo(workgroupJID);

        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
        connection.sendPacket(request);

        OccupantsInfo response = (OccupantsInfo)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

        // Cancel the collector.
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from server.");
        }
        if (response.getError() != null) {
            throw new XMPPException(response.getError());
        }
        return response;
    }

    /**
     * @return the fully-qualified name of the workgroup for which this session exists
     */
    public String getWorkgroupJID() {
        return workgroupJID;
    }

    /**
     * Returns the Agent associated to this session.
     *
     * @return the Agent associated to this session.
     */
    public Agent getAgent() {
        return agent;
    }

    /**
     * @param queueName the name of the queue
     * @return an instance of WorkgroupQueue for the argument queue name, or null if none exists
     */
    public WorkgroupQueue getQueue(String queueName) {
        return queues.get(queueName);
    }

    public Iterator<WorkgroupQueue> getQueues() {
        return Collections.unmodifiableMap((new HashMap<String, WorkgroupQueue>(queues))).values().iterator();
    }

    public void addQueueUsersListener(QueueUsersListener listener) {
        synchronized (queueUsersListeners) {
            if (!queueUsersListeners.contains(listener)) {
                queueUsersListeners.add(listener);
            }
        }
    }

    public void removeQueueUsersListener(QueueUsersListener listener) {
        synchronized (queueUsersListeners) {
            queueUsersListeners.remove(listener);
        }
    }

    /**
     * Adds an offer listener.
     *
     * @param offerListener the offer listener.
     */
    public void addOfferListener(OfferListener offerListener) {
        synchronized (offerListeners) {
            if (!offerListeners.contains(offerListener)) {
                offerListeners.add(offerListener);
            }
        }
    }

    /**
     * Removes an offer listener.
     *
     * @param offerListener the offer listener.
     */
    public void removeOfferListener(OfferListener offerListener) {
        synchronized (offerListeners) {
            offerListeners.remove(offerListener);
        }
    }

    /**
     * Adds an invitation listener.
     *
     * @param invitationListener the invitation listener.
     */
    public void addInvitationListener(WorkgroupInvitationListener invitationListener) {
        synchronized (invitationListeners) {
            if (!invitationListeners.contains(invitationListener)) {
                invitationListeners.add(invitationListener);
            }
        }
    }

    /**
     * Removes an invitation listener.
     *
     * @param invitationListener the invitation listener.
     */
    public void removeInvitationListener(WorkgroupInvitationListener invitationListener) {
        synchronized (invitationListeners) {
            invitationListeners.remove(invitationListener);
        }
    }

    private void fireOfferRequestEvent(OfferRequestProvider.OfferRequestPacket requestPacket) {
        Offer offer = new Offer(this.connection, this, requestPacket.getUserID(),
                requestPacket.getUserJID(), this.getWorkgroupJID(),
                new Date((new Date()).getTime() + (requestPacket.getTimeout() * 1000)),
                requestPacket.getSessionID(), requestPacket.getMetaData(), requestPacket.getContent());

        synchronized (offerListeners) {
            for (OfferListener listener : offerListeners) {
                listener.offerReceived(offer);
            }
        }
    }

    private void fireOfferRevokeEvent(OfferRevokeProvider.OfferRevokePacket orp) {
        RevokedOffer revokedOffer = new RevokedOffer(orp.getUserJID(), orp.getUserID(),
                this.getWorkgroupJID(), orp.getSessionID(), orp.getReason(), new Date());

        synchronized (offerListeners) {
            for (OfferListener listener : offerListeners) {
                listener.offerRevoked(revokedOffer);
            }
        }
    }

    private void fireInvitationEvent(String groupChatJID, String sessionID, String body,
                                     String from, Map metaData) {
        WorkgroupInvitation invitation = new WorkgroupInvitation(connection.getUser(), groupChatJID,
                workgroupJID, sessionID, body, from, metaData);

        synchronized (invitationListeners) {
            for (WorkgroupInvitationListener listener : invitationListeners) {
                listener.invitationReceived(invitation);
            }
        }
    }

    private void fireQueueUsersEvent(WorkgroupQueue queue, WorkgroupQueue.Status status,
                                     int averageWaitTime, Date oldestEntry, Set users) {
        synchronized (queueUsersListeners) {
            for (QueueUsersListener listener : queueUsersListeners) {
                if (status != null) {
                    listener.statusUpdated(queue, status);
                }
                if (averageWaitTime != -1) {
                    listener.averageWaitTimeUpdated(queue, averageWaitTime);
                }
                if (oldestEntry != null) {
                    listener.oldestEntryUpdated(queue, oldestEntry);
                }
                if (users != null) {
                    listener.usersUpdated(queue, users);
                }
            }
        }
    }

    // PacketListener Implementation.

    private void handlePacket(Packet packet) {
        if (packet instanceof OfferRequestProvider.OfferRequestPacket) {
            // Acknowledge the IQ set.
            IQ reply = new IQ() {
                public String getChildElementXML() {
                    return null;
                }
            };
            reply.setPacketID(packet.getPacketID());
            reply.setTo(packet.getFrom());
            reply.setType(IQ.Type.RESULT);
            connection.sendPacket(reply);

            fireOfferRequestEvent((OfferRequestProvider.OfferRequestPacket)packet);
        }
        else if (packet instanceof Presence) {
            Presence presence = (Presence)packet;

            // The workgroup can send us a number of different presence packets. We
            // check for different packet extensions to see what type of presence
            // packet it is.

            String queueName = StringUtils.parseResource(presence.getFrom());
            WorkgroupQueue queue = queues.get(queueName);
            // If there isn't already an entry for the queue, create a new one.
            if (queue == null) {
                queue = new WorkgroupQueue(queueName);
                queues.put(queueName, queue);
            }

            // QueueOverview packet extensions contain basic information about a queue.
            QueueOverview queueOverview = (QueueOverview)presence.getExtension(QueueOverview.ELEMENT_NAME, QueueOverview.NAMESPACE);
            if (queueOverview != null) {
                if (queueOverview.getStatus() == null) {
                    queue.setStatus(WorkgroupQueue.Status.CLOSED);
                }
                else {
                    queue.setStatus(queueOverview.getStatus());
                }
                queue.setAverageWaitTime(queueOverview.getAverageWaitTime());
                queue.setOldestEntry(queueOverview.getOldestEntry());
                // Fire event.
                fireQueueUsersEvent(queue, queueOverview.getStatus(),
                        queueOverview.getAverageWaitTime(), queueOverview.getOldestEntry(),
                        null);
                return;
            }

            // QueueDetails packet extensions contain information about the users in
            // a queue.
            QueueDetails queueDetails = (QueueDetails)packet.getExtension(QueueDetails.ELEMENT_NAME, QueueDetails.NAMESPACE);
            if (queueDetails != null) {
                queue.setUsers(queueDetails.getUsers());
                // Fire event.
                fireQueueUsersEvent(queue, null, -1, null, queueDetails.getUsers());
                return;
            }

            // Notify agent packets gives an overview of agent activity in a queue.
            DefaultPacketExtension notifyAgents = (DefaultPacketExtension)presence.getExtension("notify-agents", "http://jabber.org/protocol/workgroup");
            if (notifyAgents != null) {
                int currentChats = Integer.parseInt(notifyAgents.getValue("current-chats"));
                int maxChats = Integer.parseInt(notifyAgents.getValue("max-chats"));
                queue.setCurrentChats(currentChats);
                queue.setMaxChats(maxChats);
                // Fire event.
                // TODO: might need another event for current chats and max chats of queue
                return;
            }
        }
        else if (packet instanceof Message) {
            Message message = (Message)packet;

            // Check if a room invitation was sent and if the sender is the workgroup
            MUCUser mucUser = (MUCUser)message.getExtension("x",
                    "http://jabber.org/protocol/muc#user");
            MUCUser.Invite invite = mucUser != null ? mucUser.getInvite() : null;
            if (invite != null && workgroupJID.equals(invite.getFrom())) {
                String sessionID = null;
                Map metaData = null;

                SessionID sessionIDExt = (SessionID)message.getExtension(SessionID.ELEMENT_NAME,
                        SessionID.NAMESPACE);
                if (sessionIDExt != null) {
                    sessionID = sessionIDExt.getSessionID();
                }

                MetaData metaDataExt = (MetaData)message.getExtension(MetaData.ELEMENT_NAME,
                        MetaData.NAMESPACE);
                if (metaDataExt != null) {
                    metaData = metaDataExt.getMetaData();
                }

                this.fireInvitationEvent(message.getFrom(), sessionID, message.getBody(),
                        message.getFrom(), metaData);
            }
        }
        else if (packet instanceof OfferRevokeProvider.OfferRevokePacket) {
            // Acknowledge the IQ set.
            IQ reply = new IQ() {
                public String getChildElementXML() {
                    return null;
                }
            };
            reply.setPacketID(packet.getPacketID());
            reply.setType(IQ.Type.RESULT);
            connection.sendPacket(reply);

            fireOfferRevokeEvent((OfferRevokeProvider.OfferRevokePacket)packet);
        }
    }

    /**
     * Creates a ChatNote that will be mapped to the given chat session.
     *
     * @param sessionID the session id of a Chat Session.
     * @param note      the chat note to add.
     * @throws XMPPException is thrown if an error occurs while adding the note.
     */
    public void setNote(String sessionID, String note) throws XMPPException {
        note = ChatNotes.replace(note, "\n", "\\n");
        note = StringUtils.escapeForXML(note);

        ChatNotes notes = new ChatNotes();
        notes.setType(IQ.Type.SET);
        notes.setTo(workgroupJID);
        notes.setSessionID(sessionID);
        notes.setNotes(note);
        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(notes.getPacketID()));
        // Send the request
        connection.sendPacket(notes);

        IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

        // Cancel the collector.
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from server on status set.");
        }
        if (response.getError() != null) {
            throw new XMPPException(response.getError());
        }
    }

    /**
     * Retrieves the ChatNote associated with a given chat session.
     *
     * @param sessionID the sessionID of the chat session.
     * @return the <code>ChatNote</code> associated with a given chat session.
     * @throws XMPPException if an error occurs while retrieving the ChatNote.
     */
    public ChatNotes getNote(String sessionID) throws XMPPException {
        ChatNotes request = new ChatNotes();
        request.setType(IQ.Type.GET);
        request.setTo(workgroupJID);
        request.setSessionID(sessionID);

        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
        connection.sendPacket(request);

        ChatNotes response = (ChatNotes)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

        // Cancel the collector.
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from server.");
        }
        if (response.getError() != null) {
            throw new XMPPException(response.getError());
        }
        return response;

    }

    /**
     * Retrieves the AgentChatHistory associated with a particular agent jid.
     *
     * @param jid the jid of the agent.
     * @param maxSessions the max number of sessions to retrieve.
     * @param startDate the starting date of sessions to retrieve.
     * @return the chat history associated with a given jid.
     * @throws XMPPException if an error occurs while retrieving the AgentChatHistory.
     */
    public AgentChatHistory getAgentHistory(String jid, int maxSessions, Date startDate) throws XMPPException {
        AgentChatHistory request;
        if (startDate != null) {
            request = new AgentChatHistory(jid, maxSessions, startDate);
        }
        else {
            request = new AgentChatHistory(jid, maxSessions);
        }

        request.setType(IQ.Type.GET);
        request.setTo(workgroupJID);

        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
        connection.sendPacket(request);

        AgentChatHistory response = (AgentChatHistory)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

        // Cancel the collector.
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from server.");
        }
        if (response.getError() != null) {
            throw new XMPPException(response.getError());
        }
        return response;
    }

    /**
     * Asks the workgroup for it's Search Settings.
     *
     * @return SearchSettings the search settings for this workgroup.
     * @throws XMPPException if an error occurs while getting information from the server.
     */
    public SearchSettings getSearchSettings() throws XMPPException {
        SearchSettings request = new SearchSettings();
        request.setType(IQ.Type.GET);
        request.setTo(workgroupJID);

        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
        connection.sendPacket(request);


        SearchSettings response = (SearchSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

        // Cancel the collector.
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from server.");
        }
        if (response.getError() != null) {
            throw new XMPPException(response.getError());
        }
        return response;
    }

    /**
     * Asks the workgroup for it's Global Macros.
     *
     * @param global true to retrieve global macros, otherwise false for personal macros.
     * @return MacroGroup the root macro group.
     * @throws XMPPException if an error occurs while getting information from the server.
     */
    public MacroGroup getMacros(boolean global) throws XMPPException {
        Macros request = new Macros();
        request.setType(IQ.Type.GET);
        request.setTo(workgroupJID);
        request.setPersonal(!global);

        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
        connection.sendPacket(request);


        Macros response = (Macros)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

        // Cancel the collector.
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from server.");
        }
        if (response.getError() != null) {
            throw new XMPPException(response.getError());
        }
        return response.getRootGroup();
    }

    /**
     * Persists the Personal Macro for an agent.
     *
     * @param group the macro group to save. 
     * @throws XMPPException if an error occurs while getting information from the server.
     */
    public void saveMacros(MacroGroup group) throws XMPPException {
        Macros request = new Macros();
        request.setType(IQ.Type.SET);
        request.setTo(workgroupJID);
        request.setPersonal(true);
        request.setPersonalMacroGroup(group);

        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
        connection.sendPacket(request);


        IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

        // Cancel the collector.
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from server on status set.");
        }
        if (response.getError() != null) {
            throw new XMPPException(response.getError());
        }
    }

    /**
     * Query for metadata associated with a session id.
     *
     * @param sessionID the sessionID to query for.
     * @return Map a map of all metadata associated with the sessionID.
     * @throws XMPPException if an error occurs while getting information from the server.
     */
    public Map getChatMetadata(String sessionID) throws XMPPException {
        ChatMetadata request = new ChatMetadata();
        request.setType(IQ.Type.GET);
        request.setTo(workgroupJID);
        request.setSessionID(sessionID);

        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
        connection.sendPacket(request);


        ChatMetadata response = (ChatMetadata)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

        // Cancel the collector.
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from server.");
        }
        if (response.getError() != null) {
            throw new XMPPException(response.getError());
        }
        return response.getMetadata();
    }

    /**
     * Invites a user or agent to an existing session support. The provided invitee's JID can be of
     * a user, an agent, a queue or a workgroup. In the case of a queue or a workgroup the workgroup service
     * will decide the best agent to receive the invitation.<p>
     *
     * This method will return either when the service returned an ACK of the request or if an error occured
     * while requesting the invitation. After sending the ACK the service will send the invitation to the target
     * entity. When dealing with agents the common sequence of offer-response will be followed. However, when
     * sending an invitation to a user a standard MUC invitation will be sent.<p>
     *
     * The agent or user that accepted the offer <b>MUST</b> join the room. Failing to do so will make
     * the invitation to fail. The inviter will eventually receive a message error indicating that the invitee
     * accepted the offer but failed to join the room.
     *
     * Different situations may lead to a failed invitation. Possible cases are: 1) all agents rejected the
     * offer and ther are no agents available, 2) the agent that accepted the offer failed to join the room or
     * 2) the user that received the MUC invitation never replied or joined the room. In any of these cases
     * (or other failing cases) the inviter will get an error message with the failed notification.
     *
     * @param type type of entity that will get the invitation.
     * @param invitee JID of entity that will get the invitation.
     * @param sessionID ID of the support session that the invitee is being invited.
     * @param reason the reason of the invitation.
     * @throws XMPPException if the sender of the invitation is not an agent or the service failed to process
     *         the request.
     */
    public void sendRoomInvitation(RoomInvitation.Type type, String invitee, String sessionID, String reason)
            throws XMPPException {
        final RoomInvitation invitation = new RoomInvitation(type, invitee, sessionID, reason);
        IQ iq = new IQ() {

            public String getChildElementXML() {
                return invitation.toXML();
            }
        };
        iq.setType(IQ.Type.SET);
        iq.setTo(workgroupJID);
        iq.setFrom(connection.getUser());

        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(iq.getPacketID()));
        connection.sendPacket(iq);

        IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

        // Cancel the collector.
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from server.");
        }
        if (response.getError() != null) {
            throw new XMPPException(response.getError());
        }
    }

    /**
     * Transfer an existing session support to another user or agent. The provided invitee's JID can be of
     * a user, an agent, a queue or a workgroup. In the case of a queue or a workgroup the workgroup service
     * will decide the best agent to receive the invitation.<p>
     *
     * This method will return either when the service returned an ACK of the request or if an error occured
     * while requesting the transfer. After sending the ACK the service will send the invitation to the target
     * entity. When dealing with agents the common sequence of offer-response will be followed. However, when
     * sending an invitation to a user a standard MUC invitation will be sent.<p>
     *
     * Once the invitee joins the support room the workgroup service will kick the inviter from the room.<p>
     *
     * Different situations may lead to a failed transfers. Possible cases are: 1) all agents rejected the
     * offer and there are no agents available, 2) the agent that accepted the offer failed to join the room
     * or 2) the user that received the MUC invitation never replied or joined the room. In any of these cases
     * (or other failing cases) the inviter will get an error message with the failed notification.
     *
     * @param type type of entity that will get the invitation.
     * @param invitee JID of entity that will get the invitation.
     * @param sessionID ID of the support session that the invitee is being invited.
     * @param reason the reason of the invitation.
     * @throws XMPPException if the sender of the invitation is not an agent or the service failed to process
     *         the request.
     */
    public void sendRoomTransfer(RoomTransfer.Type type, String invitee, String sessionID, String reason)
            throws XMPPException {
        final RoomTransfer transfer = new RoomTransfer(type, invitee, sessionID, reason);
        IQ iq = new IQ() {

            public String getChildElementXML() {
                return transfer.toXML();
            }
        };
        iq.setType(IQ.Type.SET);
        iq.setTo(workgroupJID);
        iq.setFrom(connection.getUser());

        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(iq.getPacketID()));
        connection.sendPacket(iq);

        IQ response = (IQ)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

        // Cancel the collector.
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from server.");
        }
        if (response.getError() != null) {
            throw new XMPPException(response.getError());
        }
    }

    /**
     * Returns the generic metadata of the workgroup the agent belongs to.
     *
     * @param con   the Connection to use.
     * @param query an optional query object used to tell the server what metadata to retrieve. This can be null.
     * @throws XMPPException if an error occurs while sending the request to the server.
     * @return the settings for the workgroup.
     */
    public GenericSettings getGenericSettings(Connection con, String query) throws XMPPException {
        GenericSettings setting = new GenericSettings();
        setting.setType(IQ.Type.GET);
        setting.setTo(workgroupJID);

        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(setting.getPacketID()));
        connection.sendPacket(setting);

        GenericSettings response = (GenericSettings)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

        // Cancel the collector.
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from server on status set.");
        }
        if (response.getError() != null) {
            throw new XMPPException(response.getError());
        }
        return response;
    }

    public boolean hasMonitorPrivileges(Connection con) throws XMPPException {
        MonitorPacket request = new MonitorPacket();
        request.setType(IQ.Type.GET);
        request.setTo(workgroupJID);

        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
        connection.sendPacket(request);

        MonitorPacket response = (MonitorPacket)collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

        // Cancel the collector.
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from server on status set.");
        }
        if (response.getError() != null) {
            throw new XMPPException(response.getError());
        }
        return response.isMonitor();

    }

    public void makeRoomOwner(Connection con, String sessionID) throws XMPPException {
        MonitorPacket request = new MonitorPacket();
        request.setType(IQ.Type.SET);
        request.setTo(workgroupJID);
        request.setSessionID(sessionID);


        PacketCollector collector = connection.createPacketCollector(new PacketIDFilter(request.getPacketID()));
        connection.sendPacket(request);

        Packet response = collector.nextResult(SmackConfiguration.getPacketReplyTimeout());

        // Cancel the collector.
        collector.cancel();
        if (response == null) {
            throw new XMPPException("No response from server on status set.");
        }
        if (response.getError() != null) {
            throw new XMPPException(response.getError());
        }
    }
}