/**
 * $RCSfile: PresenceManagerImpl.java,v $
 * $Revision$
 * $Date$
 *
 * Copyright (C) 2004 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.
 */

package org.jivesoftware.messenger.spi;

import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.DocumentException;
import org.jivesoftware.messenger.*;
import org.jivesoftware.messenger.handler.PresenceUpdateHandler;
import org.jivesoftware.messenger.component.InternalComponentManager;
import org.jivesoftware.messenger.auth.UnauthorizedException;
import org.jivesoftware.messenger.container.BasicModule;
import org.jivesoftware.messenger.roster.Roster;
import org.jivesoftware.messenger.roster.RosterItem;
import org.jivesoftware.messenger.user.User;
import org.jivesoftware.messenger.user.UserManager;
import org.jivesoftware.messenger.user.UserNotFoundException;
import org.jivesoftware.util.CacheManager;
import org.jivesoftware.util.LocaleUtils;
import org.jivesoftware.util.Log;
import org.xmpp.component.Component;
import org.xmpp.packet.JID;
import org.xmpp.packet.PacketError;
import org.xmpp.packet.Presence;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

/**
 * Simple in memory implementation of the PresenceManager interface.
 *
 * @author Iain Shigeoka
 */
public class PresenceManagerImpl extends BasicModule implements PresenceManager {

    private static final String LAST_PRESENCE_PROP = "lastUnavailablePresence";
    private static final String LAST_ACTIVITY_PROP = "lastActivity";

    private SessionManager sessionManager;
    private XMPPServer server;
    private PacketDeliverer deliverer;
    private PresenceUpdateHandler presenceUpdateHandler;

    private InternalComponentManager componentManager;

    public PresenceManagerImpl() {
        super("Presence manager");

        // Use component manager for Presence Updates.
        componentManager = InternalComponentManager.getInstance();
    }

    public boolean isAvailable(User user) {
        return sessionManager.getSessionCount(user.getUsername()) > 0;
    }

    public Presence getPresence(User user) {
        if (user == null) {
            return null;
        }
        Presence presence = null;

        for (ClientSession session : sessionManager.getSessions(user.getUsername())) {
            if (presence == null) {
                presence = session.getPresence();
            }
            else {
                // Get the ordinals of the presences to compare. If no ordinal is available then
                // assume a value of -1
                int o1 = presence.getShow() != null ? presence.getShow().ordinal() : -1;
                int o2 = session.getPresence().getShow() != null ?
                        session.getPresence().getShow().ordinal() : -1;
                // Compare the presences' show ordinals
                if (o1 > o2) {
                    presence = session.getPresence();
                }
            }
        }
        return presence;
    }

    public Collection<Presence> getPresences(String username) {
        if (username == null) {
            return null;
        }
        List<Presence> presences = new ArrayList<Presence>();

        for (ClientSession session : sessionManager.getSessions(username)) {
            presences.add(session.getPresence());
        }
        return Collections.unmodifiableCollection(presences);
    }

    public String getLastPresenceStatus(User user) {
        String answer = null;
        String presenceXML = user.getProperties().get(LAST_PRESENCE_PROP);
        if (presenceXML != null) {
            try {
                // Parse the element
                Document element = DocumentHelper.parseText(presenceXML);
                answer = element.getRootElement().elementTextTrim("status");
            }
            catch (DocumentException e) {
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
            }
        }
        return answer;
    }

    public long getLastActivity(User user) {
        long answer = -1;
        String offline = user.getProperties().get(LAST_ACTIVITY_PROP);
        if (offline != null) {
            try {
                answer = (System.currentTimeMillis() - Long.parseLong(offline)) / 1000;
            }
            catch (NumberFormatException e) {
                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
            }
        }
        return answer;
    }

    public void userAvailable(Presence presence) {
        // Delete the last unavailable presence of this user since the user is now
        // available. Only perform this operation if this is an available presence sent to
        // THE SERVER and the presence belongs to a local user.
        if (presence.getTo() == null && server.isLocal(presence.getFrom())) {
            String username = presence.getFrom().getNode();
            if (username == null) {
                // Ignore anonymous users
                return;
            }
            try {
                User probeeUser = UserManager.getInstance().getUser(username);
                probeeUser.getProperties().remove(LAST_PRESENCE_PROP);
            }
            catch (UserNotFoundException e) {
            }
        }
    }

