package org.jivesoftware.smackx;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.jivesoftware.smack.Connection;
import org.jivesoftware.smack.Roster;
import org.jivesoftware.smack.RosterEntry;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smackx.packet.DiscoverInfo;
import org.jivesoftware.smackx.packet.DiscoverItems;
import org.jivesoftware.smackx.packet.DiscoverInfo.Identity;
import org.jivesoftware.smackx.packet.DiscoverItems.Item;

/**
 * This class is the general entry point to gateway interaction (XEP-0100).
 * This class discovers available gateways on the users servers, and
 * can give you also a list of gateways the you user is registered with which
 * are not on his server. All actual interaction with a gateway is handled in the
 * class {@see Gateway}.
 *
 * @author Till Klocke
 */
public class GatewayManager {

    private static Map<Connection, GatewayManager> instances =
            new HashMap<Connection, GatewayManager>();

    private ServiceDiscoveryManager sdManager;

    private Map<String, Gateway> localGateways = new HashMap<String, Gateway>();

    private Map<String, Gateway> nonLocalGateways = new HashMap<String, Gateway>();

    private Map<String, Gateway> gateways = new HashMap<String, Gateway>();

    private Connection connection;

    private Roster roster;

    private GatewayManager() {

    }

    /**
     * Creates a new instance of GatewayManager
     *
     * @param connection
     * @throws XMPPException
     */
    private GatewayManager(Connection connection) throws XMPPException {
        this.connection = connection;
        this.roster = connection.getRoster();
        sdManager = ServiceDiscoveryManager.getInstanceFor(connection);
    }

    /**
     * Loads all gateways the users server offers
     *
     * @throws XMPPException
     */
    private void loadLocalGateways() throws XMPPException {
        DiscoverItems items = sdManager.discoverItems(connection.getHost());
        Iterator<Item> iter = items.getItems();
        while (iter.hasNext()) {
            String itemJID = iter.next().getEntityID();
            discoverGateway(itemJID);
        }
    }

    /**
     * Discovers {@link DiscoveryInfo} and {@link DiscoveryInfo.Identity} of a gateway
     * and creates a {@link Gateway} object representing this gateway.
     *
     * @param itemJID
     * @throws XMPPException
     */
    private void discoverGateway(String itemJID) throws XMPPException {
        DiscoverInfo info = sdManager.discoverInfo(itemJID);
        Iterator<Identity> i = info.getIdentities();

        while (i.hasNext()) {
            Identity identity = i.next();
            String category = identity.getCategory();
            if (category.toLowerCase().equals("gateway")) {
                gateways.put(itemJID, new Gateway(connection, itemJID));
                if (itemJID.contains(connection.getHost())) {
                    localGateways.put(itemJID,
                            new Gateway(connection, itemJID, info, identity));
                } else {
                    nonLocalGateways.put(itemJID,
                            new Gateway(connection, itemJID, info, identity));
                }
                break;
            }
        }
    }

    /**
     * Loads all getways which are in the users roster, but are not supplied by the
     * users server
     *
     * @throws XMPPException
     */
    private void loadNonLocalGateways() throws XMPPException {
        if (roster != null) {
            for (RosterEntry entry : roster.getEntries()) {
                if (entry.getUser().equalsIgnoreCase(StringUtils.parseServer(entry.getUser())) &&
                        !entry.getUser().contains(connection.getHost())) {
                    discoverGateway(entry.getUser());
                }
            }
        }
    }

    /**
     * Returns an instance of GatewayManager for the given connection. If no instance for
     * this connection exists a new one is created and stored in a Map.
     *
     * @param connection
     * @return an instance of GatewayManager
     * @throws XMPPException
     */
    public GatewayManager getInstanceFor(Connection connection) throws XMPPException {
        synchronized (instances) {
            if (instances.containsKey(connection)) {
                return instances.get(connection);
            }
            GatewayManager instance = new GatewayManager(connection);
            instances.put(connection, instance);
            return instance;
        }
    }

    /**
     * Returns a list of gateways which are offered by the users server, wether the
     * user is registered to them or not.
     *
     * @return a List of Gateways
     * @throws XMPPException
     */
    public List<Gateway> getLocalGateways() throws XMPPException {
        if (localGateways.size() == 0) {
            loadLocalGateways();
        }
        return new ArrayList<Gateway>(localGateways.values());
    }

    /**
     * Returns a list of gateways the user has in his roster, but which are offered by
     * remote servers. But note that this list isn't automatically refreshed. You have to
     * refresh is manually if needed.
     *
     * @return a list of gateways
     * @throws XMPPException
     */
    public List<Gateway> getNonLocalGateways() throws XMPPException {
        if (nonLocalGateways.size() == 0) {
            loadNonLocalGateways();
        }
        return new ArrayList<Gateway>(nonLocalGateways.values());
    }

    /**
     * Refreshes the list of gateways offered by remote servers.
     *
     * @throws XMPPException
     */
    public void refreshNonLocalGateways() throws XMPPException {
        loadNonLocalGateways();
    }

    /**
     * Returns a Gateway object for a given JID. Please note that it is not checked if
     * the JID belongs to valid gateway. If this JID doesn't belong to valid gateway
     * all operations on this Gateway object should fail with a XMPPException. But there is
     * no guarantee for that.
     *
     * @param entityJID
     * @return a Gateway object
     */
    public Gateway getGateway(String entityJID) {
        if (localGateways.containsKey(entityJID)) {
            return localGateways.get(entityJID);
        }
        if (nonLocalGateways.containsKey(entityJID)) {
            return nonLocalGateways.get(entityJID);
        }
        if (gateways.containsKey(entityJID)) {
            return gateways.get(entityJID);
        }
        Gateway gateway = new Gateway(connection, entityJID);
        if (entityJID.contains(connection.getHost())) {
            localGateways.put(entityJID, gateway);
        } else {
            nonLocalGateways.put(entityJID, gateway);
        }
        gateways.put(entityJID, gateway);
        return gateway;
    }

}
