/*
 *  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: SWDownloadCandidate.java,v 1.39 2004/09/23 22:07:10 nxf Exp $
 */
package phex.download.swarming;

import java.util.*;

import javax.xml.bind.JAXBException;

import org.apache.crimson.util.XmlChars;

import phex.common.*;
import phex.download.*;
import phex.host.*;
import phex.http.*;
import phex.msg.GUID;
import phex.query.QueryHitHost;
import phex.utils.Logger;
import phex.xml.*;

/**
 * A representation of a download candidate. A download candidate contains all
 * information required for a given endpoint that can offer us data for download.
 * <p>
 * 
 */
public class SWDownloadCandidate implements IDownloadCandidate, SWDownloadConstants
{
    /**
     * The time this download candidate was first added to the list of download
     * candidates of the corresponding download file. This is the first creation
     * time of the SWDownloadCandidate object.
     */
    //private Date firstSeenDate;
    
    /**
     * The time a connection to this download candidate could be established
     * successful.
     */
    //private Date lastConnectionDate;
        
    /**
     * The number of failed connection tries to this download candidate, since
     * the last successful connection.
     */
    private int failedConnectionTries;
    
    /** 
     * The GUID of the client.
     */
    private GUID guid;

    /**
     * The file index of the file at the download candidate.
     */
    private long fileIndex;

    /**
     * The name of the file at the download candidate.
     */
    private String fileName;

    /**
     * Rate at which the last segment was transferred
     */
    private int lastTransferRateBPS;

    /**
     * The resource URN of the file.
     */
    private URN resourceURN;

    /**
     * The host address of the download candidate.
     */
    private HostAddress hostAddress;

    /**
     * The status of the download.
     */
    private ShortObj statusObj;

    /**
     * The last error status of the download to track status changes.
     */
    private short errorStatus;

    /**
     * The time after which the current status times out and things continue.
     */
    private long statusTimeout;

    /**
     * Counts the number of times the status keeps repeating.
     */
    private int errorStatusRepetition;

    /**
     * The vendor of the client running at the candidate.
     */
    private String vendor;

    /**
     * Defines if a push is needed for this candidate.
     */
    private boolean isPushNeeded;

    /**
     * Defines if the candidate supports chat connections.
     */
    private boolean isChatSupported;

    /**
     * The QHD calculated host rating.
     */
    private Short hostRating;

    /**
     * The available range set of the candidate file.
     */
    private HTTPRangeSet availableRangeSet;

    /**
     * The time the available range was last updated.
     */
    private long availableRangeSetTime = 0;

    /**
     * The download file that this segment belongs to.
     */
    private SWDownloadFile downloadFile;

    /**
     * The download segment that is currently assiciated by this download
     * candidate or null if no association exists.
     */
    private SWDownloadSegment downloadSegment;

    /**
     * The parameters that are available in case the candidate is remotly queueing
     * our download request. This is referenced for information purpose only.
     */
    private XQueueParameters xQueueParameters;
    
    /**
     * This is a Map holding all AltLocs already send to this candidate during
     * this session. It is used to make sure the same AltLocs are not send twice
     * to the same candidate. The list is lazy initialized on first access.
     */
    private Set sendAltLocSet;
    
    public SWDownloadCandidate( RemoteFile remoteFile,
        SWDownloadFile aDownloadFile )
    {
        availableRangeSet = null;
        downloadFile = aDownloadFile;
        fileIndex = remoteFile.getFileIndex();
        fileName = remoteFile.getFilename();
        resourceURN = remoteFile.getURN();
        guid = remoteFile.getRemoteClientID();
        QueryHitHost qhHost = remoteFile.getQueryHitHost();
        vendor = qhHost.getVendor();
        isPushNeeded = qhHost.isPushNeeded();
        hostRating = qhHost.getHostRatingObject();
        hostAddress = remoteFile.getHostAddress();
        isChatSupported = qhHost.isChatSupported();
        statusObj = new ShortObj( STATUS_CANDIDATE_WAITING );
        lastTransferRateBPS = 0;
    }

