package com.limegroup.gnutella.search;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import com.limegroup.gnutella.ManagedConnection;
import com.limegroup.gnutella.messages.QueryRequest;

/**
 * This class handles query "probes."  Probe queries are the initial queries
 * that are sent out to determine the popularity of the file.  This allows 
 * queries down new connections to have more information for choosing the TTL.
 */
final class ProbeQuery {
    
    /**
     * Constant list of hosts to probe query at ttl=1.
     */
    private final List TTL_1_PROBES;
    
    /**
     * Constant list of hosts to probe query at ttl=2.
     */
    private final List TTL_2_PROBES;

    /**
     * Constant reference to the query handler instance.
     */
    private final QueryHandler QUERY_HANDLER;


    /**
     * Constructs a new <tt>ProbeQuery</tt> instance with the specified
     * list of connections to query and with the data enclosed in the
     * <tt>QueryHandler</tt>.
     *
     * @param connections the <tt>List</tt> of connections to query
     * @param qh the <tt>QueryHandler</tt> instance containing data
     *  for the probe
     */
    ProbeQuery(List connections, QueryHandler qh) {
        QUERY_HANDLER = qh;
        LinkedList[] lists = 
            createProbeLists(connections, qh.QUERY);

        TTL_1_PROBES = lists[0];
        TTL_2_PROBES = lists[1];        
    }
    

    /**
     * Obtains the time to wait for probe results to return.
     *
     * @return the time to wait for this probe to complete, in
     *  milliseconds
     */
    long getTimeToWait() {

        // determine the wait time.  we wait a little longer per
        // hop for probes to give them more time -- also weight
        // this depending on how many TTL=1 probes we're sending
        if(!TTL_2_PROBES.isEmpty()) 
            return (long)((double)QUERY_HANDLER.getTimeToWaitPerHop()*1.3);
        if(!TTL_1_PROBES.isEmpty()) 
            return (long)((double)QUERY_HANDLER.getTimeToWaitPerHop()*
                ((double)TTL_1_PROBES.size()/2.0));
        return 0L;
    }
    
    /**
     * Sends the next probe query out on the network if there 
     * are more to send.
     *
     * @return the number of hosts theoretically hit by this new 
     *  probe
     */
    int sendProbe() {
        Iterator iter = TTL_1_PROBES.iterator();
        int hosts = 0;
        QueryRequest query = QUERY_HANDLER.createQuery((byte)1);
        while(iter.hasNext()) {
            ManagedConnection mc = (ManagedConnection)iter.next();
            hosts += 
                QueryHandler.sendQueryToHost(query, 
                                             mc, QUERY_HANDLER);
        }
        
        query = QUERY_HANDLER.createQuery((byte)2);
        iter = TTL_2_PROBES.iterator();
        while(iter.hasNext()) {
            ManagedConnection mc = (ManagedConnection)iter.next();
            hosts += 
                QueryHandler.sendQueryToHost(query, 
                                             mc, QUERY_HANDLER);
        }
        
        TTL_1_PROBES.clear();
        TTL_2_PROBES.clear();

        return hosts;
    }

