package com.limegroup.gnutella.dht;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.mojito.KUID;
import org.limewire.mojito.MojitoDHT;
import org.limewire.mojito.concurrent.DHTExecutorService;
import org.limewire.mojito.concurrent.DHTFuture;
import org.limewire.mojito.concurrent.DHTFutureAdapter;
import org.limewire.mojito.concurrent.DHTFutureListener;
import org.limewire.mojito.result.PingResult;
import org.limewire.mojito.routing.Bucket;
import org.limewire.mojito.routing.ClassfulNetworkCounter;
import org.limewire.mojito.routing.Contact;
import org.limewire.mojito.routing.RouteTable;

/**
 * Passive Nodes (Ultrapeers) use this slightly extended version 
 * of the <code>RouteTable</code>. It maintains an internal mapping of DHT 
 * enabled leaves that are currently connected to the Ultrapeer (Gnutella 
 * connections). 
 */
@SuppressWarnings("serial")
class PassiveDHTNodeRouteTable implements RouteTable {
    
    private static final Log LOG = LogFactory.getLog(PassiveDHTNodeRouteTable.class);
    
    /**
     * MojitoDHT instance
     */
    private final MojitoDHT dht;
    
    /**
     * The actual RouteTable
     */
    private final RouteTable delegate;
    
    /**
     * The Map storing the leaf nodes connected to this ultrapeer. The mapping is
     * used to go from Gnutella <tt>IpPort</tt> to DHT <tt>RemoteContact</tt>. 
     */
    private final Map<SocketAddress, KUID> leafDHTNodes = new HashMap<SocketAddress, KUID>();
    
    public PassiveDHTNodeRouteTable(MojitoDHT dht) {
        assert (dht.isFirewalled()); // Must be firewalled
        
        this.dht = dht;
        delegate = dht.getRouteTable();
    }

    /**
     * Adds a DHT leaf node to the DHT routing table, setting a very high timestamp
     * to make sure it always gets contacted first for the first hop of lookups, and 
     * make sure it always gets returned first when selecting the Most Recently Seen
     * (MRS) nodes from the RT.
     * 
     * @param host the IP address of the remote host as a string
     * @param port the listening port for the remote host
     */
    public void addLeafDHTNode(String host, int port) {
        if(LOG.isDebugEnabled()) {
            LOG.debug("Pinging leaf: " + host + ": " + port);
        }
        
        final InetSocketAddress addr = new InetSocketAddress(host, port);
        DHTFuture<PingResult> future = dht.ping(addr);
        
        DHTFutureListener<PingResult> listener = new DHTFutureAdapter<PingResult>() {
            @Override
            public void handleFutureSuccess(PingResult result) {
                if(LOG.isDebugEnabled()) {
                    LOG.debug("Ping succeeded to: " + result);
                }
                
                Contact node = result.getContact();
                synchronized (PassiveDHTNodeRouteTable.this) {
                    KUID previous = leafDHTNodes.put(addr, node.getNodeID());
                    
                    if (previous == null || !previous.equals(node.getNodeID())) {
                        // Add it as a priority Node
                        node.setTimeStamp(Contact.PRIORITY_CONTACT);
                        add(node);
                    }
                }
            }

            @Override
            public void handleExecutionException(ExecutionException e) {
                if(LOG.isDebugEnabled()) {
                    LOG.debug("Ping failed to: " + addr, e);
                }
            }
        };
        
        future.addDHTFutureListener(listener);
    }
    
    /**
     * Removes this DHT leaf from our routing table and returns it.
     */
    public synchronized SocketAddress removeLeafDHTNode(String host, int port) {
        
        SocketAddress addr = new InetSocketAddress(host, port);
        KUID nodeId = leafDHTNodes.remove(addr);
        if(nodeId != null) {
            
            if(LOG.isDebugEnabled()) {
                LOG.debug("Removed leaf: " + host + ": " + port);
            }
            
            removeAndReplaceWithMRSCachedContact(nodeId);
            return addr;
        }
        return null;
    }
    
