/**
 * $RCSfile$
 * $Revision$
 * $Date: 2006-08-07 21:12:21 -0700 (Mon, 07 Aug 2006) $
 *
 * Copyright (C) 2004-2008 Jive Software. All rights reserved.
 *
 * This software is published under the terms of the GNU Public License (GPL),
 * a copy of which is included in this distribution, or a commercial license
 * agreement with Jive.
 */

package org.jivesoftware.xmpp.workgroup;

import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import org.dom4j.Element;
import org.jivesoftware.database.DbConnectionManager;
import org.jivesoftware.database.SequenceManager;
import org.jivesoftware.openfire.PresenceManager;
import org.jivesoftware.openfire.XMPPServer;
import org.jivesoftware.openfire.commands.AdHocCommandManager;
import org.jivesoftware.openfire.event.GroupEventDispatcher;
import org.jivesoftware.openfire.event.GroupEventListener;
import org.jivesoftware.openfire.fastpath.commands.CreateWorkgroup;
import org.jivesoftware.openfire.fastpath.commands.DeleteWorkgroup;
import org.jivesoftware.openfire.fastpath.events.EmailTranscriptEvent;
import org.jivesoftware.openfire.fastpath.settings.chat.ChatSettingsManager;
import org.jivesoftware.openfire.fastpath.util.TaskEngine;
import org.jivesoftware.openfire.fastpath.util.WorkgroupUtils;
import org.jivesoftware.openfire.group.Group;
import org.jivesoftware.openfire.net.SASLAuthentication;
import org.jivesoftware.openfire.user.UserManager;
import org.jivesoftware.openfire.user.UserNotFoundException;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.StringUtils;
import org.jivesoftware.xmpp.workgroup.disco.IQDiscoInfoHandler;
import org.jivesoftware.xmpp.workgroup.disco.IQDiscoItemsHandler;
import org.jivesoftware.xmpp.workgroup.event.WorkgroupEventDispatcher;
import org.jivesoftware.xmpp.workgroup.routing.RoutingManager;
import org.jivesoftware.xmpp.workgroup.search.ChatSearchManager;
import org.jivesoftware.xmpp.workgroup.search.IQChatSearchHandler;
import org.jivesoftware.xmpp.workgroup.utils.FastpathConstants;
import org.xmpp.component.Component;
import org.xmpp.component.ComponentException;
import org.xmpp.component.ComponentManager;
import org.xmpp.component.ComponentManagerFactory;
import org.xmpp.packet.IQ;
import org.xmpp.packet.JID;
import org.xmpp.packet.Message;
import org.xmpp.packet.Packet;
import org.xmpp.packet.PacketError;
import org.xmpp.packet.Presence;

/**
 * Manages workgroups in the system. This manager primarily defers to the workgroups
 * to manage themselves and serves as a 'factory' for creating, obtaining and deleting them.
 *
 * @author Derek DeMoro
 */
public class WorkgroupManager implements Component {

    private static final String LOAD_WORKGROUPS =
        "SELECT workgroupID FROM fpWorkgroup";
    private static final String ADD_WORKGROUP =
        "INSERT INTO fpWorkgroup (workgroupID, jid, displayName, description, status, " +
            "creationDate, modificationDate, maxchats, minchats, offerTimeout, requestTimeout, " +
            "modes) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)";
    private static final String DELETE_WORKGROUP =
        "DELETE FROM fpWorkgroup WHERE workgroupID=?";

    private static WorkgroupManager instance = new WorkgroupManager();
    private EmailTranscriptEvent emailTranscriptEvent;

    /**
     * Returns a workgroup manager instance (singleton).
     *
     * @return a workgroup manager instance.
     */
    public synchronized static WorkgroupManager getInstance() {
        if (instance == null) {
            instance = new WorkgroupManager();
        }
        return instance;
    }

    /**
     * Track all Workgroups
     */
    private Map<String, Workgroup> workgroups = new ConcurrentHashMap<String, Workgroup>();

