/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2004 Gregor Koukkoullis ( phex <at> kouk <dot> de )
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 *  --- CVS Information ---
 *  $Id: SWDownloadFile.java,v 1.83 2004/09/23 22:07:16 nxf Exp $
 */
package phex.download.swarming;

import java.io.*;
import java.lang.Thread;
import java.lang.Runtime;
import java.util.*;
import java.util.regex.*;

import javax.xml.bind.JAXBException;

import phex.common.*;
import phex.common.bandwidth.BandwidthManager;
import phex.download.*;
import phex.event.*;
import phex.host.*;
import phex.host.HostAddress;
import phex.http.*;
import phex.query.*;
import phex.statistic.*;
import phex.utils.*;
import phex.xml.*;

public class SWDownloadFile implements IDownloadFile, SWDownloadConstants
{    
    /**
     * The index of the candidate which was tried last.
     */
    private int indexOfAttemptedCandidate = -1;
    
    /**
     * A list of download segments.
     */
    private List downloadSegments;
    
    /**
     * Alternate location container which holds all correctly validate alt locs.
     * A validated alt loc is one which was prooved to connect correctly during
     * the running session.
     */
    private AlternateLocationContainer goodAltLocContainer;
    
    /**
     * Alternate location container which holds all bad alt locs. A bad alt loc
     * is one which was prooved to not be reachable during the running session.
     */
    private AlternateLocationContainer badAltLocContainer;
    
    /**
     * A lock object used to lock access to good and bad download candidate lists.
     */
    private Object candidatesLock = new Object();
    
    /**
     * A list of all download candidates. On access candidatesLock should be locked. 
     */
    private ArrayList allCandidatesList;
    
    /**
     * A list of HTTPRange objects. Each candidate registers its Ranges here.
     */
    public List rangeSetList;


    /**
     * Derived from rangeSetList periodically.
     */
    private List rangePriorityList;

    /**
     * Next time rangePriorityList is to be updated
     */
    private long rangePriorityRefreshTime;

    /**
     * A list of all good download candidates for this
     * download file.
     * Good download candidates that have been tested to be bad are moved to the
     * badCandidatesList. When no "good" candidates are available we might also
     * try additional bad list connects.
     */
    private ArrayList goodCandidatesList;
     
    /**
     * A list of known bad download candidates for this download file. Most 
     * likley they are offline.
     * Bad download candidates will not be retried for a long time and only if
     * there are no usefull good candidates available. They have a high error
     * status timeout. 
     */ 
    private ArrayList badCandidatesList;
    //
    // TODO maintain queued download workers. This allows keeping track of the 
    // queue position and drop connections where we would need to wait longest
    // Always try to maintain one active worker which is able to look for better
    // queues..
    // TODO use a separate "File" object for adding download data and checking 
    // overlapping segments to find corrupt download states. 
    
    // TODO1 maintain a allCandidateList containing all candidates for display
    // with bad flag or something
    
    /**
     * A hash map of candidate worker associations. They represent from worker 
     * allocated download candidates.
     * Modifing access needs to be locked with candidatesLock
     */
    private HashMap/*<SWDownloadCandidate,SWDownloadWorker>*/
        allocatedCandidateWorkerMap;
    

    /**
     * A cached buffer object that contains the candidate count.
     * The value is not always up to date and only validated and updated when
     * calling getCandidateCountObject();
     */
    private IntObj candidateCountObj;
    private IntObj downloadingCandidateCountObj;
    private IntObj queuedCandidateCountObj;

    /**
     * The file size of the download.
     */
    private long fileSize;

    /**
     * The next segment number that is assigned to the next created segment.
     */
    private int nextSegmentNumber;

    /**
     * The destination file of the finished download. The path leads to the
     * directory where the file is stored after the download is completed. This
     * must not always be the download directory for temporary files.
     */
    private File destinationFile;
    private String destinationPrefix;
    private String destinationSuffix;
    private short destinationType;
    public final short DESTTYPE_UNKNOWN = 0;
    public final short DESTTYPE_STREAMABLE = 1;
    public final short DESTTYPE_UNSTREAMABLE = 2;


    /**
     * The time indicating when the download file was created for download.
     */
    private Date createdDate;

    /**
     * The time indicating the last modification (download) of the download file.
     */
    private Date modifiedDate;

    /**
     * The status if the download.
     */
    private short status;

    /**
     * holds the timestamp.
     */
    private long transferRateTimestamp;

    /**
     * holds the transfered bytes since the last timestamp.
     */
    private int transferRateBytes;

    /**
     * Defines the bytes downloaded durring this download session. A download
     * session starts when the download starts and stops when its interrupted.
     * When a download is resumed a new session is started.
     */
    private long sessionTransferRateBytes;

    /**
     * holds the temporary transfer rate that is used when calculation would
     * give a inaccurate number.
     */
    private int transferRate;

    /**
     * Transfer start time
     */
    private long transferStartTime;

    /**
     * Transfer stop time
     */
    private long transferStopTime;

    /**
     * Defines the total length to download,
     * calculated as the sum of all segment sizes
     */
    private long transferDataSize;

    /**
     * Defines the length already downloaded.
     */
    private long transferredDataSize;

    /**
     * The time after which the transfer data needs to be updated
     */
    private long transferDataUpdateTime;

    /**
     * Used to store the current progress.
     */
    private Integer currentProgress;

    /**
     * The number of workers currently working on downloading this file
     */
    private short workerCount;

    /**
     * The URN of the download. This is the most unique identifier of the file
     * network wide. If it is none null we should only accept candidates with
     * the same URN and also add this urn to researchs for better results.
     */
    private URN fileURN;

    /**
     * Settings for the research.
     */
    private ResearchSetting researchSetting;

    public final static short ORDER_BY_POSITION = 1; // sort purely by position
    public final static short ORDER_BY_RATING = 2; // sort purely by rating
    public final static short ORDER_IN_PREVIEW_MODE = 3; // first by position, then by rating

    /*
     * One of the above values. Determines the order in which segments are allocated
     */
    private int orderingMode;
    Comparator orderingObject;

    /*
     * If preview mode is selected, this is the size at the start of the file to prefer.
     */
    private long previewSize;

    /**
     * Constructs a new SWDownloadFile.
     *
     * @param fileURN The URN of the file. This is the most unique identifier of the file
     * network wide. If it is not null we should only accept candidates with
     * the same URN and also add this urn to researchs for better results.
     */
    public SWDownloadFile( String fullLocalFilename, String searchString,
        long aFileSize, URN aFileURN )
    {
        this( fullLocalFilename, searchString, aFileSize, true );
        if ( aFileURN != null )
        {
            this.fileURN = aFileURN;
            initAltLocContainers();
        }
    }

    /**
     * Constructs a new SWDownloadFile from a XML definition.
     * @param xmlFile the XML definition.
     */
    // XJB way
    public SWDownloadFile( XJBSWDownloadFile xjbFile )
    {
        this( ServiceManager.sCfg.mDownloadDir + File.separator
            + xjbFile.getLocalFileName(), xjbFile.getSearchTerm(),
            // the urn is null and will be set manually
            xjbFile.getFileSize(), false );
        createdDate = new Date( xjbFile.getCreatedTime() );
        modifiedDate = new Date( xjbFile.getModifiedTime() );

        status = xjbFile.getStatus();
        if ( xjbFile.getFileURN() != null )
        {
            URN aFileURN = new URN( xjbFile.getFileURN() );
            fileURN = aFileURN;
            initAltLocContainers();
        }
        createDownloadCandidates( xjbFile );
        createDownloadSegments( xjbFile );
        forceCollectionOfTransferData();
        transferRateBytes = 0;
        sessionTransferRateBytes = 0;

        verifyStatus();

        if ( status == STATUS_FILE_COMPLETED )
        {
            // set transfer and transferred data... this is lazy collected from
            // segments when the download is not completed
            transferDataSize = transferredDataSize = fileSize;
        }
    }
    
    /**
     * Constructs a new SWDownloadFile.
     * @param fileURN The URN of the file. This is the most unique identifier of the file
     * network wide. If it is none null we should only accept candidates with
     * the same URN and also add this urn to researchs for better results.
     */
    private SWDownloadFile( String fullLocalFilename, String searchTerm,
        long aFileSize, boolean createSegments )
    {
        destinationFile = new File( fullLocalFilename );
        updateDestinationData();
        fileSize = aFileSize;
        rangeSetList = Collections.synchronizedList(new LinkedList());
        rangePriorityList = null;
        rangePriorityRefreshTime = 0;
        allCandidatesList = new ArrayList();
        goodCandidatesList = new ArrayList();
        badCandidatesList = new ArrayList();
        allocatedCandidateWorkerMap = new HashMap();
        candidateCountObj = new IntObj( 0 );
        downloadingCandidateCountObj = new IntObj( 0 );
        queuedCandidateCountObj = new IntObj( 0 );
        currentProgress = new Integer( 0 );
        createdDate = modifiedDate = new Date( System.currentTimeMillis() );
        previewSize = fileSize / 10;
        setInitialOrdering(ServiceManager.sCfg.orderingMethod);
        status = STATUS_FILE_WAITING;
        if ( createSegments )
        {
            createDownloadSegments();
        }
        else
        {
            downloadSegments = Collections.synchronizedList(new LinkedList());
        }
        researchSetting = new ResearchSetting( this );
        researchSetting.setSearchTerm( searchTerm );

        BandwidthManager.getInstance().getTransferRateService().registerTransferDataProvider( this );
    }

    /*
     * Iterate through a group of regexes, and return a
     * matching regex, or null if none match.
     */
    private static String matches(Iterator regexes, String name)
    {
        StringBuffer regex;
        while (regexes.hasNext())
        {
            String entry = (String) regexes.next();
            if ( entry.startsWith("^") && entry.endsWith("$") ) // a 'true' RE
            {
                regex = new StringBuffer(entry);
            } else {
                regex = new StringBuffer();
                regex.append("^.*"); // any character string may precede it
                regex.append(entry);
                regex.append("$"); // but it must be at the end of the filename
            }

            Pattern p = Pattern.compile(regex.toString(), Pattern.CASE_INSENSITIVE);
            Matcher m = p.matcher(name);
            if ( m.matches() )
                return entry;
        }
        return null;
    }

    /*
     * Calculate the initial ordering mode, based on configuration
     * value and file type
     */
    private void setInitialOrdering(int defaultMode)
    {
        Logger.logMessage(Logger.FINE, Logger.DOWNLOAD, "Setting initial ordering for " + destinationFile.getName()  + " using " + defaultMode);
        int mode;
        switch ( destinationType )
        {
            case DESTTYPE_STREAMABLE:
                mode = ServiceManager.sCfg.orderingMethod / 100;
                break;
            case DESTTYPE_UNSTREAMABLE:
                mode = ( ServiceManager.sCfg.orderingMethod / 10 ) % 10;
                break;
            case DESTTYPE_UNKNOWN:
                mode = ServiceManager.sCfg.orderingMethod % 10;
                break;
            default:
                return; // invalid type so do nothing
        }
        setOrdering(mode);
    }

    /*
     * Dodgy workaround to prevent candidates from looping, trying to get a segment.
     * If I understood things better there'd probably be an easier/better way.
     */
    public boolean isFull()
    {
        synchronized (downloadSegments)
        {
            int size = downloadSegments.size();
            if ( size > 1 ) return false;
            if ( size == 0 ) return true; // not normally the case, but you never know
            SWDownloadSegment seg = (SWDownloadSegment) downloadSegments.get( 0 );
            synchronized ( seg )
            {
                return ( seg.getTransferDataSizeLeft() == 0 );
            }
        }
           
    }

    /*
     * Set the file to use the specified ordering method
     */
    public void setOrdering(int mode)
    {
        if ( mode == orderingMode) return;
        try {
            switch (mode)
            {
                case ORDER_BY_RATING:
                    orderingObject = new SWDownloadSegmentComparatorByRating(0L);
                    break;

                    case ORDER_BY_POSITION:
                        orderingObject = new SWDownloadSegmentComparatorByPosition();
                        break;

                case ORDER_IN_PREVIEW_MODE:
                    orderingObject = new SWDownloadSegmentComparatorByRating(previewSize);
                    break;
                default:
                    throw new Exception();
            }
            orderingMode = mode;
        }
        catch (Exception ex)
        {
            Logger.logMessage(Logger.SEVERE, Logger.DOWNLOAD, "Invalid ordering method requested; not changing anything");
        }
    }

    public int getOrdering()
    {
        return orderingMode;
    }