    public void userUnavailable(Presence presence) {
        // Only save the last presence status and keep track of the time when the user went
        // offline if this is an unavailable presence sent to THE SERVER and the presence belongs
        // to a local user.
        if (presence.getTo() == null && server.isLocal(presence.getFrom())) {
            String username = presence.getFrom().getNode();
            if (username == null) {
                // Ignore anonymous users
                return;
            }
            try {
                User probeeUser = UserManager.getInstance().getUser(username);
                if (!presence.getElement().elements().isEmpty()) {
                    // Save the last unavailable presence of this user if the presence contains any
                    // child element such as <status>
                    probeeUser.getProperties().put(LAST_PRESENCE_PROP, presence.toXML());
                }
                // Keep track of the time when the user went offline
                probeeUser.getProperties().put(LAST_ACTIVITY_PROP,
                        String.valueOf(System.currentTimeMillis()));
            }
            catch (UserNotFoundException e) {
            }
        }
    }

    public void handleProbe(Presence packet) throws UnauthorizedException {
        String username = packet.getTo().getNode();
        // Check for a cached roster:
        Roster roster = (Roster)CacheManager.getCache("username2roster").get(username);
        if (roster == null) {
            synchronized(username.intern()) {
                roster = (Roster)CacheManager.getCache("username2roster").get(username);
                if (roster == null) {
                    // Not in cache so load a new one:
                    roster = new Roster(username);
                    CacheManager.getCache("username2roster").put(username, roster);
                }
            }
        }
        try {
            RosterItem item = roster.getRosterItem(packet.getFrom());
            if (item.getSubStatus() == RosterItem.SUB_FROM
                    || item.getSubStatus() == RosterItem.SUB_BOTH) {
                probePresence(packet.getFrom(),  packet.getTo());
            }
            else {
                PacketError.Condition error = PacketError.Condition.not_authorized;
                if ((item.getSubStatus() == RosterItem.SUB_NONE &&
                        item.getRecvStatus() != RosterItem.RECV_SUBSCRIBE) ||
                        (item.getSubStatus() == RosterItem.SUB_TO &&
                        item.getRecvStatus() != RosterItem.RECV_SUBSCRIBE)) {
                    error = PacketError.Condition.forbidden;
                }
                Presence presenceToSend = new Presence();
                presenceToSend.setError(error);
                presenceToSend.setTo(packet.getFrom());
                presenceToSend.setFrom(packet.getTo());
                deliverer.deliver(presenceToSend);
            }
        }
        catch (UserNotFoundException e) {
            Presence presenceToSend = new Presence();
            presenceToSend.setError(PacketError.Condition.forbidden);
            presenceToSend.setTo(packet.getFrom());
            presenceToSend.setFrom(packet.getTo());
            deliverer.deliver(presenceToSend);
        }
    }

    public boolean canProbePresence(JID prober, String probee) throws UserNotFoundException {
        // Check that the probee is a valid user
        UserManager.getInstance().getUser(probee);
        // Check for a cached roster:
        Roster roster = (Roster)CacheManager.getCache("username2roster").get(probee);
        if (roster == null) {
            synchronized(probee.intern()) {
                roster = (Roster)CacheManager.getCache("username2roster").get(probee);
                if (roster == null) {
                    // Not in cache so load a new one:
                    roster = new Roster(probee);
                    CacheManager.getCache("username2roster").put(probee, roster);
                }
            }
        }
        RosterItem item = roster.getRosterItem(prober);
        if (item.getSubStatus() == RosterItem.SUB_FROM
                || item.getSubStatus() == RosterItem.SUB_BOTH) {
            return true;
        }
        return false;
    }

