/**
 * $RCSfile$
 * $Revision: 2407 $
 * $Date: 2004-11-02 15:37:00 -0800 (Tue, 02 Nov 2004) $
 *
 * 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.smack;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.FromContainsFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.ThreadFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.collections.ReferenceMap;

import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * The chat manager keeps track of references to all current chats. It will not hold any references
 * in memory on its own so it is neccesary to keep a reference to the chat object itself. To be
 * made aware of new chats, register a listener by calling {@link #addChatListener(ChatManagerListener)}.
 *
 * @author Alexander Wenckus
 */
public class ChatManager {

    /**
     * Returns the next unique id. Each id made up of a short alphanumeric
     * prefix along with a unique numeric value.
     *
     * @return the next id.
     */
    private static synchronized String nextID() {
        return prefix + Long.toString(id++);
    }

    /**
     * A prefix helps to make sure that ID's are unique across mutliple instances.
     */
    private static String prefix = StringUtils.randomString(5);

    /**
     * Keeps track of the current increment, which is appended to the prefix to
     * forum a unique ID.
     */
    private static long id = 0;

    /**
     * Maps thread ID to chat.
     */
    private Map<String, Chat> threadChats = Collections.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
            ReferenceMap.WEAK));

    /**
     * Maps jids to chats
     */
    private Map<String, Chat> jidChats = Collections.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
            ReferenceMap.WEAK));

    /**
     * Maps base jids to chats
     */
    private Map<String, Chat> baseJidChats = Collections.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
            ReferenceMap.WEAK));

    private Set<ChatManagerListener> chatManagerListeners
            = new CopyOnWriteArraySet<ChatManagerListener>();

    private Map<PacketInterceptor, PacketFilter> interceptors
            = new WeakHashMap<PacketInterceptor, PacketFilter>();

    private Connection connection;

    ChatManager(Connection connection) {
        this.connection = connection;

        PacketFilter filter = new PacketFilter() {
            public boolean accept(Packet packet) {
                if (!(packet instanceof Message)) {
                    return false;
                }
                Message.Type messageType = ((Message) packet).getType();
                return messageType != Message.Type.groupchat &&
                        messageType != Message.Type.headline;
            }
        };
        // Add a listener for all message packets so that we can deliver errant
        // messages to the best Chat instance available.
        connection.addPacketListener(new PacketListener() {
            public void processPacket(Packet packet) {
                Message message = (Message) packet;
                Chat chat;
                if (message.getThread() == null) {
                    chat = getUserChat(message.getFrom());
                } else {
                    chat = getThreadChat(message.getThread());
                    if (chat == null) {
                        // Try to locate the chat based on the sender of the message
                        chat = getUserChat(message.getFrom());
                    }
                }

                if (chat == null) {
                    chat = createChat(message);
                }
                deliverMessage(chat, message);
            }
        }, filter);
    }

    /**
     * Creates a new chat and returns it.
     *
     * @param userJID  the user this chat is with.
     * @param listener the listener which will listen for new messages from this chat.
     * @return the created chat.
     */
    public Chat createChat(String userJID, MessageListener listener) {
        String threadID;
        do {
            threadID = nextID();
        } while (threadChats.get(threadID) != null);

        return createChat(userJID, threadID, listener);
    }

    /**
     * Creates a new chat using the specified thread ID, then returns it.
     *
     * @param userJID  the jid of the user this chat is with
     * @param thread   the thread of the created chat.
     * @param listener the listener to add to the chat
     * @return the created chat.
     */
    public Chat createChat(String userJID, String thread, MessageListener listener) {
        if (thread == null) {
            thread = nextID();
        }
        Chat chat = threadChats.get(thread);
        if (chat != null) {
            throw new IllegalArgumentException("ThreadID is already used");
        }
        chat = createChat(userJID, thread, true);
        chat.addMessageListener(listener);
        return chat;
    }

    private Chat createChat(String userJID, String threadID, boolean createdLocally) {
        Chat chat = new Chat(this, userJID, threadID);
        threadChats.put(threadID, chat);
        jidChats.put(userJID, chat);
        baseJidChats.put(StringUtils.parseBareAddress(userJID), chat);

        for (ChatManagerListener listener : chatManagerListeners) {
            listener.chatCreated(chat, createdLocally);
        }

        return chat;
    }

    private Chat createChat(Message message) {
        String threadID = message.getThread();
        if (threadID == null) {
            threadID = nextID();
        }
        String userJID = message.getFrom();

        return createChat(userJID, threadID, false);
    }

    /**
     * Try to get a matching chat for the given user JID.  Try the full
     * JID map first, the try to match on the base JID if no match is
     * found.
     *
     * @param userJID
     * @return
     */
    private Chat getUserChat(String userJID) {
        Chat match = jidChats.get(userJID);

        if (match == null) {
            match = baseJidChats.get(StringUtils.parseBareAddress(userJID));
        }
        return match;
    }

    public Chat getThreadChat(String thread) {
        return threadChats.get(thread);
    }

    /**
     * Register a new listener with the ChatManager to recieve events related to chats.
     *
     * @param listener the listener.
     */
    public void addChatListener(ChatManagerListener listener) {
        chatManagerListeners.add(listener);
    }

    /**
     * Removes a listener, it will no longer be notified of new events related to chats.
     *
     * @param listener the listener that is being removed
     */
    public void removeChatListener(ChatManagerListener listener) {
        chatManagerListeners.remove(listener);
    }

    /**
     * Returns an unmodifiable collection of all chat listeners currently registered with this
     * manager.
     *
     * @return an unmodifiable collection of all chat listeners currently registered with this
     * manager.
     */
    public Collection<ChatManagerListener> getChatListeners() {
        return Collections.unmodifiableCollection(chatManagerListeners);
    }

    private void deliverMessage(Chat chat, Message message) {
        // Here we will run any interceptors
        chat.deliver(message);
    }

    void sendMessage(Chat chat, Message message) {
        for (Map.Entry<PacketInterceptor, PacketFilter> interceptor : interceptors.entrySet()) {
            PacketFilter filter = interceptor.getValue();
            if (filter != null && filter.accept(message)) {
                interceptor.getKey().interceptPacket(message);
            }
        }
        // Ensure that messages being sent have a proper FROM value
        if (message.getFrom() == null) {
            message.setFrom(connection.getUser());
        }
        connection.sendPacket(message);
    }

    PacketCollector createPacketCollector(Chat chat) {
        return connection.createPacketCollector(new AndFilter(new ThreadFilter(chat.getThreadID()),
                new FromContainsFilter(chat.getParticipant())));
    }

    /**
     * Adds an interceptor which intercepts any messages sent through chats.
     *
     * @param packetInterceptor the interceptor.
     */
    public void addOutgoingMessageInterceptor(PacketInterceptor packetInterceptor) {
        addOutgoingMessageInterceptor(packetInterceptor, null);
    }

    public void addOutgoingMessageInterceptor(PacketInterceptor packetInterceptor, PacketFilter filter) {
        if (packetInterceptor != null) {
            interceptors.put(packetInterceptor, filter);
        }
    }
}