    public SWDownloadCandidate( HostAddress aHostAddress, long aFileIndex,
        String aFileName, Short hostRating, URN aResourceURN,
        SWDownloadFile aDownloadFile )
    {
        availableRangeSet = null;
        downloadFile = aDownloadFile;
        fileIndex = aFileIndex;
        fileName = aFileName;
        resourceURN = aResourceURN;
        guid = null;
        vendor = null;
        isPushNeeded = false;
        // assume chat is supported but we dont know...
        isChatSupported = true;
        this.hostRating = hostRating;
        hostAddress = aHostAddress;
        statusObj = new ShortObj( STATUS_CANDIDATE_WAITING );
        lastTransferRateBPS = 0;
        /*setAvailableRangeSet(new HTTPRangeSet(0, downloadFile.getTotalDataSize())); */
    }

    public SWDownloadCandidate( XJBSWDownloadCandidate xjbCandidate,
        SWDownloadFile aDownloadFile ) throws MalformedHostAddressException
    {
        availableRangeSet = null;
        downloadFile = aDownloadFile;
        fileIndex = xjbCandidate.getFileIndex();
        fileName = xjbCandidate.getFileName();
        lastTransferRateBPS = 0;
        /* setAvailableRangeSet(new HTTPRangeSet(0, downloadFile.getTotalDataSize())); */

        String guidHexStr = xjbCandidate.getGUID();
        if ( guidHexStr != null )
        {
            guid = new GUID( guidHexStr );
        }
        vendor = xjbCandidate.getVendor();
        isPushNeeded = xjbCandidate.isPushNeeded();
        isChatSupported = xjbCandidate.isChatSupported();

        hostRating = new Short( xjbCandidate.getRating() );

        try
        {
            hostAddress = new HostAddress( xjbCandidate.getRemoteHost() );
        }
        catch ( MalformedHostAddressException exp )
        {
            Logger.logWarning( Logger.DOWNLOAD,
                "Malformed host address in: " + aDownloadFile.toString() +
                " - " + xjbCandidate.getRemoteHost() + " - " + this.toString() );
            Logger.logWarning( Logger.DOWNLOAD, exp );
            throw exp;
        }
        resourceURN = aDownloadFile.getFileURN();
        
        if ( xjbCandidate.getConnectionFailedRepetition() > 0 )
        {
            errorStatus = STATUS_CANDIDATE_CONNECTION_FAILED;
            errorStatusRepetition = xjbCandidate.getConnectionFailedRepetition();
            failedConnectionTries = errorStatusRepetition;
        }
        
        statusObj = new ShortObj( STATUS_CANDIDATE_WAITING );
    }

    /**
     * Recalcuate the 'rating' for this connection and return it.
     *
     * At the moment this is simply the BPS the last segment was done in.
     */
    private long getRating()
    {
        if ( lastTransferRateBPS == 0 ) // is this a good way of knowing we only just connected?
            return WORST_RATING;
        else
            return lastTransferRateBPS;
    }
    
    public long getSpeed()
    {
        return lastTransferRateBPS;
    }

    /**
     * Updates the rating of the rangeset for this candidate.
     *
     */
    protected void updateRating()
    {
        if ( availableRangeSet == null ) return;
        Logger.logMessage (Logger.INFO, Logger.DOWNLOAD, "Updating ratings for " + this);
        availableRangeSet.setRating(new CandidateRating(getRating()));
        synchronized (downloadFile.rangeSetList)
        {
            Iterator i = downloadFile.rangeSetList.iterator();
            while ( i.hasNext() )
            {
                Object range = i.next();
                Logger.logMessage (Logger.FINER, Logger.DOWNLOAD, range);
            }
        }
    }

    /**
     * Makes sure the rating for this candidate is correct before being thrown out.
     * I know there's no guarantee this will be called at any particular time, but
     * this is not a critical action. The worst effect of this not being called is
     * that a few errant range entries affect segment prioritisation.
     */
    protected void finalize()
    {
        setAvailableRangeSet(null);
    }

    /**
     * Returns the HostAddress of the download candidate
     */
    public HostAddress getHostAddress()
    {
        return hostAddress;
    }

    /**
     * Returns the name of the file at the download candidate.
     */
    public String getFileName()
    {
        return fileName;
    }

    /**
     * Returns the resource URN of the file at the download candidate.
     */
    public URN getResourceURN()
    {
        return resourceURN;
    }

    /**
     * Returns the GUID of the candidate for push.
     */
    public GUID getGUID()
    {
        return guid;
    }

    /**
     * Returns the file index of the file at the download candidate.
     */
    public long getFileIndex()
    {
        return fileIndex;
    }

