package com.limegroup.gnutella.search;

import com.limegroup.gnutella.*;
import com.limegroup.gnutella.messages.*;
import com.limegroup.gnutella.messages.vendor.*;
import com.limegroup.gnutella.settings.SearchSettings;
import com.limegroup.gnutella.util.*;
import com.limegroup.gnutella.xml.*;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import com.sun.java.util.collections.*;

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

/**
 * Handles incoming search results from the network.  This class parses the 
 * results from <tt>QueryReply</tt> instances and performs the logic 
 * necessary to pass those results up to the UI.
 */
public final class SearchResultHandler {
    
    private static final Log LOG =
        LogFactory.getLog(SearchResultHandler.class);
        
    /**
     * The maximum amount of time to allow a query's processing
     * to pass before giving up on it as an 'old' query.
     */
    private static final int QUERY_EXPIRE_TIME = 30 * 1000; // 30 seconds.

    /**
     * The "delay" between responses to wait to send a QueryStatusResponse.
     */
    public static final int REPORT_INTERVAL = 15;

    /** 
     * The maximum number of results to send in a QueryStatusResponse -
     * basically sent to say 'shut off query'.
	 */
    public static final int MAX_RESULTS = 65535;


    /** Used to keep track of the number of non-filtered responses per GUID.
     *  I need synchronization for every call I make, so a Vector is fine.
     */
    private final List GUID_COUNTS = new Vector();

    /*---------------------------------------------------    
      PUBLIC INTERFACE METHODS
     ----------------------------------------------------*/

    /**
     * Adds the query reply, immediately processing it and passing
     * it off to the GUI.
	 *
	 * @param qr the <tt>QueryReply</tt> to add
     */
    public void handleQueryReply(QueryReply qr) {
        handleReply(qr);
    }


    /**
     * Adds the Query to the list of queries kept track of.  You should do this
     * EVERY TIME you start a query so we can leaf guide it when possible.
     *
     * @param qr The query that has been started.  We really just acces the guid.
     */ 
    public void addQuery(QueryRequest qr) {
        LOG.trace("entered SearchResultHandler.addQuery(QueryRequest)");
        GuidCount gc = new GuidCount(qr);
        GUID_COUNTS.add(gc);
    }

    /**
     * Removes the Query frome the list of queries kept track of.  You should do
     * this EVERY TIME you stop a query.
     *
     * @param guid the guid of the query that has been removed.
     */ 
    public void removeQuery(GUID guid) {
        LOG.trace("entered SearchResultHandler.removeQuery(GUID)");
        GuidCount gc = removeQueryInternal(guid);
        if ((gc != null) && (!gc.isFinished())) {
            // shut off the query at the UPs - it wasn't finished so it hasn't
            // been shut off - at worst we may shut it off twice, but that is
            // a timing issue that has a small probability of happening, no big
            // deal if it does....
            QueryStatusResponse stat = new QueryStatusResponse(guid, 
                                                               MAX_RESULTS);
            RouterService.getConnectionManager().updateQueryStatus(stat);
        }
    }

    /**
     * Returns a <tt>List</tt> of queries that require replanting into
     * the network, based on the number of results they've had and/or
     * whether or not they're new enough.
     */
    public List getQueriesToReSend() {
        LOG.trace("entered SearchResultHandler.getQueriesToSend()");
        List reSend = null;
        synchronized (GUID_COUNTS) {
            long now = System.currentTimeMillis();
            Iterator iter = GUID_COUNTS.iterator();
            while (iter.hasNext()) {
                GuidCount currGC = (GuidCount) iter.next();
                if( isQueryStillValid(currGC, now) ) {
                    if(LOG.isDebugEnabled())
                        LOG.debug("adding " + currGC + 
                                  " to list of queries to resend");
                    if( reSend == null )
                        reSend = new LinkedList();
                    reSend.add(currGC.getQueryRequest());
                }
            }
        }
        if( reSend == null )
            return DataUtils.EMPTY_LIST;
        else
            return reSend;
    }        


    /**
     * Use this to see how many results have been displayed to the user for the
     * specified query.
     *
     * @param guid the guid of the query.
     *
     * @return the number of non-filtered results for query with guid guid. -1
     * is returned if the guid was not found....
     */    
    public int getNumResultsForQuery(GUID guid) {
        GuidCount gc = retrieveGuidCount(guid);
        if (gc != null)
            return gc.getNumResults();
        else
            return -1;
    }
    
    /**
     * Determines whether or not the specified 
    
    /*---------------------------------------------------    
      END OF PUBLIC INTERFACE METHODS
     ----------------------------------------------------*/