    /**
     * Used to check if a segment is allocateable. This check is done early
     * before a connection to a candidate is opend. The actual allocation happens
     * after the connection to the candidate is established. It might happen
     * that all segments are allocated until then.
     * @param wantedRangeSet the ranges that are wanted for this download if
     *        set to null all ranges are allowed.
     * @return true if there is a free segment. false otherwise.
     */
    public boolean isDownloadSegmentAllocateable( HTTPRangeSet wantedRangeSet )
    {
        if ( wantedRangeSet != null )
        {
            Iterator rangeIterator = wantedRangeSet.getIterator();
            Range range;
            SWDownloadSegment segment = null;
            while( rangeIterator.hasNext() )
            {
                range = (Range) rangeIterator.next();
                long rangeStart = range.getStartOffset( fileSize );
                long rangeEnd = range.getEndOffset( fileSize );
                synchronized( downloadSegments )
                {
                    Iterator iterator = downloadSegments.iterator();
                    while( iterator.hasNext() )
                    {
                        segment = (SWDownloadSegment) iterator.next();
                        synchronized (segment)
                        {
                            long segmentStart = segment.getTransferStartPosition();
                            long segmentEnd = segment.getEndOffset();
                            //     s--s
                            // r--r    r--r
                            if ( segmentStart == segmentEnd ||
                                rangeEnd <= segmentStart || rangeStart >= segmentEnd )
                            {
                                continue;
                            }
                            // s------------s
                            // r--r r--r r--r
                            if ( segment.isAbleToBeAllocated() )
                            {
                                return true;
                            }
                        }
                    }
                }
            }
            return false;
        }
        else
        {
            synchronized( downloadSegments )
            {
                SWDownloadSegment segment = null;
                Iterator iterator = downloadSegments.iterator();
                while( iterator.hasNext() )
                {
                    segment = (SWDownloadSegment) iterator.next();
                    synchronized (segment)
                    {
                        if ( segment.isAbleToBeAllocated() )
                        {
                            return true;
                        }
                    }
                }
                return false;
            }
        }
    }


    /**
     * Performs internal consistency checking on the segments. Usually you don't
     * want this running, as it will slow down your system. 
     * This was written to make sure merging and splitting work properly - or more
     * precisely, to find out were it wasn't working.
     *
     * I assume downloadSegments is already synchronised when this is called.
     */
    private void validateSegments ()
    {
        return;
        /*
        if ( ! Thread.holdsLock(downloadSegments))
        {
            Logger.logMessage(Logger.SEVERE, Logger.DOWNLOAD,
                        "Segments are not locked." );
        }
        SWDownloadSegment segment = null;
        StringBuffer info = new StringBuffer();
        // fileSize should be the the sum of the segments' sizes
        long calculatedFileSize = 0;
        long calculatedDone = 0;
        ListIterator i = downloadSegments.listIterator();
        if ( i.hasPrevious() )
            throw new Error("wtf?");
        while ( i.hasNext() )
        {
            segment = (SWDownloadSegment)i.next();
            synchronized (segment) 
            {
                info.append(segment + "\n");
                calculatedFileSize += segment.getTransferDataSize();
                calculatedDone += segment.getTransferredDataSize();

                if ( segment.getTransferredDataSize() > segment.getTransferDataSize() ) 
                {
                    Logger.logMessage(Logger.SEVERE, Logger.DOWNLOAD,
                            "Segment has more data than it should when full: " + segment );
                }
            }
        }
        if ( calculatedFileSize != fileSize )
        {
            Logger.logMessage(Logger.SEVERE, Logger.DOWNLOAD,
                "Segments' total sizes " + calculatedFileSize + " do not equal nominal filesize of " + fileSize + "!\n" + info.toString());
        }
        if ( calculatedFileSize != fileSize )
        {
            Logger.logMessage(Logger.SEVERE, Logger.DOWNLOAD,
                "Segments' total completed bytes " + calculatedDone + " do not equal overall completed of  " + transferredDataSize + "!\n" + info.toString());
        }
        */
    }

    /**
     * Used to allocate and reserve a download segment for a download worker.
     * @param wantedRangeSet the ranges that are wanted for this download if
     *        set to null all ranges are allowed.
     */
    public SWDownloadSegment allocateDownloadSegment( SWDownloadWorker worker,
        HTTPRangeSet wantedRangeSet, long preferredSegmentSize, long speed )
    {
        SWDownloadSegment result = null;
        boolean retry;
        do
        {
            retry = false;
            if ( wantedRangeSet != null )
            {
                result = allocateSegmentForRangeSet( worker, wantedRangeSet, preferredSegmentSize );
            }
            else
            {
                result = allocateSegment( worker, preferredSegmentSize );
            }
            if (result == null && canHijackSegment(wantedRangeSet, speed))
            {
                retry = true;
                try {
                    Thread.sleep(50); // give the worker thread time to clean up before trying to allocate
                } catch (Exception ex)
                {
                    throw new Error("This thread should never be interrupted!");
                }
            }
        } while (retry);
        return result;
    }

    /**
     * Used to allocate and reserve a download candidate for a download worker.
     */
    public SWDownloadCandidate allocateDownloadCandidate( SWDownloadWorker worker )
    {
        SWDownloadCandidate candidate = allocateGoodCandidate( worker );
        if ( candidate == null )
        {
            candidate = allocateBadCandidate( worker );
        }
        return candidate;
    }
    
    private SWDownloadCandidate allocateGoodCandidate( SWDownloadWorker worker )
    {
        SWDownloadCandidate candidate = null;
        synchronized( candidatesLock )
        {
            int numCandidates = goodCandidatesList.size();
            
            // return quickly if there are no candidates
            if ( numCandidates == 0 )
            {
                return null;
            }

            // sanity check on persistent index, because since the last call to this method,
            // the number of candidates may have decreased, leaving the persistent index
            // out of range.
            if (++indexOfAttemptedCandidate >= numCandidates)
                indexOfAttemptedCandidate = 0;

            // Iterate over candidates to find the next available
            for (int i=0; i < numCandidates; i++)
            {
                // currentIndex holds the index of the candidate that will be
                // checked for availability
                int currentIndex = i + indexOfAttemptedCandidate;
                if (currentIndex >= numCandidates)
                    currentIndex -= numCandidates;
                    
                candidate = (SWDownloadCandidate)goodCandidatesList.get(currentIndex);
                if ( candidate.isAbleToBeAllocated() &&
                     !allocatedCandidateWorkerMap.containsKey( candidate ) )
                {
                   candidate.log( "Allocated from " + worker );
                   // Sets the segment to be allocated by a worker.
                   allocatedCandidateWorkerMap.put( candidate, worker );
                   indexOfAttemptedCandidate = currentIndex;
                   return candidate;
                }
            }
        }

        // No valid candidate found
        // Don't bother updating the persistent index, because it probably
        // doesn't matter where we begin the search from on the next call.
        return null;
    }
    
    private SWDownloadCandidate allocateBadCandidate( SWDownloadWorker worker )
    {
        SWDownloadCandidate candidate = null;
        synchronized( candidatesLock )
        {
            int numCandidates = badCandidatesList.size();
            
            // return quickly if there are no candidates
            if ( numCandidates == 0 )
            {
                return null;
            }

            // Iterate over candidates to find the next available
            for (int i=0; i < numCandidates; i++)
            {
                candidate = (SWDownloadCandidate)badCandidatesList.get(i);
                if ( candidate != null && candidate.isAbleToBeAllocated() &&
                     !allocatedCandidateWorkerMap.containsKey( candidate ) )
                {
                   candidate.log( "Allocated from " + worker );
                   // Sets the segment to be allocated by a worker.
                   allocatedCandidateWorkerMap.put( candidate, worker );
                   
                   // move candidate to the back of bad candidate list
                   Object removed = badCandidatesList.remove( i );
                   if ( removed != null )
                   {
                       badCandidatesList.add( removed );
                   }
                   
                   return candidate;
                }
            }
        }
        // No valid candidate found
        return null;
    }

    /**
     * Releases a allocated download segment.
     */
    public void releaseDownloadCandidate( SWDownloadCandidate candidate )
    {
        synchronized( candidatesLock )
        {
            SwarmingManager.getInstance().releaseCandidateIP( candidate );
            candidate.log( "Releasing worker allocation" );
            // Sets the segment to be not allocated by a worker.
            allocatedCandidateWorkerMap.remove( candidate );
        }
    }

    public boolean addDownloadCandidate( RemoteFile remoteFile )
    {
        SWDownloadCandidate candidate = new SWDownloadCandidate( remoteFile, this );
        return addDownloadCandidate( candidate );
    }

    public boolean addDownloadCandidate( AlternateLocation altLoc )
    {
        URN altLocURN = altLoc.getURN();
        if ( fileURN != null && !altLocURN.equals( fileURN ) )
        {
            Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                "AlternateLocation URN does not match!" );
            return false;
        }

