package com.limegroup.gnutella.connection;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.io.IOUtils;
import org.limewire.net.SocketsManager;
import org.limewire.nio.observer.ConnectObserver;
import org.limewire.util.OSUtils;

import com.limegroup.gnutella.ConnectionServices;
import com.limegroup.gnutella.DownloadServices;
import com.limegroup.gnutella.UploadServices;

/**
 * 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 class ConnectionChecker {

    private static final Log LOG = LogFactory.getLog(ConnectionChecker.class);

    /**
     * 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;

    private final ConnectionServices connectionServices;

    private final UploadServices uploadServices;

    private final SocketsManager socketsManager;

    private final AtomicInteger numWorkarounds;

    private final UDPConnectionChecker udpConnectionChecker;

    private final DownloadServices downloadServices;

    private final List<String> hostList;

    public ConnectionChecker(AtomicInteger numWorkarounds, String[] hosts,
            ConnectionServices connectionServices, UploadServices uploadServices,
            DownloadServices downloadServices, SocketsManager socketsManager,
            UDPConnectionChecker udpConnectionChecker) {
        this.numWorkarounds = numWorkarounds;
        this.hostList = new ArrayList<String>(Arrays.asList(hosts));
        this.connectionServices = connectionServices;
        this.uploadServices = uploadServices;
        this.downloadServices = downloadServices;
        this.socketsManager = socketsManager;
        this.udpConnectionChecker = udpConnectionChecker;
    }

    /**
     * Checks for a live internet connection.
     */
    public synchronized void run(ConnectionCheckerListener connectionCheckerListener) {
        // Add some randomization.
        Collections.shuffle(hostList);

        for (String curHost : hostList) {
            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 && !connectionServices.isConnected()
                        && !connectionServices.isConnecting()) {
                    LOG.debug("Reconnecting RouterService");
                    connectionServices.connect();
                }

                connectionCheckerListener.connected();
                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 || !shouldTrySP2Workaround()) {
                    break;
                } else if (hasNoTransfers() && udpConnectionChecker.udpIsDead()) {
                    // really disconnected
                    break;
                } else {
                    // otherwise shut off all attempts until sp2's limit times
                    // out
                    triedSP2Workaround = true;
                    trySP2Workaround();
                }
            }
        }

        connectionCheckerListener.noInternetConnection();
    }

    /**
     * For testing.
     */
    boolean shouldTrySP2Workaround() {
        return OSUtils.isSocketChallengedWindows();
    }

    /**
     * Terminates all attempts to open new sockets
     */
    void trySP2Workaround() {
        connectionServices.disconnect();
        numWorkarounds.incrementAndGet();
        try {
            Thread.sleep(5 * 1000);
        } catch (InterruptedException ignored) {
        }
        unsuccessfulAttempts = 0;
    }

    /**
     * @return true if we don't have any transfers going at non-zero speed
     */
    private boolean hasNoTransfers() {
        return !downloadServices.hasActiveDownloads() && !uploadServices.hasActiveUploads();
    }

    /**
     * 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 = socketsManager.connect(new InetSocketAddress(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 unsuccessful if we were able to remove it
                    // 'cause if it couldn't be removed, a response is still
                    // pending...
                    if (socketsManager.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;
        }

    }

}