    /**
     * Helper method that creates the list of nodes to query for the probe.
     * This list will vary in size depending on how popular the content appears
     * to be.
     */
    private static LinkedList[] createProbeLists(List connections, 
        QueryRequest query) {
        Iterator iter = connections.iterator();
        
        LinkedList missConnections = new LinkedList();
        LinkedList oldConnections  = new LinkedList();
        LinkedList hitConnections  = new LinkedList();

        // iterate through our connections, adding them to the hit, miss, or
        // old connections list
        while(iter.hasNext()) {
            ManagedConnection mc = (ManagedConnection)iter.next();
            
            if(mc.isUltrapeerQueryRoutingConnection()) {
                if(mc.shouldForwardQuery(query)) { 
                    hitConnections.add(mc);
                } else {
                    missConnections.add(mc);
                }
            } else {
                oldConnections.add(mc);
            }
        }

        // final list of connections to query
        LinkedList[] returnLists = new LinkedList[2];
        LinkedList ttl1List = new LinkedList();
        LinkedList ttl2List = new LinkedList();
        returnLists[0] = ttl1List;
        returnLists[1] = ttl2List;        

        // do we have adequate data to determine some measure of the file's 
        // popularity?
        boolean adequateData = 
            (missConnections.size()+hitConnections.size()) > 8;

        // if we don't have enough data from QRP tables, just send out a 
        // traditional probe also, if we don't have an adequate number of QRP 
        // tables to access the popularity of the file, just send out an 
        // old-style probe at TTL=2
        if(hitConnections.size() == 0 || !adequateData) {
            return createAggressiveProbe(oldConnections, missConnections, 
                                         hitConnections, returnLists);
        } 

        int numHitConnections = hitConnections.size();
        double popularity = 
            (double)((double)numHitConnections/
                     ((double)missConnections.size()+numHitConnections));
        
        // if the file appears to be very popular, send it to only one host
        if(popularity == 1.0) {
            ttl1List.add(hitConnections.removeFirst());
            return returnLists;
        }

        if(numHitConnections > 3) {
            // TTL=1 queries are cheap -- send a lot of them if we can
            int numToTry = Math.min(9, numHitConnections);

            int startIndex = numHitConnections-numToTry;
            int endIndex   = numHitConnections;
            ttl1List.addAll(hitConnections.subList(startIndex, endIndex));
            return returnLists;
        }

        // otherwise, it's not very widely distributed content -- send
        // the query to all hit connections plus 3 TTL=2 connections
        ttl1List.addAll(hitConnections);        
        addToList(ttl2List, oldConnections, missConnections, 3);

        return returnLists;        
    }


    /**
     * Helper method that adds as many elements as possible up to the
     * desired number from two lists into a third list.  This method
     * takes as many elements as possible from <tt>list1</tt>, only
     * using elements from <tt>list2</tt> if the desired number of
     * elements to add cannot be fulfilled from <tt>list1</tt> alone.
     *
     * @param listToAddTo the list that elements should be added to
     * @param list1 the first list to add elements from, with priority 
     *  given to this list
     * @param list2 the second list to add elements from -- only used
     *  in the case where <tt>list1</tt> is smaller than <tt>numElements</tt>
     * @param numElements the desired number of elements to add to 
     *  <tt>listToAddTo</tt> -- note that this number will not be reached
     *  if the list1.size()+list2.size() < numElements
     */
    private static void addToList(List listToAddTo, List list1, List list2, 
                                  int numElements) {
        if(list1.size() >= numElements) {
            listToAddTo.addAll(list1.subList(0, numElements));
            return;
        } else {
            listToAddTo.addAll(list1);
        }

        numElements = numElements - list1.size();

        if(list2.size() >= numElements) {
            listToAddTo.addAll(list2.subList(0, numElements));
        } else {
            listToAddTo.addAll(list2);
        }
    }
       

    /**
     * Helper method that creates lists of TTL=1 and TTL=2 connections to query
     * for an aggressive probe.  This is desired, for example, when the desired
     * file appears to be rare or when there is not enough data to determine
     * the file's popularity.
     *
     * @param oldConnections the <tt>List</tt> of old-style connections
     * @param missConnections the <tt>List</tt> of new connections that did
     *  not have a hit for this query
     * @param hitConnections the <tt>List</tt> of connections with hits
     * @param returnLists the array of TTL=1 and TTL=2 connections to query
     */
    private static LinkedList[] 
        createAggressiveProbe(List oldConnections, List missConnections,
                              List hitConnections, LinkedList[] returnLists) {
        
        // add as many connections as possible from first the old connections
        // list, then the connections that did not have hits
        addToList(returnLists[1], oldConnections, missConnections, 3);

        // add any hits there are to the TTL=1 list
        int maxIndex = Math.min(4, hitConnections.size());
        returnLists[0].addAll(hitConnections.subList(0, maxIndex));

        return returnLists;        
    }
}













