package com.limegroup.gnutella.connection;

import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.limegroup.gnutella.MessageListener;
import com.limegroup.gnutella.ReplyHandler;
import com.limegroup.gnutella.RouterService;
import com.limegroup.gnutella.UDPPinger;
import com.limegroup.gnutella.UDPService;
import com.limegroup.gnutella.io.ConnectObserver;
import com.limegroup.gnutella.messages.Message;
import com.limegroup.gnutella.messages.PingRequest;
import com.limegroup.gnutella.util.Cancellable;
import com.limegroup.gnutella.util.CommonUtils;
import com.limegroup.gnutella.util.IOUtils;
import com.limegroup.gnutella.util.Sockets;
import com.limegroup.gnutella.util.ThreadFactory;

/**
 * Specialized class that attempts to connect to a rotating list of well-known
 * Internet addresses to check whether or not this host has a live connection
 * to the Internet.
 */
public final class ConnectionChecker implements Runnable {

    /**
     * Flag for whether or not we know for sure that we're connected from
     * successfully connecting to an external host.
     */
    private volatile boolean _connected;

    /**
     * Variable for the number of unsuccessful connection attempts.
     */
    private int _unsuccessfulAttempts;
    
    /**
     * Whether we have tried to work around SP2 cutting us off.
     */
    private boolean _triedSP2Workaround;

    /**
     * Log for logging this class.
     */
    private static final Log LOG =
        LogFactory.getLog(ConnectionChecker.class);
        

    /**
     * Array of standard internet hosts to connect to when determining whether
     * or not the user has a live Internet connection.  These are randomized
     * so a minimum number is hit on each check.  Note that we only hit one 
     * random server per test and that we only test the connection if we have
     * ample evidence that the users machine is no longer connected, resulting
     * in minimal traffic to these sites.  NON-FINAL FOR TESTING.
     */
    private static String[] STANDARD_HOSTS = {
        "www.wanadoo.fr",
        "www.tiscali.com",
        "www.ntt.com",
        "www.tonline.com",
        "www.download.com",
        "www.ibm.com",
        "www.sun.com",
        "www.apple.com",
        "www.ebay.com",
        "www.sun.com",
        "www.monster.com",
        "www.uunet.com",
        "www.real.com",
        "www.microsoft.com",
        "www.sco.com",
        "www.google.com",
        "www.cnn.com",
        "www.amazon.com",
        "www.espn.com", 
        "www.yahoo.com",
        "www.oracle.com",
        "www.dell.com",
        "www.ge.com",
        "www.sprint.com",
        "www.att.com",
        "www.mci.com",
        "www.cisco.com",
        "www.intel.com",
        "www.motorola.com",
        "www.hp.com",
        "www.gateway.com",
        "www.sony.com",
        "www.ford.com",
        "www.gm.com",
        "www.aol.com",
        "www.verizon.com",
        "www.passport.com",
        "www.go.com",
        "www.overture.com",
        "www.earthlink.net",
        "www.bellsouth.net",
        "www.excite.com",
        "www.paypal.com",
        "www.altavista.com",
        "www.weather.com",
        "www.mapquest.com",
        "www.geocities.com",
        "www.juno.com",
        "www.msnbc.com",
        "www.lycos.com",
        "www.comcast.com",
    };
    
    /**
     * Private constructor ensures that only this class can create instances of
     * itself.
     */
    private ConnectionChecker() {}

    private static ConnectionChecker current;
    
    /**
     * Creates a new <tt>ConnectionChecker</tt> instance that checks for a live
     * internet connection.  If the checker determines that there is no active 
     * connection, it will notify the <tt>ConnectionManager</tt> to take
     * appropriate action.
     * 
     * @return a new <tt>ConnectionChecker</tt> instance
     */
    public static ConnectionChecker checkForLiveConnection() {
        LOG.trace("checking for live connection");

        ConnectionChecker checker;
        boolean startThread = false;
        synchronized(ConnectionChecker.class) {
            if (current == null) {
                startThread = true;
                current = new ConnectionChecker();
            }
            checker = current;
        }
        
        // Only create a new thread if one isn't alive.
        if(startThread) {
            LOG.debug("Starting a new connection-checker thread");
            ThreadFactory.startThread(checker, "check for live connection");
        }
        
        return checker;
    }

    /**
     * Checks for a live internet connection.
     */
    public synchronized void run() {
        try {
            List hostList = Arrays.asList(STANDARD_HOSTS);
            
            // Add some randomization.
            Collections.shuffle(hostList);
            
            Iterator iter = hostList.iterator();
            while(iter.hasNext()) {
                String curHost = (String)iter.next();
                connectToHost(curHost);
                
                // Break out of the loop if we've already discovered that we're 
                // connected -- we only need to successfully connect to one host
                // to know for sure that we're up.
                if(_connected) {
                    LOG.debug("Connection exists.");
                    
                    // if we did disconnect as an attempt to work around SP2, connect now.
                    if (_triedSP2Workaround && !RouterService.isConnected() && !RouterService.isConnecting()) {
                        LOG.debug("Reconnecting RouterService");
                        RouterService.connect();
                    }
                    return;
                }
                
                // Stop if we've failed to connect to more than 2 of the hosts
                // that should be up all of the time.  We do this to make extra
                // sure the user's connection is down.  If it is down, trying
                // multiple times adds no load to the test servers.
                if(_unsuccessfulAttempts > 2) {
                    LOG.debug("Failed connection check more than twice.");
                    if (_triedSP2Workaround || !CommonUtils.isSocketChallengedWindows()) {
                        RouterService.getConnectionManager().noInternetConnection();
                        return;
                    } else {
                        _triedSP2Workaround = true;
                        trySP2Workaround();
                    }
                }
            }
            
        } finally {
            synchronized(ConnectionChecker.class) {
                current = null;
            }
        }
    }
    