    public void probePresence(JID prober, JID probee) {
        try {
            Component component = getPresenceComponent(probee);
            if (server.isLocal(probee)) {
                // If the probee is a local user then don't send a probe to the contact's server.
                // But instead just send the contact's presence to the prober
                if (probee.getNode() != null && !"".equals(probee.getNode())) {
                    Collection<ClientSession> sessions =
                            sessionManager.getSessions(probee.getNode());
                    if (sessions.isEmpty()) {
                        // If the probee is not online then try to retrieve his last unavailable
                        // presence which may contain particular information and send it to the
                        // prober
                        try {
                            User probeeUser = UserManager.getInstance().getUser(probee.getNode());
                            String presenceXML = probeeUser.getProperties().get(LAST_PRESENCE_PROP);
                            if (presenceXML != null) {
                                try {
                                    // Parse the element
                                    Document element = DocumentHelper.parseText(presenceXML);
                                    // Create the presence from the parsed element
                                    Presence presencePacket = new Presence(element.getRootElement());
                                    presencePacket.setFrom(probee.toBareJID());
                                    presencePacket.setTo(prober);
                                    // Send the presence to the prober
                                    deliverer.deliver(presencePacket);
                                }
                                catch (Exception e) {
                                    Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
                                }

                            }
                        }
                        catch (UserNotFoundException e) {
                        }
                    }
                    else {
                        // The contact is online so send to the prober all the resources where the
                        // probee is connected
                        for (ClientSession session : sessions) {
                            Presence presencePacket = session.getPresence().createCopy();
                            presencePacket.setFrom(session.getAddress());
                            presencePacket.setTo(prober);
                            try {
                                deliverer.deliver(presencePacket);
                            }
                            catch (Exception e) {
                                Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
                            }
                        }
                    }
                }
            }
            else if (component != null) {
                // If the probee belongs to a component then ask the component to process the
                // probe presence
                Presence presence = new Presence();
                presence.setType(Presence.Type.probe);
                presence.setFrom(prober);
                presence.setTo(probee);
                component.processPacket(presence);
            }
            else {
                // Check if the probee may be hosted by this server
                /*String serverDomain = server.getServerInfo().getName();
                if (!probee.getDomain().contains(serverDomain)) {*/
                if (server.isRemote(probee)) {
                    // Send the probe presence to the remote server
                    Presence probePresence = new Presence();
                    probePresence.setType(Presence.Type.probe);
                    probePresence.setFrom(prober);
                    probePresence.setTo(probee.toBareJID());
                    // Send the probe presence
                    deliverer.deliver(probePresence);
                }
                else {
                    // The probee may be related to a component that has not yet been connected so
                    // we will keep a registry of this presence probe. The component will answer
                    // this presence probe when he becomes online
                    componentManager.addPresenceRequest(prober, probee);
                }
            }
        }
        catch (Exception e) {
            Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
        }
    }

    public void sendUnavailableFromSessions(JID recipientJID, JID userJID) {
        if (userJID.getNode() != null && !"".equals(userJID.getNode())) {
            for (ClientSession session : sessionManager.getSessions(userJID.getNode())) {
                // Do not send an unavailable presence if the user sent a direct available presence
                if (presenceUpdateHandler.hasDirectPresence(session, recipientJID)) {
                    continue;
                }
                Presence presencePacket = new Presence();
                presencePacket.setType(Presence.Type.unavailable);
                presencePacket.setFrom(session.getAddress());
                presencePacket.setTo(recipientJID);
                try {
                    deliverer.deliver(presencePacket);
                }
                catch (Exception e) {
                    Log.error(LocaleUtils.getLocalizedString("admin.error"), e);
                }
            }
        }
    }

    // #####################################################################
    // Module management
    // #####################################################################

    public void initialize(XMPPServer server) {
        super.initialize(server);
        this.server = server;
        deliverer = server.getPacketDeliverer();
        sessionManager = server.getSessionManager();
        presenceUpdateHandler = server.getPresenceUpdateHandler();
    }

    public Component getPresenceComponent(JID probee) {
        // Check for registered components
        Component component = componentManager.getComponent(probee.toBareJID());
        if (component != null) {
            return component;
        }
        return null;
    }
}