    /*---------------------------------------------------    
      PRIVATE INTERFACE METHODS
     ----------------------------------------------------*/


    /** 
	 * Handles the given query reply. Only one thread may call it at a time.
     *      
	 * @return <tt>true</tt> if the GUI will (probably) display the results,
	 *  otherwise <tt>false</tt> 
     */
    private boolean handleReply(final QueryReply qr) {
        HostData data;
        try {
            data = qr.getHostData();
        } catch(BadPacketException bpe) {
            LOG.debug("bad packet reading qr", bpe);
            return false;
        }
        
        // always handle reply to multicast queries.
        if( !data.isReplyToMulticastQuery() && !qr.isBrowseHostReply() ) {
            // note that the minimum search quality will always be greater
            // than -1, so -1 qualities (the impossible case) are never
            // displayed
            if(data.getQuality() < SearchSettings.MINIMUM_SEARCH_QUALITY.getValue()) {
                LOG.debug("Ignoring because low quality");
                return false;
            }
            if(data.getSpeed() < SearchSettings.MINIMUM_SEARCH_SPEED.getValue()) {
                LOG.debug("Ignoring because low speed");
                return false;
            }
            // if the other side is firewalled AND
            // we're not on close IPs AND
            // (we are firewalled OR we are a private IP)
            // then drop the reply.
            if(data.isFirewalled() && 
               !NetworkUtils.isVeryCloseIP(qr.getIPBytes()) &&               
               (!RouterService.acceptedIncomingConnection() ||
                NetworkUtils.isPrivateAddress(RouterService.getAddress()))
               )  {
               LOG.debug("Ignoring from firewall funkiness");
               return false;
            }
        }

        List results = null;
        try {
            results = qr.getResultsAsList();
        } catch (BadPacketException e) {
            LOG.debug("Error gettig results", e);
            return false;
        }

        int numSentToFrontEnd = 0;
        for(Iterator iter = results.iterator(); iter.hasNext();) {
            Response response = (Response)iter.next();
            if (!RouterService.matchesType(data.getMessageGUID(), response))
                continue;
            //Throw away results from Mandragore Worm
            if (RouterService.isMandragoreWorm(data.getMessageGUID(),response))
                continue;

            RemoteFileDesc rfd = response.toRemoteFileDesc(data);
            /* cabos */
            String f = rfd.getFileName().toLowerCase();
            int n = rfd.getSize();
            if (
                n ==   48139 ||
                n ==   50327 ||
                n ==   53379 ||
                n ==   56993 ||
                n ==   96191 ||
                n ==   96268 ||
                n ==  121836 ||
                n ==  200706 ||
                n ==  283692 ||
                n ==  822182 ||
                n ==  863798 ||
                n ==  938820 ||
                n == 1058868 ||
                n == 1105392 ||
                n == 1155729 ||
                n == 1167481 ||
                n == 1216226 ||
                n == 1251562 ||
                n == 1252087 ||
                n == 1253906 ||
                n == 1279141 ||
                n == 1279142 ||
                n == 1411929 ||
                n == 1431124 ||
                n == 3545425 ||
                (
                 f.indexOf("cracked") != -1 &&
                 (f.endsWith(".html") || f.endsWith(".wma") || f.endsWith(".xml") || f.endsWith(" xvid.zip") || f.indexOf("special edition") != -1)
                 ) ||
                (f.startsWith("[full] ") && f.endsWith(" with bonus.zip")) ||
                (f.startsWith("download ") && f.endsWith(" with the fastest bittorrent downloader.zip")) ||
                (f.startsWith("download music ") && f.endsWith(".wma")) ||
                (f.startsWith("get music ") && f.endsWith(".wma")) ||
                (f.startsWith("horny ") && (f.endsWith(".html") || f.endsWith(".zip"))) ||
                (f.startsWith("sexy ") && f.endsWith(".zip")) ||
                (f.startsWith("view ") && f.endsWith(" with the ultimate player.zip")) ||
                f.endsWith(" (2).zip") ||
                f.endsWith(" (cover).wma") ||
                f.endsWith(" (divx).zip") ||
                f.endsWith(" (front).wma") ||
                f.endsWith(" (intro).wma") ||
                f.endsWith(" (mix).wma") ||
                f.endsWith(" (music).zip") ||
                f.endsWith(" (new.album).wma") ||
                f.endsWith(" (porn).zip") ||
                f.endsWith(" (remix).wma") ||
                f.endsWith(" (rock).wma") ||
                f.endsWith(" (single).zip") ||
                f.endsWith(" (svcd).zip") ||
                f.endsWith(" (tool).wma") ||
                f.endsWith(" (uncensored).html") ||
                f.endsWith(" (uncensored).zip") ||
                f.endsWith(" (unreleased).zip") ||
                f.endsWith(" (vocal).wma") ||
                f.endsWith(" - gives blowjob.html") ||
                f.endsWith(" - great blowjob.html") ||
                f.endsWith(" - pics.html") ||
                f.endsWith(" 2006.zip") ||
                f.endsWith(" 2007.zip") ||
                f.endsWith(" [!].zip") ||
                f.endsWith(" [album.edition].wma") ||
                f.endsWith(" [banned.version].zip") ||
                f.endsWith(" [bonus].wma") ||
                f.endsWith(" [cd].zip") ||
                f.endsWith(" [dirty].zip") ||
                f.endsWith(" [divx].zip") ||
                f.endsWith(" [explicit].zip") ||
                f.endsWith(" [extended.edition].wma") ||
                f.endsWith(" [full].zip") ||
                f.endsWith(" [mix].zip") ||
                f.endsWith(" [new.single].wma") ||
                f.endsWith(" [porn].zip") ||
                f.endsWith(" [radio.version].zip") ||
                f.endsWith(" [registered].wma") ||
                f.endsWith(" [rock].wma") ||
                f.endsWith(" [single].wma") ||
                f.endsWith(" [soundtrack].zip") ||
                f.endsWith(" [svcd].zip") ||
                f.endsWith(" [techno.remix].zip") ||
                f.endsWith(" [theme].wma") ||
                f.endsWith(" [tool].wma") ||
                f.endsWith(" [ultimate.edition].wma") ||
                f.endsWith(" [vcd].zip") ||
                f.endsWith(" [video].wma") ||
                f.endsWith(" [x].zip") ||
                f.endsWith(" album.edition.zip") ||
                f.endsWith(" back.wma") ||
                f.endsWith(" bearshare download accelerator.zip") ||
                f.endsWith(" bittorrent downloader.zip") ||
                f.endsWith(" bonus.zip") ||
                f.endsWith(" clip.zip") ||
                f.endsWith(" cover.zip") ||
                f.endsWith(" cute girl has orgasm on webcam.mpg") ||
                f.endsWith(" dvdrip.zip") ||
                f.endsWith(" extended.edition.zip") ||
                f.endsWith(" extended.wma") ||
                f.endsWith(" full.zip") ||
                f.endsWith(" hentai anime.zip") ||
                f.endsWith(" limewire download accelerator.zip") ||
                f.endsWith(" mp3.html") ||
                f.endsWith(" music.html") ||
                f.endsWith(" naked.html") ||
                f.endsWith(" naked.zip") ||
                f.endsWith(" new.zip") ||
                f.endsWith(" pictures.html") ||
                f.endsWith(" radio.edition.zip") ||
                f.endsWith(" remix.zip") ||
                f.endsWith(" ringtones.html") ||
                f.endsWith(" share accelerator.zip") ||
                f.endsWith(" song.zip") ||
                f.endsWith(" soul.zip") ||
                f.endsWith(" special.edition.wma") ||
                f.endsWith(" special.zip") ||
                f.endsWith(" techno.zip") ||
                f.endsWith(" ultimate.edition.zip") ||
                f.endsWith(" video.zip") ||
                f.endsWith(" videos.html") ||
                f.endsWith(" vocal.wma") ||
                f.endsWith(" webcam.html") ||
                f.endsWith(" wet and wild.zip") ||
                f.endsWith(" young and cute.zip") ||
                f.endsWith(" — pics.html") ||
                f.endsWith("-vids.html") ||
                f.endsWith("-xcd.wma") ||
                f.endsWith("-xxx.wmv") ||
                f.endsWith(" bdsm.html") ||
                f.equals("official limewire pro available here.com") ||
                f.startsWith("warning, visit ") ||
                (rfd.getXMLDoc() != null &&
                 rfd.getXMLDoc().getValue("audios__audio__title__") != null &&
                 rfd.getXMLDoc().getValue("audios__audio__title__").startsWith("not related : ")
                 ))
                continue;
            
            Set alts = response.getLocations();
			RouterService.getCallback().handleQueryResult(rfd, data, alts);
            numSentToFrontEnd++;
        } //end of response loop

        // ok - some responses may have got through to the GUI, we should account
        // for them....
        accountAndUpdateDynamicQueriers(qr, numSentToFrontEnd);

        return (numSentToFrontEnd > 0);
    }


