package com.limegroup.gnutella.downloader;

import static com.limegroup.gnutella.Constants.MAX_FILE_SIZE;

import java.io.File;
import java.io.IOException;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledExecutorService;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.limewire.collection.ApproximateMatcher;
import org.limewire.collection.FixedSizeExpiringSet;
import org.limewire.concurrent.ThreadExecutor;
import org.limewire.io.DiskException;
import org.limewire.io.IOUtils;
import org.limewire.io.InvalidDataException;
import org.limewire.listener.EventListener;
import org.limewire.listener.EventListenerList;
import org.limewire.service.ErrorService;
import org.limewire.util.FileUtils;

import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.limegroup.gnutella.ApplicationServices;
import com.limegroup.gnutella.BandwidthTracker;
import com.limegroup.gnutella.DownloadCallback;
import com.limegroup.gnutella.DownloadManager;
import com.limegroup.gnutella.Endpoint;
import com.limegroup.gnutella.FileDesc;
import com.limegroup.gnutella.FileManager;
import com.limegroup.gnutella.GUID;
import com.limegroup.gnutella.IncompleteFileDesc;
import com.limegroup.gnutella.InsufficientDataException;
import com.limegroup.gnutella.MessageRouter;
import com.limegroup.gnutella.NetworkManager;
import com.limegroup.gnutella.RemoteFileDesc;
import com.limegroup.gnutella.RemoteHostData;
import com.limegroup.gnutella.SaveLocationManager;
import com.limegroup.gnutella.SavedFileManager;
import com.limegroup.gnutella.SpeedConstants;
import com.limegroup.gnutella.URN;
import com.limegroup.gnutella.UrnCache;
import com.limegroup.gnutella.UrnSet;
import com.limegroup.gnutella.altlocs.AltLocListener;
import com.limegroup.gnutella.altlocs.AltLocManager;
import com.limegroup.gnutella.altlocs.AlternateLocation;
import com.limegroup.gnutella.altlocs.AlternateLocationCollection;
import com.limegroup.gnutella.altlocs.AlternateLocationFactory;
import com.limegroup.gnutella.altlocs.DirectAltLoc;
import com.limegroup.gnutella.altlocs.DirectDHTAltLoc;
import com.limegroup.gnutella.altlocs.PushAltLoc;
import com.limegroup.gnutella.auth.ContentManager;
import com.limegroup.gnutella.auth.ContentResponseData;
import com.limegroup.gnutella.auth.ContentResponseObserver;
import com.limegroup.gnutella.downloader.RequeryManager.QueryType;
import com.limegroup.gnutella.downloader.serial.DownloadMemento;
import com.limegroup.gnutella.downloader.serial.GnutellaDownloadMemento;
import com.limegroup.gnutella.downloader.serial.GnutellaDownloadMementoImpl;
import com.limegroup.gnutella.downloader.serial.RemoteHostMemento;
import com.limegroup.gnutella.filters.IPFilter;
import com.limegroup.gnutella.guess.GUESSEndpoint;
import com.limegroup.gnutella.guess.OnDemandUnicaster;
import com.limegroup.gnutella.messages.QueryRequest;
import com.limegroup.gnutella.messages.QueryRequestFactory;
import com.limegroup.gnutella.settings.ConnectionSettings;
import com.limegroup.gnutella.settings.DownloadSettings;
import com.limegroup.gnutella.settings.SharingSettings;
import com.limegroup.gnutella.tigertree.HashTree;
import com.limegroup.gnutella.tigertree.HashTreeCache;
import com.limegroup.gnutella.util.QueryUtils;
import com.limegroup.gnutella.xml.LimeXMLDocument;

/**
 * A smart download.  Tries to get a group of similar files by delegating
 * to DownloadWorker threads.  Does retries and resumes automatically.
 * Reports all changes to a DownloadManager.  This class is thread safe.<p>
 *
 * Smart downloads can use many policies, and these policies are free to change
 * as allowed by the Downloader specification.  This implementation provides
 * swarmed downloads, the ability to download copies of the same file from
 * multiple hosts.  <p>
 *
 * Subclasses may refine the requery behavior by overriding {@link #newRequery()}
 * {@link #allowAddition(RemoteFileDesc)}, {@link #addDownload(Collection, boolean)}.
 */