    /**
     * Removes the given node from the routing table and tries to replace
     * it with the Most Recently Seen (MRS) cached contact.
     * 
     * @param nodeId the KUID of the node to remove
     */
    private synchronized void removeAndReplaceWithMRSCachedContact(KUID nodeId) {
        Bucket bucket = getBucket(nodeId);
        boolean removed = bucket.removeActiveContact(nodeId);

        if (removed) {
            if (bucket.getCacheSize() > 0) {
                ClassfulNetworkCounter counter = bucket.getClassfulNetworkCounter();
                
                Contact mrs = null;
                while((mrs = bucket.getMostRecentlySeenCachedContact()) != null) {
                    removed = bucket.removeCachedContact(mrs.getNodeID());
                    assert (removed == true);
                    
                    if (counter == null || counter.isOkayToAdd(mrs)) {
                        bucket.addActiveContact(mrs);
                        break;
                    }
                }
            }
        } else {
            bucket.removeCachedContact(nodeId);
        }
    }
    
    /**
     * Returns whether or not this Ultrapeer has any
     * DHT enabled leaves
     */
    public synchronized boolean hasDHTLeaves() {
        return !leafDHTNodes.isEmpty();
    }
    
    /**
     * Returns the IP:Ports of this Ultrapeer's DHT enabled leaves
     * 
     * Hold a lock on 'this' when using the Iterator! 
     */
    public synchronized Set<SocketAddress> getDHTLeaves() {
        return Collections.unmodifiableSet(leafDHTNodes.keySet());
    }
    
    // --- ROUTE TABLE ---
    
    public synchronized void add(Contact node) {
        delegate.add(node);
    }

    public synchronized void addRouteTableListener(RouteTableListener l) {
        delegate.addRouteTableListener(l);
    }

    public synchronized void removeRouteTableListener(RouteTableListener l) {
        delegate.removeRouteTableListener(l);
    }
    
    public synchronized Contact get(KUID nodeId) {
        return delegate.get(nodeId);
    }

    public synchronized Collection<Contact> getActiveContacts() {
        return delegate.getActiveContacts();
    }

    public synchronized Bucket getBucket(KUID nodeId) {
        return delegate.getBucket(nodeId);
    }

    public synchronized Collection<Bucket> getBuckets() {
        return delegate.getBuckets();
    }

    public synchronized Collection<Contact> getCachedContacts() {
        return delegate.getCachedContacts();
    }

    public synchronized Collection<Contact> getContacts() {
        return delegate.getContacts();
    }

    public synchronized Contact getLocalNode() {
        return delegate.getLocalNode();
    }

    public synchronized Collection<KUID> getRefreshIDs(boolean bootstrapping) {
        return delegate.getRefreshIDs(bootstrapping);
    }

    public synchronized void handleFailure(KUID nodeId, SocketAddress address) {
        delegate.handleFailure(nodeId, address);
    }

    public synchronized boolean isLocalNode(Contact node) {
        return delegate.isLocalNode(node);
    }

    public synchronized void purge(long elapsedTimeSinceLastContact) {
        delegate.purge(elapsedTimeSinceLastContact);
    }

    public synchronized void purge(PurgeMode first, PurgeMode... rest) {
        delegate.purge(first, rest);
    }
    
    public synchronized Contact select(KUID nodeId) {
        return delegate.select(nodeId);
    }
    
    public synchronized Collection<Contact> select(KUID nodeId, int count, SelectMode mode) {
        return delegate.select(nodeId, count, mode);
    }
    
    public synchronized void setContactPinger(ContactPinger pinger) {
        delegate.setContactPinger(pinger);
    }
    
    public synchronized void setNotifier(DHTExecutorService e) {
        delegate.setNotifier(e);
    }

    public synchronized int size() {
        return delegate.size();
    }
    
    public synchronized void clear() {
        delegate.clear();
    }
    
    @Override
    public synchronized String toString() {
        return "Passive RouteTable: " + delegate.toString();
    }
}