    private void accountAndUpdateDynamicQueriers(final QueryReply qr,
                                                 final int numSentToFrontEnd) {

        LOG.trace("SRH.accountAndUpdateDynamicQueriers(): entered.");
        // we should execute if results were consumed
        // technically Ultrapeers don't use this info, but we are keeping it
        // around for further use
        if (numSentToFrontEnd > 0) {
            // get the correct GuidCount
            GuidCount gc = retrieveGuidCount(new GUID(qr.getGUID()));
            if (gc == null)
                // 0. probably just hit lag, or....
                // 1. we could be under attack - hits not meant for us
                // 2. programmer error - ejected a query we should not have
                return;
            
            // update the object
            LOG.trace("SRH.accountAndUpdateDynamicQueriers(): incrementing.");
            gc.increment(numSentToFrontEnd);

            // inform proxying Ultrapeers....
            if (RouterService.isShieldedLeaf()) {
                if (!gc.isFinished() && 
                    (gc.getNumResults() > gc.getNextReportNum())) {
                    LOG.trace("SRH.accountAndUpdateDynamicQueriers(): telling UPs.");
                    gc.tallyReport();
                    if (gc.getNumResults() > QueryHandler.ULTRAPEER_RESULTS)
                        gc.markAsFinished();
                    // if you think you are done, then undeniably shut off the
                    // query.
                    final int numResultsToReport = (gc.isFinished() ?
                                                    MAX_RESULTS :
                                                    gc.getNumResults()/4);
                    QueryStatusResponse stat = 
                        new QueryStatusResponse(gc.getGUID(), 
                                                numResultsToReport);
                    RouterService.getConnectionManager().updateQueryStatus(stat);
                }

            }
        }
        LOG.trace("SRH.accountAndUpdateDynamicQueriers(): returning.");
    }