class ManagedDownloaderImpl extends AbstractCoreDownloader implements AltLocListener,
        ManagedDownloader, DownloadWorkerSupport {
    
    /*
     * IMPLEMENTATION NOTES: The basic idea behind swarmed (multisource)
     * downloads is to download one file in parallel from multiple servers. For
     * example, one might simultaneously download the first half of a file from
     * server A and the second half from server B. This increases throughput if
     * the downstream capacity of the downloader is greater than the upstream
     * capacity of the fastest uploader.
     * 
     * The ideal way of identifying duplicate copies of a file is to use hashes
     * via the HUGE proposal.
     * 
     * When discussing swarmed downloads, it's useful to divide parts of a file
     * into three categories: black, grey, and white. Black regions have already
     * been downloaded to disk. Grey regions have been assigned to a downloader
     * but not yet completed. White regions have not been assigned to a
     * downloader.
     * 
     * ManagedDownloader delegates to multiple DownloadWorker instances, one for
     * each HTTP connection. They use a shared VerifyingFile object that keeps
     * track of which blocks have been written to disk.
     * 
     * ManagedDownloader uses one thread to control the smart downloads plus one
     * thread per DownloadWorker instance. The call flow of ManagedDownloader's
     * "master" thread is as follows:
     * 
     * performDownload: initializeDownload fireDownloadWorkers (asynchronously
     * start workers) verifyAndSave
     * 
     * The core downloading loop is done by fireDownloadWorkers.Currently the
     * desired parallelism is fixed at 2 for modem users, 6 for cable/T1/DSL,
     * and 8 for T3 and above.
     * 
     * DownloadManager notifies a ManagedDownloader when it should start
     * performDownload. An inactive download (waiting for a busy host, waiting
     * for a user to requery, waiting for GUESS responses, etc..) is essentially
     * a state-machine, pumped forward by DownloadManager. The 'master thread'
     * of a ManagedDownloader is recreated every time DownloadManager moves the
     * download from inactive to active.
     * 
     * All downloads start QUEUED. From there, it will stay queued until a slot
     * is available.
     * 
     * If at least one host is available to download from, then the first state
     * is always CONNECTING. After connecting, a downloader can become: a)
     * DOWNLOADING (actively downloading) b) WAITING_FOR_RETRY (busy hosts) c)
     * ABORTED (user manually stopped the download) c2) PAUSED (user paused the
     * download) d) REMOTE_QUEUED (the remote host queued us)
     * 
     * If no hosts existed for connecting, or we exhausted our attempts at
     * connecting to all possible hosts, the state will become one of: e)
     * GAVE_UP (max'ed out on requeries) f) WAITING_FOR_USER (waiting for the
     * user to initiate a requery) g) ITERATIVE_GUESSING (targeted location of
     * more sources) If the user resumes the download and we were
     * WAITING_FOR_USER, a requery is sent out and we go into
     * WAITING_FOR_RESULTS stage. After we have finished waiting for results (if
     * none arrived), we will either go back to WAITING_FOR_USER (if we are
     * allowed more requeries), or GAVE_UP (if we maxed out the requeries).
     * After ITERATIVE_GUESSING completes, if no results arrived then we go to
     * WAITING_FOR_USER. Prior to WAITING_FOR_RESULTS, if no connections are
     * active then we wait at WAITING_FOR_CONNECTIONS until connections exist.
     * 
     * If more results come in while waiting in these states, the download will
     * either immediately become active (CONNECTING ...) again, or change its
     * state to QUEUED and wait for DownloadManager to activate it.
     * 
     * The download can finish in one of the following states: - COMPLETE
     * (download completed just fine) - ABORTED (user pressed stopped at some
     * point) - DISK_PROBLEM (LimeWire couldn't manipulate the file) -
     * CORRUPT_FILE (the file was corrupt) - INVALID (content authority didn't
     * allow the transfer)
     * 
     * There are a few intermediary states: - HASHING - SAVING HASHING & SAVING
     * are seen by the GUI, and are used just prior to COMPLETE, to let the user
     * know what is currently happening in the closing states of the download.
     * RECOVERY_FAILED is used as an indicator that we no longer want to retry
     * the download, because we've tried and recovered from corruption too many
     * times.
     * 
     * How corruption is handled: There are two general cases where corruption
     * can be discovered - during a download or after the download has finished.
     * 
     * During the download, each worker thread checks periodically whether the
     * amount of data lost to corruption exceeds 10% of the completed file size.
     * Whenever that happens, the worker thread asks the user whether the
     * download should be terminated. If the user chooses to delete the file,
     * the downloader is stopped asynchronously and _corruptState is set to
     * CORRUPT_STOP_STATE. The master download thread is interrupted, it checks
     * _corruptState and either discards or removes the file.
     * 
     * After the download, if the sha1 does not match the expected, the master
     * download thread prompts the user whether they want to keep the file or
     * discard it. If we did not have a tree during the download we remove the
     * file from partial sharing, otherwise we keep it until the user answers
     * the prompt (which may take a very long time for overnight downloads). The
     * tree itself is purged.
     * 
     */
    
    private static final Log LOG = LogFactory.getLog(ManagedDownloaderImpl.class);
    
    /*********************************************************************
     * LOCKING: obtain this's monitor before modifying any of the following.
     * files, _activeWorkers, busy and setState.  We should  not hold lock 
     * while performing blocking IO operations.
     * 
     * Never acquire incompleteFileManager's monitor if you have commonOutFile's
     * monitor.
     *
     * Never obtain manager's lock if you hold this.
     ***********************************************************************/
        
    
    /** The complete Set of files passed to the constructor.  Must be
     *  maintained in memory to support resume.  allFiles may only contain
     *  elements of type RemoteFileDesc and URLRemoteFileDesc
     */
    private Set<RemoteFileDesc> cachedRFDs;

	/**
	 * The ranker used to select the next host we should connect to
	 */
	private SourceRanker ranker;

    /**
     * How long we'll wait after sending a GUESS query before we try something
     * else.
     */
    private static final int GUESS_WAIT_TIME = 5000;
    

    /** The size of the approx matcher 2d buffer... */
    private static final int MATCHER_BUF_SIZE = 120;

    /** This is used for matching of filenames.  kind of big so we only want
     *  one. */
    private static ApproximateMatcher matcher = 
        new ApproximateMatcher(MATCHER_BUF_SIZE);    

    ////////////////////////// Core Variables /////////////////////////////

    /** If started, the thread trying to coordinate all downloads.  
     *  Otherwise null. */
    private volatile Thread dloaderManagerThread;
    /** True iff this has been forcibly stopped. */
    private volatile boolean stopped;
    /** True iff this has been paused.  */
    private volatile boolean paused;
    /** True if this has been invalidated. */
    private volatile boolean invalidated;

    
    /** 
     * The connections we're using for the current attempts.
     * LOCKING: copy on write on this 
     * 
     */    
    private volatile List<DownloadWorker> _activeWorkers;
    
    /**
     * A List of workers in progress.  Used to make sure that we do
     * not terminate in fireDownloadWorkers without hope if threads are
     * connecting to hosts but not have not yet been added to _activeWorkers.
     * 
     * Also, if the download completes and any workers are queued, those
     * workers need to be signalled to stop.
     * 
     * LOCKING: synchronize on this
     */
    private List<DownloadWorker> _workers;

    /**
     * Stores the queued threads and the corresponding queue position
     * LOCKING: copy on write on this
     */
    private volatile Map<DownloadWorker, Integer> _queuedWorkers;

    /**
     * Set of RFDs where we store rfds we are currently connected to or
     * trying to connect to.
     */
    private Set<RemoteFileDesc> currentRFDs;
    
    /**
     * The SHA1 hash of the file that this ManagedDownloader is controlling.
     */
    private volatile URN downloadSHA1;
	
    /**
     * The collection of alternate locations we successfully downloaded from
     * something from.
     */
	private Set<AlternateLocation> validAlts; 
	
	/**
	 * A list of the most recent failed locations, so we don't try them again.
	 */
	private Set<RemoteHostData> invalidAlts;

    /**
     * Cache the most recent failed locations. 
     * Holds <tt>AlternateLocation</tt> instances
     */
    private Set<AlternateLocation> recentInvalidAlts;
    
    /**
     * Manages writing stuff to disk, remember what's leased, what's verified,
     * what is valid, etc........
     */
    protected VerifyingFile commonOutFile;
    
    /** A list of pushing hosts. */
    private volatile PushList pushes;

    ///////////////////////// Variables for GUI Display  /////////////////
    /** The current state.  One of Downloader.CONNECTING, Downloader.ERROR,
      *  etc.   Should be modified only through setState. */
    private DownloadStatus state = DownloadStatus.INITIALIZING;
    /** The system time that we expect to LEAVE the current state, or
     *  Integer.MAX_VALUE if we don't know. Should be modified only through
     *  setState. */
    private long stateTime;
    
    /** The current incomplete file that we're downloading, or the last
     *  incomplete file if we're not currently downloading, or null if we
     *  haven't started downloading.  Used for previewing purposes. */
    private File incompleteFile;
   
    /**
     * The position of the downloader in the uploadQueue */
    private int queuePosition;
    /**
     * The vendor the of downloader we're queued from.
     */
    private String queuedVendor;

    /** If in CORRUPT_FILE state, the number of bytes downloaded.  Note that
     *  this is less than corruptFile.length() if there are holes. */
    private volatile long corruptFileBytes;
    /** If in CORRUPT_FILE state, the name of the saved corrupt file or null if
     *  no corrupt file. */
    private volatile File corruptFile;

	/** The list of all chat-enabled hosts for this <tt>ManagedDownloader</tt>
	 *  instance.
	 */
	private DownloadChatList chatList;

	/** The list of all browsable hosts for this <tt>ManagedDownloader</tt>
	 *  instance.
	 */
	private DownloadBrowseHostList browseList;


    /** The various states of the ManagedDownloade with respect to the 
     * corruption state of this download. 
     */
    private static final int NOT_CORRUPT_STATE = 0;
    private static final int CORRUPT_WAITING_STATE = 1;
    private static final int CORRUPT_STOP_STATE = 2;
    private static final int CORRUPT_CONTINUE_STATE = 3;
    /**
     * The actual state of the ManagedDownloader with respect to corruption
     * LOCKING: obtain corruptStateLock
     * INVARIANT: one of NOT_CORRUPT_STATE, CORRUPT_WAITING_STATE, etc.
     */
    private volatile int corruptState;
    private Object corruptStateLock;

    /**
     * Locking object to be used for accessing all alternate locations.
     * LOCKING: never try to obtain monitor on this if you hold the monitor on
     * altLock 
     */
    private Object altLock;

    /**
     * The number of times we've been bandwidth measured
     */
    private int numMeasures = 0;
    
    /**
     * The average bandwidth over all managed downloads.
     */
    private float averageBandwidth = 0f;

    /**
     * The GUID of the original query.  may be null.
     */
    private volatile GUID originalQueryGUID;
    
    /**
     * Whether or not we've sent a GUESS query.
     */
    private boolean triedLocatingSources;
    
    /**
     * Whether or not we've gotten new files since the last time this download
     * started.
     */
    private volatile boolean receivedNewSources;
    
    /**
	 * The number of hosts that were tried to be connected to. Value is reset
	 * in {@link #startDownload()};
	 */
    private volatile int triedHosts;
    
    private long contentLength = -1;
    
    private final EventListenerList<DownloadStatusEvent> listeners = 
        new EventListenerList<DownloadStatusEvent>(); 
    
    protected final DownloadManager downloadManager;
    protected final FileManager fileManager;
    protected final IncompleteFileManager incompleteFileManager;
    protected final DownloadCallback downloadCallback;    
    protected final NetworkManager networkManager;
    protected final AlternateLocationFactory alternateLocationFactory;
    protected final RequeryManager requeryManager;
    protected final QueryRequestFactory queryRequestFactory;
    protected final OnDemandUnicaster onDemandUnicaster;
    protected final DownloadWorkerFactory downloadWorkerFactory;
    protected final AltLocManager altLocManager;
    protected final ContentManager contentManager;
    protected final SourceRankerFactory sourceRankerFactory;
    protected final UrnCache urnCache;
    protected final SavedFileManager savedFileManager;
    protected final VerifyingFileFactory verifyingFileFactory;
    protected final DiskController diskController;
    protected final IPFilter ipFilter;
    protected final ScheduledExecutorService backgroundExecutor;
    protected final Provider<MessageRouter> messageRouter;
    protected final Provider<HashTreeCache> tigerTreeCache;
    protected final ApplicationServices applicationServices;
    protected final RemoteFileDescFactory remoteFileDescFactory;
    protected final Provider<PushList> pushListProvider;

    /**
     * Creates a new ManagedDownload to download the given files.
     * 
     * You must set initial source via {@link #addInitialSources},
     * set the save file via {@link #setSaveFile(File, String, boolean)},
     * and call {@link #initialize} prior to starting this download.
     */
    @Inject
    protected ManagedDownloaderImpl(SaveLocationManager saveLocationManager,
            DownloadManager downloadManager, FileManager fileManager,
            IncompleteFileManager incompleteFileManager, DownloadCallback downloadCallback,
            NetworkManager networkManager, AlternateLocationFactory alternateLocationFactory,
            RequeryManagerFactory requeryManagerFactory, QueryRequestFactory queryRequestFactory,
            OnDemandUnicaster onDemandUnicaster, DownloadWorkerFactory downloadWorkerFactory,
            AltLocManager altLocManager, ContentManager contentManager,
            SourceRankerFactory sourceRankerFactory, UrnCache urnCache,
            SavedFileManager savedFileManager, VerifyingFileFactory verifyingFileFactory,
            DiskController diskController, 
            IPFilter ipFilter, @Named("backgroundExecutor")
            ScheduledExecutorService backgroundExecutor, Provider<MessageRouter> messageRouter,
            Provider<HashTreeCache> tigerTreeCache, ApplicationServices applicationServices,
            RemoteFileDescFactory remoteFileDescFactory, Provider<PushList> pushListProvider) {
        super(saveLocationManager);
        this.downloadManager = downloadManager;
        this.fileManager = fileManager;
        this.incompleteFileManager = incompleteFileManager;
        this.downloadCallback = downloadCallback;
        this.networkManager = networkManager;
        this.alternateLocationFactory = alternateLocationFactory;
        this.requeryManager = requeryManagerFactory.createRequeryManager(new RequeryListenerImpl());
        this.queryRequestFactory = queryRequestFactory;
        this.onDemandUnicaster = onDemandUnicaster;
        this.downloadWorkerFactory = downloadWorkerFactory;
        this.altLocManager = altLocManager;
        this.contentManager = contentManager;
        this.sourceRankerFactory = sourceRankerFactory;
        this.urnCache = urnCache;
        this.savedFileManager = savedFileManager;
        this.verifyingFileFactory = verifyingFileFactory;
        this.diskController = diskController;
        this.ipFilter = ipFilter;
        this.backgroundExecutor = backgroundExecutor;
        this.messageRouter = messageRouter;
        this.tigerTreeCache = tigerTreeCache;
        this.applicationServices = applicationServices;
        this.remoteFileDescFactory = remoteFileDescFactory;
        this.cachedRFDs = new HashSet<RemoteFileDesc>();
        this.pushListProvider = pushListProvider;
    }
    
    public synchronized void addInitialSources(Collection<RemoteFileDesc> rfds, String defaultFileName) {
        if(rfds == null)
            rfds = Collections.emptyList();
        
        cachedRFDs.addAll(rfds);
        if (rfds.size() > 0) 
            initPropertiesMap(rfds.iterator().next());

        assert rfds.size() > 0 || defaultFileName != null;
        if (!hasDefaultFileName())
            setDefaultFileName(defaultFileName);
    }
    
    public void setQueryGuid(GUID queryGuid) {
        this.originalQueryGUID = queryGuid;
    }

    protected synchronized void initPropertiesMap(RemoteFileDesc rfd) {
        if(!hasDefaultFileName()) {
            setDefaultFileName(rfd.getFileName());
        }
        if(getContentLength() == -1)
            setContentLength(rfd.getSize());
    }

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#initialize()
     */
    public void initialize() {
        setState(DownloadStatus.INITIALIZING);
        
        // Every setter needs the lock.
        synchronized(this) {
            currentRFDs = new HashSet<RemoteFileDesc>();
            _activeWorkers=new LinkedList<DownloadWorker>();
            _workers=new ArrayList<DownloadWorker>();
            _queuedWorkers = new HashMap<DownloadWorker, Integer>();
    		chatList=new DownloadChatList();
            browseList=new DownloadBrowseHostList();
            stopped=false;
            paused = false;
            pushes = pushListProvider.get();
            corruptState=NOT_CORRUPT_STATE;
            corruptStateLock=new Object();
            altLock = new Object();
            numMeasures = 0;
            averageBandwidth = 0f;
            queuePosition=Integer.MAX_VALUE;
            queuedVendor = "";
            triedLocatingSources = false;
    		ranker = getSourceRanker(null);
            ranker.setMeshHandler(this);            
            for(RemoteFileDesc rfd : cachedRFDs) {
                if(getSha1Urn() != null)
                    break;
                if(rfd.getSHA1Urn() != null)
                    setSha1Urn(rfd.getSHA1Urn());
            }
        }        
        setState(DownloadStatus.QUEUED);
        
		if (getSha1Urn() != null) 
		    altLocManager.addListener(getSha1Urn(),this);
        
		
		// make sure all rfds have the same sha1
        verifyAllFiles();
		
        synchronized(altLock) {
            validAlts = new HashSet<AlternateLocation>();
            // stores up to 1000 locations for up to an hour each
            invalidAlts = new FixedSizeExpiringSet<RemoteHostData>(1000,60*60*1000L);
            // stores up to 10 locations for up to 10 minutes
            recentInvalidAlts = new FixedSizeExpiringSet<AlternateLocation>(10, 10*60*1000L);
        }
        synchronized (this) {
            if(shouldInitAltLocs()) {
                initializeAlternateLocations();
            }
        }
        
        try {
            //initializeFilesAndFolders();
            initializeIncompleteFile();
            initializeVerifyingFile();
        }catch(IOException bad) {
            setState(DownloadStatus.DISK_PROBLEM);
            reportDiskProblem(bad);
            return;
        }
        
        setState(DownloadStatus.QUEUED);
    }
    
    private void reportDiskProblem(IOException cause) {
        if (DownloadSettings.REPORT_DISK_PROBLEMS.getBoolean()) {
            if (!(cause instanceof DiskException))
                cause = new DiskException(cause);
            ErrorService.error(cause);
        }
    }
    
    protected void reportDiskProblem(String cause) {
        if (DownloadSettings.REPORT_DISK_PROBLEMS.getBoolean())
            ErrorService.error(new DiskException(cause));
    }
    
    /** 
     * Verifies the integrity of the RemoteFileDesc set.
     *
     * At one point in time, LimeWire somehow allowed files with different
     * SHA1s to be placed in the same ManagedDownloader.  This breaks
     * the invariants of the current ManagedDownloader, so we must
     * remove the extraneous RFDs.
     */
    private synchronized void verifyAllFiles() {
        if(getSha1Urn() == null)
            return;
        
		for (Iterator<RemoteFileDesc> iter = cachedRFDs.iterator(); iter.hasNext();) {
			RemoteFileDesc rfd = iter.next();
			if (rfd.getSHA1Urn() != null && !getSha1Urn().equals(rfd.getSHA1Urn()))
				iter.remove();
		}
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#startDownload()
     */
    public synchronized void startDownload() {
        assert dloaderManagerThread == null : "already started";
        ThreadExecutor.startThread(new Runnable() {
            public void run() {
                try {
                    dloaderManagerThread = Thread.currentThread();
                    validateDownload();
                    receivedNewSources = false;
                    // reset tried hosts count
                    triedHosts = 0;
                    DownloadStatus status = performDownload();
                    completeDownload(status);
                } catch(Throwable t) {
                    // if any unhandled errors occurred, remove this
                    // download completely and message the error.
                    ManagedDownloaderImpl.this.stop();
                    setState(DownloadStatus.ABORTED);
                    downloadManager.remove(ManagedDownloaderImpl.this, true);
                    
                    ErrorService.error(t);
                } finally {
                    dloaderManagerThread = null;
                }
            }
        }, "ManagedDownload");
    }
    
    /**
     * Completes the download process, possibly sending off requeries
     * that may later restart it.
     *
     * This essentially pumps the state of the download to different
     * areas, depending on what is required or what has already occurred.
     */
    private void completeDownload(DownloadStatus status) {
        boolean complete;
        boolean clearingNeeded = false;
        int waitTime = 0;
        // If TAD2 gave a completed state, set the state correctly & exit.
        // Otherwise...
        // If we manually stopped then set to ABORTED, else set to the 
        // appropriate state (either a busy host or no hosts to try).
        synchronized(this) {
            switch(status) {
            case COMPLETE:
            case DISK_PROBLEM:
            case CORRUPT_FILE:
                clearingNeeded = true;
                setState(status);
                break;
			case BUSY:
            case GAVE_UP:
                if(invalidated) {
                    clearingNeeded = true;
                    setState(DownloadStatus.INVALID);
                } else if(stopped) {
                    setState(DownloadStatus.ABORTED);
                } else if(paused) {
                    setState(DownloadStatus.PAUSED);
                } else {
                    setState(status); // BUSY or GAVE_UP
                }
                break;
            default:
                assert false : "Bad status from tad2: "+status;
            }
            
            complete = isCompleted();
            
            waitTime = ranker.calculateWaitTime();
            ranker.stop();
            if (clearingNeeded)
                ranker = null;
        }
        
        // Notify the manager that this download is done.
        // This MUST be done outside of this' lock, else
        // deadlock could occur.
        downloadManager.remove(this, complete);
        
        if (clearingNeeded) {
            synchronized(altLock) {
                recentInvalidAlts.clear();
                invalidAlts.clear();
                validAlts.clear();
            }
            if (complete) {
                synchronized(this) {
                    cachedRFDs.clear(); // the call right before this serializes. 
                }
            }
        }
        
        if(LOG.isTraceEnabled()) {
            LOG.trace("MD completing <" + getSaveFile().getName() 
                    + "> completed download, state: " + getState());
        }
        
        diskController.clearCaches();

        // if this is all completed, nothing else to do.
        if(complete) {
            ; // all done.
            
        // if this is paused, nothing else to do also.
        } else if(getState() == DownloadStatus.PAUSED) {
            ; // all done for now.
            
        // Try iterative GUESSing...
        // If that sent some queries, don't do anything else.
        // TODO: consider moving this inside the monitor
        } else if(tryGUESSing()) {
            ; // all done for now.
            
        } else {
            // the next few checks need to be atomic wrt dht callbacks to
            // requeryManager.
            
            // do not issue actual requeries while holding this.
            boolean requery = false;
            synchronized(this) {
                // If busy, try waiting for that busy host.
                if (getState() == DownloadStatus.BUSY) {
                    setState(DownloadStatus.BUSY, waitTime);

                // If we sent a query recently, then we don't want to send another,
                // nor do we want to give up.  Just continue waiting for results
                // from that query.
                } else if(requeryManager.isWaitingForResults()) {
                    switch(requeryManager.getLastQueryType()) {
                    case DHT: setState(DownloadStatus.QUERYING_DHT, requeryManager.getTimeLeftInQuery()); break;
                    case GNUTELLA: setState(DownloadStatus.WAITING_FOR_GNET_RESULTS, requeryManager.getTimeLeftInQuery()); break;
                    default: 
                        throw new IllegalStateException("Not any query type!");
                    }

                // If we're allowed to immediately send a query, do it!
                } else if(canSendRequeryNow()) {
                    requery = true;

                // If we can send a query after we activate, wait for the user.
                } else if(requeryManager.canSendQueryAfterActivate()) {
                    setState(DownloadStatus.WAITING_FOR_USER);

                // Otherwise, there's nothing we can do, give up.
                } else {
                    setState(DownloadStatus.GAVE_UP);

                }
            }

            if (requery)
                requeryManager.sendQuery();
        }
        
        if(LOG.isTraceEnabled()) {
            LOG.trace("MD completed <" + getSaveFile().getName() 
                    + "> completed download, state: " + getState()); 
        }
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#handleInactivity()
     */
    public synchronized void handleInactivity() {
//        if(LOG.isTraceEnabled())
            //LOG.trace("handling inactivity. state: " + 
                      //getState() + ", hasnew: " + hasNewSources() + 
                      //", left: " + getRemainingStateTime());
        switch(getState()) {
        case BUSY:
        case WAITING_FOR_CONNECTIONS:
        case ITERATIVE_GUESSING:
            // If we're finished waiting on busy hosts,
            // stable connections, or GUESSing,
            // but we're still inactive, then we queue ourselves
            // and wait till we get restarted.
            if(getRemainingStateTime() <= 0 || hasNewSources())
                setState(DownloadStatus.QUEUED);
            break;
        case QUERYING_DHT:
        case WAITING_FOR_GNET_RESULTS:
            // If we have new sources but are still inactive,
            // then queue ourselves and wait to restart.
            if(hasNewSources())
                setState(DownloadStatus.QUEUED);
            // Otherwise, if we've ran out of time waiting for results,
            // give up.  If another requery can be sent, the GAVE_UP
            // pump will trigger it to start.
            else if(requeryManager.getTimeLeftInQuery() <= 0)
                setState(DownloadStatus.GAVE_UP);
            break;
        case WAITING_FOR_USER:
            if (hasNewSources() || requeryManager.canSendQueryNow())
                setState(DownloadStatus.QUEUED);
            break;
        case GAVE_UP:
        	if (hasNewSources() || requeryManager.canSendQueryAfterActivate()) 
        		setState(DownloadStatus.QUEUED);
        case QUEUED:
        case PAUSED:
            // If we're waiting for the user to do something,
            // have given up, or are queued, there's nothing to do.
            break;
        default:
            throw new IllegalStateException("invalid state: " + getState() +
                                            ", workers: " + _workers.size() + 
                                            ", _activeWorkers: " + _activeWorkers.size() +
                                            ", _queuedWorkers: " + _queuedWorkers.size());
        }
    }   
    
    /**
     * Tries iterative GUESSing of sources.
     */
    private boolean tryGUESSing() {
        if(originalQueryGUID == null || triedLocatingSources || getSha1Urn() == null)
            return false;
            
        Set<GUESSEndpoint> guessLocs = messageRouter.get().getQueryLocs(this.originalQueryGUID);
        if(guessLocs.isEmpty())
            return false;

        setState(DownloadStatus.ITERATIVE_GUESSING, GUESS_WAIT_TIME);
        triedLocatingSources = true;

        //TODO: should we increment a stat to get a sense of
        //how much this is happening?
        for(GUESSEndpoint ep : guessLocs) {
            onDemandUnicaster.query(ep, getSha1Urn());
            // TODO: see if/how we can wait 750 seconds PER send again.
            // if we got a result, no need to continue GUESSing.
            if(receivedNewSources)
                break;
        }
        
        return true;
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#isAlive()
     */
    public boolean isAlive() {
        return dloaderManagerThread != null;
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#isCompleted()
     */
    public boolean isCompleted() {
        switch(getState()) {
        case COMPLETE:
        case ABORTED:
        case DISK_PROBLEM:
        case CORRUPT_FILE:
        case INVALID:
            return true;
        }
        return false;
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#isRelocatable()
     */
    public boolean isRelocatable() {
        if (isInactive())
            return true;
        switch (getState()) {
        case INITIALIZING:
        case CONNECTING:
        case DOWNLOADING:
        case REMOTE_QUEUED:
            return true;
        default:
            return false;
        }
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#isActive()
     */
    public boolean isActive() {
        switch(getState()) {
        case CONNECTING:
        case DOWNLOADING:
        case REMOTE_QUEUED:
        case HASHING:
        case SAVING:
        case IDENTIFY_CORRUPTION:
            return true;
        }
        return false;
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#isInactive()
     */
    public boolean isInactive() {
        switch(getState()) {
        case INITIALIZING:
        case QUEUED:
        case GAVE_UP:
        case WAITING_FOR_GNET_RESULTS:
        case WAITING_FOR_USER:
        case WAITING_FOR_CONNECTIONS:
        case ITERATIVE_GUESSING:
        case QUERYING_DHT:
        case BUSY:
        case PAUSED:
            return true;
        }
        return false;
    }   
    
    /**
     * reloads any previously busy hosts in the ranker, as well as other
     * hosts that we know about 
     */
    private synchronized void initializeRanker() {
        ranker.setMeshHandler(this);
        ranker.addToPool(cachedRFDs);
    }
    
    /**
     * initializes the verifying file if the incompleteFile is initialized.
     */
	protected void initializeVerifyingFile() throws IOException {
		if (incompleteFile == null)
			return;

		// get VerifyingFile
		commonOutFile = incompleteFileManager.getEntry(incompleteFile);
		if (commonOutFile == null) {// no entry in incompleteFM
			long completedSize = IncompleteFileManager.getCompletedSize(incompleteFile);
            if (completedSize > MAX_FILE_SIZE)
                throw new IOException("invalid incomplete file "+completedSize);
			commonOutFile = verifyingFileFactory.createVerifyingFile(completedSize);
			commonOutFile.setScanForExistingBlocks(true, incompleteFile.length());
			incompleteFileManager.addEntry(incompleteFile, commonOutFile, shouldPublishIFD());
		}
	}

	protected void initializeIncompleteFile() throws IOException {
        if (incompleteFile != null)
            return;
        
        URN sha1 = getSha1Urn();
        if (sha1 != null) {
            incompleteFile = incompleteFileManager.getFileForUrn(sha1);
        }
        if (incompleteFile == null) { 
            incompleteFile = getIncompleteFile(getSaveFile().getName(), sha1,
                    getContentLength());
        }
        
        if(LOG.isWarnEnabled())
            LOG.warn("Incomplete File: " + incompleteFile);
    }
    
    /**
     * Retrieves an incomplete file from the given incompleteFileManager with the
     * given name, URN & content-length.
     * <p>
     * It can be overridden in subclasses.
     * </p> 
     */
    protected File getIncompleteFile(String name, URN urn,
                                     long length) throws IOException {
        return incompleteFileManager.getFile(name, urn, length);
    }
    
    /**
     * Adds alternate locations that may have been stored in the
     * IncompleteFileDesc for this download.
     */
    private synchronized void initializeAlternateLocations() {
        if( incompleteFile == null ) // no incomplete, no big deal.
            return;
        
        FileDesc fd = fileManager.getFileDescForFile(incompleteFile);
        if( fd != null && fd instanceof IncompleteFileDesc) {
            IncompleteFileDesc ifd = (IncompleteFileDesc)fd;
            // Assert that the SHA1 of the IFD and our sha1 match.
            if(getSha1Urn() != null && !getSha1Urn().equals(ifd.getSHA1Urn())) {
                ErrorService.error(new IllegalStateException(
                           "wrong IFD." +
                           "\nclass: " + getClass().getName() +
                           "\nours  :   " + incompleteFile +
                           "\ntheirs: " + ifd.getFile() +
                           "\nour hash    : " + getSha1Urn() +
                           "\ntheir hashes: " + ifd.getUrns()+
                           "\nifm.hashes : "+incompleteFileManager.dumpHashes()));
                fileManager.removeFileIfSharedOrStore(incompleteFile);
            }
        }
        
        // Locate the hash for this incomplete file, to retrieve the 
        // IncompleteFileDesc.
        URN hash = incompleteFileManager.getCompletedHash(incompleteFile);
        if( hash != null ) {
            long size = IncompleteFileManager.getCompletedSize(incompleteFile);
            //create validAlts
            addLocationsToDownload(altLocManager.getDirect(hash),
                    altLocManager.getPushNoFWT(hash),
                    altLocManager.getPushFWT(hash),
                    size);
        }
    }
    
    /**
     * Adds the alternate locations from the collections as possible
     * download sources.
     */
    private void addLocationsToDownload(AlternateLocationCollection<? extends AlternateLocation> direct,
                                        AlternateLocationCollection<? extends AlternateLocation>  push,
                                        AlternateLocationCollection<? extends AlternateLocation>  fwt,
                                        long size) {
        List<RemoteFileDesc> locs =
            new ArrayList<RemoteFileDesc>(direct.getAltLocsSize()+push.getAltLocsSize()+fwt.getAltLocsSize());

        synchronized(direct) {
            for(AlternateLocation loc : direct)
                locs.add(loc.createRemoteFileDesc(size, remoteFileDescFactory));
        }
        
        synchronized(push) {
            for(AlternateLocation loc : push)
                locs.add(loc.createRemoteFileDesc(size, remoteFileDescFactory));
        }
        
        synchronized(fwt) {
            for(AlternateLocation loc : fwt)
                locs.add(loc.createRemoteFileDesc(size, remoteFileDescFactory));
        }
                
        addPossibleSources(locs);
    }

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#conflictsWithIncompleteFile(java.io.File)
     */
    public boolean conflictsWithIncompleteFile(File incFile) {
		File iFile = incompleteFile;
		if (iFile != null) {
			return iFile.equals(incFile);
		}
		URN urn = getSha1Urn();
		if (urn != null) {
			iFile = incompleteFileManager.getFileForUrn(urn);
		}
		if (iFile != null) {
			return iFile.equals(incFile);
		}
	
		RemoteFileDesc rfd = null;
		synchronized (this) {
			if (!hasRFD()) {
				return false;
			}
			rfd = cachedRFDs.iterator().next();
		}
		if (rfd != null) {
			try {
				File thisFile = incompleteFileManager.getFile(rfd);
				return thisFile.equals(incFile);
			} catch(IOException ioe) {
				return false;
			}
		}
		return false;
    }

	/* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#conflicts(com.limegroup.gnutella.URN, long, java.io.File)
     */
	public boolean conflicts(URN urn, long fileSize, File... fileName) {
		if (urn != null && getSha1Urn() != null) {
			return urn.equals(getSha1Urn());
		}
		if (fileSize > 0) {
			try {
				File file = incompleteFileManager.getFile(fileName[0].getName(), null, fileSize);
				return conflictsWithIncompleteFile(file);
			} catch (IOException e) {
			}
		}
		return false;
	}
	

    /////////////////////////////// Requery Code ///////////////////////////////

    /** 
     * Returns a new <code>QueryRequest</code> for requery purposes.  Subclasses 
     * may wish to override this to be more or less specific.<p>
     *
     * @exception CantResumeException if this doesn't know what to search for 
	 * @return a new <tt>QueryRequest</tt> for making the requery
     */
	public synchronized QueryRequest newRequery()
      throws CantResumeException {
		    
        String queryString = QueryUtils.createQueryString(getDefaultFileName());
        if(queryString == null || queryString.equals(""))
            throw new CantResumeException(getSaveFile().getName());
        else
            return queryRequestFactory.createQuery(queryString);
            
    }
    
    /** Subclasses should override this method when necessary.
     *  If you return false, then AltLocs are not initialized from the
     *  incomplete file upon invocation of tryAllDownloads.
     *  The true case can be used when the partial file is being shared
     *  through PFS and we've learned about AltLocs we want to use.
     */
    protected boolean shouldInitAltLocs() {
        return false;
    }
    
    /**
     * Determines if the specified host is allowed to download.
     */
    protected boolean hostIsAllowed(RemoteFileDesc other) {
         // If this host is banned, don't add.
        if ( !ipFilter.allow(other.getHost()) )
            return false;            

        if (networkManager.acceptedIncomingConnection() ||
                !other.isFirewalled() ||
                (other.supportsFWTransfer() && networkManager.canDoFWT())) {
            // See if we have already tried and failed with this location
            // This is only done if the location we're trying is an alternate..
            synchronized(altLock) {
                if (other.isFromAlternateLocation() && 
                        invalidAlts.contains(other.getRemoteHostData())) {
                    return false;
                }
            }
            
            return true;
        }
        return false;
    }
              


    private static boolean initDone = false; // used to init

    /**
     * Returns true if 'other' should be accepted as a new download location.
     */
    protected boolean allowAddition(RemoteFileDesc other) {
        if (!initDone) {
            synchronized (matcher) {
                matcher.setIgnoreCase(true);
                matcher.setIgnoreWhitespace(true);
                matcher.setCompareBackwards(true);
            }
            initDone = true;
        }

        // before doing expensive stuff, see if connection is even possible...
        if (other.getQuality() < 1) // I only want 2,3,4 star guys....
            return false;        

        // get other info...
		final URN otherUrn = other.getSHA1Urn();
        final String otherName = other.getFileName();
        final long otherLength = other.getFileSize();

        synchronized (this) {
            long ourLength = getContentLength();
            
            if (ourLength != -1 && ourLength != otherLength) 
                return false;
            
            if (otherUrn != null && getSha1Urn() != null) 
                return otherUrn.equals(getSha1Urn());
            
            // compare to previously cached rfds
            for(RemoteFileDesc rfd : cachedRFDs) {
                final String thisName = rfd.getFileName();
                final long thisLength = rfd.getFileSize();
				
                // if they are similarly named and same length
                // do length check first, much less expensive.....
                if (otherLength == thisLength) 
                    if (namesClose(otherName, thisName)) 
                        return true;                
            }
        }
        return false;
    }

    private final boolean namesClose(final String one, 
                                     final String two) {
        boolean retVal = false;

        // copied from TableLine...
        //Filenames close?  This is the most expensive test, so it should go
        //last.  Allow 10% edit difference in filenames or 6 characters,
        //whichever is smaller.
        int allowedDifferences=Math.round(Math.min(
             0.10f*((QueryUtils.ripExtension(one)).length()),
             0.10f*((QueryUtils.ripExtension(two)).length())));
        allowedDifferences=Math.min(allowedDifferences, 6);

        synchronized (matcher) {
            retVal = matcher.matches(matcher.process(one),
                                     matcher.process(two),
                                     allowedDifferences);
        }

        if(LOG.isDebugEnabled()) {
            LOG.debug("MD.namesClose(): one = " + one);
            LOG.debug("MD.namesClose(): two = " + two);
            LOG.debug("MD.namesClose(): retVal = " + retVal);
        }
            
        return retVal;
    }

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#locationAdded(com.limegroup.gnutella.altlocs.AlternateLocation)
     */
    public synchronized void locationAdded(AlternateLocation loc) {
        assert(loc.getSHA1Urn().equals(getSha1Urn()));
        
        if (LOG.isDebugEnabled()) {
            LOG.debug("alt loc added: " + loc);
        }
        
        long contentLength = -1L;
        if (loc instanceof DirectDHTAltLoc) {
            long fileSize = ((DirectDHTAltLoc)loc).getFileSize();
            
            // Compare the file size from the AltLoc with the contentLength
            // if possible.
            
            if (fileSize >= 0L) {
                // Get the current contentLength and compare it with
                // the file size from the AltLocValue
                synchronized (this) {
                    contentLength = getContentLength();
                    if (contentLength < 0L) {
                        if (LOG.isDebugEnabled()) {
                            LOG.debug("Using file size from AltLocValue: " + fileSize);
                        }
                        contentLength = fileSize;
                        
                        if (contentLength <= MAX_FILE_SIZE) {
                            setContentLength(contentLength);
                        }
                    }
                }
                
                if (fileSize != contentLength) {
                    if (LOG.isErrorEnabled()) {
                        LOG.error("File sizes do not match: " 
                                + fileSize + " vs. " + contentLength);
                    }
                    return;
                }
            }
        }
        
        contentLength = getContentLength();
        if (contentLength < 0L) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Unknown file size: " + contentLength);
            }
            
            return;
        }
        
        if (contentLength > MAX_FILE_SIZE) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Content length is too big: " + contentLength);
            }
            return;
        }
        
        addDownload(loc.createRemoteFileDesc(contentLength, remoteFileDescFactory), false);
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#addDownload(com.limegroup.gnutella.RemoteFileDesc, boolean)
     */
    public synchronized boolean addDownload(RemoteFileDesc rfd, boolean cache) {
        return addDownload(Collections.singleton(rfd), cache);
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#addDownload(java.util.Collection, boolean)
     */
    public synchronized boolean addDownload(Collection<? extends RemoteFileDesc> c, boolean cache) {
        if (stopped || isCompleted())
            return false;
        
        List<RemoteFileDesc> l = new ArrayList<RemoteFileDesc>(c.size());
        for(RemoteFileDesc rfd : c) {
            if (allowAddition(rfd)) {
                // this is set here and not for all remote descs since only these
                // could have possible come from the UI, this should be solved differently
                // this is set even if the host is not allowed as a download source for UI's sake
                rfd.setDownloading(true);
                if (hostIsAllowed(rfd)) {
                    l.add(rfd);
                }
            }
        }
        
        return addDownloadForced(l,cache);
    }

    /**
     * Like addDownload, but doesn't call allowAddition(..).
     *
     * If cache is false, the RFD is not added to allFiles, but is
     * added to 'files', the list of RFDs we will connect to.
     *
     * If the RFD matches one already in allFiles, the new one is
     * NOT added to allFiles, but IS added to the list of RFDs to connect to
     * if and only if a matching RFD is not currently in that list.
     *
     * This ALWAYS returns true, because the download is either allowed
     * or silently ignored (because we're already downloading or going to
     * attempt to download from the host described in the RFD).
     */
    protected synchronized boolean addDownloadForced(RemoteFileDesc rfd,
                                                           boolean cache) {
        // the singleton impl calls the collection impl and not the other way round,
        // so that the ping ranker gets a collection and only fires off one round of pinging
        return addDownloadForced(Collections.singleton(rfd), cache);
    }
    
    protected synchronized final boolean addDownloadForced(Collection<? extends RemoteFileDesc> c, boolean cache) {
        // create copy, argument might not be modifiable
        Set<RemoteFileDesc> copy = new HashSet<RemoteFileDesc>(c);
        // remove any rfds we're currently downloading from 
        copy.removeAll(currentRFDs);
        
        byte[] myGUID = applicationServices.getMyGUID();
        for (Iterator<RemoteFileDesc> iter = copy.iterator(); iter.hasNext();) {
            RemoteFileDesc rfd =  iter.next();
            // do not download from ourselves
            if (rfd.isMe(myGUID)) {
                iter.remove();
            } else { 
                prepareRFD(rfd,cache);
            }
         //   if(LOG.isTraceEnabled())
         //       LOG.trace("added rfd: " + rfd);
        }
        
        if ( ranker.addToPool(copy) ) {
         //   if(LOG.isTraceEnabled())
        //        LOG.trace("added rfds: " + c);
            receivedNewSources = true;
        }
        
        return true;
    }
    
    private void prepareRFD(RemoteFileDesc rfd, boolean cache) {
        if(getSha1Urn() == null && rfd.getSHA1Urn() != null) {
            setSha1Urn(rfd.getSHA1Urn());
            altLocManager.addListener(getSha1Urn(),this);
        }

        //add to allFiles for resume purposes if caching...
        if(cache) 
            cachedRFDs.add(rfd);        
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#hasNewSources()
     */
    public boolean hasNewSources() {
        return !paused && receivedNewSources;
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#shouldBeRestarted()
     */
    public boolean shouldBeRestarted() {
        DownloadStatus status = getState();
        return hasNewSources() || 
        (getRemainingStateTime() <= 0 
                && status != DownloadStatus.WAITING_FOR_GNET_RESULTS &&
                status != DownloadStatus.QUERYING_DHT);
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#shouldBeRemoved()
     */
    public boolean shouldBeRemoved() {
    	return isCancelled() || isCompleted();
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#isQueuable()
     */
    public boolean isQueuable() {
    	return !isPaused();
    }

    ///////////////////////////////////////////////////////////////////////////

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#acceptDownload(java.lang.String, java.net.Socket, int, byte[])
     */
    public boolean acceptDownload(String file, Socket socket, int index, byte[] clientGUID) {
        if (stopped)
            return false;
        
        HTTPConnectObserver observer = pushes.getHostFor(clientGUID, socket.getInetAddress().getHostAddress());
        if(observer != null)
            observer.handleConnect(socket);
        return observer != null;
    }
    
    public void registerPushObserver(HTTPConnectObserver observer, PushDetails details) {
        pushes.addPushHost(details, observer);
    }
    
    public void unregisterPushObserver(PushDetails details, boolean shutdown) {
        HTTPConnectObserver observer = pushes.getExactHostFor(details);
        if(observer != null && shutdown)
            observer.shutdown();
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#isCancelled()
     */
    public boolean isCancelled() {
        return stopped;
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#pause()
     */
    public synchronized void pause() {
        // do not pause if already stopped.
        if(!stopped && !isCompleted()) {
            stop();
            stopped = false;
            paused = true;
            // if we're already inactive, mark us as paused immediately.
            if(isInactive())
                setState(DownloadStatus.PAUSED);
        }
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#isPaused()
     */
    public boolean isPaused() {
        return paused == true;
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#isPausable()
     */
    public boolean isPausable() {
        DownloadStatus state = getState();
    	return !isPaused() && !isCompleted() && state != DownloadStatus.SAVING && state != DownloadStatus.HASHING;
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#isResumable()
     */
    public boolean isResumable() {
    	// inactive but not queued
    	return isInactive() && state != DownloadStatus.QUEUED;
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#isLaunchable()
     */
    public boolean isLaunchable() {
    	return state == DownloadStatus.COMPLETE || amountForPreview() > 0;
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#stop()
     */
    public void stop() {
    
        if(paused) {
            stopped = true;
            paused = false;
        }

        // make redundant calls to stop() fast
        // this change is pretty safe because stopped is only set in two
        // places - initialized and here.  so long as this is true, we know
        // this is safe.
        if (stopped || paused)
            return;

        LOG.debug("STOPPING ManagedDownloader");

        //This method is tricky.  Look carefully at run.  The most important
        //thing is to set the stopped flag.  That guarantees run will terminate
        //eventually.
        stopped=true;
        killAllWorkers();
        
        synchronized(this) {
            // must capture in local variable so the value doesn't become null
            // between if & contents of if.
            Thread dlMan = dloaderManagerThread;
            if(dlMan != null)
                dlMan.interrupt();
            else
                LOG.warn("MANAGER: no thread to interrupt");
        }
    }

    /**
     * Kills all workers & shuts down all push waiters.
     */    
    private void killAllWorkers() {
        List<DownloadWorker> workers = getAllWorkers();
        
        // cannot interrupt while iterating through the main list, because that
        // could cause ConcurrentMods.
        for(DownloadWorker doomed : workers)
            doomed.interrupt();
        
        List<HTTPConnectObserver> pushObservers = pushes.getAllAndClear();
        for(HTTPConnectObserver next : pushObservers)
            next.shutdown();
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#informMesh(com.limegroup.gnutella.RemoteFileDesc, boolean)
     */
    public synchronized void informMesh(RemoteFileDesc rfd, boolean good) {
        if (LOG.isDebugEnabled())
            LOG.debug("informing mesh that "+rfd+" is "+good);
        
        if (good)
            cachedRFDs.add(rfd);
        
        if(!rfd.isAltLocCapable())
            return;
        
        // Verify that this download has a hash.  If it does not,
        // we should not have been getting locations in the first place.
        assert getSha1Urn() != null : "null hash.";
        
        assert getSha1Urn().equals(rfd.getSHA1Urn()) : "wrong loc SHA1";
        
        AlternateLocation loc;
        try {
            loc = alternateLocationFactory.create(rfd);
        } catch(IOException iox) {
            return;
        }
        
        AlternateLocation local;
        
        // if this is a pushloc, update the proxies accordingly
        if (loc instanceof PushAltLoc) {
            
            // Note: we update the proxies of a clone in order not to lose the
            // original proxies
            local = loc.createClone();
            PushAltLoc ploc = (PushAltLoc)loc;
            
            // no need to notify mesh about pushlocs w/o any proxies
            if (ploc.getPushAddress().getProxies().isEmpty())
                return;
            
            ploc.updateProxies(good);
        } else
            local = loc;
        
        // and to the global collection
        if (good)
            altLocManager.add(loc, this);
        else
            altLocManager.remove(loc, this);

        // add to the downloaders
        for(DownloadWorker worker : getActiveWorkers()) {
            HTTPDownloader httpDloader = worker.getDownloader();
            RemoteFileDesc r = httpDloader.getRemoteFileDesc();
            
            // no need to tell uploader about itself and since many firewalled
            // downloads may have the same port and host, we also check their
            // push endpoints
            if(! (local instanceof PushAltLoc) ? 
                    (r.getHost().equals(rfd.getHost()) && r.getPort()==rfd.getPort()) :
                    r.getPushAddr()!=null && r.getPushAddr().equals(rfd.getPushAddr()))
                continue;
            
            //no need to send push altlocs to older uploaders
            if (local instanceof DirectAltLoc || httpDloader.wantsFalts()) {
            	if (good)
            		httpDloader.addSuccessfulAltLoc(local);
            	else
            		httpDloader.addFailedAltLoc(local);
            }
        }
        
        // add to the local collections
        synchronized(altLock) {
            if(good) {
                //check if validAlts contains loc to avoid duplicate stats, and
                //spurious count increments in the local
                //AlternateLocationCollections
                if(!validAlts.contains(local)) {
                    validAlts.add(local);
                }
            }  else {
                    validAlts.remove(local);
                    invalidAlts.add(rfd.getRemoteHostData());
                    recentInvalidAlts.add(local);
            }
        }
    }

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#addPossibleSources(java.util.Collection)
     */
    public synchronized void addPossibleSources(Collection<? extends RemoteFileDesc> c) {
        addDownload(c,false);
    }
    
    /** Delegates requerying to the RequeryManager. */
    protected boolean canSendRequeryNow() {
        return requeryManager.canSendQueryNow();
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#resume()
     */
    public synchronized boolean resume() {
        //Ignore request if already in the download cycle.
        if (!isInactive())
            return false;

        // if we were waiting for the user to start us,
        // then try to send the requery.
        if(getState() == DownloadStatus.WAITING_FOR_USER) {
            requeryManager.activate();
        }
        
        // if any guys were busy, reduce their retry time to 0,
        // since the user really wants to resume right now.
        for(RemoteFileDesc rfd : cachedRFDs)
            rfd.setRetryAfter(0);

        if(paused) {
            paused = false;
            stopped = false;
        }
            
        // queue ourselves so we'll try and become active immediately
        setState(DownloadStatus.QUEUED);
        
        return true;
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getFile()
     */
    public File getFile() {
        if(incompleteFile == null)
            return null;
            
        if(state == DownloadStatus.COMPLETE)
            return getSaveFile();
        else
            return incompleteFile;
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getSHA1Urn()
     */
    public URN getSha1Urn() {
        return downloadSHA1;
    }
    
    protected void setSha1Urn(URN sha1) {
        if(!sha1.isSHA1())
            throw new IllegalArgumentException("not sha1: " + sha1);
        if(downloadSHA1 != null && !sha1.equals(downloadSHA1))
            throw new IllegalStateException("sha1 already set to: " + downloadSHA1);
        this.downloadSHA1 = sha1;
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getDownloadFragment()
     */
    public File getDownloadFragment() {
        //We haven't started yet.
        if (incompleteFile==null)
            return null;
        
        //a) Special case for saved corrupt fragments.  We don't worry about
        //removing holes.
        if (state==DownloadStatus.CORRUPT_FILE) 
            return corruptFile; //may be null
        //b) If the file is being downloaded, create *copy* of first
        //block of incomplete file.  The copy is needed because some
        //programs, notably Windows Media Player, attempt to grab
        //exclusive file locks.  If the download hasn't started, the
        //incomplete file may not even exist--not a problem.
        else if (state!=DownloadStatus.COMPLETE) {
            File file=new File(incompleteFile.getParent(),
                               IncompleteFileManager.PREVIEW_PREFIX
                                   +incompleteFile.getName());
            //Get the size of the first block of the file.  (Remember
            //that swarmed downloads don't always write in order.)
            long size=amountForPreview();
            if (size<=0)
                return null;
            //Copy first block, returning if nothing was copied.
            if (FileUtils.copy(incompleteFile, size, file)<=0) 
                return null;
            return file;
        }
        //c) Otherwise, choose completed file.
        else {
            return getSaveFile();
        }
    }


    /** 
     * Returns the amount of the file written on disk that can be safely
     * previewed. 
     */
    private synchronized long amountForPreview() {
        //And find the first block.
        if (commonOutFile == null)
            return 0; // trying to preview before incomplete file created
        
        return commonOutFile.getOffsetForPreview();
    }

    /**
     * Returns the save file from the default save directory.
     */
    @Override
    protected File getDefaultSaveFile() {
        String fileName = getDefaultFileName(); 
        return new File(SharingSettings.getSaveDirectory(fileName), fileName);
    }  
    
    //////////////////////////// Core Downloading Logic /////////////////////

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#finish()
     */
    public synchronized void finish() {
        if (getSha1Urn() != null)
            altLocManager.removeListener(getSha1Urn(), this);
        requeryManager.cleanUp();
        if(cachedRFDs != null) {
            for(RemoteFileDesc rfd : cachedRFDs)
				rfd.setDownloading(false);
        }       
    }

    /** 
     * Actually does the download, finding duplicate files, trying all
     * locations, resuming, waiting, and retrying as necessary. Also takes care
     * of moving file from incomplete directory to save directory and adding
     * file to the library.  Called from dloadManagerThread.  
     */
    protected DownloadStatus performDownload() {
        if(checkHosts()) {//files is global
            setState(DownloadStatus.GAVE_UP);
            return DownloadStatus.GAVE_UP;
        }

        // 1. initialize the download
        DownloadStatus status = initializeDownload();
        if ( status == DownloadStatus.CONNECTING) {
            try {
                //2. Do the download
                try {
                    status = fireDownloadWorkers();//Exception may be thrown here.
                }finally {
                    //3. Close the file controlled by commonOutFile.
                    commonOutFile.close();
                }
                
                // 4. if all went well, save
                if (status == DownloadStatus.COMPLETE) 
                    status = verifyAndSave();
                else if(LOG.isDebugEnabled())
                    LOG.debug("stopping early with status: " + status); 
                
            } catch (InterruptedException e) {
                
                // nothing should interrupt except for a stop
                if (!stopped && !paused)
                    ErrorService.error(e);
                else
                    status = DownloadStatus.GAVE_UP;
                
                // if we were stopped due to corrupt download, cleanup
                if (corruptState == CORRUPT_STOP_STATE) {
                    // TODO is this really what cleanupCorrupt expects?
                    cleanupCorrupt(incompleteFile, getSaveFile().getName());
                    status = DownloadStatus.CORRUPT_FILE;
                }
            }
        }
        
        if(LOG.isDebugEnabled())
            LOG.debug("MANAGER: TAD2 returned: " + status);
                   
        return status;
    }

    /**
     * Tries to initialize the download location and the verifying file. 
     * @return GAVE_UP if we had no sources, DISK_PROBLEM if such occurred, 
     * CONNECTING if we're ready to connect
     */
    protected DownloadStatus initializeDownload() {
        
        synchronized (this) {
            if (cachedRFDs.size()==0 && !ranker.hasMore()) 
                return DownloadStatus.GAVE_UP;
        }
        
        try {
            initializeIncompleteFile();
            initializeVerifyingFile();
            openVerifyingFile();
        } catch (IOException iox) {
            reportDiskProblem(iox);
            return DownloadStatus.DISK_PROBLEM;
        }

        // Create a new validAlts for this sha1.
        // initialize the HashTree
        if( getSha1Urn() != null ) 
            initializeHashTree();
        
        // load up the ranker with the hosts we know about
        initializeRanker();
        
        return DownloadStatus.CONNECTING;
    }
    
    /**
     * Verifies the completed file against the SHA1 hash and saves it.  If
     * there is corruption, it asks the user whether to discard or keep the file 
     * @return {@link DownloadStatus#COMPLETE} if all went fine, 
     * {@link DownloadStatus#DISK_PROBLEM} if not.
     * @throws InterruptedException if we get interrupted while waiting for user
     * response.
     */
    private DownloadStatus verifyAndSave() throws InterruptedException {
        
        // Find out the hash of the file and verify that its the same
        // as our hash.
        URN fileHash = scanForCorruption();
        if (corruptState == CORRUPT_STOP_STATE) {
            // TODO is this what cleanup Corrupt expects?
            cleanupCorrupt(incompleteFile, getSaveFile().getName());
            return DownloadStatus.CORRUPT_FILE;
        }
        
        // Save the file to disk.
        return saveFile(fileHash);
    }
    
    /**
     * Validates the current download.
     */
    private void validateDownload() {
        if(shouldValidate()) {
            if(getSha1Urn() != null) {
                contentManager.request(getSha1Urn(), new ContentResponseObserver() {
                    public void handleResponse(URN urn, ContentResponseData response) {
                        if(response != null && !response.isOK()) {
                            invalidated = true;
                            stop();
                        }
                    }
                }, 5000);           
            }
        }
    }
    
    /**
     * Determines if validation with a content authority should occur for this
     * download. By default, this always returns true. Subclasses can override
     * this to return other values.
     */
    protected boolean shouldValidate() {
        return true;
    }
    
    /**
     * Waits indefinitely for a response to the corrupt message prompt, if
     * such was displayed.
     */
    private void waitForCorruptResponse() {
        if(corruptState != NOT_CORRUPT_STATE) {
            synchronized(corruptStateLock) {
                try {
                    while(corruptState==CORRUPT_WAITING_STATE)
                        corruptStateLock.wait();
                } catch(InterruptedException ignored) {}
            }
        }
    }  
    
    /**
     * Scans the file for corruption, returning the hash of the file on disk.
     */
    private URN scanForCorruption() throws InterruptedException {
        // if we already were told to stop, then stop.
        if (corruptState==CORRUPT_STOP_STATE)
            return null;
        
        //if the user has not been asked before.               
        URN fileHash=null;
        try {
            // let the user know we're hashing the file
            setState(DownloadStatus.HASHING);
            fileHash = URN.createSHA1Urn(incompleteFile);
        }
        catch(IOException ignored) {}
        
        // If we have no hash, we can't check at all.
        if(getSha1Urn() == null)
            return fileHash;

        // If they're equal, everything's fine.
        //if fileHash == null, it will be a mismatch
        if(getSha1Urn().equals(fileHash))
            return fileHash;
        
        if(LOG.isWarnEnabled()) {
            LOG.warn("hash verification problem, fileHash="+
                           fileHash+", ourHash="+getSha1Urn());
        }

        // unshare the file if we didn't have a tree
        // otherwise we will have shared only the parts that verified
        if (commonOutFile.getHashTree() == null) 
            fileManager.removeFileIfSharedOrStore(incompleteFile);
        
        // purge the tree
        tigerTreeCache.get().purgeTree(getSha1Urn());
        commonOutFile.setHashTree(null);

        // ask what to do next 
        promptAboutCorruptDownload();
        waitForCorruptResponse();
        
        return fileHash;        
    }

    /**
     * checks the TT cache and if a good tree is present loads it 
     */
    private void initializeHashTree() {
        HashTree tree = tigerTreeCache.get().getHashTree(getSha1Urn()); 
	    
		// if we have a valid tree, update our chunk size and disable overlap checking
		if (tree != null && tree.isDepthGoodEnough()) {
				commonOutFile.setHashTree(tree);
		}
    }
	
    /**
     * Saves the file to disk.
     */
    protected DownloadStatus saveFile(URN fileHash){
        // let the user know we're saving the file...
        setState( DownloadStatus.SAVING );
        
        //4. Move to library.
        // Make sure we can write into the complete file's directory.
        if (!FileUtils.setWriteable(getSaveFile().getParentFile())) {
            reportDiskProblem("could not set file writeable " + 
                    getSaveFile().getParentFile());
            return DownloadStatus.DISK_PROBLEM;
        }
        File saveFile = getSaveFile();
        //Delete target.  If target doesn't exist, this will fail silently.
        saveFile.delete();

        //Try moving file.  If we couldn't move the file, i.e., because
        //someone is previewing it or it's on a different volume, try copy
        //instead.  If that failed, notify user.  
        //   If move is successful, we should remove the corresponding blocks
        //from the IncompleteFileManager, though this is not strictly necessary
        //because IFM.purge() is called frequently in DownloadManager.
        
        try {
            saveFile = getSuggestedSaveLocation(saveFile, incompleteFile);
        } catch (IOException e) {
            return DownloadStatus.DISK_PROBLEM;
        }
        
        // First attempt to rename it.
        boolean success = FileUtils.forceRename(incompleteFile,saveFile);

        incompleteFileManager.removeEntry(incompleteFile);
        
        // If that didn't work, we're out of luck.
        if (!success) {
            reportDiskProblem("forceRename failed "+incompleteFile+
                    " -> "+ saveFile);
            return DownloadStatus.DISK_PROBLEM;
        }
            
        //Add file to library.
        // first check if it conflicts with the saved dir....
        if (saveFile.exists())
            fileManager.removeFileIfSharedOrStore(saveFile);

        // add file hash to manager for fast lookup
        addFileHash(fileHash, saveFile);

        // determine where and how to share the file
        shareSavedFile(saveFile);

		return DownloadStatus.COMPLETE;
    }
    
    /**
     * Provides alternate file location based on new data obtained after downloading the file.
     * For example, could create a folder substructure and use a template based on ID3 information
     * for music. 
     * 
     * @param defaultSaveFile the current file location to save the incomplete download to
     * @return the location to save the actual download to
     * @throws IOException
     */
    protected File getSuggestedSaveLocation(File defaultSaveFile, File newDownloadFile) throws IOException{
        return defaultSaveFile;
    }
    
    /**
     *  Add the URN of this file to the cache so that it won't
     *  be hashed again when added to the library -- reduces
     *  the time of the 'Saving File' state.
     */
    private void addFileHash(URN fileHash, File saveFile){
        if(fileHash != null) {
            UrnSet urns = new UrnSet(fileHash);
            File file = saveFile;
            try {
                file = FileUtils.getCanonicalFile(saveFile);
            } catch(IOException ignored) {}
            // Always cache the URN, so results can lookup to see
            // if the file exists.
            URN ttroot = saveTreeHash(fileHash);
            if (ttroot != null)
                urns.add(ttroot);
            urnCache.addUrns(file, urns);
            // Notify the SavedFileManager that there is a new saved
            // file.
            savedFileManager.addSavedFile(file, urns);
            
        }
    }
    
    /**
     * Upon saving a downloaded file, if the file is to be shared the tiger tree should
     * be saved in order to speed up sharing the file across gnutella
     * 
     * @param fileHash urn to save the tree of
     * @return the root of the tree
     */
    protected URN saveTreeHash(URN fileHash) {
            // save the trees!
            if (getSha1Urn() != null && getSha1Urn().equals(fileHash) && commonOutFile.getHashTree() != null) {
                tigerTreeCache.get().addHashTree(getSha1Urn(),commonOutFile.getHashTree());
                return commonOutFile.getHashTree().getTreeRootUrn();
            }
            return null;
        }

    /**
     * Shares the newly downloaded file
     */
    protected void shareSavedFile(File saveFile){
		if (SharingSettings.SHARE_DOWNLOADED_FILES_IN_NON_SHARED_DIRECTORIES.getValue())
			fileManager.addFileAlways(saveFile, getXMLDocuments());
		else
		    fileManager.addFileIfShared(saveFile, getXMLDocuments());
    }

    /** Removes all entries for incompleteFile from incompleteFileManager 
     *  and attempts to rename incompleteFile to "CORRUPT-i-...".  Deletes
     *  incompleteFile if rename fails. */
    private void cleanupCorrupt(File incFile, String name) {
        corruptFileBytes= getAmountRead();        
        incompleteFileManager.removeEntry(incFile);

        //Try to rename the incomplete file to a new corrupt file in the same
        //directory (INCOMPLETE_DIRECTORY).
        boolean renamed = false;
        for (int i=0; i<10 && !renamed; i++) {
            corruptFile=new File(incFile.getParent(),
                                 "CORRUPT-"+i+"-"+name);
            if (corruptFile.exists())
                continue;
            renamed=incFile.renameTo(corruptFile);
        }

        //Could not rename after ten attempts?  Delete.
        if(!renamed) {
            incFile.delete();
            this.corruptFile=null;
        }
    }
    
    /**
     * Initializes the verifying file.
     */
    private void openVerifyingFile() throws IOException {

        //need to get the VerifyingFile ready to write
        try {
            commonOutFile.open(incompleteFile);
        } catch (IOException e) {
            IOUtils.handleException(e, IOUtils.ErrorType.DOWNLOAD);
            throw e;
        }
    }
    
    /**
     * Starts a new Worker thread for the given <code>RemoteFileDesc</code>.
     */
    private void startWorker(final RemoteFileDesc rfd) {
        DownloadWorker worker = downloadWorkerFactory.create(this, rfd, commonOutFile);
        synchronized(this) {
            _workers.add(worker);
            currentRFDs.add(rfd);
        }        
        worker.start();
    }        
    

    public synchronized void workerFinished(DownloadWorker finished) {
        if (LOG.isDebugEnabled())
            LOG.debug("worker "+finished+" finished.");
        removeWorker(finished); 
        notify();
    }
    
    public synchronized void workerStarted(DownloadWorker worker) {
        if (LOG.isDebugEnabled())
            LOG.debug("worker "+worker + " started.");
        if(!_workers.contains(worker))
            throw new IllegalStateException("attempting to start invalid worker: " + worker);
        
        setState(DownloadStatus.DOWNLOADING);
        addActiveWorker(worker);
        chatList.addHost(worker.getDownloader());
        browseList.addHost(worker.getDownloader());
    }
    
    public void workerFailed(DownloadWorker failed) {
        HTTPDownloader downloader = failed.getDownloader();
        if (downloader != null) {
            chatList.removeHost(downloader);
            browseList.removeHost(downloader);
        }
    }
    
    synchronized void removeWorker(DownloadWorker worker) {
        boolean rA = removeActiveWorker(worker);
        workerFailed(worker); // make sure its out of the chat list & browse list
        boolean rW = _workers.remove(worker);
        if(rA && !rW)
            throw new IllegalStateException("active removed but not in workers");
    }
    
    public synchronized boolean removeActiveWorker(DownloadWorker worker) {
        currentRFDs.remove(worker.getRFD());
        List<DownloadWorker> l = new ArrayList<DownloadWorker>(getActiveWorkers());
        boolean removed = l.remove(worker);
        _activeWorkers = Collections.unmodifiableList(l);
        return removed;
    }
    
    synchronized void addActiveWorker(DownloadWorker worker) {
        // only add if not already added.
        if(!getActiveWorkers().contains(worker)) {
            List<DownloadWorker> l = new ArrayList<DownloadWorker>(getActiveWorkers());
            l.add(worker);
            _activeWorkers = Collections.unmodifiableList(l);
        }
    }

    synchronized String getWorkersInfo() {
        String workerState = "";
        for(DownloadWorker worker : _workers)
            workerState += worker.getInfo();
        return workerState;
    }

    public Set<AlternateLocation> getValidAlts() {
        synchronized(altLock) {
            Set<AlternateLocation> ret;
            
            if (validAlts != null) {
                ret = new HashSet<AlternateLocation>();
                for(AlternateLocation next : validAlts)
                    ret.add(next);
            } else
                ret = Collections.emptySet();
            
            return ret;
        }
    }
    

    public Set<AlternateLocation> getInvalidAlts() {
        synchronized(altLock) {
            Set<AlternateLocation>  ret;
            
            if (invalidAlts != null) {
                ret = new HashSet<AlternateLocation> ();
                for(AlternateLocation next : recentInvalidAlts)
                    ret.add(next);
            } else {
                ret = Collections.emptySet();
            }
            
            return ret;
        }
    }
    
    /** 
     * Like tryDownloads2, but does not deal with the library, cleaning
     * up corrupt files, etc.  Caller should look at corruptState to
     * determine if the file is corrupted; a return value of COMPLETE
     * does not mean no corruptions where encountered.
     *
     * @return COMPLETE if a file was successfully downloaded
     *         WAITING_FOR_RETRY if no file was downloaded, but it makes sense 
     *             to try again later because some hosts reported busy.
     *             The caller should usually wait before retrying.
     *         GAVE_UP the download attempt failed, and there are 
     *             no more locations to try.
     *         COULDNT_MOVE_TO_LIBRARY couldn't write the incomplete file
     * @exception InterruptedException if the someone stop()'ed this download.
     *  stop() was called either because the user killed the download or
     *  a corruption was detected and they chose to kill and discard the
     *  download.  Calls to resume() do not result in InterruptedException.
     */
    private DownloadStatus fireDownloadWorkers() throws InterruptedException {
        LOG.trace("MANAGER: entered fireDownloadWorkers");

        //While there is still an unfinished region of the file...
        while (true) {
            if (stopped || paused) {
                LOG.warn("MANAGER: terminating because of stop|pause");
                throw new InterruptedException();
            } 
            
            // are we just about to finish downloading the file?
            
         //   LOG.debug("About to wait for pending if needed");
            
            try {            
                commonOutFile.waitForPendingIfNeeded();
            } catch(DiskException dio) {
                if (stopped || paused) {
                    LOG.warn("MANAGER: terminating because of stop|pause");
                    throw new InterruptedException();
                }
                stop();
                reportDiskProblem(dio);
                return DownloadStatus.DISK_PROBLEM;
            }
            
          //  LOG.debug("Finished waiting for pending");
            
            
            // Finished.
            if (commonOutFile.isComplete()) {
                killAllWorkers();
                
                LOG.trace("MANAGER: terminating because of completion");
                return DownloadStatus.COMPLETE;
            }
            
            synchronized(this) { 
                // if everybody we know about is busy (or we don't know about anybody)
                // and we're not downloading from anybody - terminate the download.
                if (_workers.size() == 0 && !ranker.hasNonBusy())   {
                    
                    receivedNewSources = false;
                    
                    if ( ranker.calculateWaitTime() > 0) {
                        LOG.trace("MANAGER: terminating with busy");
                        return DownloadStatus.BUSY;
                    } else {
                        LOG.trace("MANAGER: terminating w/o hope");
                        return DownloadStatus.GAVE_UP;
                    }
                }
                
                if(LOG.isDebugEnabled())
                    LOG.debug("MANAGER: kicking off workers.  " + 
                            "state: " + getState() + 
                          ", allWorkers: " + _workers.size() + 
                          ", activeWorkers: " + _activeWorkers.size() +
                          ", queuedWorkers: " + _queuedWorkers.size() +
                          ", swarm cap: " + getSwarmCapacity()
                              );
                //+ ", allActive: " + _activeWorkers.toString());
                
                
                //OK. We are going to create a thread for each RFD. The policy for
                //the worker threads is to have one more thread than the max swarm
                //limit, which if successfully starts downloading or gets a better
                //queued slot than some other worker kills the lowest worker in some
                // remote queue.
                if (shouldStartWorker()) {
                    // see if we need to update our ranker
                    ranker = getSourceRanker(ranker);

                    RemoteFileDesc rfd = ranker.getBest();

                    if (rfd != null) {
                        // If the rfd was busy, that means all possible RFDs
                        // are busy - store for later
                        if (rfd.isBusy()) {
                            addRFD(rfd);
                        } else {
                            if(LOG.isDebugEnabled())
                                LOG.debug("Staring worker for RFD: " + rfd);
                            startWorker(rfd);
                        }
                    }
                    
                } else if (LOG.isDebugEnabled())
                    LOG.debug("no blocks but can't steal - sleeping."); //  parts required: " + commonOutFile.listMissingPieces());
                
                //wait for a notification before we continue.
                try {
                    //if no workers notify in a while, iterate. This is a problem
                    //for stalled downloaders which will never notify. So if we
                    //wait without a timeout, we could wait forever.
                    this.wait(DownloadSettings.WORKER_INTERVAL.getValue()); // note that this relinquishes the lock
                } catch (InterruptedException ignored) {}
            }
        }//end of while
    }
    
    /**
     * Retrieves the appropriate source ranker (or returns the current one).
     */
    protected SourceRanker getSourceRanker(SourceRanker ranker) {
        return sourceRankerFactory.getAppropriateRanker(ranker);
    }
    
    /**
     * @return if we should start another worker - means we have more to download,
     * have not reached our swarm capacity and the ranker has something to offer
     * or we have some rfds to re-try
     */
    private boolean shouldStartWorker() {
        return (commonOutFile.hasFreeBlocksToAssign() > 0 || victimsExist()) &&
               ((_workers.size() - _queuedWorkers.size()) < getSwarmCapacity()) &&
               ranker.hasMore();
    }
    
    /**
     * Returns true if a new worker should be started because an existing
     * one is going below MIN_ACCEPTABLE_SPEED.
     * 
     * @return true if a new worker should be started that would steal.
     */
    private boolean victimsExist() {
        if (_workers.isEmpty())
            return false;
            
        // there needs to be at least one slow worker.
        for(DownloadWorker victim : _workers) {
            if (!victim.isStealing() && victim.isSlow())
                return true;
        }
        
        return false;
    }
	
    public synchronized void addRFD(RemoteFileDesc rfd) {
        if (ranker != null)
            ranker.addToPool(rfd);
	}
    
    public synchronized void forgetRFD(RemoteFileDesc rfd) {
        if (cachedRFDs.remove(rfd) && cachedRFDs.isEmpty()) {
            // remember our last RFD
            rfd.setSerializeProxies();
            cachedRFDs.add(rfd);
        }
    }
    
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getNumberOfAlternateLocations()
     */
	public int getNumberOfAlternateLocations() {
	    synchronized(altLock) {
	        if ( validAlts == null ) return 0;
            return validAlts.size();
        }
    }

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getNumberOfInvalidAlternateLocations()
     */
    public int getNumberOfInvalidAlternateLocations() {
        synchronized(altLock) {
            if ( invalidAlts == null ) return 0;
            return invalidAlts.size();
        }
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getPossibleHostCount()
     */
    public synchronized int getPossibleHostCount() {
        return ranker == null ? 0 : ranker.getNumKnownHosts();
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getBusyHostCount()
     */
    public synchronized int getBusyHostCount() {
        return ranker == null ? 0 : ranker.getNumBusyHosts();
    }

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getQueuedHostCount()
     */
    public synchronized int getQueuedHostCount() {
        return _queuedWorkers.size();
    }

    int getSwarmCapacity() {
        int capacity = ConnectionSettings.CONNECTION_SPEED.getValue();
        if(capacity <= SpeedConstants.MODEM_SPEED_INT) //modems swarm = 2
            return SpeedConstants.MODEM_SWARM;
        else if (capacity <= SpeedConstants.T1_SPEED_INT) //DSL, Cable, T1 = 6
            return SpeedConstants.T1_SWARM;
        else // T3
            return SpeedConstants.T3_SWARM;
    }


    public void promptAboutCorruptDownload() {
        synchronized(corruptStateLock) {
            if(corruptState == NOT_CORRUPT_STATE) {
                corruptState = CORRUPT_WAITING_STATE;
                //Note:We are going to inform the user. The GUI will notify us
                //when the user has made a decision. Until then the corruptState
                //is set to waiting. We are not going to move files unless we
                //are out of this state
                sendCorruptCallback();
                //Note2:ActivityCallback is going to ask a message to be show to
                //the user asynchronously
            }
        }
    }
    
    /**
     * Hook for sending a corrupt callback.
     */
    protected void sendCorruptCallback() {
        downloadCallback.promptAboutCorruptDownload(this);
    }

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#discardCorruptDownload(boolean)
     */
    public void discardCorruptDownload(final boolean delete) {
        if (LOG.isDebugEnabled())
            LOG.debug("User chose to delete corrupt "+delete);
        
        // offload this from the swing thread since it will require
        // access to the verifying file.
        Runnable r = new Runnable() {
            public void run() {
                synchronized(corruptStateLock) {
                    if(delete) {
                        corruptState = CORRUPT_STOP_STATE;
                    } else {
                        corruptState = CORRUPT_CONTINUE_STATE;
                    }
                }

                if (delete)
                    stop();
                else 
                    commonOutFile.setDiscardUnverified(false);
                
                synchronized(corruptStateLock) {
                    corruptStateLock.notify();
                }
            }
        };
        
        backgroundExecutor.execute(r);

    }
            

    /**
     * Returns the union of all XML metadata documents from all hosts.
     */
    private synchronized List<LimeXMLDocument> getXMLDocuments() {
        //TODO: we don't actually union here.  Also, should we only consider
        //those locations that we download from?
        List<LimeXMLDocument> allDocs = new ArrayList<LimeXMLDocument>();

        // get all docs possible
        for(RemoteFileDesc rfd : cachedRFDs) {
			LimeXMLDocument doc = rfd.getXMLDocument();
			if(doc != null)
				allDocs.add(doc);
        }

        return allDocs;
    }

    /////////////////////////////Display Variables////////////////////////////

    public void setState(DownloadStatus newState) {
        setState(newState, Long.MAX_VALUE);
    }

    /** 
     * Sets this' state.
     * @param newState the state we're entering, which MUST be one of the 
     *  constants defined in Downloader
     * @param time the time we expect to state in this state, in 
     *  milliseconds. 
     */
    void setState(DownloadStatus newState, long time) {
        DownloadStatus oldState = null;
        synchronized (this) {
            oldState = this.state;
            this.state=newState;
            this.stateTime=System.currentTimeMillis()+time;
        }
        if (oldState != newState) {
            fireEventLater(newState);
        }
    }
    
    /**
     * Sets this' state to newState if the current state is 'oldState'.
     * 
     * @return true if the state changed, false otherwise
     */
    synchronized boolean setStateIfExistingStateIs(DownloadStatus newState, DownloadStatus oldState) {
        if(getState() == oldState) {
            setState(newState);
            return true;
        } else {
            return false;
        }
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getQueryGUID()
     */
    public GUID getQueryGUID() {
        return this.originalQueryGUID;
    }

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getState()
     */
    public synchronized DownloadStatus getState() {
        return state;
    }

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getRemainingStateTime()
     */
    public synchronized int getRemainingStateTime() {
        long remaining;
        switch (state) {
        case CONNECTING:
        case BUSY:
        case ITERATIVE_GUESSING:
        case WAITING_FOR_CONNECTIONS:
            remaining=stateTime-System.currentTimeMillis();
            return (int)Math.ceil(Math.max(remaining, 0)/1000f);
        case WAITING_FOR_GNET_RESULTS:
        case QUERYING_DHT:
            return (int)Math.ceil(Math.max(requeryManager.getTimeLeftInQuery(), 0)/1000f); 
        case QUEUED:
            return 0;
        default:
            return Integer.MAX_VALUE;
        }
    }

	/**
     *  Certain subclasses would like to know whether we have at least one good
	 *  RFD.
     */
	protected synchronized boolean hasRFD() {
        return ( cachedRFDs != null && !cachedRFDs.isEmpty());
	}
	
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getContentLength()
     */
    public synchronized long getContentLength() {
        return contentLength;
    }
    
    protected synchronized void setContentLength(long contentLength) {
        this.contentLength = contentLength;
    }

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getAmountRead()
     */
    public long getAmountRead() {
        VerifyingFile ourFile;
        synchronized(this) {
            if ( state == DownloadStatus.CORRUPT_FILE )
                return corruptFileBytes;
            else if ( state == DownloadStatus.HASHING ) {
                if ( incompleteFile == null )
                    return 0;
                else
                    return URN.getHashingProgress(incompleteFile);
            } else {
                ourFile = commonOutFile;
            }
        }
        
        return ourFile == null ? 0 : ourFile.getBlockSize();                
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getAmountPending()
     */
    public int getAmountPending() {
        VerifyingFile ourFile;
        synchronized(this) {
            ourFile = commonOutFile;
        }
        
        return (int)(ourFile == null ? 0 : ourFile.getPendingSize());
    }
     
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getNumHosts()
     */
    public int getNumHosts() {
        return _activeWorkers.size();
    }
   
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getChatEnabledHost()
     */
	public synchronized Endpoint getChatEnabledHost() {
		return chatList.getChatEnabledHost();
	}

	/* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#hasChatEnabledHost()
     */
	public synchronized boolean hasChatEnabledHost() {
		return chatList.hasChatEnabledHost();
	}

	/* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getBrowseEnabledHost()
     */
	public synchronized RemoteFileDesc getBrowseEnabledHost() {
		return browseList.getBrowseHostEnabledHost();
	}

	/* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#hasBrowseEnabledHost()
     */
	public synchronized boolean hasBrowseEnabledHost() {
		return browseList.hasBrowseHostEnabledHost();
	}

	/* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getQueuePosition()
     */
    public synchronized int getQueuePosition() {
        return queuePosition;
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getNumDownloaders()
     */
    public int getNumDownloaders() {
        return getActiveWorkers().size() + getQueuedWorkers().size();
    }
    
    /** Returns the list of all active workers. */
    public List<DownloadWorker> getActiveWorkers() {
        return _activeWorkers;
    }
    
    /** Returns a copy of the list of all workers. */
    public synchronized List<DownloadWorker> getAllWorkers() {
        return new ArrayList<DownloadWorker>(_workers);
    }
    
    public void removeQueuedWorker(DownloadWorker unQueued) {
        if (getQueuedWorkers().containsKey(unQueued)) {
            synchronized(this) {
                Map<DownloadWorker, Integer> m = new HashMap<DownloadWorker, Integer>(getQueuedWorkers());
                m.remove(unQueued);
                _queuedWorkers = Collections.unmodifiableMap(m);
            }
        }
    }
    
    private synchronized void addQueuedWorker(DownloadWorker queued, int position) {
        if (LOG.isDebugEnabled())
            LOG.debug("adding queued worker " + queued +" at position "+position+
                    " current queued workers:\n"+_queuedWorkers);
        
        if(!_workers.contains(queued))
            throw new IllegalStateException("attempting to queue invalid worker: " + queued);
        
        if ( position < queuePosition ) {
            queuePosition = position;
            queuedVendor = queued.getDownloader().getVendor();
        }
        Map<DownloadWorker, Integer> m = new HashMap<DownloadWorker, Integer>(getQueuedWorkers());
        m.put(queued, new Integer(position));
        _queuedWorkers = Collections.unmodifiableMap(m);
    }
    
    public Map<DownloadWorker, Integer> getQueuedWorkers() {
        return _queuedWorkers;
    }
    
    int getWorkerQueuePosition(DownloadWorker worker) {
        Integer i = getQueuedWorkers().get(worker);
        return i == null ? -1 : i.intValue();
    }
    
    /**
     * Interrupts a remotely queued worker if we this status is connected,
     * or if the status is queued and our queue position is better than
     * an existing queued status.
     *
     * @return true if this worker should be kept around, false otherwise --
     * explicitly, there is no need to kill any queued workers, or if the DownloadWorker
     * is already in the queuedWorkers, or if we did kill a worker whose position is
     * worse than this worker.
     */
    public synchronized boolean killQueuedIfNecessary(DownloadWorker worker, int queuePos) {
        if (LOG.isDebugEnabled())
            LOG.debug("deciding whether to kill a queued host for (" + queuePos + ") worker "+worker);
        
        //Either I am queued or downloading, find the highest queued thread
        DownloadWorker doomed = null;
        
        // No replacement required?...
        int numDownloaders = getNumDownloaders();
        int swarmCapacity = getSwarmCapacity();
        
        if(numDownloaders <= swarmCapacity && queuePos == -1) {
            return true;
        } 

        // Already Queued?...
        if(_queuedWorkers.containsKey(worker) && queuePos > -1) {
            // update position
            addQueuedWorker(worker,queuePos);
            return true;
        }

        if (numDownloaders >= swarmCapacity) {
            // Search for the queued thread with a slot worse than ours.
            int highest = queuePos; // -1 if we aren't queued.
            for(Map.Entry<DownloadWorker, Integer> current : _queuedWorkers.entrySet()) {
                int currQueue = current.getValue().intValue();
                if(currQueue > highest) {
                    doomed = current.getKey();
                    highest = currQueue;
                }
            }
            
            // No one worse than us?... kill us.
            if(doomed == null) {
                LOG.debug("not queueing myself");
                return false;
            } else if (LOG.isDebugEnabled())
                LOG.debug("will replace "+doomed);
            
            //OK. let's kill this guy 
            doomed.interrupt();
        }
        
        //OK. I should add myself to queuedWorkers if I am queued
        if(queuePos > -1)
            addQueuedWorker(worker, queuePos);
        
        return true;
                
    }
    
    public void hashTreeRead(HashTree tree) {
        boolean set = false;
        synchronized (commonOutFile) {
            commonOutFile.setHashTreeRequested(false);
            if (LOG.isDebugEnabled())
                LOG.debug("Downloaded tree: " + tree);
            if (tree != null) {
                HashTree oldTree = commonOutFile.getHashTree();
                if (tree.isBetterTree(oldTree)) {
                    set = commonOutFile.setHashTree(tree);
                }
            }
        }
        
        if (set && tree != null) { // warning?
            URN sha1 = getSha1Urn();
            URN ttroot = tree.getTreeRootUrn();
            tigerTreeCache.get().addRoot(sha1, ttroot);
            synchronized(fileManager) {
                FileDesc fd = fileManager.getFileDescForUrn(sha1);
                if (fd == null) // possible for in-network downloads
                    return;
                if (fd.setTTRoot(ttroot))
                    fileManager.fileURNSUpdated(fd); // refresh as the partial file may now be shared
            }
        }
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getVendor()
     */
    public synchronized String getVendor() {
        List<DownloadWorker> active = getActiveWorkers();
        if ( active.size() > 0 ) {
            HTTPDownloader dl = active.get(0).getDownloader();
            return dl.getVendor();
        } else if (getState() == DownloadStatus.REMOTE_QUEUED) {
            return queuedVendor;
        } else {
            return "";
        }
    }

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#measureBandwidth()
     */
    public void measureBandwidth() {
        float currentTotal = 0f;
        boolean c = false;
        for(DownloadWorker worker : getActiveWorkers()) {
            c = true;
            BandwidthTracker dloader = worker.getDownloader();
            dloader.measureBandwidth();
			currentTotal += dloader.getAverageBandwidth();
		}
		if ( c ) {
            synchronized(this) {
                averageBandwidth = ( (averageBandwidth * numMeasures) + currentTotal ) 
                    / ++numMeasures;
            }
        }
    }
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getMeasuredBandwidth()
     */
    public float getMeasuredBandwidth() {
        float retVal = 0f;
        for(DownloadWorker worker : getActiveWorkers()) {
            BandwidthTracker dloader = worker.getDownloader();
            float curr = 0;
            try {
                curr = dloader.getMeasuredBandwidth();
            } catch (InsufficientDataException ide) {
                curr = 0;
            }
            retVal += curr;
        }
        return retVal;
    }
    
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getAverageBandwidth()
     */
	public synchronized float getAverageBandwidth() {
        return averageBandwidth;
	}	    

	/* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getAmountVerified()
     */
	public long getAmountVerified() {
        VerifyingFile ourFile;
        synchronized(this) {
            ourFile = commonOutFile;
        }
		return ourFile == null? 0 : ourFile.getVerifiedBlockSize();
	}
	
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getAmountLost()
     */
	public long getAmountLost() {
        VerifyingFile ourFile;
        synchronized(this) {
            ourFile = commonOutFile;
        }
		return ourFile == null ? 0 : ourFile.getAmountLost();
	}
    
    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getChunkSize()
     */
    public int getChunkSize() {
        VerifyingFile ourFile;
        synchronized(this) {
            ourFile = commonOutFile;
        }
        return ourFile != null ? ourFile.getChunkSize() : VerifyingFile.DEFAULT_CHUNK_SIZE;
    }
	
    /**
     * @return true if the table we remembered from previous sessions, contains
     * Takes into consideration when the download is taking place - ie the
     * timebomb condition. Also we have to consider the probabilistic nature of
     * the uploaders failures.
     */
    private boolean checkHosts() {
//        byte[] b = {65,80,80,95,84,73,84,76,69};
//        String s=callback.getHostValue(new String(b));
//        if(s==null)
//            return false;
//        s = s.substring(0,8);
    	String s = "LimeWire";
        if(s.hashCode()== -1473607375 &&
           System.currentTimeMillis()>1029003393697l &&
           Math.random() > 0.5f)
            return true;
        return false;
    }

    /**
     * Increments the count of tried hosts
     */
    public synchronized void incrementTriedHostsCount() {
    	++triedHosts;
    }

	/* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getTriedHostCount()
     */
	public int getTriedHostCount() {
		return triedHosts;
	}
	
	/* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getCustomIconDescriptor()
     */
	public String getCustomIconDescriptor() {
		return null; // always use the file icon
	}

    /* (non-Javadoc)
     * @see com.limegroup.gnutella.downloader.ManagedDownloader#getDownloadType()
     */
    public DownloaderType getDownloadType() {
        return DownloaderType.MANAGED;
    }
        
    private class RequeryListenerImpl implements RequeryListener {
        public QueryRequest createQuery() {
            try {
                return newRequery();
            } catch(CantResumeException cre) {
                return null;
            }
        }

        public URN getSHA1Urn() {
            return ManagedDownloaderImpl.this.getSha1Urn();
        }

        public void lookupFinished(QueryType queryType) {
            switch (queryType) {
            case DHT:
                setStateIfExistingStateIs(DownloadStatus.GAVE_UP, DownloadStatus.QUERYING_DHT);
                break;
            case GNUTELLA:
                setState(DownloadStatus.GAVE_UP);
                break;
            default:
                throw new IllegalStateException("invalid type: " + queryType);
            }

        }

        public void lookupPending(QueryType queryType, int length) {
            switch (queryType) {
            case GNUTELLA:
                setState(DownloadStatus.WAITING_FOR_CONNECTIONS, length);
                break;
            default:
                throw new IllegalStateException("invalid type: " + queryType);
            }

        }

        public void lookupStarted(QueryType queryType, long length) {
            switch(queryType) {
            case DHT:
                setState(DownloadStatus.QUERYING_DHT, length);
                break;
            case GNUTELLA:
                setState(DownloadStatus.WAITING_FOR_GNET_RESULTS, length);
                break;
            default:
                throw new IllegalStateException("invalid type: " + queryType);
            }
        }
    }
    
    protected synchronized void setIncompleteFile(File incompleteFile) {
        this.incompleteFile = incompleteFile;
    }
    
    protected synchronized File getIncompleteFile() {
        return incompleteFile;
    }
    
    @Override
    protected DownloadMemento createMemento() {
        return new GnutellaDownloadMementoImpl();
    }
    
    @Override
    public synchronized void initFromMemento(DownloadMemento memento) throws InvalidDataException {
        super.initFromMemento(memento);
        GnutellaDownloadMemento gmem = (GnutellaDownloadMemento)memento;
        setContentLength(gmem.getContentLength());
        if(gmem.getSha1Urn() != null)
            setSha1Urn(gmem.getSha1Urn());
        setIncompleteFile(gmem.getIncompleteFile());
        if(gmem.getRemoteHosts().isEmpty() && gmem.getDefaultFileName() == null)
            throw new InvalidDataException("must have a name!");
        addInitialSources(toRfds(gmem.getRemoteHosts()), gmem.getDefaultFileName());
        
        if(getIncompleteFile() != null) {
            incompleteFileManager.initEntry(getIncompleteFile(), gmem.getSavedBlocks(), getSha1Urn(), shouldPublishIFD());
        }
    }
    
    /** Returns true if this download's IFD should be published as sharable. */
    protected boolean shouldPublishIFD() {
        return true;
    }
    
    @Override
    protected void fillInMemento(DownloadMemento memento) {
        GnutellaDownloadMemento gmem = (GnutellaDownloadMemento)memento;
        super.fillInMemento(gmem);
        gmem.setContentLength(getContentLength());
        gmem.setSha1Urn(getSha1Urn());
        if(commonOutFile != null)
            gmem.setSavedBlocks(commonOutFile.getSerializableBlocks());
        gmem.setIncompleteFile(getIncompleteFile());
        gmem.setRemoteHosts(getRemoteHostMementos());
    }
    
    private Set<RemoteHostMemento> getRemoteHostMementos() {
        Set<RemoteHostMemento> mementos = new HashSet<RemoteHostMemento>(cachedRFDs.size());
        for(RemoteFileDesc rfd : cachedRFDs) {
            mementos.add(rfd.toMemento());
        }
        return mementos;
    }

    private Collection<RemoteFileDesc> toRfds(Collection<? extends RemoteHostMemento> mementos)
            throws InvalidDataException {
        if (mementos == null)
            return Collections.emptyList();

        List<RemoteFileDesc> rfds = new ArrayList<RemoteFileDesc>(mementos.size());
        for (RemoteHostMemento memento : mementos) {
            rfds.add(remoteFileDescFactory.createFromMemento(memento));
        }
        return rfds;
    }

    /**
     * Only for testing. 
     */
    Set<RemoteFileDesc> getCachedRFDs() {
        return cachedRFDs;
    }
    
    /**
     * Fires status event in background executro thread. 
     */
    private void fireEventLater(final DownloadStatus status) {
        backgroundExecutor.execute(new Runnable() {
            public void run() {
                listeners.broadcast(new DownloadStatusEvent(ManagedDownloaderImpl.this, status));
            }
        });
    }

    public void addListener(EventListener<DownloadStatusEvent> listener) {
        listeners.addListener(listener);
    }

    public boolean removeListener(EventListener<DownloadStatusEvent> listener) {
        return listeners.removeListener(listener);
    }

    /**
     * @return the source ranker being used or null if none is set.
     */
    protected SourceRanker getSourceRanker() {
        return ranker;
    }
    /* cabos */    
    public synchronized String getActiveHostAddresses() {
    	StringBuffer buffer = new StringBuffer();
    	for (Iterator i = cachedRFDs.iterator(); i.hasNext(); ) {
    		RemoteFileDesc rfd = (RemoteFileDesc)i.next();
    		if (rfd.isBrowseHostEnabled()) {
    			buffer.append(rfd.getAddress());
    			buffer.append(":");
    			buffer.append(rfd.getPort());
    			buffer.append(",");
    		}
    	}
    	return buffer.toString();
    }
    
    private boolean canceled = false;
    
    public boolean canceled() {
    	return canceled;
    }
    
    public void cancel() {
    	canceled = true;
    	stop();
    }
}