        // use get request class to parse url
        HostAddress hostAddress = altLoc.getHostAddress();
        if ( hostAddress.isLocalHost() )
        {// dont add myself as candidate.
            return false;
        }
        SWDownloadCandidate candidate = new SWDownloadCandidate( hostAddress,
            -1, null, AlternateLocation.DEFAULT_HOST_RATING, altLocURN, this );
        return addDownloadCandidate( candidate );
    }

    /*
     * For now this isn't used. But it could be - min/max/avg of candidate ratings??
     */
    public IRating getRating()
    {
        return null;
    }

    private boolean addDownloadCandidate( SWDownloadCandidate candidate )
    {
        URN candidateURN = candidate.getResourceURN();
        // update the urn of the file if null
        if ( fileURN == null && candidateURN != null )
        {
            fileURN = candidateURN;
            initAltLocContainers();
        }
        if ( ( fileURN != null && candidateURN != null )
            && !fileURN.equals( candidateURN ) )
        {// make sure URNs match!
            Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                "Candidate URN to add does not match!" );
            return false;
        }

        synchronized( candidatesLock )
        {
            if ( allCandidatesList.contains( candidate ) )
            {
                //Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                //    "Duplicate download candidate" );
                return false;
            }
            Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                "Adding download candidate " + candidate );
            int pos = allCandidatesList.size();
            allCandidatesList.add( pos, candidate );
            fireDownloadCandidateAdded( pos );
            goodCandidatesList.add( candidate );
        }

        SwarmingManager.getInstance().notifyWaitingWorkers();

        return true;
    }

    /**
     * Makes the given download candidate a bad download candidate. This operation
     * will NOT stop a running download of this candidate.
     * 
     * @param candidate the candidate to make bad.
     * @param isIgnored if true the candidate is not looked at when searching for
     *    possible candidates in the bad list, if false the candidate might be used
     *    again.
     */
    public void markCandidateBad( SWDownloadCandidate candidate, boolean isIgnored )
    {
        if ( candidate == null )
        {
            throw new NullPointerException( "Candidate is null.");
        }
        synchronized( candidatesLock )
        {
            int pos = goodCandidatesList.indexOf( candidate );
            if ( pos >= 0 )
            {
                // remove from good...
                goodCandidatesList.remove( pos );
            }
            
            // ...and add to bad.
            if ( !badCandidatesList.contains( candidate ) )
            {
                badCandidatesList.add( candidate );
                Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                    "Moving candidate to bad list: " + candidate.getHostAddress() );
            }
        }
        if ( isIgnored )
        {
            candidate.setStatus( STATUS_CANDIDATE_IGNORED );
        }
        else
        {
            candidate.setStatus( STATUS_CANDIDATE_BAD );
        }
    }
    
    /**
     * Makes the given download candidate as a good candidate, in case it was a
     * bad one...
     * 
     * @param candidate the candidate to make good.
     */
    public void markCandidateGood( SWDownloadCandidate candidate )
    {
        if ( candidate == null )
        {
            throw new NullPointerException( "Candidate is null.");
        }
        synchronized( candidatesLock )
        {
            int pos = badCandidatesList.indexOf( candidate );
            if ( pos >= 0 )
            {
                // remove from bad...
                badCandidatesList.remove( pos );
            }
            
            // ...and add to good.
            if ( !goodCandidatesList.contains( candidate ) )
            {
                goodCandidatesList.add( candidate );
                Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                    "Moving candidate to good list: " + candidate.getHostAddress() );
            }
        }
    }
    
    /**
     * Makes the given download candidate a bad download candidate. This operation
     * will NOT stop a running download of this candidate.
     * 
     * @param candidate the candidate to make bad.
     */
    public void addBadAltLoc( SWDownloadCandidate candidate )
    {
        URN candidateURN = candidate.getResourceURN();
        if ( candidateURN != null && fileURN != null )
        {
            
            AlternateLocation altLoc = new AlternateLocation(
                candidate.getHostAddress(), candidateURN );
            
            // remove good alt loc    
            goodAltLocContainer.removeAlternateLocation( altLoc );
            
            // add bad alt loc
            badAltLocContainer.addAlternateLocation( altLoc );            
        }
        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
            "Adding bad alt loc: " + candidate.getHostAddress() );
    }
    
    /**
     * Makes the given download candidate a bad download candidate. This operation
     * will NOT stop a running download of this candidate.
     * 
     * @param candidate the candidate to make bad.
     */
    public void addGoodAltLoc( SWDownloadCandidate candidate )
    {
        URN candidateURN = candidate.getResourceURN();
        if ( candidateURN != null && fileURN != null )
        {
            
            AlternateLocation altLoc = new AlternateLocation(
                candidate.getHostAddress(), candidateURN );
            
            // remove bad alt loc    
            badAltLocContainer.removeAlternateLocation( altLoc );
            
            // add good alt loc
            goodAltLocContainer.addAlternateLocation( altLoc );            
        }
        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
            "Adding good alt loc: " + candidate.getHostAddress() );
    }

    /**
     * Releases a allocated download segment.
     */
    public void releaseDownloadSegment( SWDownloadSegment segment )
    {
        synchronized( segment )
        {
            segment.setAllocatedByWorker( null );
        }
    }

    /**
     * Returns the number of available candidates
     */
    public int getCandidatesCount()
    {
        return allCandidatesList.size();
    }

    /**
     * Returns the number of available candidates as an Integer object.
     */
    public IntObj getCandidatesCountObject()
    {
        if ( candidateCountObj.intValue() != allCandidatesList.size() )
        {
            candidateCountObj.setValue( allCandidatesList.size() );
        }
        return candidateCountObj;
    }
    
    public IntObj getDownloadingCandidatesCountObject()
    {
        // to save performance search in allocated set for 
        // downloading candidates.
        Set allocatedSet = allocatedCandidateWorkerMap.keySet();
        Iterator iterator = allocatedSet.iterator();
        int count = 0;
        while ( iterator.hasNext() )
        {
            SWDownloadCandidate candidate = (SWDownloadCandidate) iterator.next();
            if ( candidate.isDownloading() )
            {
                count++;
            }
        }
        
        if ( downloadingCandidateCountObj.intValue() != count )
        {
            downloadingCandidateCountObj.setValue( count );
        }
        return downloadingCandidateCountObj;
    }
    
    public IntObj getQueuedCandidatesCountObject()
    {
        // to save performance search in allocated set for 
        // downloading candidates.
        Set allocatedSet = allocatedCandidateWorkerMap.keySet();
        Iterator iterator = allocatedSet.iterator();
        int count = 0;
        while ( iterator.hasNext() )
        {
            SWDownloadCandidate candidate = (SWDownloadCandidate) iterator.next();
            if ( candidate.isRemotlyQueued() )
            {
                count++;
            }
        }
        
        if ( queuedCandidateCountObj.intValue() != count )
        {
            queuedCandidateCountObj.setValue( count );
        }
        return queuedCandidateCountObj;
    }

    /**
     * Gets the candidate at the given position. Or null if the index is not
     * available.
     */
    public SWDownloadCandidate getCandidate( int index )
    {
        if ( index < 0 || index >= allCandidatesList.size() )
        {
            return null;
        }
        return (SWDownloadCandidate) allCandidatesList.get( index );
    }

    /**
     * Returns the container of all known good alternate download locations or null
     * if the download has no valid file urn.
     * @return the container of all known good alternate download locations or null
     * if the download has no valid file urn.
     */
    public AlternateLocationContainer getGoodAltLocContainer()
    {
        return goodAltLocContainer;
    }
    
    /**
     * Returns the container of all known bad alternate download locations or null
     * if the download has no valid file urn.
     * @return the container of all known bad alternate download locations or null
     * if the download has no valid file urn.
     */
    public AlternateLocationContainer getBadAltLocContainer()
    {
        return badAltLocContainer;
    }

    public URN getFileURN()
    {
        return fileURN;
    }

    public Date getCreatedDate()
    {
        return createdDate;
    }

    public Date getDownloadedDate()
    {
        return modifiedDate;
    }

    /**
     * Returns the number of available candidates
     */
    public int getSegmentCount()
    {
        return downloadSegments.size();
    }

    /**
     * Gets the candidate at the given position. Or null if the index is not
     * available.
     */
    public SWDownloadSegment getSegment( int index )
    {
        if ( index >= downloadSegments.size() )
        {
            return null;
        }
        return (SWDownloadSegment) downloadSegments.get( index );
    }

    /**
     * Returns the download segment where the given range fits into.
     * @param range the range that must fit into the returned download segment.
     * @return the download segment where the given range fits into.
     */
    public SWDownloadSegment getSegmentForRange( Range range )
    {
        long startOffset = range.getStartOffset( fileSize );
        long endOffset = range.getEndOffset( fileSize );
        synchronized( downloadSegments )
        {
            SWDownloadSegment segment;
            int size = downloadSegments.size();
            for ( int i = 0; i < size; i++ )
            {
                segment = (SWDownloadSegment)downloadSegments.get( i );
                if (   segment.getStartOffset() <= startOffset
                    && segment.getEndOffset() >= endOffset )
                {
                    return segment;
                }
            }
        }
        return null;
    }

    public HTTPRangeSet createAvailableRangeSet()
    {
        HTTPRangeSet rangeSet = new HTTPRangeSet();
        synchronized( downloadSegments )
        {
            SWDownloadSegment segment;
            int size = downloadSegments.size();
            for ( int i = 0; i < size; i++ )
            {
                segment = (SWDownloadSegment)downloadSegments.get( i );
                if ( segment.getTransferredDataSize() > 0 )
                {
                    rangeSet.addRange( segment.getStartOffset(),
                        segment.getTransferStartPosition() );
                }
            }
        }
        return rangeSet;
    }

    /**
     * The research settings.
     */
    public ResearchSetting getResearchSetting()
    {
        return researchSetting;
    }

    /**
     * This method is used when a user triggers a search from the user interface
     * it should not be used for any automatic searching! Automatic searching
     * is done vie the ResearchService class.
     */
    public void startSearchForCandidates()
    {
        if ( isDownloadCompleted() )
        {
            return;
        }
        researchSetting.stopSearch();
        // user triggered search with default timeout
        researchSetting.startSearch( Search.DEFAULT_SEARCH_TIMEOUT );
    }

    public void setStatus( short newStatus )
    {
        // dont care for same status
        if ( status == newStatus )
        {
            return;
        }
        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD, "DownloadFile Status "
            + newStatus );
        // lock downloadCandidates since verifyStatus changes status with
        // downloadCandidate lock.
        synchronized( candidatesLock )
        {
            switch( newStatus )
            {
                case STATUS_FILE_COMPLETED:
                    // count the completed download
                    SimpleStatisticProvider provider = (SimpleStatisticProvider)
                        StatisticsManager.getInstance().getStatisticProvider(
                        StatisticsManager.SESSION_DOWNLOAD_COUNT_PROVIDER );
                    provider.increment( 1 );
                case STATUS_FILE_WAITING:
                case STATUS_FILE_STOPPED:
                    downloadStopNotify();
                    break;
                case STATUS_FILE_DOWNLOADING:
                    // only trigger if the status is not already set to downloading
                    downloadStartNotify();
                    break;
            }
            status = newStatus;
            SwarmingManager.getInstance().fireDownloadFileChanged( this );
        }
    }

    /**
     * Returns the current status of the download file.
     */
    public short getStatus()
    {
        return status;
    }

    public boolean isAbleToBeAllocated()
    {
        return status != STATUS_FILE_STOPPED &&
               status != STATUS_FILE_COMPLETED &&
               workerCount <= ServiceManager.sCfg.maxWorkerPerDownload;
    }

    /**
     * Raises the number of downloading worker for this file by one.
     */
    public void incrementWorkerCount()
    {
        workerCount ++;
    }

    /**
     * Lowers the number of downloading worker for this file by one
     */
    public void decrementWorkerCount()
    {
        workerCount --;
    }


    /**
     * The methods checks all download candidates if there is still a download
     * going on or if the download was completed.
     * The status is set appropriately. If the download is completed the status
     * is set to STATUS_FILE_COMPLETED.
     */
    public void verifyStatus()
    {
        forceCollectionOfTransferData();
        if ( getTransferDataSize() == getTransferredDataSize() )
        {
            setStatus( STATUS_FILE_COMPLETED );
            // stop running search...
            researchSetting.stopSearch();
            return;
        }
        synchronized( candidatesLock )
        {
            SWDownloadCandidate candidate;
            Iterator iterator = allocatedCandidateWorkerMap.keySet().iterator();
            int highestStatus = STATUS_CANDIDATE_WAITING;
            while ( iterator.hasNext() )
            {
                candidate = (SWDownloadCandidate)iterator.next();
                switch ( candidate.getStatus() )
                {
                    case STATUS_CANDIDATE_DOWNLOADING:
                    {
                        setStatus( STATUS_FILE_DOWNLOADING );
                        return;
                    }
                    case STATUS_CANDIDATE_REMOTLY_QUEUED:
                    {
                        highestStatus = STATUS_CANDIDATE_REMOTLY_QUEUED;
                    }
                }
            }

            // when we are here no download is running...
            // we dont need to set status if file is stopped or completed
            if ( status != STATUS_FILE_STOPPED && status != STATUS_FILE_COMPLETED )
            {
                if ( highestStatus == STATUS_CANDIDATE_REMOTLY_QUEUED )
                {
                    setStatus( STATUS_FILE_QUEUED );
                }
                else
                {
                    setStatus( STATUS_FILE_WAITING );
                }
            }
        }
    }

    public boolean isDownloadCompleted()
    {
        return status == STATUS_FILE_COMPLETED;
    }

    public boolean isDownloadInProgress()
    {
        return status == STATUS_FILE_DOWNLOADING;
    }

    public boolean isDownloadStopped()
    {
        return status == STATUS_FILE_STOPPED;
    }

    /**
     * The destination file of the finished download. The path leads to the
     * directory where the file is stored after the download is completed. This
     * must not always be the download directory for temporary files.
     */
    public File getDestinationFile( )
    {
        return destinationFile;
    }

    /**
     * Returns the file name of the destination file.
     */
    public String getDestinationFileName( )
    {
        return destinationFile.getName();
    }

    /**
     * Sets a new destination file name.
     * Be aware to only do this when the download is stopped. Otherwise you might
     * screw the download!
     */
    public void setDestinationFile( File aDestinationFile, boolean rename )
        throws FileHandlingException
    {
        if ( aDestinationFile.compareTo( destinationFile ) == 0 )
        {
            return;
        }
        if ( rename )
        {
            // first check if download mgr has any running download with this
            // filename
            if ( SwarmingManager.getInstance().isNewLocalFilenameUsed(
                this, aDestinationFile ) )
            {
                // cant rename to file that already exists
                throw new FileHandlingException(
                    FileHandlingException.FILE_ALREADY_EXISTS );
            }
            renameSegmentsToDestinationRoot( aDestinationFile );
            FileUtils.renameLocalFile( destinationFile, aDestinationFile );
        }
        destinationFile = aDestinationFile;
        updateDestinationData();
    }

    /*
     * Recalculate prefix, suffix & filetype values.
     * To be called when the destinationFile is updated.
     */
    private void updateDestinationData()
    {
        String[] result = new String[2];
        Pattern p = Pattern.compile("^(.*)(\\..+?)$");
        Matcher m = p.matcher( destinationFile.getName() );
        if ( m.matches() == true )
        {
            destinationPrefix = new String( m.group(1) + " ");
            destinationSuffix = m.group(2);
        } else {
            destinationPrefix = destinationFile.getName();
            destinationSuffix = new String( "???" );
        }
        if ( matches(ServiceManager.sCfg.streamableSuffixes.iterator(), destinationSuffix ) != null )
        {
            Logger.logMessage(Logger.FINE, Logger.DOWNLOAD, "Appears to be a streamable file.");
            destinationType = DESTTYPE_STREAMABLE;
        } else if ( matches(ServiceManager.sCfg.unstreamableSuffixes.iterator(), destinationSuffix ) != null )
        {
            Logger.logMessage(Logger.FINE, Logger.DOWNLOAD, "Appears to be an unstreamable file.");
            destinationType = DESTTYPE_UNSTREAMABLE;
        } else {
            Logger.logMessage(Logger.FINE, Logger.DOWNLOAD, "Not recognised as either streamable or unstreamable.");
            destinationType = DESTTYPE_UNKNOWN;
        }
    }

    // returns whether we have a valid viewer for this file
    public synchronized boolean canPreview()
    {
        synchronized ( downloadSegments ) 
        {
            if ( downloadSegments.size() < 1 ) return true; // technically it's not a preview anymore, but...
            if ( destinationType == DESTTYPE_UNSTREAMABLE ) return false; // only completed unstreamable files are usable
            SWDownloadSegment segment;
            segment = (SWDownloadSegment) downloadSegments.get(0);
            // first segment will be sg0 if sg0 is complete
            synchronized (segment)
            {
                if ( segment.getStartOffset() != 0
                    || segment.getTransferDataSizeLeft() > 0 
                    || segment.getTransferDataSize() < 1
                    || segment.getAllocatedByWorker() != null)
                    return false;
            }
        }
        return ( getViewCommand() != null);
    }

    /*
     * Create a copy of the first segment if the segment is complete and unused.
     * Try to name this something sensible and put it in the same directory as the finished
     * downloads.
     *
     * Consider making the behaviour OS-dependant. e.g on linux, no need to make a copy of the file,
     * but invoking a previewer program could be nice.
     */
    public synchronized void generatePreview()
    {
        synchronized ( downloadSegments ) 
        {
            SWDownloadSegment segment = null;
            if ( downloadSegments.isEmpty() ) // download has finished, so it's simple
            {
                new Thread ( new Executer ( destinationFile, getViewCommand() ) ).start();
                return;
            }
            Iterator iterator = downloadSegments.iterator();
            while( iterator.hasNext() )
            {
                segment = (SWDownloadSegment) iterator.next();
                synchronized (segment)
                {
                    if ( segment.getStartOffset() == 0 )
                    {
                        // abort if the segment isn't complete and available
                        if ( segment.getTransferDataSizeLeft() > 0 
                                || segment.getTransferDataSize() < 1
                                || segment.getAllocatedByWorker() != null) return;

                        File previewFile = null;
                        StringBuffer prefix = new StringBuffer("Preview of " + destinationPrefix);
                        if ( ServiceManager.sCfg.copyBeforePreviewing == true )
                        {
                            try
                            {
                                File inputFile = segment.getIncompleteFile();
                                File outputFile = File.createTempFile(prefix.toString(),
                                                    destinationSuffix, 
                                                    null);
                                                    //destinationFile.getParentFile());
                                outputFile.deleteOnExit();

                                InputStream in = new BufferedInputStream( new FileInputStream (inputFile) );
                                OutputStream out = new BufferedOutputStream( new FileOutputStream (outputFile) );
                                byte[] buffer = new byte[1024 * 1024];
                                int len;
                                while ( (len = in.read(buffer)) > 0 )
                                   out.write(buffer, 0, len);
                                in.close();
                                out.close();
                                previewFile = outputFile;
                            }
                            catch (Exception ex)
                            {
                                Logger.logMessage( Logger.SEVERE, Logger.DOWNLOAD,
                                        "Having trouble creating preview copy for " + segment.getIncompleteFile().getName() 
                                        + "(" + prefix.toString() + ", " + destinationSuffix + ", " + destinationFile.getParentFile());
                                Logger.logMessage( Logger.SEVERE, Logger.DOWNLOAD, ex);
                                ex.printStackTrace();
                            }
                        } else {
                            previewFile = segment.getIncompleteFile();
                        }
                        if ( previewFile != null )
                        {
                            // Run this in another thread
                            new Thread ( new Executer ( previewFile, getViewCommand() ) ).start();
                        }
                        return; // no need to continue iterating
                    }
                }
            }
        }
    }

    private String getViewCommand ()
    {
        String cmd = ServiceManager.sCfg.fallbackPreviewMethod;
        // Cycle through the keys seeing if any is an RE-match for this suffix
        Set mySet = ServiceManager.sCfg.previewMethod.keySet();
        String key = matches( mySet.iterator() , destinationSuffix );
        if ( key != null ) 
        {
            cmd = (String) ServiceManager.sCfg.previewMethod.get ( key );
        }
        return cmd;
    }

    private class Executer implements Runnable
    {
        File previewFile;
        String command;

        public Executer ( File previewFile, String command )
        {
            this.previewFile = previewFile;
            this.command = command;
        }

        public void run ()
        {
            if ( previewFile == null ) return;
            // Do this before keyword expansion so that the grouping/splitting is done properly
            String[] commands = phex.utils.Runner.parseCommandLine ( command );

            // The replaceAll operator eats backslashes, the stupid thing
            StringBuffer absFilename = new StringBuffer(previewFile.getAbsolutePath().length() + 20);
            try {
                for (int offset=0; offset < previewFile.getAbsolutePath().length() ; offset++)
                {
                    char currentChar = previewFile.getAbsolutePath().charAt(offset);
                    if ( currentChar == '\\' )
                        absFilename.append("\\\\");
                    else
                        absFilename.append( currentChar );
                }
            }
            catch (IndexOutOfBoundsException ex)
            {
            }
            for (int i = 0; i < commands.length ; i++)
            {
                commands[i] = commands[i].replaceAll( "%s", absFilename.toString() );
                try {
                commands[i] = commands[i].replaceAll( "%u", previewFile.toURI().toURL().toExternalForm() );
                } catch (Exception ex) {} // URL cannot be malformed, nothing to handle
            }
            try {
                if ( commands.length > 0 )
                {
                    phex.utils.Runner r = new phex.utils.Runner(commands); 
                }
            }
            catch (Exception ex)
            {
                ex.printStackTrace();
                Logger.logMessage( Logger.SEVERE, Logger.DOWNLOAD,
                        "Cannot invoke previewer for " + previewFile.getAbsolutePath() );
                if ( commands != null )
                {
                    Logger.logMessage( Logger.SEVERE, Logger.DOWNLOAD,
                        "Actual command was '" + commands + "'");
                }
            }
        }
    }

    /**
     * Moves a completed download file to the destination file.
     */
    public synchronized void moveToDestinationFile()
    {
        // this is an assertion... go crazy if fails...
        if ( downloadSegments.size() != 1 && status == STATUS_FILE_COMPLETED)
        {
            throw new RuntimeException( "There must be exactly one download segment (found " + downloadSegments.size() + 
                ") and the download must be completed to move to destination file '" + destinationFile.getName() + "'" );
        }

        synchronized( downloadSegments )
        {
            SWDownloadSegment segment = getSegment(0);
            File incompleteFile = segment.getIncompleteFile();
            File destFile = destinationFile;
            // this is a bug workaround when the destination file is not a absolut
            // pathname.
            if( !destFile.isAbsolute() )
            {
                 destFile = new File( ServiceManager.sCfg.mDownloadDir, destinationFile.getName() );
            }

            // find a free file spot...
            int tryCount = 0;
            while( destFile.exists() )
            {
                tryCount ++;
                StringBuffer newName = new StringBuffer();
                newName.append( destinationFile.getParent() );
                newName.append( File.separatorChar );
                newName.append( '(' );
                newName.append( tryCount );
                newName.append( ") " );
                newName.append( destinationFile.getName() );
                destFile = new File( newName.toString()  );
            }
            Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                "Renaming final segment from " + incompleteFile.getAbsolutePath()
                + " to " + destFile.getAbsoluteFile() + ".");
            boolean succ = incompleteFile.renameTo( destFile );
            if ( succ )
            {
                // delete last segment...
                downloadSegments.remove (segment);
                Logger.logMessage( Logger.FINE, Logger.DOWNLOAD, "File " + destinationFile.getName()
                        + " has been moved to its final location" );
            }
            else
            {
                Logger.logMessage( Logger.FINE, Logger.DOWNLOAD, "Renaming from "
                    + incompleteFile.getAbsolutePath() + " to " + destFile.getAbsolutePath()
                    + " failed.");
            }
        }
        // auto remove download file if set
        if ( ServiceManager.sCfg.mDownloadAutoRemoveCompleted )
        {
            SwarmingManager.getInstance().removeDownloadFile( this );
            // removeDownloadFile triggers save...
        }
        else
        {
            SwarmingManager.getInstance().notifyDownloadListChange();
        }
    }

    /**
     * To be able to provide a constantly valid data transfer rate the time
     * to calculate the data rate from needs to be updated.
     * If you want your timestamp to be updated regularly you need to register
     * your TransferDataProvider with the TransferRateService.
     */
    public void setTransferRateTimestamp( long timestamp )
    {
        transferRateTimestamp = timestamp;
        // TODO add...
        transferRateBytes = 0;
    }

    /**
     * Returns the data transfer rate in bytes. The rate should depend on the
     * transfer rate timestamp.
     */
    public int getShortTermTransferRate()
    {
        if ( status == STATUS_FILE_COMPLETED )
        {
            transferRate = 0;
        }
        else if ( transferRateTimestamp != 0 )
        {
            double sec = (System.currentTimeMillis() - transferRateTimestamp) / 1000;
            // don't drop transfer rate to 0 if we just have a new timestamp and
            // no bytes transfered
            if ( ( transferRateBytes > 0 || sec > 1 ) && sec != 0)
            {
                transferRate = (int) ( transferRateBytes / sec );
            }
        }
        return transferRate;
    }

    /**
     * Return the data transfer status.
     * It can be TRANSFER_RUNNING, TRANSFER_NOT_RUNNING, TRANSFER_COMPLETED,
     * TRANSFER_ERROR.
     */
    public short getDataTransferStatus()
    {
        switch ( status )
        {
            case STATUS_FILE_DOWNLOADING:
                return TRANSFER_RUNNING;
            case STATUS_FILE_COMPLETED:
                return TRANSFER_COMPLETED;
            default:
                return TRANSFER_NOT_RUNNING;
        }
    }


    /**
     * Return the size of data that is attempting to be transfered. This is
     * NOT necessarily the full size of the file as could be the case during
     * a download resumption.
     */
    public long getTransferDataSize()
    {
        lazyCollectTransferData( );
        return transferDataSize;
    }

    /**
     * This is the total size of the available data. Even if its not importend
     * for the transfer itself.
     * Dont change the meaning of this it is way importent for researching and
     * adding candidates!!
     */
    public long getTotalDataSize()
    {
        return fileSize;
    }

    public long getTransferredDataSize()
    {
        lazyCollectTransferData( );
        return transferredDataSize;
    }

    /**
     * Returns the long term data transfer rate in bytes. This is the rate of
     * the transfer since the last start of the transfer. This means after a
     * transfer was interrupted and is resumed again the calculation restarts.
     */
    public int getLongTermTransferRate()
    {
        long transferTime;
        // There are two cases; a download is still in progress, or it's been stopped.
        // If stopTime is non zero, then it's been stopped.
        if( transferStopTime != 0 )
        {
            transferTime = (transferStopTime - transferStartTime) / 1000;
        }
        else
        {
            // Get current elapsed time and convert millis into seconds
            // return totalTransferTime / 1000;
            transferTime = (System.currentTimeMillis() - transferStartTime) / 1000;
        }
        return (int)( sessionTransferRateBytes / (transferTime + 1) );
    }

    public Integer getProgress()
    {
        int percentage;
        if ( status == STATUS_FILE_COMPLETED )
        {
            percentage = 100;
        }
        else
        {
            long tmpTransferDataSize = transferDataSize;
            if ( tmpTransferDataSize == 0 )
            {
                tmpTransferDataSize = 1;
            }
            percentage = (int)( transferredDataSize * 100L / tmpTransferDataSize );
        }

        if ( currentProgress.intValue() != percentage )
        {
            // only create new object if necessary
            currentProgress = new Integer( percentage );
        }
        return currentProgress;
    }

    public void startDownload()
    {
        setStatus( STATUS_FILE_WAITING );
        SwarmingManager.getInstance().notifyWaitingWorkers();
    }
    
    /**
     * Stops a possible running download from the given candidate
     * @param candidate the candidate to stop the possibly running download from.
     */
    public void stopDownload( SWDownloadCandidate candidate )
    {
        SWDownloadWorker worker = (SWDownloadWorker)allocatedCandidateWorkerMap.get(
            candidate );
        if ( worker != null )
        {
            worker.stopDownload();
        }
    }

    public void stopDownload()
    {
        setStatus( STATUS_FILE_STOPPED );
        synchronized( candidatesLock )
        {
            Iterator iterator = allocatedCandidateWorkerMap.values().iterator();
            while ( iterator.hasNext() )
            {
                SWDownloadWorker worker = (SWDownloadWorker)iterator.next();
                if ( worker != null )
                {
                    worker.stopDownload();
                }
            }
        }
    }

    /**
     * Removes all download files of the segments.
     */
    public void removeDownloadSegmentFiles()
    {
        if ( status != STATUS_FILE_STOPPED )
        {
            Logger.logMessage( Logger.WARNING, Logger.DOWNLOAD,
                "Can't clean temp files of not stopped download file.");
            return;
        }

        synchronized( downloadSegments )
        {
            Iterator iterator = downloadSegments.iterator();
            while ( iterator.hasNext() )
            {
                SWDownloadSegment segment = (SWDownloadSegment)iterator.next();
                segment.removeDownloadDestinationFile();
            }
        }
    }

    

    /**
     * Renames all the segments to the new destination root. If a segment file
     * name already exists it will create a different file name with a sub number
     * until it found a none existing file name.
     */
    private void renameSegmentsToDestinationRoot( File destinationRoot )
        throws FileHandlingException
    {
        synchronized ( downloadSegments )
        {
            Iterator iterator = downloadSegments.iterator();
            while ( iterator.hasNext() )
            {
                SWDownloadSegment segment = (SWDownloadSegment) iterator.next();
                segment.renameToDestinationRoot( destinationRoot );
            }
        }
    }

    /**
     * Indicate that the download is just starting.
     * Triggered internally when status changes to STATUS_FILE_DOWNLOADING.
     */
    private void downloadStartNotify( )
    {
        transferStartTime = System.currentTimeMillis();
        modifiedDate = new Date( transferStartTime );
        transferRateTimestamp = transferStartTime;
        transferStopTime = 0;
        sessionTransferRateBytes = 0;
    }

    /**
     * Indicate that the download is no longer running.
     * Triggered internally when status is set to STATUS_FILE_COMPLETED or
     * STATUS_FILE_QUEUED.
     */
    private void downloadStopNotify( )
    {
        // Ignore nested calls.
        if( transferStopTime == 0 )
        {
            transferStopTime = System.currentTimeMillis();
            if ( status == STATUS_FILE_DOWNLOADING )
            {// only update if the status is currently switched from downloading to stopped..
                modifiedDate = new Date( transferStopTime );
            }
        }
    }

    /**
     * Forces the collection of transfer data to have real time results. Only
     * call this very rarely and only when real time transfer data is necessary.
     * Normaly you would use lazyCollectTransferData for this task. It only
     * performes updates when some time (1 sec) is passed since the last update.
     */
    public void forceCollectionOfTransferData()
    {
        long tmpTransferredDataSize = 0;
        long tmpTransferDataSize = 0;
        SWDownloadSegment segment;
        synchronized( downloadSegments )
        {
            Iterator iterator = downloadSegments.iterator();
            while( iterator.hasNext() )
            {
                segment = (SWDownloadSegment) iterator.next();
                synchronized (segment)
                {
                    tmpTransferredDataSize += segment.getTransferredDataSize();
                    tmpTransferDataSize += segment.getTransferDataSize();
                }
            }
        }
        // determine the bytes transferd by looking add the diff of the
        // transferredData since last calculation. The result is not exact
        // but performant
        transferRateBytes += tmpTransferredDataSize - transferredDataSize;
        sessionTransferRateBytes += tmpTransferredDataSize - transferredDataSize;
        transferDataSize = tmpTransferDataSize;
        transferredDataSize = tmpTransferredDataSize;
        // next update in min 1000 millis...
        transferDataUpdateTime = System.currentTimeMillis() + 1000;
    }

    /**
     * Collects the transfer data if a certain time has passed or isForced is set
     * to true.
     */
    private void lazyCollectTransferData()
    {
        long now = System.currentTimeMillis();
        if ( rangePriorityRefreshTime < now )
        {
            rangePriorityRefreshTime = now + 10000; // TODO: make this config value rather than hardcode at 10 seconds?
            rebuildRangePriorityList();
            compressRangePriorityList();
            rerateSegments();
            sortSegments();
            mergeSegments();
        }
        // dont collect if file is completed
        if ( status == STATUS_FILE_COMPLETED ||
             transferDataUpdateTime > now )
        {
            return;
        }
        forceCollectionOfTransferData();
    }

    private boolean closeEnough(Range previous, Range current)
    {
        // this only allows merges if 2 ratings are identical:
        //return previous.getRating().equals(current.getRating());
      
        // This allows merges if the *values* (ie: sort criteria)
        // are identical, OR one of the values is 0
        if ( previous.getRating().getValue() == 0 
                || current.getRating().getValue() == 0) 
            return true;

        return previous.getRating().getValue()
                == current.getRating().getValue();
        /*
        if ( previous.getRating() <= 0 || current.getRating() <= 0 )
            return ( previous.getRating() == current.getRating() );

        float ratio = (float)previous.getRating()  / (float)current.getRating();
        return ( ratio >= 0.5 && ratio <= 2.0 ); // hard-code a 2:1 ratio as sufficient to call them the same
        */
    }

    /**
     * Iterates through the rangePriorityList and merges adjacent ranges if they're "close enough".
     * This is a good idea as when we apply this to our downloadSegments, their maximum size will be the size of these chunks.
     * Plus it'll be a bit faster to mangle the segments if we have fewer ranges to compare.
     *
     * rangePriorityList's ordering by range will be preserved.
     */
    private void compressRangePriorityList()
    {
        Iterator i = rangePriorityList.iterator();
        int nCompressed = 0; // record how many ranges have been merged
        if ( ! i.hasNext() ) return;
        Range current, previous;
        previous = (Range) i.next();
        while ( i.hasNext() )
        {
            current = (Range) i.next();
            if ( closeEnough(previous, current) )
                {
                    // make the previous range span both the rangesets
                    previous.update(previous.getStartOffset(fileSize), current.getEndOffset(fileSize));
                    // if we're merging a zero rating with another, take the other rating
                    if (previous.getRating().getValue() == 0)
                        previous.setRating(current.getRating());

                    // remove the second one from the list
                    i.remove();

                    nCompressed++;
                }
            else
                previous = current;
        }
        if ( nCompressed > 0 )
        {
            Logger.logMessage( Logger.INFO, Logger.DOWNLOAD,
                    "Compression of priority list is complete (compressed by " + nCompressed + " elements):\n" + rangePriorityList );
        }
    }

    /**
     * Regenerates the priority list. When this is done, we'll be ready to reorganise the remaining downloadSegments.
     * This function doesn't assume any ordering of rangeSetList. 
     * rangePriorityList will be ordered by range, not rating!
     */
    private void rebuildRangePriorityList()
    {
        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD, "Rebuilding range priority list");
        rangePriorityList = new LinkedList();
        synchronized (rangeSetList)
        {
            ListIterator rangeSetIter = rangeSetList.listIterator();
            ListIterator rangePriIter;
            Range rSet, rPri;
            long sStart, sEnd;
            CandidateRating sRating;
            long pStart, pEnd;
            while ( rangeSetIter.hasNext() ) // iterate through each range
            {
                /*
                 * rSet is the current Range object we're assimilating. It goes from sStart to sEnd
                 * and has a rating of sRating
                 * NOTE: if start == end, there's 1 byte in this range, not 0!
                 */
                rSet = (Range) rangeSetIter.next();
                Logger.logMessage( Logger.FINER, Logger.DOWNLOAD, "Adding source range: " + rSet);
                sStart = rSet.getStartOffset(fileSize); sEnd = rSet.getEndOffset(fileSize); sRating = (CandidateRating)rSet.getRating();

                rangePriIter = rangePriorityList.listIterator();
                Logger.logMessage( Logger.FINEST, Logger.DOWNLOAD, "Adding to RPL (dst): " + rangePriorityList);
                while ( rangePriIter.hasNext() && sStart <= sEnd)
                {
                    rPri = (Range) rangePriIter.next();
                    pStart = rPri.getStartOffset(fileSize); pEnd = rPri.getEndOffset(fileSize);
                    if (sStart > pEnd) continue;
                    Logger.logMessage( Logger.FINER, Logger.DOWNLOAD, "Src isn't completely after: " + rPri);

                    // If we've got this far the range to insert overlaps with the current element OR completely precedes it

                    // First deal with s starting before p. It may or may not overlap.
                    if ( sStart < pStart )
                    {
                        Logger.logMessage( Logger.FINEST, Logger.DOWNLOAD, "Adding entry from src prior to " + rPri);
                        if ( sEnd < pStart )
                        {   // s fits fully before current entry (p)
                            // ssssssssss
                            //           pppppppp
                            rangePriIter.previous();
                            rangePriIter.add(new Range(sStart, sEnd, PriorityRating.derive(sRating))); 
                            rangePriIter.next(); // now we're back to where we started
                            sStart = sEnd + 1;
                        } else {
                            // s overlaps with start
                            // sssssssssss
                            //     pppppppp
                            // or
                            // sssssssssss
                            //     ppppp
                            rangePriIter.previous();
                            rangePriIter.add(new Range(sStart, pStart, PriorityRating.derive(sRating))); 
                            rangePriIter.next(); // now we're back to where we started
                            sStart = pStart;
                        }
                    }
                    if (sStart > sEnd) continue; // if our source has been fully dealt with, iterate to next source

                    // By the time we're here, the current src block starts at or after the current dst block.
                    //
                    // First check if there's a part at the start of p which isn't covered by s. If so, split it off and focus on the overlap.
                    if ( sStart > pStart )
                    {   // Split (by prepending to iterator so pStart = sStart)
                        //    sssss
                        // ppppppp...
                        Logger.logMessage( Logger.FINEST, Logger.DOWNLOAD, "Splitting " + rPri + " at start of src (" + sStart + ")");
                        rangePriIter.previous();
                        rangePriIter.add(new Range(pStart, sStart - 1, new PriorityRating( rPri.getRating()))); 
                        rangePriIter.next(); // now we're back to where we started
                        rPri.update(sStart, rPri.getEndOffset(fileSize));
                        pStart = sStart;
                        // TODO: get rid of last line when I know it's working. I don't think there's
                        // a problem near here
                        Logger.logMessage( Logger.FINEST, Logger.DOWNLOAD, "After split, RPL is: " + rangePriorityList);
                    }

                    // This should always be true now
                    if ( sStart == pStart)
                    {  
                        if ( sEnd < pEnd ) 
                        {
                            // sssssssssss
                            // pppppppppppp
                            // s goes only partway through p, so split (by prepending), then inc the current one
                            Logger.logMessage( Logger.FINEST, Logger.DOWNLOAD, "Splitting " + rPri + " to end of src (" + sEnd + ")");
                            rangePriIter.previous();
                            PriorityRating newPri = new PriorityRating(rPri.getRating()); // this is the overlapping range
                            rangePriIter.add(new Range(pStart, sEnd, newPri)); 
                            rangePriIter.next(); // now we're back to where we started
                            rPri.update(sEnd + 1, rPri.getEndOffset(fileSize));
                            newPri.accumulate(sRating);
                            sStart = sEnd + 1; // fully merged src into dst
                        } else {
                            // s goes at least to the end of p. So just adjust the rating, bump sStart, and iterate on p.
                            // ssssss
                            // ppppp.
                            rPri.accumulate(sRating);
                            sStart = pEnd + 1;
                        }
                    }
                }
                if ( sStart <= sEnd )
                { // This is after the end of all existing members
                    rangePriIter.add(new Range(sStart, sEnd, PriorityRating.derive(sRating)));
                }
            }
            Logger.logMessage( Logger.INFO, Logger.DOWNLOAD,
                    "Rebuild of priority list is complete:\n" + rangePriorityList );
        }
    }

    /**
     * Assigns ratings to segments, splitting where necessary.
     * This will not merge and sort the segments.
     *
     * rangePriorityList is sorted by range. For this reason we don't use an iterator to traverse downloadSegments, but rather
     * its getNextSegment() method.
     */
    private void rerateSegments()
    {
        int nSplits = 0;
        int nReratings = 0;
        ListIterator rpl = rangePriorityList.listIterator();
        Range currentRange = null;
        if (rpl.hasNext()) 
            currentRange = (Range) rpl.next();

        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                "Rerating segments using: " + rangePriorityList);
        synchronized( downloadSegments )
        {
            ListIterator segIter = downloadSegments.listIterator();
            SWDownloadSegment segment = null;
            while( segIter.hasNext() )
            {
                segment = (SWDownloadSegment) segIter.next();
                Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                        "Processing segment: " + segment);

                synchronized (segment) // maybe we don't need this, but I'll leave it for now
                {
                    // First find the range we're up to, then continue until we've either passed all of them
                    // (which means all candidates can provide this segment) OR we find a range which overlaps
                    // with the segment in question
                    while ( currentRange != null && currentRange.getEndOffset(fileSize) < segment.getStartOffset())
                    {
                        if ( rpl.hasNext() )
                        {
                            currentRange = (Range) rpl.next();
                        } else {
                            currentRange = null;
                        }
                    }
                    if ( currentRange == null) // nothing more in the range
                    {
                        segment.rating = new PriorityRating(WORST_RATING, 0);
                        Logger.logMessage( Logger.FINER, Logger.DOWNLOAD, "No overlapping range found.");
                        continue;
                    }
                    Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                            "Applicable range: " + currentRange);
                    // now the current segment's start is before the end of the current range.
                    // We only want to split the segment if it's free. If it's in use, we'll just give it a rating, even if it
                    // shouldn't strictly apply to the whole segment. In fact, to be safe we won't split it if there's any data in it at all.
                    // We'll be happy to wait for it to be reclaimed by garbage collection, then rated once merged
                    // Why is there "1 +" below? Because the range's endoffset is correctly named, but
                    // the segment's endoffset is really the first byte of the NEXT segment!
                    // ie: if a range is [0, 1000] and a segment says [500,1001] the segment is a subset of the range.
                    if ( segment.isAbleToBeAllocated() && segment.getAllocatedByWorker() == null
                            && segment.getTransferredDataSize() == 0
                            && segment.getEndOffset() > (1 + currentRange.getEndOffset(fileSize))) // doesn't need splitting otherwise
                    {
                        Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                                "trying to split with s: " + segment.getStartOffset()
                                + "-" + (currentRange.getEndOffset(fileSize) - segment.getStartOffset())
                                + " for re-rating." );
                        validateSegments();
                        segment = splitDownloadSegment1( segIter, 0, 1 + currentRange.getEndOffset(fileSize) - segment.getStartOffset() );
                        validateSegments();
                        Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                                "After merge it looks like: " + downloadSegments);
                        // backtrack the iterator to before the segment
                        while ( (SWDownloadSegment) segIter.previous() != segment);
                        // jump to the right of segment
                        segIter.next();
                        nSplits++;
                    }
                    if (! segment.rating.equals(currentRange.getRating())) {
                        nReratings++;
                        segment.rating = (PriorityRating) currentRange.getRating();
                        Logger.logMessage( Logger.FINER, Logger.DOWNLOAD, "New rating is: " + segment.rating);
                    }
                }
            }
        }
        if (nSplits > 0 || nReratings > 0)
        {
            Logger.logMessage( Logger.INFO, Logger.DOWNLOAD, "Rerated " + nReratings
                    + " segments and split " + nSplits);
        }
    }

    public void sortSegments()
    {
        synchronized( downloadSegments )
        {
            // before the actual sort happens, make sure any completed segments have the same rating.
            // This will ensure they're sorted next to each other and will be merged.
            ListIterator segIter = downloadSegments.listIterator();
            SWDownloadSegment segment = null;
            while( segIter.hasNext() )
            {
                segment = (SWDownloadSegment) segIter.next();
                synchronized (segment)
                {
                    if (segment.getTransferredDataSize() == segment.getTransferDataSize())
                        segment.rating = new PriorityRating(WORST_RATING, 0);
                }
            }
            Logger.logMessage( Logger.FINER, Logger.DOWNLOAD, "Unsorted segments look like this:\n" + downloadSegments);
            Collections.sort( downloadSegments, orderingObject );
            Logger.logMessage( Logger.FINER, Logger.DOWNLOAD, "Sorted segments look like this:\n" + downloadSegments);
        }
    }

    /**
     * Allocates a download segment and assignes it to the given worker.
     * @param worker the worker that likes to allocate a segment for download.
     * @return a download segment ready to download.
     */
    private SWDownloadSegment allocateSegment( SWDownloadWorker worker, long preferredSize )
    {
        synchronized( downloadSegments )
        {
            ListIterator segIter = downloadSegments.listIterator();
            SWDownloadSegment segment = null;
            while( segIter.hasNext() )
            {
                segment = (SWDownloadSegment) segIter.next();
                synchronized (segment)
                {
                    if ( segment.isAbleToBeAllocated())
                    {
                        if ( segment.getTransferDataSizeLeft() > preferredSize )
                        {
                            Logger.logMessage( Logger.FINER, Logger.DOWNLOAD, "trying to split with s: " + segment.getTransferredDataSize() + "-" + preferredSize + " for allocation.");
                            validateSegments();
                            segment = splitDownloadSegment1( segIter,
                                segment.getTransferredDataSize(), preferredSize );
                            validateSegments();
                        }
                        synchronized (segment)  // this is not the same segment as was locked above. shouldn't really
                                               // need to lock this one, but it's easier to be safe
                        {
                            Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                                "Allocating worker " + worker + " to segment: " + segment );
                            segment.setAllocatedByWorker( worker );
                            return segment;
                        }
                    }
                }
            }
            return null;
        }
    }
    
    private SWDownloadSegment allocateSegmentForRangeSet( SWDownloadWorker worker,
        HTTPRangeSet wantedRangeSet, long preferredSize)
    {
        synchronized( downloadSegments )
        {
            ListIterator segIter = downloadSegments.listIterator();
            Iterator rangeIterator = null;
            Range range;
            SWDownloadSegment segment = null;

            while( segIter.hasNext() )
            {
                segment = (SWDownloadSegment) segIter.next();
                synchronized (segment)
                {
                if ( ! segment.isAbleToBeAllocated()) continue; // this segment's not available
                long segmentStart = segment.getTransferStartPosition();
                long segmentEnd = segment.getEndOffset();

                // if we can find at least part of this segment in the rangeset, get it.
                rangeIterator = wantedRangeSet.getIterator();
                while( rangeIterator.hasNext() )
                    {
                        range = (Range) rangeIterator.next();
                        long rangeStart = range.getStartOffset( fileSize );
                        long rangeEnd = range.getEndOffset( fileSize );

                        //     s--s
                        // r--r    r--r
                        if ( segmentEnd == segmentStart || rangeStart == rangeEnd
                            || rangeEnd <= segmentStart || rangeStart >= segmentEnd )
                            continue; // no overlap

                        long tmpRangeStart = Math.max( rangeStart, segmentStart );
                        long tmpRangeEnd = Math.min( rangeEnd, segmentEnd );
                        // Need to split if the segment isn't exactly the right size
                        if ( segmentStart != tmpRangeStart || 
                             segmentEnd != tmpRangeEnd ||  
                             tmpRangeEnd - tmpRangeStart > preferredSize )
                        {
                            Logger.logMessage( Logger.FINER, Logger.DOWNLOAD, "trying to split with s: " + segmentStart + "-" + segmentEnd + ", r: "
                                    + rangeStart + "-" + rangeEnd + " for allocation purposes");
                            validateSegments();
                            segment = splitDownloadSegment1(
                                segIter, tmpRangeStart - segmentStart,
                                Math.min( tmpRangeEnd - tmpRangeStart, preferredSize ) );
                            validateSegments();
                        }
                        synchronized(segment)
                        {
                            Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                                "Allocating  worker " + worker + " to segment for rangeset: " + segment );
                            segment.setAllocatedByWorker( worker );
                            return segment;
                        }
                    }
                }
            }
            return segment;
        }
    }
    
    /**
     * Look for a segment which is being filled significantly more slowly than this worker's
     * last segment was. 
     *
     */
    
    private boolean canHijackSegment(HTTPRangeSet wantedRangeSet, long speed)
    {
        SWDownloadSegment bestSegment = null; // if we find a good one, save it here.
        if (speed <= 0) return false; // need a speed before we can hijack
        if ( status != TRANSFER_RUNNING ) return false; // must be running to hijack
        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
            "About to try to hijack a segment, speed=" + speed);
        synchronized( downloadSegments )
        {
            ListIterator segIter = downloadSegments.listIterator();
            Iterator rangeIterator = null;
            Range range;
            boolean rangeSetOK;
            SWDownloadSegment segment = null; // used by iterator
            long myBestDownloadTime = 0; // this is the estimated time remaining for the bestSegment

            while( segIter.hasNext() )
            {
                segment = (SWDownloadSegment) segIter.next();
                synchronized (segment)
                {
                    
                    long segmentStart = segment.getTransferStartPosition();
                    long segmentEnd = segment.getEndOffset();
                    long myTimeRemaining = (segmentEnd - segmentStart) / speed;
                    if ( segment.getTransferDataSizeLeft() < 1 ) continue; // segment is done

                    // first check the segment overlaps with some of a range in the rangeset, the
                    // the rangeset is not null
                    rangeSetOK = false;
                    if (wantedRangeSet != null)
                        rangeIterator = wantedRangeSet.getIterator();
                    else
                        rangeSetOK = true;
                    while( wantedRangeSet != null && ! rangeSetOK && rangeIterator.hasNext() )
                    {
                        range = (Range) rangeIterator.next();
                        long rangeStart = range.getStartOffset( fileSize );
                        long rangeEnd = range.getEndOffset( fileSize );

                        if ( rangeStart <= segmentStart && rangeEnd >= segmentEnd ) // we can fully satisfy the range constraint
                            rangeSetOK = true;
                    }
                    if ( rangeSetOK == false ) continue; // range doesn't overlap, so it's no good for us
                    if ( segment.isAbleToBeAllocated()) return true; // it's free already! :-)
                    
                    // Now we need to check whether it's worth booting the current worker and trying to get this one
                    // ourselves.
                    
                    // choose the segment which (with the current worker) will take the longest
                     // BUT only if we're at least twice as fast AND they're going to take more than 5 seconds to finish it
                     //
                    long estimatedTimeRemaining = 100;
                    if (segment.getLongTermTransferRate() > 0)
                        estimatedTimeRemaining = segment.getTransferDataSizeLeft() / segment.getLongTermTransferRate();
                    long currentSpeed = segment.getLongTermTransferRate();
                    Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                        "etr: " + estimatedTimeRemaining + ", cS: " + currentSpeed);
                    
                    if (estimatedTimeRemaining <= myBestDownloadTime)
                        continue;
                    if (speed >= 2.0 * currentSpeed && estimatedTimeRemaining > 5 )
                    {
                        myBestDownloadTime = estimatedTimeRemaining;
                        bestSegment = segment;
                    }
                }
            }
        }
        if (bestSegment == null) return false;
        synchronized (bestSegment)
        {
            Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                "Freeing: " + bestSegment);
            if (! bestSegment.isAbleToBeAllocated()) 
            {
                SWDownloadWorker worker = bestSegment.getAllocatedByWorker();
                if ( worker != null )
                {
                    worker.stopWorker();
                    worker.stopDownload();
                }
            }
        }
        return true;
    }
    
    /**
     * Marks a given byte range as invalid.
     * 
     * @param offset
     * @param length
     */
    // TODO1 implement.
    // TODO1 support file segment splitting on file system.
    // TODO1 make sure locking is checked!!! cases are that segment is in 
    //       allocated by worker and that segment is performing a merge operation.
    
    // TODO1 make real attribute field
    // needs downloadSegments locking!
    /*ArrayList rangeList = new ArrayList();
    private void markRangeAsInvalid( long from, long to )
    {
        synchronized( downloadSegments )
        {
            long[] range = new long[2];
            range[0] = from;
            range[1] = to;
            rangeList.add( range );
        }
    }
    
    private void checkInvalid()
    {
        // we have following cases for invalid ranges
        // - = valid; * = invalid
        // |*****|   case 1, range fits segment size
        // |--**--|  case 2, range is in middle of segment
        // |--*|*--| case 3, range is across two segments
        // |**--|    case 4, range is on begining of segment
        // |--**|    case 5, range is on end of segment
        synchronized( downloadSegments )
        {
            SWDownloadSegment segment;
            ArrayList invalidList = new ArrayList();
            boolean isAbleToInvalidate;
            Iterator rangeIterator = rangeList.iterator();
            for ( int i = invalidList.size() - 1; i >= 0; i-- )
            {
                isAbleToInvalidate = true;
                invalidList.clear();
                long[] range = (long[])invalidList.get( i );
                long invalidRangeFrom = range [0];
                long invalidRangeTo = range[1];
        
                Iterator iterator = downloadSegments.iterator();
                while( iterator.hasNext() )
                {
                    segment = (SWDownloadSegment) iterator.next();
                    if ( !segment.isIncludingRange( invalidRangeFrom, invalidRangeTo ) )
                    {
                        continue;
                    }
                    if ( !segment.isAbleToBeAllocated() )
                    {
                        // range can not be invalidated
                        isAbleToInvalidate = false;
                        break;
                    }
                    // segment is available add to invalidList
                    invalidList.add( segment );
                }
                // continue to next range if a segments is not ready for invalidate...
                if ( !isAbleToInvalidate )
                {
                    continue;
                }
                
                // loop through segments to invalidate range...
                Iterator segmentIterator = invalidList.iterator();
                while( segmentIterator.hasNext() )
                {
                    segment = (SWDownloadSegment)segmentIterator.next();
                    // assert that segments never contain partial data...
                    // otherwise the invalidation may corrupt stuff..
                    // actually this situation is never supposed to occure since
                    // only complete segment should run into the situation of
                    // needing to be invalidated.
                    long segStart = segment.getStartOffset();
                    long segEnd = segment.getEndOffset();
                    
                    long segInvalidFrom = Math.min( invalidRangeFrom - segStart, 0 );
                    long segInvalidLength = Math.min( segEnd, invalidRangeTo ) - segStart;
                    
                    splitDownloadSegment1( segment, segInvalidFrom, segInvalidLength );
                    //and IO split...
                }
                
                // remove invalidated range from list..
                rangeList.remove( i );
            }
        }        
    }*/
    
    /**
     * We try to merge all segments with data and all segemnts without data 
     * together.
     * The preconditions for a merge is that the segments to merge are not 
     * allocated by a worker.
     * Since segments only know their following segment we can only merge
     * forward.
     * 
     * 
     * @param worker
     */
    public void mergeSegments( )
    {
        ArrayList/*<SWDownloadSegment>*/ mergeList = new ArrayList();
        
        int nMerges = 0;
        SWDownloadSegment segment;
        SWDownloadSegment nextSegment = null;
        
        Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
            "About to check for possible segment merging.");
        synchronized( downloadSegments )
        {
            if (downloadSegments.size() == 0) return;
            ListIterator i = downloadSegments.listIterator();
            if ( i.hasNext() )
                nextSegment = (SWDownloadSegment) i.next();

            while ( i.hasNext() )
            {
                validateSegments();
                // ie: at the start of each iteration, we assume 'nextSegment' is possible able to be merged in with its
                // successor. the iterator 'i' must be immediately after the (old) 'nextSegment' (which becomes 'segment' now)
                segment = nextSegment;
                nextSegment = (SWDownloadSegment) i.next();
                synchronized (segment) { synchronized (nextSegment)
                {
                    if ( segment.getNextDownloadSegment() == null) continue; // this is the last segment of the file
                                                            // (but maybe not the last one in the list)
                    Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                        "Performing merge cycle on\n\t" + segment + " and\n\t" + nextSegment);
                    if ( segment.getAllocatedByWorker() != null )
                    { 
                        Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                            "Rejecting this, as the first segment is busy.");
                        continue;
                    }
                    if ( segment.getTransferredDataSize() > 0
                      && segment.getTransferDataSizeLeft() > 0 )
                    {// partial current segment... split it...
                        Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                            "current segment should be split, as it's not being used and it's incomplete:" + segment);
                        i.previous(); // as the iterator has to sit just after the segment to split
                        validateSegments();
                        // we want the 'current' segment to be the second half.
                        // No need to sync on it, as the DownloadEngine will sync on the 'old' segment before doing anything
                        segment = splitDownloadSegment1( i, segment.getTransferredDataSize(),
                            segment.getTransferDataSizeLeft() );
                        validateSegments();
                        nextSegment = (SWDownloadSegment) i.next(); // no need to explicitly set, but it's clearer
                        Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                            "current segment is now:" + segment);
                    }
                    
                    if ( nextSegment != segment.getNextDownloadSegment() ) continue; // if they're not adjacent
                    // in the list don't merge them yet; we can wait until they've been sorted if indeed they're ready

                    if ( nextSegment.getStartOffset() != segment.getEndOffset() )
                    {
                        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                            "Rejecting this, as the second segment doesn't touch the first segment.");
                        // this shouldn't really happen
                        continue; 
                    }

                    if ( nextSegment.getAllocatedByWorker() != null )
                    {
                        Logger.logMessage( Logger.FINEST, Logger.DOWNLOAD,
                            "Rejecting this, as the second segment is busy.");
                        continue; // segment is busy so this can't be the second half of a merge
                    }
                    if ( nextSegment.getTransferredDataSize() > 0 &&
                         nextSegment.getTransferDataSizeLeft() > 0 )
                    {// partial segment... split it...
                        Logger.logMessage( Logger.FINEST, Logger.DOWNLOAD,
                            "Second segment is partially complete, so need to split before we can evaluate.");
                        validateSegments();
                        splitDownloadSegment1( i, nextSegment.getTransferredDataSize(),
                            nextSegment.getTransferDataSizeLeft() );
                        validateSegments();
                        // but now we need to be careful, as the next segment isn't what we expected:
                        // first rewind until we find the current segment.
                        while ( (SWDownloadSegment) i.previous() != segment);
                        // jump to the right of segment
                        i.next();
                        // and reassign the next segment as 'next'
                        nextSegment = (SWDownloadSegment) i.next();
                        Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                            "and the next segment - after a split - is now:" + segment);
                    }
                    
                    if (
                          // both segments are empty and have same rating
                         (segment.getTransferredDataSize() == 0 &&
                          nextSegment.getTransferredDataSize() == 0 &&
                          segment.getRating().equals(nextSegment.getRating()) )
                          // both segments are completed...
                      || (segment.getTransferDataSizeLeft() == 0 &&
                          nextSegment.getTransferDataSizeLeft() == 0) )
                    {
                        
                        Logger.logMessage( Logger.FINER, Logger.DOWNLOAD,
                            "Merging " + segment + " with " + nextSegment);

                        validateSegments();
                        // merge the two segments
                        // make sure nextSegment is correct

                        if ( segment.getTransferDataSizeLeft() == 0 &&
                             nextSegment.getTransferDataSizeLeft() == 0 )
                        {
                            // merge download files...
                            try
                            {
                                FileUtils.truncateFile( segment.getIncompleteFile(),
                                    segment.getTransferDataSize() );
                                FileUtils.appendFile( segment.getIncompleteFile(),
                                    nextSegment.getIncompleteFile() );
                            }
                            catch ( IOException exp )
                            {// TODO1 handle this gracefully!
                             // if a file really no longer exists the segments transferred
                             // data must be reset to 0 and the transfer of data must restart
                             // for this segment. 
                                Logger.logWarning( exp );
                            }
                        }
                        segment.mergeWithNextSegment();
                        i.remove(); // removes nextSegment
                        i.previous(); // rewind the pointer to just before the now-merged segment
                        nextSegment = (SWDownloadSegment) i.next(); // and the next iteration will use the just-merged segment as its basis
                        nMerges++;
                        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                            "Merged completed segment: " + segment );
                    } else {
                        Logger.logMessage( Logger.FINEST, Logger.DOWNLOAD,
                            "Rejected at final check: segments need to be both full or both empty.");
                    }
                    // now release the 2 inner locks
                }}
            }
        }
        if (nMerges > 0)
        {
            Logger.logMessage( Logger.INFO, Logger.DOWNLOAD,
                "Performed " + nMerges + " merges for " + destinationFile);
        }
        logDownloadSegments();
        // save in xml
        SwarmingManager.getInstance().notifyDownloadListChange();
    }
    
    /**
     * Splits the 'length' in bytes from the given segment (ie: the one prior
     * to the iterator's position), starting from 'offset'. 
     * Never! split segments below the data border (files will not get split)
     * When calling this method make sure you locked downloadSegments.
     * Segments are not split on file system.
     * F = From offset - T = To offset.
     *  |---------------------------------| original segment
     *  F------new------T------old--------| split case 1
     *  |---old---F----new-----T---old----| split case 2
     *  |------old------F------new--------T split case 3
     * 
     * @param segment the SWDownloadSegment to split. 
     * @param offset the segment offset (not file offset) to split from. 
     * @param length the length in bytes to split of.
     * @return the new segment splitted of, it is marked in the above picture as
     *         new.
     */
    private SWDownloadSegment splitDownloadSegment1( ListIterator segIter,
        long offset, long length )
    {
        SWDownloadSegment segment = (SWDownloadSegment)segIter.previous();
        if ( ! java.lang.Thread.holdsLock(downloadSegments))
            throw new Error ("This downloadSegments object is not locked by the thread about to perform a split");
        if ( ! java.lang.Thread.holdsLock(segment))
            throw new Error ("This segment is not locked by the thread about to perform a split");
        segIter.next();  // so the iterator sits just after the segment in question, again.
        long segmentLength = segment.getTransferDataSize();
        Logger.logMessage(Logger.FINER, Logger.DOWNLOAD, "Splitting segment " + segment + " at offset " + offset + ", length " + length);
        if ( offset < 0 || length <= 0 || offset + length > segmentLength )
        {
            throw new IndexOutOfBoundsException( "DownloadSegment Size: "
                + segment.getTransferDataSize() + ", Offset: " + offset
                + ", Length: " + length );                
        }
        
        if ( offset == 0 && length == segmentLength )
        {
            return segment;
        }
        
        // for a split we have three cases:
        // |---------------------------------|
        // |------new------|------old--------| case 1
        // |---old---|----new-----|---old----| case 2
        // |------old------|------new--------| case 3
        
        long segmentStart = segment.getStartOffset();
        long segmentEnd = segment.getEndOffset();
        
        SWDownloadSegment returnSegment = null;
        long newSegmentStart;
        long newSegmentLength;
        
        // case 1
        if (offset == 0)
        {
            returnSegment = segment;
            segment.setTransferDataSize( length );
            newSegmentStart = segmentStart + length;
            newSegmentLength = segmentEnd - newSegmentStart;
        }
        // case 2 and case 3
        else 
        {
            segment.setTransferDataSize( offset );
            newSegmentStart = segmentStart + offset;
            newSegmentLength = length;
        }
        
        SWDownloadSegment newSegment = new SWDownloadSegment( this,
            nextSegmentNumber, newSegmentStart, newSegmentLength );
        newSegment.rating = segment.rating;
        nextSegmentNumber ++;
        SWDownloadSegment nextSegment = segment.getNextDownloadSegment();
        newSegment.setNextDownloadSegment( nextSegment );
        segment.setNextDownloadSegment( newSegment );
                    
        segIter.add( newSegment );
        fireDownloadSegmentAdded( segIter.previousIndex() );
        
        if ( returnSegment == null )
        {
            returnSegment = newSegment;
        }

        // check if we need to also create a new end segment
        SWDownloadSegment endSegment = null;
        long newSegmentEnd = newSegmentStart + newSegmentLength;
        if ( newSegmentEnd < segmentEnd )
        {
            endSegment = new SWDownloadSegment( this, nextSegmentNumber,
                newSegmentEnd, segmentEnd - newSegmentEnd );
            endSegment.rating = segment.rating;
            nextSegmentNumber ++;
            nextSegment = newSegment.getNextDownloadSegment();
            endSegment.setNextDownloadSegment( nextSegment );
            newSegment.setNextDownloadSegment( endSegment );

            segIter.add( endSegment );
            fireDownloadSegmentAdded( segIter.previousIndex() );
        }
        logDownloadSegments();
        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD, "Split segment: " + segment
            + " - New Segment: " + newSegment + " - End Segment: " + endSegment );
        // save in xml
        SwarmingManager.getInstance().notifyDownloadListChange();
        return returnSegment;
    }

    /**
     * Splits a given segment into segments and returns the new segement that
     * fits into the given startOffset and endOffset values or null if segement
     * is to small to split.
     * @param segment the segment to split
     * @param startOffset the start offset the new segment should fit into.
     * @param endOffset the end offset the new segment should fit into.
     * @return the new segment that fits into the given startOffset and endOffset
     *         or null if the segment is to small to split.
     */
    /*private SWDownloadSegment splitDownloadSegment( SWDownloadSegment segment,
        long startOffset, long endOffset )
    {
        if ( !isSplitAllowed( segment, startOffset, endOffset ) )
        {
            return null;
        }

        long segmentStart = segment.getTransferStartPosition() + 4 * RESIZE_BOUNDARY_SIZE;
        long segmentEnd = segment.getEndOffset();

        if ( startOffset < segmentStart )
        {
            startOffset = segmentStart;
            endOffset = Math.min( endOffset,
                segmentEnd - 4 * RESIZE_BOUNDARY_SIZE );
        }
        else if ( endOffset > segmentEnd )
        {
            endOffset = segmentEnd;
            startOffset = Math.max( startOffset, segmentStart );
        }
        else
        {
            startOffset = Math.max( startOffset, segmentStart );
            endOffset = Math.min( endOffset,
                segmentEnd - 4 * RESIZE_BOUNDARY_SIZE );
        }

        long newDownloadLength = startOffset - segment.getStartOffset();
        segment.setTransferDataSize( newDownloadLength );

        SWDownloadSegment newSegment = new SWDownloadSegment( this, nextSegmentNumber,
            startOffset, endOffset - startOffset );
        nextSegmentNumber ++;
        SWDownloadSegment nextSegment = segment.getNextDownloadSegment();
        newSegment.setNextDownloadSegment( nextSegment );
        segment.setNextDownloadSegment( newSegment );

        int pos = downloadSegments.size();
        downloadSegments.add( pos, newSegment );
        fireDownloadSegmentAdded( pos );

        // check if we need to also create a new end segment
        SWDownloadSegment endSegment = null;
        if ( endOffset < segmentEnd )
        {
            endSegment = new SWDownloadSegment( this, nextSegmentNumber,
                endOffset, segmentEnd - endOffset );
            nextSegmentNumber ++;
            nextSegment = newSegment.getNextDownloadSegment();
            endSegment.setNextDownloadSegment( nextSegment );
            newSegment.setNextDownloadSegment( endSegment );

            pos = downloadSegments.size();
            downloadSegments.add( pos, endSegment );
            fireDownloadSegmentAdded( pos );
        }


        logDownloadSegments();
        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD, "Splitted segment: " + segment
            + " - New Segment: " + newSegment + " - End Segment: " + endSegment );
        // save in xml
        SwarmingManager.getInstance().triggerSaveDownloadList();
        return newSegment;
    }
    
    private boolean isSplitAllowed( SWDownloadSegment segment, long startOffset,
        long endOffset )
    {
        long segmentStart = segment.getTransferStartPosition();
        long segmentEnd = segment.getEndOffset();

        if ( startOffset < segmentStart )
        {
            startOffset = segmentStart;
            endOffset = Math.min( endOffset,
                segmentEnd - 4 * RESIZE_BOUNDARY_SIZE );
        }
        else if ( endOffset > segmentEnd )
        {
            endOffset = segmentEnd;
            startOffset = Math.max( startOffset,
                segmentStart + 4 * RESIZE_BOUNDARY_SIZE );
        }
        else
        {
            startOffset = Math.max( startOffset,
                segmentStart + 4 * RESIZE_BOUNDARY_SIZE );
            endOffset = Math.min( endOffset,
                segmentEnd - 4 * RESIZE_BOUNDARY_SIZE );
        }

        if ( (endOffset - startOffset) >= 4 * RESIZE_BOUNDARY_SIZE )
        {
            return true;
        }
        else
        {
            return false;
        }
    }
    
    /**
     * Splits a given segment into two segments and returns the new segement or
     * null if segement is to small to split.
     */
    /*private SWDownloadSegment splitDownloadSegment( SWDownloadSegment segment )
    {
        if ( !isSplitAllowed( segment ) )
        {
            return null;
        }

        long lengthDownloaded = segment.getTransferredDataSize();
        long freeLength = segment.getTransferDataSizeLeft();
        long startPos = segment.getStartOffset();

        long newDownloadLength = (long)Math.ceil( lengthDownloaded + freeLength / 2.0 );
        segment.setTransferDataSize( newDownloadLength );

        long newStartPos = startPos + newDownloadLength;
        SWDownloadSegment newSegment = new SWDownloadSegment( this, nextSegmentNumber,
            newStartPos, (long)Math.floor( freeLength / 2.0 ) );
        nextSegmentNumber ++;
        SWDownloadSegment nextSegment = segment.getNextDownloadSegment();
        newSegment.setNextDownloadSegment( nextSegment );
        segment.setNextDownloadSegment( newSegment );

        int pos = downloadSegments.size();
        downloadSegments.add( pos, newSegment );
        fireDownloadSegmentAdded( pos );

        logDownloadSegments();
        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD, "Splitted segment: " + segment
            + " - New Segment: " + newSegment );
        // save in xml
        SwarmingManager.getInstance().triggerSaveDownloadList();
        return newSegment;
    }
    
    private boolean isSplitAllowed( SWDownloadSegment segment )
    {
        long freeLength = segment.getTransferDataSizeLeft();
        // if the new segments would be smaller then 4 times RESIZE_BOUNDARY
        // we dont want to split.
        if ( freeLength < 4 * RESIZE_BOUNDARY_SIZE )
        {
            return false;
        }
        return true;
    }
    
    **/
    
    private void initAltLocContainers()
    {
        goodAltLocContainer = new AlternateLocationContainer( fileURN );
        badAltLocContainer = new AlternateLocationContainer( fileURN );
    }

    private void createDownloadSegments()
    {
        nextSegmentNumber = 0;
        downloadSegments = new ArrayList();
        SWDownloadSegment segment = new SWDownloadSegment( this,
            nextSegmentNumber, 0, fileSize );
        downloadSegments.add( segment );
        nextSegmentNumber ++;
        logDownloadSegments();
    }
    
    // new JAXB way
    public XJBSWDownloadFile createXJBSWDownloadFile()
        throws JAXBException
    {
        XJBSWDownloadFile xjbFile = ObjectFactory.createXJBSWDownloadFile();
        xjbFile.setLocalFileName( destinationFile.getName() );
        xjbFile.setFileSize( fileSize );
        xjbFile.setSearchTerm( researchSetting.getSearchTerm() );
        xjbFile.setCreatedTime( createdDate.getTime() );
        xjbFile.setModifiedTime( modifiedDate.getTime() );
        if ( fileURN != null )
        {
            xjbFile.setFileURN( fileURN.getAsString() );
        }
        xjbFile.setStatus( status );
        synchronized ( candidatesLock )
        {
            List list = xjbFile.getCandidateList();
            Iterator iterator = goodCandidatesList.iterator();
            while( iterator.hasNext() )
            {
                SWDownloadCandidate candidate = (SWDownloadCandidate)iterator.next();
                XJBSWDownloadCandidate xjbCandidate =
                    candidate.createXJBSWDownloadCandidate();
                list.add( xjbCandidate );
            }
            iterator = badCandidatesList.iterator();
            while( iterator.hasNext() )
            {
                SWDownloadCandidate candidate = (SWDownloadCandidate)iterator.next();
                if ( candidate.getStatus() == STATUS_CANDIDATE_IGNORED )
                {
                    continue;
                }
                XJBSWDownloadCandidate xjbCandidate =
                    candidate.createXJBSWDownloadCandidate();
                list.add( xjbCandidate );
            }
        }

        synchronized ( downloadSegments )
        {
            Iterator iterator = downloadSegments.iterator();
            List list = xjbFile.getSegmentList();
            while( iterator.hasNext() )
            {
                SWDownloadSegment segment = (SWDownloadSegment)iterator.next();
                synchronized (segment) 
                {
                    XJBSWDownloadSegment xjbSegement =
                        segment.createXJBSWDownloadSegment();
                    list.add( xjbSegement );
                }
            }
        }
        return xjbFile;
    }

    // XJB way
    private void createDownloadSegments( XJBSWDownloadFile xjbFile )
    {
        // clean segments
        downloadSegments = new ArrayList();

        XJBSWDownloadSegment xjbSegment;
        SWDownloadSegment segment;
        SWDownloadSegment lastSegment = null;
        nextSegmentNumber = 0;

        ArrayList tempSegmentList = new ArrayList();
        Iterator iterator = xjbFile.getSegmentList().iterator();
        while ( iterator.hasNext() )
        {// copy segments into a sorted List... then link segments correctly...
            tempSegmentList.add( iterator.next() );
        }
        Collections.sort( tempSegmentList, new SWDownloadSegmentComparatorByStartPosition() );

        // link segments in the correct sorted order.
        iterator = tempSegmentList.iterator();
        while ( iterator.hasNext() )
        {
            xjbSegment = (XJBSWDownloadSegment) iterator.next();
            segment = new SWDownloadSegment( this, xjbSegment );
            downloadSegments.add( segment );

            if ( lastSegment != null )
            {
                lastSegment.setNextDownloadSegment( segment );
            }
            // update nextSegmentNumber to the highest num
            nextSegmentNumber = Math.max( xjbSegment.getSegmentNumber() + 1,
                nextSegmentNumber );
            lastSegment = segment;
        }
        nextSegmentNumber ++;
        logDownloadSegments();
    }

    // xjb way
    private void createDownloadCandidates( XJBSWDownloadFile xjbFile )
    {
        synchronized( candidatesLock )
        {
            // clean candidates
            allCandidatesList.clear();
            goodCandidatesList.clear();
            badCandidatesList.clear();

            // add xml definitions...
            XJBSWDownloadCandidate xjbCandidate;
            SWDownloadCandidate candidate;
            Iterator iterator = xjbFile.getCandidateList().iterator();
            while( iterator.hasNext() )
            {
                try
                {
                    xjbCandidate = (XJBSWDownloadCandidate)iterator.next();
                    candidate = new SWDownloadCandidate( xjbCandidate, this );
                    Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                        "Adding download candidate " + candidate );
                    
                    int pos = allCandidatesList.size();
                    allCandidatesList.add( pos, candidate );
                    fireDownloadCandidateAdded( pos );
                    if ( xjbCandidate.getConnectionFailedRepetition() >= BAD_CANDIDATE_CONNECTION_TRIES )
                    {
                        badCandidatesList.add( candidate );
                    }
                    else
                    {
                        goodCandidatesList.add( candidate );
                    }
                }
                catch ( MalformedHostAddressException exp )
                {
                    Logger.logWarning( exp );
                }
                catch ( Exception exp )
                {// catch all exception in case we have an error in the XML
                    Logger.logError( exp,
                        "Error loading a download candidate from XML." );
                }
            }
        }

        SwarmingManager.getInstance().notifyWaitingWorkers();
    }

    private void logDownloadSegments()
    {/*
        // TODO use logger
        Logger.logMessage(Logger.FINE, Logger.DOWNLOAD, "---------------------------");
        Logger.logMessage(Logger.FINE, Logger.DOWNLOAD,  toString() ); 
        Iterator iterator = downloadSegments.iterator();
        while ( iterator.hasNext() )
        {
            SWDownloadSegment seg = (SWDownloadSegment)iterator.next();
            Logger.logMessage( Logger.FINE, Logger.DOWNLOAD, seg );
        }
        Logger.logMessage(Logger.FINE, Logger.DOWNLOAD, "---------------------------"); */
    }

    ///////////////////// START event handling methods ////////////////////////

    private EventListenerList listenerList = new EventListenerList();

    public void addDownloadCandidatesChangeListener(
        DownloadCandidatesChangeListener listener )
    {
        listenerList.add( DownloadCandidatesChangeListener.class, listener );
    }

    public void removeDownloadCandidatesChangeListener(
        DownloadCandidatesChangeListener listener )
    {
        listenerList.remove( DownloadCandidatesChangeListener.class, listener );
    }

    private void fireDownloadCandidateChanged( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.getListeners(
                    DownloadCandidatesChangeListener.class );
                DownloadCandidatesChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (DownloadCandidatesChangeListener)listeners[ i ];
                    listener.downloadCandidateChanged( position );
                }
            }
        });
    }

    private void fireDownloadCandidateAdded( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.getListeners(
                    DownloadCandidatesChangeListener.class );
                DownloadCandidatesChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (DownloadCandidatesChangeListener)listeners[ i ];
                    listener.downloadCandidateAdded( position );
                }
            }
        });
    }

    private void fireDownloadCandidateRemoved( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.getListeners(
                    DownloadCandidatesChangeListener.class );
                DownloadCandidatesChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (DownloadCandidatesChangeListener)listeners[ i ];
                    listener.downloadCandidateRemoved( position );
                }
            }
        });
    }

    public void fireDownloadCandidateChanged( SWDownloadCandidate candidate )
    {
        int position = allCandidatesList.indexOf( candidate );
        if ( position >= 0 )
        {
            fireDownloadCandidateChanged( position );
        }
    }




    public void addDownloadSegmentChangeListener(
        DownloadSegmentsChangeListener listener )
    {
        listenerList.add( DownloadSegmentsChangeListener.class, listener );
    }

    public void removeDownloadSegmentChangeListener(
        DownloadSegmentsChangeListener listener )
    {
        listenerList.remove( DownloadSegmentsChangeListener.class, listener );
    }

    private void fireDownloadSegmentChanged( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.getListeners(
                    DownloadSegmentsChangeListener.class );
                DownloadSegmentsChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (DownloadSegmentsChangeListener)listeners[ i ];
                    listener.downloadSegmentChanged( position );
                }
            }
        });
    }

    private void fireDownloadSegmentAdded( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.getListeners(
                    DownloadSegmentsChangeListener.class );
                DownloadSegmentsChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (DownloadSegmentsChangeListener)listeners[ i ];
                    listener.downloadSegmentAdded( position );
                }
            }
        });
    }

    private void fireDownloadSegmentRemoved( final int position )
    {
        // invoke update in event dispatcher
        AsynchronousDispatcher.invokeLater(
        new Runnable()
        {
            public void run()
            {
                Object[] listeners = listenerList.getListeners(
                    DownloadSegmentsChangeListener.class );
                DownloadSegmentsChangeListener listener;
                // Process the listeners last to first, notifying
                // those that are interested in this event
                for ( int i = listeners.length - 1; i >= 0; i-- )
                {
                    listener = (DownloadSegmentsChangeListener)listeners[ i ];
                    listener.downloadSegmentRemoved( position );
                }
            }
        });
    }

    public void fireDownloadSegmentChanged( SWDownloadSegment segment )
    {
        int position = downloadSegments.indexOf( segment );
        if ( position >= 0 )
        {
            fireDownloadSegmentChanged( position );
        }
    }
    ///////////////////// END event handling methods ////////////////////////
    private static class SWDownloadSegmentComparatorByStartPosition implements Comparator
    {
        public int compare( Object obj1, Object obj2 )
        {
            XJBSWDownloadSegment segment1 = (XJBSWDownloadSegment)obj1;
            XJBSWDownloadSegment segment2 = (XJBSWDownloadSegment)obj2;
            long diff = segment1.getStartPosition() - segment2.getStartPosition();
            // this is done to maintain long/int type size safety...
            if ( diff < 0 )
            {
                return -1;
            }
            else if ( diff > 0 )
            {
                return 1;
            }
            else
            {
                return 0;
            }
        }
    }

    private static class SWDownloadSegmentComparatorByPosition implements Comparator
    {
        public int compare( Object obj1, Object obj2 )
        {
            SWDownloadSegment segment1 = (SWDownloadSegment)obj1;
            SWDownloadSegment segment2 = (SWDownloadSegment)obj2;

            long diff = segment1.getStartOffset() - segment2.getStartOffset(); // sort by natural order when rating is the same
            // this is done to maintain long/int type size safety...
            if ( diff < 0 )
            {
                return -1;
            }
            else if ( diff > 0 )
            {
                return 1;
            }
            else
            {
                return 0;
            }
        }
    }

    /*
     * This will sort by rating BUT any segments starting before the previewLength
     * will get priority, regardless of their rating. By setting this value to 0
     * it effectively disables such preferential treatment.
     */
    private static class SWDownloadSegmentComparatorByRating implements Comparator
    {
        private long previewLength;

        SWDownloadSegmentComparatorByRating(long previewLength)
        {
            super ();
            this.previewLength = previewLength;
        }
        public int compare( Object obj1, Object obj2 )
        {
            SWDownloadSegment segment1 = (SWDownloadSegment)obj1;
            SWDownloadSegment segment2 = (SWDownloadSegment)obj2;
            long r1 = segment1.rating.getValue();
            long r2 = segment2.rating.getValue();
            long diff = r1 - r2;

            // if same rating OR both segments are complete, sort by start offset
            if ( diff == 0 || segment1.getTransferDataSizeLeft() == 0 || segment2.getTransferDataSizeLeft() == 0
                    || segment1.getStartOffset() < previewLength || segment2.getStartOffset() < previewLength)
            {
                diff = segment1.getStartOffset() - segment2.getStartOffset(); // sort by natural order when rating is the same
            }
            // this is done to maintain long/int type size safety...
            if ( diff < 0 )
            {
                return -1;
            }
            else if ( diff > 0 )
            {
                return 1;
            }
            else
            {
                return 0;
            }
        }
    }

    /*public static void main( String[] args )
    {
        long fileSize = 1000000;
        // Test segment splitting and merging...
        SWDownloadFile file = new SWDownloadFile( "test", "test", fileSize, false );
        SWDownloadSegment segment = new SWDownloadSegment( file,
            0, 0, fileSize );
        file.downloadSegments.add( segment );
        System.out.println( "Before split: " + segment );
        SWDownloadSegment newSegment = file.splitDownloadSegment( segment );
        System.out.println( "After split: " + segment + " - " + newSegment );
        segment.mergeWithNextSegment();
        System.out.println( "After merge: " + segment  );

        fileSize = 1000001;
        // Test segment splitting and merging...
        file = new SWDownloadFile( "test", "test", fileSize, false );
        segment = new SWDownloadSegment( file, 0, 0, fileSize );
        file.downloadSegments.add( segment );
        System.out.println( "Before split: " + segment );
        newSegment = file.splitDownloadSegment( segment );
        System.out.println( "After split: " + segment + " - " + newSegment );
        segment.mergeWithNextSegment();
        System.out.println( "After merge: " + segment  );

        System.exit( 0 );
    }*/
}