    /**
     * Tracks the last known workgroup open status
     */
    private Map<Long, Workgroup.Status> workgroupOpenStatus = new HashMap<Long, Workgroup.Status>();

    private AgentManager agentManager;

    public List iqHandlers = new LinkedList();

    private int defaultMaxChats = 4; // default value, usually overridden by config file
    private int defaultMinChats = 1;
    private long defaultOfferTimeout = 20 * 1000; // 20 seconds
    private long defaultRequestTimeout = 4 * 60 * 1000; // 4 minutes

    /**
     * <p>Simple flag to track whether the workgroups have been loaded or not.</p>
     */
    private boolean loaded = false;

    private ReentrantReadWriteLock workgroupLock = new ReentrantReadWriteLock();

    private JID serviceAddress = null;
    private String mucServiceName;

    /**
     * Holds the object responsible for handling disco#info packets to this manager.
     */
    private IQDiscoInfoHandler iqDiscoInfoHandler;
    /**
     * Holds the object responsible for handling disco#items packets to this manager.
     */
    private IQDiscoItemsHandler iqDiscoItemsHandler;
    /**
     * Holds the object responsible for handling transcript searches for this manager.
     */
    private IQChatSearchHandler iqChatSearchHandler = new IQChatSearchHandler(this);
    /**
     * Manager that keeps the list of ad-hoc commands and processing command requests.
     */
    private AdHocCommandManager commandManager;

    private GroupEventListener groupEventListener;

    private TimerTask presenceCheckTask;

    /**
     * Creates a workgroup manager implementation.
     */
    public WorkgroupManager() {
        // Load Chat Properties
        String minChats = JiveGlobals.getProperty("xmpp.live.defaults.minchats");
        String maxChats = JiveGlobals.getProperty("xmpp.live.defaults.maxchats");
        if (minChats != null && minChats.trim().length() > 0) {
            defaultMinChats = Integer.parseInt(minChats);
        }
        if (maxChats != null && maxChats.trim().length() > 0) {
            defaultMaxChats = Integer.parseInt(maxChats);
        }
        String offerTimeout = JiveGlobals.getProperty("xmpp.live.defaults.offerTimeout");
        String requestTimeout = JiveGlobals.getProperty("xmpp.live.defaults.requestTimeout");
        if (offerTimeout != null && offerTimeout.trim().length() > 0) {
            defaultOfferTimeout = Integer.parseInt(offerTimeout);
        }
        if (requestTimeout != null && requestTimeout.trim().length() > 0) {
            defaultRequestTimeout = Integer.parseInt(requestTimeout);
        }

        // Initialize chat settings manager. TODO This will be moved over to an extension file.
        ChatSettingsManager.getInstance();

        // Initialize EmailTranscript Event. TODO This will be moved over to an extension file.
        emailTranscriptEvent = new EmailTranscriptEvent();

        addGroupManagerListener();
        // Create responsible for handling ad-hoc commands in this service
        commandManager = new AdHocCommandManager();

        iqDiscoInfoHandler = new IQDiscoInfoHandler(this, commandManager);
        iqDiscoItemsHandler = new IQDiscoItemsHandler(this, commandManager);

        presenceCheckTask = new TimerTask() {
            public void run() {
                handleOutdatePresence();
            }
        };

        TaskEngine.getInstance().scheduleAtFixedRate(presenceCheckTask, 5000, 5000);
    }

    public void start() {
        // Enable the shared secret SASL mechanism, which the Fastpath web client will use.
        // We use a custom SASL mechanism so that web-based customer chats can login without
        // a username or password. However, a shared secret key is still required so that
        // anonymous login doesn't have to be enabled for the whole server.
        if (!SASLAuthentication.isSharedSecretAllowed()) {
            SASLAuthentication.setSharedSecretAllowed(true);
        }

        // If the database was just created then create the "demo" user and "demo" workgroup
        // Workgroup creation requires MUC service address so we need to run this code after the
        // disco stuff
        if (!JiveGlobals.getBooleanProperty("fastpath.database.setup")) {
            boolean createUser = createDemoUser();
            if (createUser) {
                createDemoWorkgroup();
            }
            JiveGlobals.setProperty("fastpath.database.setup", "true");
        }

        // Register ad-hoc commands
        commandManager.addCommand(new CreateWorkgroup());
        commandManager.addCommand(new DeleteWorkgroup());
    }

