package com.limegroup.gnutella;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.nio.observer.Shutdownable;
import org.limewire.io.IP;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;

/**
 * A sanity checker for many different in-network verification
 * requests.
 * 
 * If we cannot verify the message as received from a number
 * of hosts from different areas, message the user to hit
 * the website as the installation may possible be corrupted.
 */
@Singleton
public class NetworkUpdateSanityChecker {
    
    public static enum RequestType {
        SIMPP, VERSION;
    }
    private static final Set<RequestType> allTypes = EnumSet.allOf(RequestType.class);
    
    private static final Log LOG = LogFactory.getLog(NetworkUpdateSanityChecker.class);
    
    private static final int MAXIMUM_FAILURES = 20;
    private static final String MASK = "/255.255.0.0";
    
    private final Map<RequestType, Map<ReplyHandler, Boolean>> requests =
        new HashMap<RequestType, Map<ReplyHandler, Boolean>>(RequestType.values().length);
    private final List<IP> failures = new ArrayList<IP>(MAXIMUM_FAILURES);
    private boolean finished = false;
    private final Set<RequestType> successes = EnumSet.noneOf(RequestType.class);
    
    private final Provider<ActivityCallback> activityCallback;
    
    @Inject
    public NetworkUpdateSanityChecker(
            Provider<ActivityCallback> activityCallback) {
        this.activityCallback = activityCallback;
    }

    /**
     * Stores knowledge that we've requested a network-updatable component
     * from the given source.
     * 
     * @param handler
     */
    public void handleNewRequest(ReplyHandler handler, RequestType type) {
        if(LOG.isDebugEnabled())
            LOG.debug("Adding request to handler: " + handler);
        
        synchronized(requests) {
            // We're done.
            if(finished) {
                LOG.debug("Already reached maximum failure point, ignoring.");
                return;
            }
            
            addRequest(handler, type);
        }
    }

    /**
     * Acknowledge we received a valid response from the source.
     * 
     * @param handler
     */
    public void handleValidResponse(ReplyHandler handler, RequestType type) {
        if(LOG.isDebugEnabled())
            LOG.debug("Received valid response from handler: " + handler + "of type: " + type);
        synchronized(requests) {
            if(!finished) {
                // If we've gotten all kinds of valid responses,
                // assume we're okay.
                successes.add(type);
                if(successes.containsAll(allTypes)) {
                    LOG.debug("Received every kind of success!");
                    finished = true;
                    requests.clear();
                    failures.clear();
                } else {
                    removeRequest(handler, type);
                }
            }
        }
    }
    
    /**
     * Acknowledge we received an invalid response from the source.
     * 
     * @param handler
     */
    public void handleInvalidResponse(ReplyHandler handler, RequestType type) {
        synchronized(requests) {
            // If we had a request for this handler...
            if(!finished && removeRequest(handler, type)) {
                if(LOG.isDebugEnabled())
                    LOG.debug("Had a request from handler, adding as failure... " + handler);
                
                IP ip = new IP(handler.getAddress() + MASK);
                boolean contained = false;
                // See if we already had a failure in this range, if so
                // then ignore it.
                for(IP current : failures) {
                    if(current.contains(ip)) {
                        if(LOG.isDebugEnabled())
                            LOG.debug("Already had a failure from range: " +    
                                      current + ", ignoring handler ip: " + ip);
                        contained = true;
                        break;
                    }
                }
                
                if(!contained) {
                    // If no failure already here, add it.
                    failures.add(ip);
                    if(LOG.isDebugEnabled())
                        LOG.debug("Current failure size: " + failures.size());
                    
                    if(failures.size() == MAXIMUM_FAILURES) {
                        LOG.debug("Reached failure threshold!");
                        finished = true;
                        requests.clear();
                        failures.clear();
                        activityCallback.get().installationCorrupted();
                    }
                }
            }
        }
        
        if(handler instanceof Shutdownable)
            ((Shutdownable)handler).shutdown();
    }
    
    /** Adds a single incoming request to the maps. */
    private void addRequest(ReplyHandler handler, RequestType type) {
        Map<ReplyHandler, Boolean> inner = requests.get(type);
        if(inner == null) {
            inner = new WeakHashMap<ReplyHandler, Boolean>();
            requests.put(type, inner);
        }
        inner.put(handler, Boolean.TRUE);
    }
    
    /**
     * Removes a request from the maps, returning an object if it was
     * contained.  Null if it wasn't.
     */
    private boolean removeRequest(ReplyHandler handler, RequestType type) {
        Map<ReplyHandler, Boolean> inner = requests.get(type);
        if(inner == null)
            return false;
        else
            return inner.remove(handler) != null;     
    }

}
