package com.limegroup.gnutella.dht;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import org.limewire.io.IOUtils;
import org.limewire.io.IpPort;
import org.limewire.io.SecureInputStream;
import org.limewire.io.SecureOutputStream;
import org.limewire.mojito.KUID;
import org.limewire.mojito.MojitoDHT;
import org.limewire.mojito.MojitoFactory;
import org.limewire.mojito.routing.Contact;
import org.limewire.mojito.routing.Vendor;
import org.limewire.mojito.routing.Version;
import org.limewire.mojito.util.ContactUtils;
import org.limewire.util.CommonUtils;

import com.limegroup.gnutella.connection.Connection;
import com.limegroup.gnutella.connection.ConnectionLifecycleEvent;
import com.limegroup.gnutella.dht.DHTManager.DHTMode;
import com.limegroup.gnutella.settings.DHTSettings;
import com.limegroup.gnutella.util.EventDispatcher;

/**
 * Controls a passive DHT node (an {@link ActiveDHTNodeController} but is a 
 * Gnutella Ultrapeer). 
 * <p>
 * The passive node controller also maintains the list of this ultrapeer's 
 * leafs in a separate list. These leaves are this node's most accurate knowledge 
 * of the DHT, as leaf connections are state-full TCP, and they are therefore 
 * propagated in priority in the Gnutella network.
 * <p>
 * Persistence is implemented in order to be able to bootstrap the next session 
 * by saving a few Most Recently Seen (MRS) nodes. It is not necessary
 * to persist the entire DHT, because we don't want to keep the same node ID at 
 * the next session, and accuracy of the contacts in the route table is not guaranteed 
 * when a node is passive (as it does not get contacted by the DHT).   
 */
public class PassiveDHTNodeController extends AbstractDHTController {
    
    /**
     * The file to persist the list of host
     */
    private static final File FILE = new File(CommonUtils.getUserSettingsDir(), "passive.mojito");
    
    /**
     * A RouteTable for passive Nodes
     */
    private PassiveDHTNodeRouteTable routeTable;
    
    PassiveDHTNodeController(Vendor vendor, Version version,
            EventDispatcher<DHTEvent, DHTEventListener> dispatcher,
            DHTControllerFacade dhtControllerFacade) {
        super(vendor, version, dispatcher, DHTMode.PASSIVE, dhtControllerFacade);
    }
    
    @Override
    protected MojitoDHT createMojitoDHT(Vendor vendor, Version version) {
        MojitoDHT dht = MojitoFactory.createFirewalledDHT("PassiveUltrapeerDHT", vendor, version);
        
        routeTable = new PassiveDHTNodeRouteTable(dht);
        dht.setRouteTable(routeTable);
        
        // Load the small list of MRS Nodes for bootstrap
        if (DHTSettings.PERSIST_PASSIVE_DHT_ROUTETABLE.getValue()
                && FILE.exists() && FILE.isFile()) {
            ObjectInputStream ois = null;
            try {
                ois = new ObjectInputStream(
                            new BufferedInputStream(
                                new SecureInputStream(
                                    new FileInputStream(FILE))));
                
                int routeTableVersion = ois.readInt();
                if (routeTableVersion >= getRouteTableVersion()) {
                    Contact node = null;
                    while((node = (Contact)ois.readObject()) != null) {
                        routeTable.add(node);
                    }
                }
            } catch (Throwable ignored) {
                LOG.error("Throwable", ignored);
            } finally {
                IOUtils.close(ois);
            }
        }
        
        return dht;
    }
    