    public void shutdown() {
        workgroups.clear();
        GroupEventDispatcher.removeListener(groupEventListener);
        instance = null;
        ChatSearchManager.shutdown();
        ChatSettingsManager.shutdown();
        RoutingManager.shutdown();
        WorkgroupProviderManager.shutdown();
        emailTranscriptEvent.shutdown();
        workgroupComparator = null;

        TaskEngine.getInstance().cancelScheduledTask(presenceCheckTask);
    }

    public void stop() {
        for (Workgroup workgroup : getWorkgroups()) {
            workgroup.shutdown();
        }
        workgroups.clear();
        workgroupOpenStatus.clear();
    }

    public AgentManager getAgentManager() {
        return agentManager;
    }

    public int getDefaultMinChats() {
        return defaultMinChats;
    }

    public void setDefaultMinChats(int minChats) {
        if (minChats >= 0) {
            defaultMinChats = minChats;
            JiveGlobals.setProperty("xmpp.live.defaults.minchats", Integer.toString(minChats));
        }
    }

    public int getDefaultMaxChats() {
        return defaultMaxChats;
    }

    public void setDefaultMaxChats(int maxChats) {
        if (maxChats >= 0) {
            defaultMaxChats = maxChats;
            JiveGlobals.setProperty("xmpp.live.defaults.maxchats", Integer.toString(maxChats));
        }
    }

    public long getDefaultOfferTimeout() {
        return defaultOfferTimeout;
    }

    public void setDefaultOfferTimeout(long defaultOfferTimeout) {
        if (defaultOfferTimeout >= 0) {
            this.defaultOfferTimeout = defaultOfferTimeout;
            JiveGlobals.setProperty("xmpp.live.defaults.offerTimeout",
                Long.toString(defaultOfferTimeout));
        }
    }

    public long getDefaultRequestTimeout() {
        return defaultRequestTimeout;
    }

    public void setDefaultRequestTimeout(long defaultRequestTimeout) {
        if (defaultRequestTimeout >= 0) {
            this.defaultRequestTimeout = defaultRequestTimeout;
            JiveGlobals.setProperty("xmpp.live.defaults.requestTimeout",
                Long.toString(defaultRequestTimeout));
        }
    }

    /**
     * Creates a workgroup with default settings.
     *
     * @param name the name of the workgroup.
     * @return the created workgroup
     * @throws UnauthorizedException      if not allowed to create the workgroup.
     * @throws UserAlreadyExistsException If the address is already in use
     */
    public Workgroup createWorkgroup(String name) throws UserAlreadyExistsException, UnauthorizedException {
        if (workgroups.containsKey(name + "@" + serviceAddress.toBareJID())) {
            throw new UserAlreadyExistsException(name);
        }

        // Reserve the username - user ID from the jiveUserID table
        long id = -1;
        Workgroup workgroup = null;
        try {
            id = SequenceManager.nextID(FastpathConstants.WORKGROUP);
            boolean workgroupAdded = addWorkgroup(id, name);
            if (workgroupAdded) {
                workgroupLock.writeLock().lock();
                try {
                    workgroup = new Workgroup(id, agentManager);
                    workgroups.put(workgroup.getJID().toBareJID(), workgroup);
                    workgroupOpenStatus.put(workgroup.getID(), workgroup.getStatus());
                }
                finally {
                    workgroupLock.writeLock().unlock();
                }
                // Create a chat room for this workgroup
                workgroup.createGroupChatRoom();
                // Trigger the event that a workgroup has been created
                WorkgroupEventDispatcher.workgroupCreated(workgroup);
            }
            else {
                throw new UnauthorizedException("Could not insert workgroup in database");
            }
        }
        catch (Exception e) {
            Log.error(e);
            if (id != -1) {
                try {
                    if (workgroup != null) {
                        workgroups.remove(workgroup.getJID().toBareJID());
                        workgroupOpenStatus.remove(workgroup.getID());
                        deleteWorkgroup(id);
                    }
                }
                catch (Exception e1) {
                    Log.error(e1);
                }
            }
            if (e instanceof UserAlreadyExistsException) {
                throw (UserAlreadyExistsException)e;
            }
            else {
                throw new UnauthorizedException();
            }
        }
        return workgroup;
    }