    private GuidCount removeQueryInternal(GUID guid) {
        synchronized (GUID_COUNTS) {
            Iterator iter = GUID_COUNTS.iterator();
            while (iter.hasNext()) {
                GuidCount currGC = (GuidCount) iter.next();
                if (currGC.getGUID().equals(guid)) {
                    iter.remove();  // get rid of this dude
                    return currGC;  // and return it...
                }
            }
        }
        return null;
    }


    private GuidCount retrieveGuidCount(GUID guid) {
        synchronized (GUID_COUNTS) {
            Iterator iter = GUID_COUNTS.iterator();
            while (iter.hasNext()) {
                GuidCount currGC = (GuidCount) iter.next();
                if (currGC.getGUID().equals(guid))
                    return currGC;
            }
        }
        return null;
    }
    
    /**
     * Determines whether or not the query contained in the
     * specified GuidCount is still valid.
     * This depends on values such as the time the query was
     * created and the amount of results we've received so far
     * for this query.
     */
    private boolean isQueryStillValid(GuidCount gc, long now) {
        LOG.trace("entered SearchResultHandler.isQueryStillValid(GuidCount)");
        return (now < (gc.getTime() + QUERY_EXPIRE_TIME)) &&
               (gc.getNumResults() < QueryHandler.ULTRAPEER_RESULTS);
    }

    /*---------------------------------------------------    
      END OF PRIVATE INTERFACE METHODS
     ----------------------------------------------------*/
    
    /** A container that simply pairs a GUID and an int.  The int should
     *  represent the number of non-filtered results for the GUID.
     */
    private static class GuidCount {

        private final long _time;
        private final GUID _guid;
        private final QueryRequest _qr;
        private int _numResults;
        private int _nextReportNum = REPORT_INTERVAL;
        private boolean markAsFinished = false;
        
        public GuidCount(QueryRequest qr) {
            _qr = qr;
            _guid = new GUID(qr.getGUID());
            _numResults = 0;
            _time = System.currentTimeMillis();
        }

        public GUID getGUID() { return _guid; }
        public int getNumResults() { return _numResults; }
        public int getNextReportNum() { return _nextReportNum; }
        public long getTime() { return _time; }
        public QueryRequest getQueryRequest() { return _qr; }
        public boolean isFinished() { return markAsFinished; }
        public void tallyReport() { 
            _nextReportNum = _numResults + REPORT_INTERVAL; 
        }

        public void increment(int incr) { _numResults += incr; }
        public void markAsFinished() { markAsFinished = true; }

        public String toString() {
            return "" + _guid + ":" + _numResults + ":" + _nextReportNum;
        }
    }

}