    /**
     * Returns the time in millis until the the status timesout.
     */
    public long getStatusTimeLeft()
    {
        long timeLeft = statusTimeout - System.currentTimeMillis();
        if ( timeLeft < 0 )
        {
            timeLeft = 0;
        }
        return timeLeft;
    }

    /**
     * Returns the current status of the candidate.
     */
    public short getStatus()
    {
        return statusObj.getValue();
    }
    
    public ShortObj getStatusObj()
    {
        return statusObj;
    }
    
    /**
     * Indicates how often the last error status repeated. The last error status
     * must must not be the current status, but is the current status in case the
     * current status is a error status.
     * @return how often the last error status repeated
     */
    /*public int getErrorStatusRepetition()
    {
        return errorStatusRepetition;
    }*/
    
    /**
     * Returns the number of failed connection tries since the last successful
     * connection.
     * @return the number of failed connection tries since the last successful
     * connection.
     */
    public int getFailedConnectionTries()
    {
        return failedConnectionTries;
    }

    /**
     * The download candidate vendor.
     * @return
     */
    public String getVendor()
    {
        return vendor;
    }

    public void setVendor( String aVendor )
    {
        if ( vendor == null || !vendor.equals( aVendor ) )
        {
            // verify characters - this is used to remove invalid xml characters
            for ( int i = 0; i < aVendor.length(); i++ )
            {
                if ( !XmlChars.isChar( aVendor.charAt( i ) ) )
                {
                    return;
                }
            }
            vendor = aVendor;
            downloadFile.fireDownloadCandidateChanged( this );
        }
    }

    public void updateXQueueParameters( XQueueParameters newXQueueParameters )
    {
        if ( xQueueParameters == null )
        {
            xQueueParameters = newXQueueParameters;
        }
        else
        {
            xQueueParameters.update( newXQueueParameters );
        }
    }

    public XQueueParameters getXQueueParameters()
    {
        return xQueueParameters;
    }

    public boolean isPushNeeded()
    {
        return isPushNeeded;
    }

    public boolean isChatSupported()
    {
        return isChatSupported;
    }

    public void setChatSupported( boolean state )
    {
        isChatSupported = state;
    }

    public short getHostRating()
    {
        return hostRating.shortValue();
    }

    public Short getHostRatingObject()
    {
        return hostRating;
    }
    
    /**
     * Returns true if the candidate is remotly queued, false otherwise.
     * @return true is the candidate is remotly queued, false otherwise.
     */
    public boolean isRemotlyQueued()
    {
        return statusObj.value == STATUS_CANDIDATE_REMOTLY_QUEUED;
    }
    
    /**
     * Returns true if the candidate is downloading, false otherwise.
     * @return true is the candidate is downloading, false otherwise.
     */
    public boolean isDownloading()
    {
        return statusObj.value == STATUS_CANDIDATE_DOWNLOADING;
    }
    
    /**
     * Returns the list of alt locs already send to this connection. 
     * @return the list of alt locs already send to this connection.
     */
    public Set getSendAltLocsSet()
    {
        if ( sendAltLocSet == null )
        {// TODO1 use something like a LRUMap. But current LRUMap uses maxSize
         // as initial hash size. This would be much to big in most cases!
         // Currently this HashSet has no size boundry. We would need our own
         // LRUMap implementation with a low initial size and a different max size.
            sendAltLocSet = new HashSet();
        }
        return sendAltLocSet;
    }

    /**
     * Sets the available range set.
     * @param availableRangeSet the available range set.
     */
    public void setAvailableRangeSet( HTTPRangeSet availableRangeSet )
    {
        // also register this with the SWDownloadFile object, after making
        // sure that any previous rangeset has been removed
        if ( availableRangeSet == null && this.availableRangeSet == null ) return; // nothing to do, both null.

        if ( this.availableRangeSet != null) // remove old entries from the linked list
        {
            synchronized (downloadFile.rangeSetList)
            {
                Logger.logMessage( Logger.INFO, Logger.DOWNLOAD, "Removing " + downloadFile.rangeSetList.size() + " old entries");
                downloadFile.rangeSetList.removeAll(this.availableRangeSet.getRangeSet());
                /*Iterator i = this.availableRangeSet.getRangeSet().iterator();
                while ( i.hasNext() )
                        downloadFile.rangeSetList.remove(i.next());
                */
            }
        }

        this.availableRangeSet = availableRangeSet;
        if ( this.availableRangeSet == null) return;

        Logger.logMessage( Logger.INFO, Logger.DOWNLOAD, "Adding a new rangeset for " + downloadFile.getDestinationFileName() + ": " + availableRangeSet);
        synchronized (downloadFile.rangeSetList)
        {
            Logger.logMessage( Logger.INFO, Logger.DOWNLOAD,
                "Rangeset had " + downloadFile.rangeSetList.size() + " members before addition of new ranges.");
            downloadFile.rangeSetList.addAll( availableRangeSet.getRangeSet() );
            Logger.logMessage( Logger.INFO, Logger.DOWNLOAD,
                "Rangeset now has " + downloadFile.rangeSetList.size() + " members.");
        }
        availableRangeSetTime = System.currentTimeMillis();
        updateRating();
    }

