/**
 * $RCSfile: RoutingTableImpl.java,v $
 * $Revision: 3138 $
 * $Date: 2005-12-01 02:13:26 -0300 (Thu, 01 Dec 2005) $
 *
 * 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.wildfire.spi;

import org.jivesoftware.util.Log;
import org.jivesoftware.wildfire.*;
import org.jivesoftware.wildfire.component.InternalComponentManager;
import org.jivesoftware.wildfire.container.BasicModule;
import org.jivesoftware.wildfire.server.OutgoingSessionPromise;
import org.xmpp.packet.JID;

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

/**
 * <p>Uses simple Maps for table storage.</p>
 * <p>Leaves in the tree are indicated by a PacketHandler, while branches are stored in Maps.
 * Traverse the tree according to an XMPPAddress' fields (host -> name -> resource) and when you
 * hit a PacketHandler, you have found the handler for that node and all sub-nodes. </p>
 *
 * @author Iain Shigeoka
 */
public class RoutingTableImpl extends BasicModule implements RoutingTable {

    /**
     * We need a three level tree built of hashtables: host -> name -> resource
     */
    private Map routes = new ConcurrentHashMap();

    private String serverName;
    private InternalComponentManager componentManager;

    public RoutingTableImpl() {
        super("Routing table");
        componentManager = InternalComponentManager.getInstance();
    }

    public void addRoute(JID node, RoutableChannelHandler destination) {

        String nodeJID = node.getNode() == null ? "" : node.getNode();
        String resourceJID = node.getResource() == null ? "" : node.getResource();

        if (destination instanceof ClientSession) {
            Object nameRoutes = routes.get(node.getDomain());
            if (nameRoutes == null) {
                // No route to the requested domain. Create a new entry in the table
                synchronized (node.getDomain().intern()) {
                    // Check again if a route exists now that we have a lock
                    nameRoutes = routes.get(node.getDomain());
                    if (nameRoutes == null) {
                        // Still nothing so create a new entry in the map for domain
                        nameRoutes = new ConcurrentHashMap();
                        routes.put(node.getDomain(), nameRoutes);
                    }
                }
            }
            // Check if there is something associated with the node of the JID
            Object resourceRoutes = ((Map) nameRoutes).get(nodeJID);
            if (resourceRoutes == null) {
                // Nothing was found so create a new entry for this node (a.k.a. user)
                synchronized (nodeJID.intern()) {
                    resourceRoutes = ((Map) nameRoutes).get(nodeJID);
                    if (resourceRoutes == null) {
                        resourceRoutes = new ConcurrentHashMap();
                        ((Map) nameRoutes).put(nodeJID, resourceRoutes);
                    }
                }
            }
            // Add the connected resource to the node's Map
            ((Map) resourceRoutes).put(resourceJID, destination);
        }
        else {
            routes.put(node.getDomain(), destination);
        }
    }

    public RoutableChannelHandler getRoute(JID node) {
        if (node == null) {
            return null;
        }
        return getRoute(node.toString(), node.getNode() == null ? "" : node.getNode(),
                node.getDomain(), node.getResource() == null ? "" : node.getResource());
    }

    private RoutableChannelHandler getRoute(String jid, String node, String domain,
            String resource) {
        RoutableChannelHandler route = null;

        // Check if the address belongs to a remote server
        if (!serverName.equals(domain) && routes.get(domain) == null &&
                componentManager.getComponent(domain) == null) {
            // Return a promise of a remote session. This object will queue packets pending
            // to be sent to remote servers
            return OutgoingSessionPromise.getInstance();
        }

        try {
            Object nameRoutes = routes.get(domain);
            if (nameRoutes instanceof ChannelHandler) {
                route = (RoutableChannelHandler) nameRoutes;
            }
            else if (nameRoutes != null) {
                Object resourceRoutes = ((Map) nameRoutes).get(node);
                if (resourceRoutes instanceof ChannelHandler) {
                    route = (RoutableChannelHandler) resourceRoutes;
                }
                else if (resourceRoutes != null) {
                    route = (RoutableChannelHandler) ((Map) resourceRoutes).get(resource);
                }
                else {
                    route = null;
                }
            }
        }
        catch (Exception e) {
            if (Log.isDebugEnabled()) {
                Log.debug("Route not found for JID: " + jid, e);
            }
        }

        return route;
    }