    /**
     * Remove a workgroup from the system.
     *
     * @param workgroup the workgroup to remove.
     * @throws UnauthorizedException if not allowed to delete the workgroup.
     */
    public void deleteWorkgroup(Workgroup workgroup) throws UnauthorizedException {
        if (!loaded) {
            throw new IllegalStateException("Workgroup Manager not loaded yet");
        }

        // Trigger the event that a workgroup is being deleted
        WorkgroupEventDispatcher.workgroupDeleting(workgroup);

        deleteWorkgroup(workgroup.getID());
        // Notify the workgroup that it is being destroyed
        workgroup.destroy();

        workgroupLock.writeLock().lock();
        try {
            workgroups.remove(workgroup.getJID().toBareJID());
            workgroupOpenStatus.remove(workgroup.getID());
        }
        finally {
            workgroupLock.writeLock().unlock();
        }

        // Delete the queues of the workgroup
        for (RequestQueue requestQueue : workgroup.getRequestQueues()) {
            workgroup.deleteRequestQueue(requestQueue);
        }
        // Trigger the event that a workgroup has been deleted
        WorkgroupEventDispatcher.workgroupDeleted(workgroup);
    }

    /**
     * Returns the number of workgroups in the system.
     *
     * @return the number of workgroups in the system.
     */
    public int getWorkgroupCount() {
        return workgroups.size();
    }

    /**
     * Returns the workgroup mapped to a specific JID.
     *
     * @param jid the JID mapped to the workgroup.
     * @return the workgroup with the specified JID.
     * @throws UserNotFoundException if the workgroup could not be loaded.
     */
    public Workgroup getWorkgroup(JID jid) throws UserNotFoundException {
        Workgroup wg = workgroups.get(jid.toBareJID());
        if (wg == null) {
            throw new UserNotFoundException(jid.toBareJID());
        }
        return wg;
    }

    /**
     * Return a workgroup based on it's node.
     *
     * @param workgroupName the name of the workgroup.
     * @return the workgroup or null if no workgroup is found.
     */
    public Workgroup getWorkgroup(String workgroupName) {
        for (Workgroup workgroup : getWorkgroups()) {
            if (workgroup.getJID().getNode().equalsIgnoreCase(workgroupName)) {
                return workgroup;
            }
        }

        return null;
    }

    /**
     * Returns all workgroups within the system sorted by ID.
     *
     * @return the collection of workgroups.
     */
    public Collection<Workgroup> getWorkgroups() {
        if (workgroups.isEmpty()) {
            return Collections.emptyList();
        }
        final List<Workgroup> copy = new ArrayList<Workgroup>(workgroups.values());
        Collections.sort(copy, workgroupComparator);

        return Collections.unmodifiableCollection(copy);
    }

    public Iterator getWorkgroups(WorkgroupResultFilter filter) {
        final List<Workgroup> wgroups = new ArrayList<Workgroup>(workgroups.values());
        Collections.sort(wgroups, workgroupComparator);

        Iterator groups = filter.filter(wgroups.iterator());
        if (groups == null) {
            groups = Collections.EMPTY_LIST.iterator();
        }
        return groups;
    }

    /**
     * Returns the handler for disco#info packets sent to the workgroup service. The returned
     * handler may be used for configuring its features providers.
     *
     * @return the handler for disco#info packets sent to the workgroup service.
     */
    public IQDiscoInfoHandler getIqDiscoInfoHandler() {
        return iqDiscoInfoHandler;
    }