    /**
     * Returns the available range set or null if not set.
     * @return the available range set or null if not set.
     */
    public HTTPRangeSet getAvailableRangeSet( )
    {
        if ( System.currentTimeMillis() >
            availableRangeSetTime + AVAILABLE_RANGE_SET_TIMEOUT )
        {
            //setAvailableRangeSet(null);
            
            // TODO: should this be 0 or 1??
            setAvailableRangeSet(new HTTPRangeSet(0, downloadFile.getTotalDataSize()));
        }
        return availableRangeSet;
    }

    public boolean equals( Object obj )
    {
        if ( obj instanceof SWDownloadCandidate )
        {
            return equals( (SWDownloadCandidate) obj );
        }
        return false;
    }

    public boolean equals( SWDownloadCandidate candidate )
    {
        return hostAddress.equals( candidate.hostAddress );
    }

    /**
     * Sets the status of the candidate and fulfills the required actions
     * necessary for that status. E.g. setting the statusTime.
     */
    public void setStatus( short newStatus )
    {
        setStatus( newStatus, -1 );
    }
    
    /**
     * Sets the status of the candidate and fulfills the required actions
     * necessary for that status. E.g. setting the statusTime.
     * @param statusSeconds the time in seconds the status should last.
     */
    public void setStatus( short newStatus, int statusSeconds )
    {
        // dont care for same status
        if ( statusObj.value == newStatus )
        {
            return;
        }
        statusObj.value = newStatus;
        long newStatusTimeout;
        statusTimeout = newStatusTimeout = System.currentTimeMillis();
        if ( statusObj.value != STATUS_CANDIDATE_DOWNLOADING )
        {
                availableRangeSet.setRating(new CandidateRating(WORST_RATING)); // disable participating in segment rating
        }
        switch( statusObj.value )
        {
            case STATUS_CANDIDATE_BAD:
                newStatusTimeout += BAD_CANDIDATE_STATUS_TIMEOUT;
                break;
            case STATUS_CANDIDATE_IGNORED:
                newStatusTimeout = Long.MAX_VALUE;
                break;
            case STATUS_CANDIDATE_CONNECTING:
                //connectionTries ++;
                break;
            case STATUS_CANDIDATE_CONNECTION_FAILED:
                failedConnectionTries ++;
                if ( failedConnectionTries >= IGNORE_CANDIDATE_CONNECTION_TRIES )
                {// we have tried long enough to connect to this candidate, ignore
                 // it for the future (causes delete after session).
                    downloadFile.markCandidateBad( this, true );
                    // markCandidateBad updates the statusTimeout, this value is
                    // reset here to not overwrite it...
                    newStatusTimeout = statusTimeout;
                    break;
                }
                else if ( failedConnectionTries >= BAD_CANDIDATE_CONNECTION_TRIES )
                {
                    // we dont remove candidates but put them into a bad list.
                    // once we see a new X-Alt the candidate is valid again.
                    // candidates might go into the bad list quickly.
                    // when no "good" candidates are avaiable we might also try additional
                    // bad list connects, every 3 hours or so...
                    downloadFile.markCandidateBad( this, false );
                    // markCandidateBad updates the statusTimeout, this value is
                    // reset here to not overwrite it...
                    newStatusTimeout = statusTimeout;
                    break;
                }
                newStatusTimeout += calculateConnectionFailedTimeout();
                break;
            case STATUS_CANDIDATE_REQUESTING:
                failedConnectionTries = 0;
                break;
            case STATUS_CANDIDATE_BUSY:
            case STATUS_CANDIDATE_RANGE_UNAVAILABLE:
            case STATUS_CANDIDATE_REMOTLY_QUEUED:
                failedConnectionTries = 0;
                if ( statusSeconds > 0 )
                {
                    newStatusTimeout += statusSeconds * 1000;
                }
                else
                {
                    newStatusTimeout += determineErrorStatusTimeout( statusObj.value );
                }
                break;
            case STATUS_CANDIDATE_PUSH_REQUEST:
                newStatusTimeout += ServiceManager.sCfg.mPushTransferTimeout;
                break;
            case STATUS_CANDIDATE_DOWNLOADING:
                // clear the current error status.
                errorStatus = STATUS_CLEARED;
                failedConnectionTries = 0;
                break;
            
        }
        Logger.logMessage( Logger.FINEST, Logger.DOWNLOAD, 
            "Setting status to " + newStatus + " and raise timeout from "
            + statusTimeout + " to " + newStatusTimeout + ".");
        statusTimeout = newStatusTimeout;
        downloadFile.fireDownloadCandidateChanged( this );
    }
     