    public Iterator getRoutes(JID node) {
        // Check if the address belongs to a remote server
        if (!serverName.equals(node.getDomain()) && routes.get(node.getDomain()) == null &&
                componentManager.getComponent(node) == null) {
            // Return a promise of a remote session. This object will queue packets pending
            // to be sent to remote servers
            return Arrays.asList(OutgoingSessionPromise.getInstance()).iterator();
        }

        LinkedList list = null;
        Object nameRoutes = routes.get(node.getDomain());
        if (nameRoutes != null) {
            if (nameRoutes instanceof ChannelHandler) {
                list = new LinkedList();
                list.add(nameRoutes);
            }
            else if (node.getNode() == null) {
                list = new LinkedList();
                getRoutes(list, (Map) nameRoutes);
            }
            else {
                Object resourceRoutes = ((Map) nameRoutes).get(node.getNode());
                if (resourceRoutes != null) {
                    if (resourceRoutes instanceof ChannelHandler) {
                        list = new LinkedList();
                        list.add(resourceRoutes);
                    }
                    else if (node.getResource() == null || node.getResource().length() == 0) {
                        list = new LinkedList();
                        getRoutes(list, (Map) resourceRoutes);
                    }
                    else {
                        Object entry = ((Map) resourceRoutes).get(node.getResource());
                        if (entry != null) {
                            list = new LinkedList();
                            list.add(entry);
                        }
                    }
                }
            }
        }
        if (list == null) {
            return Collections.EMPTY_LIST.iterator();
        }
        else {
            return list.iterator();
        }
    }

    /**
     * Recursive method to iterate through the given table (and any embedded map)
     * and stuff non-Map values into the given list.<p>
     *
     * There should be no recursion problems since the routing table is at most 3 levels deep.
     *
     * @param list  The list to stuff entries into
     * @param table The hashtable who's values should be entered into the list
     */
    private void getRoutes(LinkedList list, Map table) {
        Iterator entryIter = table.values().iterator();
        while (entryIter.hasNext()) {
            Object entry = entryIter.next();
            if (entry instanceof ConcurrentHashMap) {
                getRoutes(list, (Map)entry);
            }
            else {
                // Do not include the same entry many times. This could be the case when the same 
                // session is associated with the bareJID and with a given resource
                if (!list.contains(entry)) {
                    list.add(entry);
                }
            }
        }
    }

    public ChannelHandler getBestRoute(JID node) {
        ChannelHandler route = getRoute(node);
        if (route == null) {
            // Try looking for a route based on the bare JID
            String nodeJID = node.getNode() == null ? "" : node.getNode();
            route = getRoute(node.toBareJID(), nodeJID, node.getDomain(), "");
        }
        return route;
    }

    public ChannelHandler removeRoute(JID node) {

        ChannelHandler route = null;
        String nodeJID = node.getNode() == null ? "" : node.getNode();
        String resourceJID = node.getResource() == null ? "" : node.getResource();

        try {
            Object nameRoutes = routes.get(node.getDomain());
            if (nameRoutes instanceof ConcurrentHashMap) {
                Object resourceRoutes = ((Map) nameRoutes).get(nodeJID);
                if (resourceRoutes instanceof ConcurrentHashMap) {
                    // Remove the requested resource for this user
                    route = (ChannelHandler) ((Map) resourceRoutes).remove(resourceJID);
                    if (((Map) resourceRoutes).isEmpty()) {
                        ((Map) nameRoutes).remove(nodeJID);
                        if (((Map) nameRoutes).isEmpty()) {
                            routes.remove(node.getDomain());
                        }
                    }
                }
                else {
                    // Remove the unique route to this node
                    ((Map) nameRoutes).remove(nodeJID);
                }
            }
            else if (nameRoutes != null) {
                // The retrieved route points to a RoutableChannelHandler
                if (("".equals(nodeJID) && "".equals(resourceJID)) ||
                        ((RoutableChannelHandler) nameRoutes).getAddress().equals(node)) {
                    // Remove the route to this domain
                    routes.remove(node.getDomain());
                }
            }
        }
        catch (Exception e) {
            Log.error("Error removing route", e);
        }
        return route;
    }

    public void initialize(XMPPServer server) {
        super.initialize(server);
        serverName = server.getServerInfo().getName();
    }
}