    /**
     * <p>Trigger an open check every 25 seconds after an initial 45 second delay.</p>
     * <p/>
     * <p>The workgroup knows when it is open according to it's own settings including
     * schedule information. However, the schedule is only checked on demand when new
     * users requests are submitted, or agents query the status of the workgroup.
     * So we must force a check of the workgroup open status periodically to see
     * if it has changed, and if so, to have the workgroup broadcast its presence.
     * This is potentially more bandwidth intensive than having each workgroup watch
     * it's own schedule but uses less threads.</p>
     * TODO: trace down all events that cause a state change so we don't have to poll
     */
    private void startTimer() {
        TaskEngine taskEngine = TaskEngine.getInstance();
        taskEngine.schedule(new TimerTask() {
            public void run() {
                workgroupLock.readLock().lock();
                try {
                    for (Workgroup group : workgroups.values()) {
                        Workgroup.Status currentOpen = group.getStatus();
                        Workgroup.Status oldOpen = workgroupOpenStatus.get(group.getID());
                        if (oldOpen != currentOpen) {
                            group.broadcastQueuesStatus();
                            workgroupOpenStatus.put(group.getID(), currentOpen);
                            if (Workgroup.Status.OPEN != oldOpen && Workgroup.Status.OPEN == currentOpen) {
                                // Trigger the event that the workgroup has been opened
                                group.notifyOpened();
                            }
                            else if (Workgroup.Status.OPEN == oldOpen) {
                                // Trigger the event that the workgroup has been closed
                                group.notifyClosed();
                            }
                        }
                    }
                }
                finally {
                    workgroupLock.readLock().unlock();
                }
            }
        }, 45000, 9000);

        // Every 5 minutes let the workgroups clean up dead requests or dead rooms. This may occur
        // if the connections were lost or the invitations were lost or whatever
        taskEngine.schedule(new TimerTask() {
            public void run() {
                workgroupLock.readLock().lock();
                try {
                    for (Workgroup group : workgroups.values()) {
                        group.cleanup();
                    }
                }
                finally {
                    workgroupLock.readLock().unlock();
                }
            }
        }, 60000, 300000);

        // Every 15 seconds check for not answered room invitations
        taskEngine.schedule(new TimerTask() {
            public void run() {
                workgroupLock.readLock().lock();
                try {
                    for (Workgroup group : workgroups.values()) {
                        group.checkRequests();
                    }
                }
                finally {
                    workgroupLock.readLock().unlock();
                }
            }
        }, 10000, 15000);

        // Every 30 seconds check if the search index of the workgroups should be updated
        taskEngine.schedule(new TimerTask() {
            public void run() {
                workgroupLock.readLock().lock();
                try {
                    for (Workgroup group : workgroups.values()) {
                        try {
                            ChatSearchManager.getInstanceFor(group).updateIndex(false);
                        }
                        catch (IOException e) {
                            Log.error(e);
                        }
                    }
                }
                finally {
                    workgroupLock.readLock().unlock();
                }
            }
        }, 10000, 30000);
    }

    void updateWorkgroupStatus(Workgroup workgroup) {
        Workgroup.Status newStatus = workgroup.getStatus();
        Workgroup.Status oldStatus = workgroupOpenStatus.put(workgroup.getID(), newStatus);
        if (Workgroup.Status.OPEN != oldStatus && Workgroup.Status.OPEN == newStatus) {
            // Trigger the event that the workgroup has been opened
            workgroup.notifyOpened();
        }
        else if (Workgroup.Status.OPEN == oldStatus && Workgroup.Status.OPEN != newStatus) {
            // Trigger the event that the workgroup has been closed
            workgroup.notifyClosed();
        }
    }

    public String getDefaultChatServer() {
        return getMUCServiceName();
    }