    /**
     * Calculates the timeout between the last failed connection and the next
     * connection try. 
     * @return the number of millies to wait till the status expires.
     */
    private long calculateConnectionFailedTimeout()
    {   
        // when connection time is 2 it gives a sequence of
        // 2, 4, 8, 16, 32, 64, 128, 128, 128, 128
        // this would cause the values to double on each repetition.
        return CONNECTION_FAILED_STEP_TIME * (long)Math.pow( 2,
            Math.min( failedConnectionTries - 1, 7 ) );
    }

    /**
     * Maintains error status tracking. This is needed to track repeting errors
     * that will be handled by flexible status timeouts.
     * @returns the livetime of the status.
     */
    private long determineErrorStatusTimeout( short aErrorStatus )
    {
        if ( errorStatus == aErrorStatus )
        {
            errorStatusRepetition ++;
        }
        else
        {
            errorStatus = aErrorStatus;
            errorStatusRepetition = 0;
        }
        switch( errorStatus )
        {
            case STATUS_CANDIDATE_BUSY:
                // we can add here a step thing for each retry with a top limit
                // and a shorter start sleep time but currently we keep it like this.
                return HOST_BUSY_SLEEP_TIME;
                    /*( statusRepetition + 1 ) * */
            case STATUS_CANDIDATE_RANGE_UNAVAILABLE:
                // when step time is 1 it gives a sequence of
                // 1, 2, 4, 8, 16, 32...
                // this would cause the values to double on each repetition.
                return RANGE_UNAVAILABLE_STEP_TIME * (long)Math.pow( 2, errorStatusRepetition );
            case STATUS_CANDIDATE_REMOTLY_QUEUED:
                if ( xQueueParameters == null )
                {
                    return 0;
                }
                else
                {
                    return xQueueParameters.getRequestSleepTime();
                }
            default:
                Logger.logWarning( Logger.DOWNLOAD, "Unknown error status: " + errorStatus );
                return 0;
        }
    }

    /**
     * Manualy forces a connection retry. This sets the candidate to QUEUED when
     * it is the state BUSY, RANGE_UNAVAILABLE or CONNECTION_FAILED.
     * It will also decrease the errorStatusRepetition to not let the timeout
     * increase when the errorStatus remains.
     * The method should only be called from user triggered GUI action.
     */
    public void manualConnectionRetry()
    {
        if ( statusObj.value != STATUS_CANDIDATE_BUSY &&
             statusObj.value != STATUS_CANDIDATE_CONNECTION_FAILED &&
             statusObj.value != STATUS_CANDIDATE_RANGE_UNAVAILABLE &&
             statusObj.value != STATUS_CANDIDATE_BAD &&
             statusObj.value != STATUS_CANDIDATE_IGNORED )
        {
            return;
        }
        setStatus( STATUS_CANDIDATE_WAITING );
        SwarmingManager.getInstance().notifyWaitingWorkers();
    }
        