    private void trySP2Workaround() {
        if (hasNoTransfers() && udpIsDead())
            return; // really disconnected
        else
            killAndSleep(); // otherwise shut off all attempts until sp2's limit times out
    }
    
    /**
     * @return true if we don't have any transfers going at non-zero speed
     */
    private boolean hasNoTransfers(){
        RouterService.getDownloadManager().measureBandwidth();
        float down = RouterService.getDownloadManager().getMeasuredBandwidth();
        
        if (down != 0)
            return false;
        
        RouterService.getUploadManager().measureBandwidth();
        float up = RouterService.getUploadManager().getMeasuredBandwidth();
        
        return up == 0;
    }
    
    /**
     * @return if we think that udp traffic is dead
     */
    private boolean udpIsDead() {
        PingRequest ping = PingRequest.createUDPPing();
        Collection hosts = RouterService.getPreferencedHosts(false,"en",50);
        UDPPinger myPinger = RouterService.getHostCatcher().getPinger();
        UDPChecker checker = new UDPChecker();
        
        // send some hosts to be ranked
        myPinger.rank(hosts,checker,checker,ping);
        long now = System.currentTimeMillis();
        synchronized(checker) {
            try {
                // since there may be other udp packets backed up to be sent,
                // check every second if we have received something, and if so
                // cancel the hosts we sent.
                for (int i = 0; i < 5; i++) {
                    checker.wait(1000);
                    if (UDPService.instance().getLastReceivedTime() > now) {
                        checker.received = true;
                        return false;
                    }
                }
            } catch (InterruptedException ignored){}
        }
        return !checker.received;
    }
    
    /**
     * Terminates all attempts to open new sockets
     */
    private void killAndSleep() {
        RouterService.disconnect();
        try {
            Thread.sleep(5*1000);
        } catch (InterruptedException ignored){}
        _unsuccessfulAttempts = 0;
    }
    
    /**
     * Determines whether or not we have connected to an external host, 
     * verifying that we have an internet connection.
     * 
     * @return <tt>true</tt> if we have created a successful connection, 
     *  otherwise <tt>false</tt>
     */
    public boolean hasConnected() {
        return _connected;
    }

    /**
     * Connects to an individual host.
     * 
     * @param host the host to connect to
     */
    private void connectToHost(String host) {
        if(LOG.isDebugEnabled())
            LOG.debug("Checking for connection with host: " + host);
        
        try  {
        	InetAddress.getByName(host); // die fast if unresolvable
            Observer observer = new Observer();
            synchronized(observer) {
                Socket s = Sockets.connect(host, 80, 6000, observer);
                LOG.debug("Waiting for callback...");
                try {
                    observer.wait(12000);
                } catch(InterruptedException e) {}
                if(!observer.hasResponse()) {
                    LOG.debug("No response!");
                    // only consider unsuccesful if we were able to remove it
                    // 'cause if it couldn't be removed, a response is still pending...
                    if(Sockets.removeConnectObserver(observer)) {
                        LOG.debug("Removed observer");
                        _unsuccessfulAttempts++;
                        IOUtils.close(s);
                    }
                }
            }
        } catch (IOException bad) {
            LOG.debug("failed to resolve name", bad);
        	_unsuccessfulAttempts++;
        }
    }
    
    private class Observer implements ConnectObserver {
        boolean response = false;
        
        // unused.
        public void handleIOException(IOException iox) {}

        // Yay, we're connected.    
        public synchronized void handleConnect(Socket socket) throws IOException {
            if(!response) {
                LOG.debug("Socket connected OK");
                
                response = true;
                _connected = true;
                notify();
                
                IOUtils.close(socket);
            }
        }

        public synchronized void shutdown() {
            if(!response) {
                LOG.debug("Socket failed to connect");
                
                response = true;
                _unsuccessfulAttempts++;
                notify();
            }
        }
        
        public boolean hasResponse() {
            return response;
        }
        
    }
    
    private class UDPChecker implements MessageListener, Cancellable {
        volatile boolean received;
        public boolean isCancelled() {
            return received;
        }
        public void processMessage(Message m, ReplyHandler handler) {
            received = true;
            synchronized(this) {
                notify();
            }
        }
        
        public void registered(byte[] guid) {}
        
        public void unregistered(byte[] guid) {}
    }
}