    private boolean addWorkgroup(long workgroupID, String workgroupName) {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(ADD_WORKGROUP);
            Date now = new Date();
            pstmt.setLong(1, workgroupID);
            pstmt.setString(2, workgroupName);
            pstmt.setString(3, workgroupName);
            pstmt.setString(4, "None");
            pstmt.setInt(5, 0);     // start workgroups closed
            pstmt.setString(6, StringUtils.dateToMillis(now));
            pstmt.setString(7, StringUtils.dateToMillis(now));
            pstmt.setInt(8, -1);
            pstmt.setInt(9, -1);
            pstmt.setLong(10, -1);
            pstmt.setLong(11, -1);
            pstmt.setInt(12, 0);   // start schedule mode to manual
            pstmt.executeUpdate();
            return true;
        }
        catch (SQLException ex) {
            Log.error(ex);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
        return false;
    }

    private void deleteWorkgroup(long workgroupID) {
        Connection con = null;
        PreparedStatement pstmt = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(DELETE_WORKGROUP);
            pstmt.setLong(1, workgroupID);
            pstmt.executeUpdate();
        }
        catch (SQLException ex) {
            Log.error(ex);
        }
        finally {
            DbConnectionManager.closeConnection(pstmt, con);
        }
    }

    private void loadWorkgroups() {
        Connection con = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            con = DbConnectionManager.getConnection();
            pstmt = con.prepareStatement(LOAD_WORKGROUPS);
            rs = pstmt.executeQuery();
            workgroupLock.writeLock().lock();
            while (rs.next()) {
                Workgroup workgroup = new Workgroup(rs.getLong(1),
                    agentManager);

                workgroups.put(workgroup.getJID().toBareJID(), workgroup);
                workgroupOpenStatus.put(workgroup.getID(), workgroup.getStatus());
            }
        }
        catch (SQLException ex) {
            Log.error(ex);
        }
        finally {
            workgroupLock.writeLock().unlock();
            DbConnectionManager.closeConnection(rs, pstmt, con);
        }

        // Initialize routing manager
        RoutingManager.getInstance();
    }

    public void processPacket(Packet packet) {
        try {
            // Check if the packet is a disco request
            if (packet instanceof IQ) {
                if (process((IQ)packet)) {
                    return;
                }
            }
            // Check if the packet was sent to an existent workgroup. If a workgroup
            // was found then let the workgroup process the packet
            try {
                Workgroup workgroup = getWorkgroup(packet.getTo());
                workgroup.process(packet);
            }
            catch (UserNotFoundException e) {
                // Answer a not_authorized error since the workgroup was not found. A
                // not_acceptable error was chosen since we are returning that same error when
                // sending presences to a workgroup from a JID that is not an agent or when the
                // agent does not belong to the workgroup. Answering the same error code ensures
                // some kind of security since the sender cannot distinguish between any of the
                // above situations
                if (packet instanceof Presence) {
                    if (((Presence)packet).getType() == Presence.Type.error) {
                        // Skip Presence packets of type error
                        return;
                    }
                    Presence reply = new Presence();
                    reply.setID(packet.getID());
                    reply.setTo(packet.getFrom());
                    reply.setFrom(packet.getTo());
                    reply.setError(PacketError.Condition.not_authorized);
                    send(reply);
                }
                else if (packet instanceof IQ) {
                    if (((IQ)packet).getType() == IQ.Type.error) {
                        // Skip IQ packets of type error
                        return;
                    }
                    IQ reply = IQ.createResultIQ((IQ)packet);
                    reply.setChildElement(((IQ)packet).getChildElement().createCopy());
                    reply.setError(PacketError.Condition.not_authorized);
                    send(reply);
                }
                else {
                    if (((Message)packet).getType() == Message.Type.error) {
                        // Skip Message packets of type error
                        return;
                    }
                    Message reply = new Message();
                    reply.setID(packet.getID());
                    reply.setTo(packet.getFrom());
                    reply.setFrom(packet.getTo());
                    reply.setError(PacketError.Condition.not_authorized);
                    send(reply);
                }
            }
        }
        catch (Exception e) {
            Log.error(e);
        }
    }

    /**
     * Returns true if the IQ packet was processed. This method should only process disco packets
     * sent to the workgroup service.
     *
     * @param iq the IQ packet to process.
     * @return true if the IQ packet was processed.
     */
    private boolean process(IQ iq) {
        if (iq.getType() == IQ.Type.error) {
            // Skip IQ packets of type error
            return false;
        }
        Element childElement = iq.getChildElement();
        String name = null;
        String namespace = null;
        if (childElement != null) {
            namespace = childElement.getNamespaceURI();
            name = childElement.getName();
        }
        if ("http://jabber.org/protocol/disco#info".equals(namespace)) {
            IQ reply = iqDiscoInfoHandler.handleIQ(iq);
            if (reply != null) {
                send(reply);
            }
        }
        else if ("http://jabber.org/protocol/disco#items".equals(namespace)) {
            IQ reply = iqDiscoItemsHandler.handleIQ(iq);
            if (reply != null) {
                send(reply);
            }
        }
        else if ("jabber:iq:version".equals(namespace)) {
            IQ reply = IQ.createResultIQ(iq);
            Element version = reply.setChildElement("query", "jabber:iq:version");
            version.addElement("name").setText("Spark Fastpath");
            version.addElement("version").setText("3.2");
            version.addElement("os").setText("Java 5");
            send(reply);
        }
        else if ("workgroups".equals(name)) {
            try {
                // Check that the sender of this IQ is an agent
                getAgentManager().getAgent(iq.getFrom());
                // Get the agent JID to return his workgroups
                String agentJID = childElement.attributeValue("jid");
                try {
                    // Answer the workgroups where the agent can work in
                    Agent agent = getAgentManager().getAgent(new JID(agentJID));
                    sendWorkgroups(iq, agent);
                }
                catch (AgentNotFoundException e) {
                    IQ reply = IQ.createResultIQ(iq);
                    reply.setChildElement(iq.getChildElement().createCopy());
                    reply.setError(new PacketError(PacketError.Condition.item_not_found));
                    send(reply);
                }
            }
            catch (AgentNotFoundException e) {
                IQ reply = IQ.createResultIQ(iq);
                reply.setChildElement(iq.getChildElement().createCopy());
                reply.setError(new PacketError(PacketError.Condition.not_authorized));
                send(reply);
            }
        }
        else if ("transcript-search".equals(name)) {
            iqChatSearchHandler.handleIQ(iq);
        }
        else if ("http://jabber.org/protocol/commands".equals(namespace)) {
            // Process ad-hoc command
            IQ reply = commandManager.process(iq);
            send(reply);
        }
        else {
            return false;
        }
        return true;
    }

    private void sendWorkgroups(IQ request, Agent agent) {
        IQ reply = IQ.createResultIQ(request);
        Element workgroupsElement = reply.setChildElement("workgroups",
            "http://jabber.org/protocol/workgroup");
        workgroupsElement.addAttribute("jid", agent.getAgentJID().toBareJID());
        for (Workgroup workgroup : getWorkgroups()) {
            if (workgroup.getAgents().contains(agent)) {
                // Add the information of the workgroup
                Element workgroupElement = workgroupsElement.addElement("workgroup");
                workgroupElement.addAttribute("jid", workgroup.getJID().toBareJID());
            }
        }
        send(reply);
    }

    public String getName() {
        return "Workgroup Plugin";
    }

    public String getDescription() {
        return "Workgroup plugin for Live Assistance.";
    }

    public void initialize(JID jid, ComponentManager componentManager) throws ComponentException {
        // Set the full domain address that this component is serving
        serviceAddress = jid;

        agentManager = new AgentManager();

        // Set a default MUC service JID. This may be required when the server does
        // not support service discovery
        mucServiceName = "conference." + componentManager.getServerName();

        // Send a disco request to discover the MUC service address
        IQ disco = new IQ(IQ.Type.get);
        disco.setTo(componentManager.getServerName());
        disco.setFrom(jid);
        disco.setChildElement("query", "http://jabber.org/protocol/disco#items");
        send(disco);

        // Start the background processes
        startTimer();
        // Load Workgroups
        loadWorkgroups();
        // Set that the workgroups have been loaded
        loaded = true;
    }

    public void send(Packet packet) {
        try {
            ComponentManagerFactory.getComponentManager().sendPacket(this, packet);
        }
        catch (ComponentException e) {
            // Do nothing. This error should never happen
            Log.error(e);
        }
    }

    public JID getAddress() {
        return serviceAddress;
    }

    /**
     * Returns the service name for MUC
     *
     * @return the MUC Service Name
     */
    public final String getMUCServiceName() {
        return mucServiceName;
    }

    public void setMUCServiceName(String mucServiceName) {
        this.mucServiceName = mucServiceName;
    }

    /**
     * Listens for changes in the Group model to update respective agents.
     */
    private void addGroupManagerListener() {
        groupEventListener = new GroupEventListener() {
            public void groupCreated(Group group, Map params) {
            }

            public void groupDeleting(Group group, Map params) {
            }

            public void groupModified(Group group, Map params) {

            }

            public void memberAdded(Group group, Map params) {
                String userJID = (String)params.get("member");
                JID jid = new JID(userJID);

                if (!agentManager.hasAgent(jid)) {
                    for (Workgroup workgroup : workgroups.values()) {
                        for (RequestQueue queue : workgroup.getRequestQueues()) {
                            if (queue.hasGroup(group)) {
                                agentManager.getAgents(group);
                            }
                        }
                    }
                }
            }

            public void memberRemoved(Group group, Map params) {
            }

            public void adminAdded(Group group, Map params) {
            }

            public void adminRemoved(Group group, Map params) {
            }
        };
        GroupEventDispatcher.addListener(groupEventListener);
    }

    /**
     * Creates a demo user account.
     *
     * @return true if the user account was created.
     */
    private boolean createDemoUser() {
        // Do nothing if user store is read-only
        if (UserManager.getUserProvider().isReadOnly()) {
            return false;
        }
        try {
            UserManager.getInstance().createUser("demo", "demo", "Fastpath Demo Account", "demo@fastpath.com");
            return true;
        }
        catch (Exception e) {
            Log.error(e);
        }
        return false;
    }

    /**
     * Creates a demo workgroup.
     *
     * @return true if the workgroup was created.
     */
    private boolean createDemoWorkgroup() {
        // Create example workgroup
        try {
            if (WorkgroupUtils.createWorkgroup("demo", "Demo workgroup", "demo").size() == 0) {
                JiveGlobals.setProperty("demo.workgroup", "true");
            }
        }
        catch (Exception e) {
            Log.error(e);
            return false;
        }
        return true;
    }

    /**
     * Checks for outdated presences caused by network failures, etc.
     */
    private void handleOutdatePresence() {
        for (Workgroup workgroup : getWorkgroups()) {
            for (AgentSession agentSession : workgroup.getAgentSessions()) {
                final JID agentJID = agentSession.getJID();
                final PresenceManager presenceManager = XMPPServer.getInstance().getPresenceManager();
                boolean isOnline = false;
                for (Presence presence : presenceManager.getPresences(agentJID.getNode())) {
                    if (presence.getFrom().equals(agentJID)) {
                        isOnline = true;
                    }
                }

                if (!isOnline) {
                    // Send offline presence to workgroup.
                    for (Workgroup wgroup : agentSession.getWorkgroups()) {
                        Presence presence = new Presence();
                        presence.setFrom(agentJID);
                        presence.setTo(wgroup.getJID());
                        presence.setType(Presence.Type.unavailable);
                        wgroup.getWorkgroupPresenceHandler().process(presence);
                    }
                }
            }
        }
    }

    /**
     * Sorts all <code>Workgroups</code> by ID.
     */
    static Comparator<Workgroup> workgroupComparator = new Comparator<Workgroup>() {
        public int compare(Workgroup item1, Workgroup item2) {
            float int1 = item1.getID();
            float int2 = item2.getID();

            if (int1 == int2) {
                return 0;
            }

            if (int1 > int2) {
                return 1;
            }

            if (int1 < int2) {
                return -1;
            }

            return 0;
        }
    };
}
