/**
 * $RCSfile$
 * $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.interceptor;

import org.jivesoftware.messenger.Session;
import org.jivesoftware.util.Log;
import org.xmpp.packet.Packet;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * An InterceptorManager holds the list of global interceptors and interceptors per-user that will
 * be invoked before and after a packet was read in SocketReadThead and also when the packet is
 * about to be sent in SocketConnection. If the interceptor is related to a user then it will
 * get all the packets sent or received for <b>any</b> connection of the user.<p>
 *
 * PacketInterceptors that are invoked before the packet is sent or processed (when read) may
 * change the original packet or may even reject the packet by throwing an exception. If the
 * interceptor rejects a received packet then the sender of the packet will get a not_allowed
 * answer.<p>
 *
 * @see PacketInterceptor
 * @author Gaston Dombiak
 */
public class InterceptorManager {

    private static InterceptorManager instance = new InterceptorManager();

    private List<PacketInterceptor> globalInterceptors = new CopyOnWriteArrayList<PacketInterceptor>();
    private Map<String, List<PacketInterceptor>> usersInterceptors = new ConcurrentHashMap<String, List<PacketInterceptor>>();

    public static InterceptorManager getInstance() {
        return instance;
    }

    /**
     * Returns the list of global packet interceptors. These are the interceptors that will be
     * used for all the read and sent packets.
     *
     * @return the list of global packet interceptors.
     */
    public Collection<PacketInterceptor> getInterceptors() {
        return Collections.unmodifiableCollection(globalInterceptors);
    }

    /**
     * Inserts a new interceptor at the end of the list of currently configured
     * interceptors. This interceptor will be used for all the sent and received packets.
     *
     * @param interceptor the interceptor to add.
     */
    public void addInterceptor(PacketInterceptor interceptor) {
        addInterceptor(globalInterceptors.size(), interceptor);
    }

    /**
     * Inserts a new interceptor at specified index in the list of currently configured
     * interceptors. This interceptor will be used for all the sent and received packets.
     *
     * @param index       the index in the list to insert the new interceptor at.
     * @param interceptor the interceptor to add.
     */
    public void addInterceptor(int index, PacketInterceptor interceptor) {
        // Remove the interceptor from the list since the position might have changed
        if (globalInterceptors.contains(interceptor)) {
            globalInterceptors.remove(interceptor);
        }
        globalInterceptors.add(index, interceptor);
    }

    /**
     * Removes the global interceptor from the list.
     *
     * @param interceptor the interceptor to remove.
     * @return true if the item was present in the list
     */
    public boolean removeInterceptor(PacketInterceptor interceptor) {
        return globalInterceptors.remove(interceptor);
    }

    /**
     * Returns the list of packet interceptors that are related to the specified username. These
     * are the interceptors that will be used only when a packet was sent or received by the
     * specified username.
     *
     * @param username the name of the user.
     * @return the list of packet interceptors that are related to the specified username.
     */
    public Collection<PacketInterceptor> getUserInterceptors(String username) {
        List<PacketInterceptor> userInterceptors = usersInterceptors.get(username);
        if (userInterceptors == null) {
            return new ArrayList<PacketInterceptor>();
        }
        else {
            return Collections.unmodifiableCollection(userInterceptors);
        }
    }

    /**
     * Inserts a new interceptor at specified index in the list of currently configured
     * interceptors for a specific username. This interceptor will be used only when a packet
     * was sent or received by the specified username.
     *
     * @param username    the name of the user.
     * @param index       the index in the list to insert the new interceptor at.
     * @param interceptor the interceptor to add.
     */
    public void addUserInterceptor(String username, int index, PacketInterceptor interceptor) {
        List<PacketInterceptor> userInterceptors = usersInterceptors.get(username);
        if (userInterceptors == null) {
            userInterceptors = new CopyOnWriteArrayList<PacketInterceptor>();
            usersInterceptors.put(username, userInterceptors);
        }
        else {
            // Remove the interceptor from the list since the position might have changed
            if (userInterceptors.contains(interceptor)) {
                userInterceptors.remove(interceptor);
            }
        }
        userInterceptors.add(index, interceptor);
    }

    /**
     * Removes the interceptor from the list of interceptors that are related to a specific
     * username.
     *
     * @param username    the name of the user.
     * @param interceptor the interceptor to remove.
     * @return true if the item was present in the list
     */
    public boolean removeUserInterceptor(String username, PacketInterceptor interceptor) {
        boolean answer = false;
        List<PacketInterceptor> userInterceptors = usersInterceptors.get(username);
        if (userInterceptors != null) {
            answer = userInterceptors.remove(interceptor);
            // Remove the entry for this username if the list is now empty
            if (userInterceptors.isEmpty()) {
                usersInterceptors.remove(username);
            }
        }
        return answer;
    }

    /**
     * Invokes all currently-installed interceptors on the specified packet. All global
     * interceptors will be invoked as well as interceptors that are related to the address of the
     * session that received or is sending the packet.<p>
     *
     * Interceptors may be executed before processing a read packet or sending a packet to a user.
     * This means that interceptors are able to alter the read or packet to send. If possible
     * interceptors should perform their work in a short time so the overall performance is not
     * compromised.
     *
     * @param packet    the packet that has been read or sent.
     * @param session   the session that received the packet or the packet was sent to.
     * @param read      flag that indicates if the packet was read or sent.
     * @param processed flag that indicates if the action (read/send) was performed. (PRE vs. POST).
     * @throws PacketRejectedException if the packet should be prevented from being processed.
     */
    public void invokeInterceptors(Packet packet, Session session, boolean read, boolean processed)
            throws PacketRejectedException {
        // Invoke the global interceptors for this packet
        for (PacketInterceptor interceptor : globalInterceptors) {
            try {
                interceptor.interceptPacket(packet, session, read, processed);
            }
            catch (PacketRejectedException e) {
                // Throw this exception since we don't really want to catch it
                throw e;
            }
            catch (Exception e) {
                Log.error("Error in interceptor", e);
            }
        }
        // Invoke the interceptors that are related to the address of the session
        String username = session.getAddress().getNode();
        if (username != null) {
            Collection<PacketInterceptor> userInterceptors = usersInterceptors.get(username);
            if (userInterceptors != null && !userInterceptors.isEmpty()) {
                for (PacketInterceptor interceptor : userInterceptors) {
                    try {
                        interceptor.interceptPacket(packet, session, read, processed);
                    }
                    catch (PacketRejectedException e) {
                        // Throw this exception since we don't really want to catch it
                        throw e;
                    }
                    catch (Exception e) {
                        Log.error("Error in interceptor", e);
                    }
                }
            }
        }
    }
}