    /**
     * This method first adds the given host to the list of bootstrap nodes and 
     * then adds it to this passive node's routing table.
     * <p>
     * Note: This method makes sure the DHT is running already, as adding a node
     * as a leaf involves sending it a DHT ping (in order to get its KUID).
     */
    protected void addLeafDHTNode(String host, int port) {
        if(!isRunning()) {
            return;
        }
        
        SocketAddress addr = new InetSocketAddress(host, port);
        //add to bootstrap nodes if we need to.
        addActiveDHTNode(addr, false);
        //add to our DHT leaves
        if(LOG.isDebugEnabled()) {
            LOG.debug("Adding host: "+addr+" to leaf dht nodes");
        }
        routeTable.addLeafDHTNode(host, port);
    }
    
    protected SocketAddress removeLeafDHTNode(String host, int port) {
        if(!isRunning()) {
            return null;
        }
        
        SocketAddress removed = routeTable.removeLeafDHTNode(host, port);

        if(LOG.isDebugEnabled() && removed != null) {
            LOG.debug("Removed host: "+removed+" from leaf dht nodes");
        }
        return removed;
    }

    @Override
    public void stop() {
        if (!isRunning()) {
            return;
        }
        
        super.stop();
        
        if (DHTSettings.PERSIST_PASSIVE_DHT_ROUTETABLE.getValue()) {
            Collection<Contact> contacts = routeTable.getActiveContacts(); 
            if (contacts.size() >= 2) {
                ObjectOutputStream oos = null;
                try {
                    oos = new ObjectOutputStream(
                                new BufferedOutputStream(
                                    new SecureOutputStream(
                                        new FileOutputStream(FILE))));
                    
                    oos.writeInt(getRouteTableVersion());
                    
                    // Sort by MRS and save only some Nodes
                    contacts = ContactUtils.sort(contacts, 
                            DHTSettings.MAX_PERSISTED_NODES.getValue());
                    
                    KUID localNodeID = getMojitoDHT().getLocalNodeID();
                    for(Contact node : contacts) {
                        if(!node.getNodeID().equals(localNodeID)) {
                            oos.writeObject(node);
                        }
                    }
                    
                    // EOF Terminator
                    oos.writeObject(null);
                    oos.flush();
                    
                } catch (IOException ignored) {
                } finally {
                    IOUtils.close(oos);
                }
            }
        }
    }

    /**
     * This method return this passive node's leafs first (they have the highest timestamp)
     * <p>
     * Note: Although a passive node does not have accurate info in its route table 
     * (except for direct leafs), we still return nodes. 
     */
    @Override
    public List<IpPort> getActiveDHTNodes(int maxNodes) {
        if(!isRunning() || !getMojitoDHT().isBootstrapped()) {
            return Collections.emptyList();
        }
        
        return getMRSNodes(maxNodes, true);
    }
    
    /**
     * Handle connection-specific life cycle events only. 
     */
    @Override
    public void handleConnectionLifecycleEvent(ConnectionLifecycleEvent evt) {
        //handle connection specific events
        Connection c = evt.getConnection();
        if( c == null) {
            return;
        }
        
        String host = c.getAddress();
        int port = c.getPort();
        
        if(evt.isConnectionClosedEvent()) {
            
            if(LOG.isDebugEnabled()) {
                LOG.debug("Got a connection closed event for connection: "+ c);
            }
            
            removeLeafDHTNode( host , port );
            
        } else if(evt.isConnectionCapabilitiesEvent()){
            
            if(c.getConnectionCapabilities().remostHostIsActiveDHTNode() > -1) {
                if(LOG.isDebugEnabled()) {
                    LOG.debug("Connection is active dht node: "+ c);
                }
                addLeafDHTNode( host , port );
            } else if(c.getConnectionCapabilities().remostHostIsPassiveDHTNode() > -1) {
                if(LOG.isDebugEnabled()) {
                    LOG.debug("Connection is passive dht node: "+ c);
                }
                addPassiveDHTNode(new InetSocketAddress(host, port));
            } else {
                if(LOG.isDebugEnabled()) {
                    LOG.debug("Connection is node not connected to the DHT network: "+ c);
                }
                removeLeafDHTNode( host , port );
            }
        } 
    }
}