    /**
     * Returns if the candidate is able to be allocated. To be allocated a
     * candidate must not have a worker assigned and the nextRetryTime must be
     * passed.
     * @param currentTime is given for performance reason. We don't need to
     *        get the system time so often.
     */
    public boolean isAbleToBeAllocated( )
    {
        // Do not allow allocation if it's too slow!
        if (lastTransferRateBPS < ServiceManager.sCfg.minimumAllowedTransferRate
            && lastTransferRateBPS > 0)
        {
            Logger.logMessage(Logger.FINE, Logger.DOWNLOAD, "Refusing candidate allocation as last transfer rate was only " + lastTransferRateBPS + " bps");
            return false;
        }
        long currentTime = System.currentTimeMillis();
        return statusTimeout <= currentTime;
    }

    public void associateDownloadSegment( SWDownloadSegment aSegment )
    {
        if (availableRangeSet != null)
        {
            Logger.logMessage(Logger.INFO, Logger.DOWNLOAD, "Associating a segment, so updating ratings to " + lastTransferRateBPS);
            updateRating();
        }
        downloadSegment = aSegment;
    }

    /**
     * Returns the preferred (ie: largest) segment size to use for this candidate.
     * This will be DEFAULT_SEGMENT_SIZE initially, but will then be calculated so that if
     * the transfer rate for the previous segment were maintained, the next segment
     * would take DEFAULT_SEGMENT_TIME seconds.
     * The value MAXIMUM_ALLOWED_SEGMENT_SIZE is respected.
     */
    public long getPreferredSegmentSize()
    {
        // default is rate * seconds
        long result = lastTransferRateBPS * ServiceManager.sCfg.segmentTransferTime;

        // No previous segment has been transferred
        if (lastTransferRateBPS == 0)
            result = ServiceManager.sCfg.initialSegmentSize;

        // Too fast (ie: segment would be bigger than allowed
        if (result > ServiceManager.sCfg.maximumSegmentSize)
            result = ServiceManager.sCfg.maximumSegmentSize;
            
        if ( result < 1 )
        {
            Logger.logMessage(Logger.WARNING, Logger.DOWNLOAD, "Preferred size looks strange. bps=" + lastTransferRateBPS + " and stt=" + ServiceManager.sCfg.segmentTransferTime);
            result = ServiceManager.sCfg.initialSegmentSize;
        }
        Logger.logMessage(Logger.FINE, Logger.DOWNLOAD, "Preferred segment size is " + result);
        return result;
    }

    public void releaseDownloadSegment()
    {
        if ( downloadSegment != null )
        {
            lastTransferRateBPS = downloadSegment.getLongTermTransferRate();
            updateRating();
            downloadSegment = null;
        }
    }

    public SWDownloadSegment getDownloadSegment()
    {
        return downloadSegment;
    }

    // new JAXB way
    public XJBSWDownloadCandidate createXJBSWDownloadCandidate()
        throws JAXBException
    {
        XJBSWDownloadCandidate xjbCandidate = ObjectFactory.createXJBSWDownloadCandidate();
        xjbCandidate.setFileIndex( fileIndex );
        xjbCandidate.setFileName( fileName );
        if ( guid != null )
        {
            xjbCandidate.setGUID( guid.toHexString() );
        }
        xjbCandidate.setPushNeeded( isPushNeeded );
        xjbCandidate.setChatSupported( isChatSupported );
        xjbCandidate.setRating( hostRating.shortValue() );
        xjbCandidate.setRemoteHost( hostAddress.getFullHostName() );
        xjbCandidate.setVendor( vendor );
        
        // also maintain count how often a connection was failed in a row...
        if (  failedConnectionTries >= BAD_CANDIDATE_CONNECTION_TRIES )
        {
            // reduce by one because we dont want the last connection try to count.
            xjbCandidate.setConnectionFailedRepetition( failedConnectionTries );
        }
        /*else
        {
            xjbCandidate.setConnectionFailedRepetition( 0 );
        }*/
        return xjbCandidate;
    }

    /**
     * Convinience method for logging stuff with candidate infos.
     */
    public void log( String msg )
    {
        if ( !Logger.isLevelLogged( Logger.FINE ) )
        {
            return;
        }
        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
            "Candidate " + hostAddress + ": " + msg );
    }

    public String toString()
    {
        StringBuffer buffer = new StringBuffer( "[Candidate: ");
        if ( vendor != null )
        {
            buffer.append( vendor );
            buffer.append( ',' );
        }
        buffer.append( "Adr:" );
        buffer.append( hostAddress );
        buffer.append( " ->" );
        buffer.append( super.toString() );
        buffer.append( "]" );
        return buffer.toString();
    }

}
