/*
 *  PHEX - The pure-java Gnutella-servent.
 *  Copyright (C) 2001 - 2005 Phex Development Group
 *
 *  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: DownloadEngine.java,v 1.87 2005/11/13 10:08:09 gregork Exp $
 */
package phex.download;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.SocketException;
import java.util.*;

import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.httpclient.ChunkedInputStream;

import phex.common.*;
import phex.common.address.*;
import phex.common.bandwidth.BandwidthController;
import phex.common.file.ManagedFile;
import phex.common.file.ManagedFileException;
import phex.connection.ConnectionFailedException;
import phex.connection.NetworkManager;
import phex.download.swarming.*;
import phex.host.UnusableHostException;
import phex.http.*;
import phex.net.connection.Connection;
import phex.net.connection.OIOSocketFactory;
import phex.net.presentation.PresentationManager;
import phex.net.presentation.SocketFacade;
import phex.utils.*;

/**
 * This class is responsible to download a file using a HTTP connection.
 * The DownloadEngine is usually managed by a SWDownloadWorker.
 */
public class DownloadEngine
{
    private static final int BUFFER_LENGTH = 16 * 1024;
    private SWDownloadCandidate candidate;
    private SWDownloadSegment segment;

    /**
     * The download file object the big parent of all the download stuff of this
     * file.
     */
    private SWDownloadFile downloadFile;

    private Connection connection;
    private SocketFacade socket;
    private InputStream inStream;
    private boolean isKeepAliveSupported;
    private boolean isDownloadSuccessful;
    
    /**
     * Indicates if the download was stoped from externally.
     * Usually per user request.
     */
    private boolean isDownloadStopped;
    
    private ContentRange replyContentRange;
    private long replyContentLength;

    /**
     * Create a download engine
     * @param aDownloadFile the file to download
     * @param aCandidate the candidate to download the file from.
     */
    public DownloadEngine( SWDownloadFile aDownloadFile,
        SWDownloadCandidate aCandidate )
    {
        downloadFile = aDownloadFile;
        candidate = aCandidate;
    }
    
    /**
     * Sets a already available socket connection for the download engine and
     * prepares the DownloadEngine to use it.
     * @param socket
     * @throws IOException
     */
    public void setSocket( SocketFacade socket )
        throws IOException
    {
        assert this.socket != null;
        this.socket = socket;
        prepareConnection();
    }

    /**
     * Connects a unconnected Download engine and prepares the DownloadEngine for use.
     * @param timeout
     * @throws IOException
     * @throws InterruptedException 
     */
    public void connect( int timeout )
        throws IOException
    {
        assert socket == null;
        
        DestAddress address = candidate.getHostAddress();
        
        try
        {
            candidate.addToCandidateLog( "Wait for connect slot " + address.getHostName() + ":"
                + address.getPort() );
            NLogger.debug(NLoggerNames.Download_Engine,
                "Wait for connect slot " + address.getHostName() + ":"
                + address.getPort() );
            
            Runnable acquireCallback = new Runnable() {
                public void run()
                {
                    DestAddress address = candidate.getHostAddress();
                    candidate.addToCandidateLog( "Connecting to " + address.getHostName() + ":"
                        + address.getPort() );
                    NLogger.debug(NLoggerNames.Download_Engine,
                        "Connecting to " + address.getHostName() + ":"
                        + address.getPort() );
                    candidate.setStatus(SWDownloadConstants.STATUS_CANDIDATE_CONNECTING);
                }
            };
            
            socket = OIOSocketFactory.connect( address, timeout,
                acquireCallback );
            
        }
        catch ( SocketException exp )
        {// indicates a general communication error while connecting
            throw new ConnectionFailedException( exp.getMessage() );
        }
        prepareConnection( );
    }
    
    private void prepareConnection()
        throws IOException
    {
        BandwidthController bwController = downloadFile.getBandwidthController();
        connection = new Connection( socket, bwController );
        candidate.addToCandidateLog( "Connected successfully to " 
            + candidate.getHostAddress() + "." );
        candidate.setLastConnectionTime( System.currentTimeMillis() );
        NLogger.debug(NLoggerNames.Download_Engine,
            "Download Engine @" + Integer.toHexString(hashCode()) + " connected successfully to " 
            + candidate.getHostAddress() + ".");
        
        if ( isDownloadStopped )
        {
            throw new DownloadStoppedException( );
        }
    }

    public void exchangeHTTPHandshake( SWDownloadSegment aSegment )
        throws IOException, UnusableHostException, HTTPMessageException
    {
        NetworkManager networkMgr = NetworkManager.getInstance();
        isDownloadSuccessful = false;
        segment = aSegment;
        long downloadOffset = segment.getTransferStartPosition();

        OutputStreamWriter writer = new OutputStreamWriter(
            connection.getOutputStream() );
        // reset to default input stream
        inStream = connection.getInputStream();

        String requestUrl = candidate.getDownloadRequestUrl();
        
        HTTPRequest request = new HTTPRequest( "GET", requestUrl, true );
        request.addHeader( new HTTPHeader( HTTPHeaderNames.HOST,
            candidate.getHostAddress().getFullHostName() ) );
        request.addHeader( new HTTPHeader( GnutellaHeaderNames.LISTEN_IP,
             networkMgr.getLocalAddress().getFullHostName() ) );
        long segmentEndOffset = segment.getEnd();
        if ( segmentEndOffset == -1 )
        {// create header with open end
            request.addHeader( new HTTPHeader( HTTPHeaderNames.RANGE,
                "bytes=" + downloadOffset + "-" ) );
        }
        else
        {
            request.addHeader( new HTTPHeader( HTTPHeaderNames.RANGE,
                "bytes=" + downloadOffset + "-" + segmentEndOffset ) );
        }
        request.addHeader( new HTTPHeader( GnutellaHeaderNames.X_QUEUE,
            "0.1" ) );
        // request a HTTP keep alive connection, needed for queuing to work.
        request.addHeader( new HTTPHeader( HTTPHeaderNames.CONNECTION,
            "Keep-Alive" ) );
        if ( candidate.isG2FeatureAdded() )
        {
            request.addHeader( new HTTPHeader( "X-Features",
                "g2/1.0" ) );
        }

        buildAltLocRequestHeader(request);

        if ( ServiceManager.sCfg.isChatEnabled )
        {
            DestAddress ha = networkMgr.getLocalAddress();
            IpAddress myIp = ha.getIpAddress();
            if ( myIp == null || !myIp.isSiteLocalIP() )
            {
                request.addHeader( new HTTPHeader( "Chat", ha.getFullHostName() ) );
            }
        }

        String httpRequestStr = request.buildHTTPRequestString();

        NLogger.debug(NLoggerNames.Download_Engine,
            "HTTP Request to: " + candidate.getHostAddress() + "\n" + httpRequestStr );
        candidate.addToCandidateLog( "HTTP Request:\n" + httpRequestStr );
        // write request...
        writer.write( httpRequestStr );
        writer.flush();

        HTTPResponse response = HTTPProcessor.parseHTTPResponse( connection );
        if ( NLogger.isDebugEnabled( NLoggerNames.Download_Engine ) )
        {
            NLogger.debug(NLoggerNames.Download_Engine,
                "HTTP Response from: " + candidate.getHostAddress() + "\n" 
                + response.buildHTTPResponseString() );
        }
        if ( ServiceManager.sCfg.downloadCandidateLogBufferSize > 0 )
        {
            candidate.addToCandidateLog( "HTTP Response:\n" 
                + response.buildHTTPResponseString() );
        }

        HTTPHeader header = response.getHeader( HTTPHeaderNames.SERVER );
        if ( header != null )
        {
            candidate.setVendor( header.getValue() );
        }
        
        header = response.getHeader( HTTPHeaderNames.TRANSFER_ENCODING );
        if ( header != null )
        {
            if ( header.getValue().equals("chunked") )
            {
                inStream = new ChunkedInputStream( connection.getInputStream() );
            }
        }

        replyContentRange = null;
        header = response.getHeader( HTTPHeaderNames.CONTENT_RANGE );
        if ( header != null )
        {
            replyContentRange = parseContentRange( header.getValue() );
            // startPos of -1 indicates '*' (free to choose)
            if ( replyContentRange.startPos != -1 && 
                 replyContentRange.startPos != downloadOffset )
            {
                throw new IOException( "Invalid 'CONTENT-RANGE' start offset." );
            }
        }
        
        replyContentLength = -1;
        header = response.getHeader( HTTPHeaderNames.CONTENT_LENGTH );
        if ( header != null )
        {
            try
            {
                replyContentLength = header.longValue();
            }
            catch ( NumberFormatException exp )
            { //unknown 
            }
        }
        
        URN downloadFileURN = downloadFile.getFileURN();
        ArrayList contentURNHeaders = new ArrayList();
        header = response.getHeader( GnutellaHeaderNames.X_GNUTELLA_CONTENT_URN );
        if ( header != null )
        {
            contentURNHeaders.add( header );
        }
        // Shareaza 1.8.10.4 send also a bitprint urn in multiple X-Content-URN headers!
        HTTPHeader[] headers = response.getHeaders( GnutellaHeaderNames.X_CONTENT_URN );
        CollectionUtils.addAll( contentURNHeaders, headers );
        if ( downloadFileURN != null )
        {
            Iterator contentURNIterator = contentURNHeaders.iterator();
            while ( contentURNIterator.hasNext() )
            {
                header = (HTTPHeader)contentURNIterator.next();
                String contentURNStr = header.getValue();
                // check if I can understand urn.
                if ( URN.isValidURN( contentURNStr ) )
                {
                    URN contentURN = new URN( contentURNStr );
                    if ( !downloadFileURN.equals( contentURN ) )
                    {
                        throw new IOException( "Required URN and content URN do not match." );
                    }
                }
            }
        }

        // check Limewire chat support header.
        header = response.getHeader( GnutellaHeaderNames.CHAT );
        if ( header != null )
        {
            candidate.setChatSupported( true );
        }
        // read out REMOTE-IP header... to update my IP
        header = response.getHeader( GnutellaHeaderNames.REMOTE_IP );
        if ( header != null )
        {
            byte[] remoteIP = AddressUtils.parseIP( header.getValue() );
            if ( remoteIP != null )
            {
                IpAddress ip = new IpAddress( remoteIP );
                DestAddress address = PresentationManager.getInstance().createHostAddress(ip, -1);
                networkMgr.updateLocalAddress( address );                
            }
        }
        
        int httpCode = response.getStatusCode();

        // read available ranges
        header = response.getHeader( GnutellaHeaderNames.X_AVAILABLE_RANGES );
        if ( header != null )
        {
            HTTPRangeSet availableRanges =
                HTTPRangeSet.parseHTTPRangeSet( header.getValue() );
            if ( availableRanges == null )
            {// failed to parse... give more detailed error report
                NLogger.error(NLoggerNames.Download_Engine,
                    "Failed to parse X-Available-Ranges in "
                    + candidate.getVendor() + " request: "
                    + response.buildHTTPResponseString() );
            }
            candidate.setAvailableRangeSet( availableRanges );
        }
        else if ( httpCode >= 200 && httpCode < 300 
            && downloadFile.getTotalDataSize() != SWDownloadConstants.UNKNOWN_FILE_SIZE)
        {// OK header and no available range header.. we assume candidate
         // shares the whole file
            candidate.setAvailableRangeSet( new HTTPRangeSet(
                0, downloadFile.getTotalDataSize() - 1) );
        }
        
        // collect alternate locations...
        List altLocList = new ArrayList();
        headers = response.getHeaders( GnutellaHeaderNames.ALT_LOC );
        List altLocTmpList = AlternateLocationContainer.parseUriResAltLocFromHTTPHeaders( headers );
        altLocList.addAll( altLocTmpList );
        
        headers = response.getHeaders( GnutellaHeaderNames.X_ALT_LOC );
        altLocTmpList = AlternateLocationContainer.parseUriResAltLocFromHTTPHeaders( headers );
        altLocList.addAll( altLocTmpList );
        
        headers = response.getHeaders( GnutellaHeaderNames.X_ALT );
        altLocTmpList = AlternateLocationContainer.parseCompactIpAltLocFromHTTPHeaders( headers,
            downloadFileURN );
        altLocList.addAll( altLocTmpList );
        
        // TODO1 huh?? dont we pare X-NALT???? 
        
        Iterator iterator = altLocList.iterator();
        while( iterator.hasNext() )
        {
            downloadFile.addDownloadCandidate( (AlternateLocation)iterator.next() );
        }
        
        // collect push proxies.
        // first the old headers..
        headers = response.getHeaders( "X-Pushproxies" );
        handlePushProxyHeaders( headers );
        headers = response.getHeaders( "X-Push-Proxies" );
        handlePushProxyHeaders( headers );
        // now the standard header
        headers = response.getHeaders( GnutellaHeaderNames.X_PUSH_PROXY );
        handlePushProxyHeaders( headers );
        
        updateKeepAliveSupport( response );

        if ( httpCode >= 200 && httpCode < 300 )
        {// code accepted
            
            // check if we can accept the urn...
            if ( contentURNHeaders.size() == 0 
                && requestUrl.startsWith(GnutellaRequest.GNUTELLA_URI_RES_PREFIX) )
            {// we requested a download via /uri-res resource urn.
             // we expect that the result contains a x-gnutella-content-urn
             // or Shareaza X-Content-URN header.
                throw new IOException(
                    "Response to uri-res request without valid Content-URN header." );
            }
            
            // check if we need and can update our file and segment size.
            if ( downloadFile.getTotalDataSize() == SWDownloadConstants.UNKNOWN_FILE_SIZE )
            {
                // we have a file with an unknown data size. For aditional check assert
                // certain parameters
                assert( segment.getTotalDataSize() == -1 );
                // Now verify if we have the great chance to update our file data!
                if ( replyContentRange != null && 
                     replyContentRange.totalLength != SWDownloadConstants.UNKNOWN_FILE_SIZE )
                {
                    downloadFile.setFileSize( replyContentRange.totalLength );
                    // we learned the file size. To allow normal segment use 
                    // interrupt the download!
                    stopDownload();
                    throw new ReconnectException();
                }
            }
            
            // connection successfully finished
            NLogger.debug(NLoggerNames.Download_Engine,
                "HTTP Handshake successfull.");
            return;
        }
        // check error type
        else if ( httpCode == 503 )
        {// 503 -> host is busy (this can also be returned when remotly queued)
            header = response.getHeader( GnutellaHeaderNames.X_QUEUE );
            XQueueParameters xQueueParameters = null;
            if ( header != null )
            {
                xQueueParameters = XQueueParameters.parseHTTPRangeSet( header.getValue() );
            }
            // check for persistent connection (gtk-gnutella uses queuing with 'Connection: close')
            if ( xQueueParameters != null && isKeepAliveSupported )
            {
                throw new RemotelyQueuedException( xQueueParameters );
            }
            else
            {
                header = response.getHeader( HTTPHeaderNames.RETRY_AFTER );
                if ( header != null )
                {
                    int delta = HTTPRetryAfter.parseDeltaInSeconds( header );
                    if ( delta > 0 )
                    {
                        throw new HostBusyException( delta );
                    }
                }
                throw new HostBusyException();
            }
        }
        else if ( httpCode == HTTPCodes.HTTP_401_UNAUTHORIZED 
               || httpCode == HTTPCodes.HTTP_403_FORBIDDEN )
        {
            if ( "Network Disabled".equals( response.getStatusReason() ) )
            {
                if ( candidate.isG2FeatureAdded() )
                {
                    // already tried G2 but no success.. better give up and dont hammer..
                    throw new UnusableHostException( "Request Forbidden" );
                }
                else
                {
                    // we have not tried G2 but we could..
                    candidate.setG2FeatureAdded( true );
                    throw new HostBusyException( 60 * 5 );
                }
            }
            else
            {
                throw new UnusableHostException( "Request Forbidden" );
            }
        }
        else if ( httpCode == 408 )
        {
            // 408 -> Time out. Try later?
            throw new HostBusyException();
        }
        else if ( httpCode == 404 || httpCode == 410 )
        {// 404: File not found / 410: Host not sharing
            throw new FileNotAvailableException();
        }
        else if ( httpCode == 416 )
        {// 416: Requested Range Unavailable
            header = response.getHeader( HTTPHeaderNames.RETRY_AFTER );
            if ( header != null )
            {
                int delta = HTTPRetryAfter.parseDeltaInSeconds( header );
                if ( delta > 0 )
                {
                    throw new RangeUnavailableException( delta );
                }
            }
            throw new RangeUnavailableException();
        }
        else
        {
            throw new IOException( "Unknown HTTP code: " + httpCode );
        }
    }

    private void buildAltLocRequestHeader(HTTPRequest request)
    {
        URN downloadFileURN = downloadFile.getFileURN();
        if ( downloadFileURN == null )
        {
            return;
        }
        
        // add good alt loc http header
        
        AlternateLocationContainer altLocContainer = new AlternateLocationContainer(
            downloadFileURN );
        // downloadFile.getGoodAltLocContainer() always returns a alt-loc container
        // when downloadFileURN != null
        altLocContainer.addContainer( downloadFile.getGoodAltLocContainer() );

        // create a temp copy of the container and add local alt location
        // if partial file sharing is active and we are not covered by a firewall
        if ( ServiceManager.sCfg.arePartialFilesShared &&
             NetworkManager.getInstance().hasConnectedIncoming() )
        {
            // add the local peer to the alt loc on creation, but only if its
            // not a private IP
            DestAddress ha = NetworkManager.getInstance().getLocalAddress();
            IpAddress myIp = ha.getIpAddress();
            if ( myIp == null || !myIp.isSiteLocalIP() )
            {
                AlternateLocation newAltLoc = new AlternateLocation( ha,
                    downloadFileURN );
                altLocContainer.addAlternateLocation( newAltLoc );
            }
        }
        
        HTTPHeader header = altLocContainer.getAltLocHTTPHeaderForAddress(
            GnutellaHeaderNames.X_ALT, candidate.getHostAddress(),
            candidate.getSendAltLocsSet() );
        if ( header != null )
        {
            request.addHeader( header );
        }
        
        // add bad alt loc http header
        
        // downloadFile.getBadAltLocContainer() always returns a alt-loc container
        // when downloadFileURN != null
        altLocContainer = downloadFile.getBadAltLocContainer();
        header = altLocContainer.getAltLocHTTPHeaderForAddress(
            GnutellaHeaderNames.X_NALT, candidate.getHostAddress(),
            candidate.getSendAltLocsSet() );
        if ( header != null )
        {
            request.addHeader( header );
        }
        
    }

    public void startDownload( ) throws IOException
    {
        String snapshotOfSegment;
        NLogger.debug(NLoggerNames.Download_Engine,
            "Download Engine starts download.");
        
        DirectByteBuffer directByteBuffer = null;
        LengthLimitedInputStream downloadStream = null;
        try
        {
            ManagedFile destFile = downloadFile.getIncompleteDownloadFile();
            directByteBuffer = DirectByteBufferProvider.requestBuffer(
                DirectByteBufferProvider.BUFFER_SIZE_64K );
            
            segment.downloadStartNotify();
            snapshotOfSegment = segment.toString();
            
            // determine the length to download, we start with the MAX
            // which would cause a download until the stream ends.
            long downloadLengthLeft = Long.MAX_VALUE;
            // maybe we know the file size
            if ( replyContentRange != null && replyContentRange.totalLength != -1 )
            {
                downloadLengthLeft = replyContentRange.totalLength;
            }
            // maybe we know a reply content length
            if ( replyContentLength != -1 )
            {
                downloadLengthLeft = Math.min( replyContentLength, downloadLengthLeft );
            }
            // maybe the segment has a smaler length (usually not the case)
            long segmentDataSizeLeft = segment.getTransferDataSizeLeft();
            if ( segmentDataSizeLeft != -1 )
            {
                downloadLengthLeft = Math.min( segmentDataSizeLeft, downloadLengthLeft );
            }
            
            downloadStream = new LengthLimitedInputStream( 
                inStream, downloadLengthLeft );
            long fileOffset = segment.getStart() + segment.getTransferredDataSize();
            long lengthDownloaded = segment.getTransferredDataSize();
            int len;
            byte[] buffer = new byte[BUFFER_LENGTH];
            while( (len = downloadStream.read( buffer, 0, BUFFER_LENGTH )) > 0 )
            {
                synchronized (segment)
                {
                    long tmpCheckLength = lengthDownloaded + len;
                    if ( tmpCheckLength < segment.getTransferredDataSize() )
                    {
                        NLogger.error(NLoggerNames.Download_Engine, 
                            "TransferredDataSize would be going down! " + 
                            " ll " + downloadLengthLeft + " l " + len + " ld "
                            + lengthDownloaded + " gtds "
                            + segment.getTransferredDataSize()
                            + " seg: " + segment
                            + " originally: " + snapshotOfSegment);
                        throw new IOException( "TransferredDataSize would be going down!" );
                    }
                    else if (segment.getTransferDataSize() > -1 
                        && tmpCheckLength > segment.getTransferDataSize())
                    {
                        NLogger.error(NLoggerNames.Download_Engine, 
                            "TransferredDataSize would be larger then segment! " +
                            " ll " + downloadLengthLeft + " l " + len + " ld "
                            + lengthDownloaded + " gtds "
                            + segment.getTransferredDataSize()
                            + " seg: " + segment
                            + " originally: " + snapshotOfSegment);
                        throw new IOException( "TransferredDataSize would be larger then segment!" );
                    }
                    
                    directByteBuffer.put( buffer, 0, len );
                    directByteBuffer.flip();
                    destFile.write( directByteBuffer, fileOffset );
                    fileOffset += directByteBuffer.limit();
                    directByteBuffer.clear();

                    lengthDownloaded += len;
                    segment.setTransferredDataSize( lengthDownloaded );
                    candidate.incTotalDownloadSize( len );

                    // get transfer size since it might have changed in the meantime.
                    segmentDataSizeLeft = segment.getTransferDataSizeLeft();
                    if ( segmentDataSizeLeft != -1 )
                    {
                        downloadLengthLeft = Math.min( segmentDataSizeLeft, downloadLengthLeft );
                        downloadStream.setLengthLimit(downloadLengthLeft);
                    }
                }
            }
            isDownloadSuccessful = true;
            // if we successful downloaded and we still dont know the total file size,
            // we can assume that the file was completly downloaded.
            if ( downloadFile.getTotalDataSize() == SWDownloadConstants.UNKNOWN_FILE_SIZE )
            {
                // we have a file with an unknown data size. For aditional check assert
                // certain parameters
                assert( segment.getTotalDataSize() == -1 );
                downloadFile.setFileSize( segment.getTransferredDataSize() );
            }
        }
        catch ( FileHandlingException exp )
        {
            NLogger.error(NLoggerNames.Download_Engine, exp, exp);
            IOException ioExp = new IOException( exp.getMessage() );
            ioExp.initCause(exp);
            throw ioExp;
        }
        catch ( ManagedFileException exp )
        {
            if ( Thread.currentThread().isInterrupted() )
            {
                return;
            }
            NLogger.error(NLoggerNames.Download_Engine, exp, exp);
            IOException ioExp = new IOException( exp.getMessage() );
            ioExp.initCause(exp);
            throw ioExp;
        }
        finally
        {// dont close managed file since it might be used by parallel threads.
            if ( directByteBuffer != null )
            {
                directByteBuffer.release();
            }
            
            boolean isAcceptingNextSegment = isAcceptingNextSegment();
            candidate.addToCandidateLog( "Is accepting next segment: " + isAcceptingNextSegment );
            // this is for keep alive support...
            if ( isAcceptingNextSegment )
            {
                // only need to close and consume left overs if we plan to
                // continue using this connection.
                downloadStream.close();
            }
            else
            {
                stopDownload();
            }
        }
    }

    public void stopDownload()
    {
        NLogger.debug(NLoggerNames.Download_Engine,
            "Closing pipe and socket and telling segment we've stopped.");
        isDownloadStopped = true;
        candidate.addToCandidateLog( "Stop download." );
        IOUtil.closeQuietly(inStream);

        if ( segment != null )
        {
            segment.downloadStopNotify();
        }
        // dont close managed file since it might be used by parallel threads.
        if ( connection != null )
        {
            connection.disconnect();
        }
        IOUtil.closeQuietly(socket);
    }

    /**
     * Indicates whether the connection is keept alive and the next http request
     * can be send.
     * @return true if the next http request can be send.
     */
    public boolean isAcceptingNextSegment()
    {
        return isDownloadSuccessful && isKeepAliveSupported && replyContentLength != -1;
    }

    /**
     * We only care for the start offset since this is the importent point to
     * begin the download from. Wherever it ends we try to download as long as we
     * stay connected or until we reach our goal.
     *
     * Possible Content-Range Headers ( maybe not complete / header is upper
     * cased by Phex )
     *
     *   Content-range:bytes abc-def/xyz
     *   Content-range:bytes abc-def/*
     *   Content-range:bytes *\/xyz
     *   Content-range: bytes=abc-def/xyz (wrong but older Phex version and old clients use this)
     *
     * @param contentRangeLine the content range value
     * @throws WrongHTTPHeaderException if the content range line has wrong format.
     * @return the content range start offset.
     */
    private ContentRange parseContentRange( String contentRangeLine )
        throws WrongHTTPHeaderException
    {
        try
        {
            ContentRange range = new ContentRange();
            contentRangeLine = contentRangeLine.toLowerCase();
            // skip over bytes plus extra char
            int idx = contentRangeLine.indexOf( "bytes" ) + 6;
            String rangeStr = contentRangeLine.substring( idx ).trim();
            
            int slashIdx = rangeStr.indexOf('/');
            String leadingPart = rangeStr.substring(0, slashIdx);
            String trailingPart = rangeStr.substring( slashIdx + 1 );
            
            // ?????/*
            if ( trailingPart.charAt(0) == '*' )
            {
                range.totalLength = -1;
            }
            else // ?????/789
            {
                long fileLength = Long.parseLong(trailingPart);
                range.totalLength = fileLength;
            }
            
            // */???
            if ( leadingPart.charAt( 0 ) == '*' )
            {
                // startPos of -1 indicates '*' (free to choose)
                range.startPos = -1;
                range.endPos = range.totalLength;
            }
            else
            {
                // 123-456/???
                int dashIdx = rangeStr.indexOf( '-' );
                String startOffsetStr = leadingPart.substring( 0, dashIdx );
                long startOffset = Long.parseLong( startOffsetStr );
                String endOffsetStr = leadingPart.substring( dashIdx+1 );
                long endOffset = Long.parseLong( endOffsetStr );
                range.startPos = startOffset;
                range.endPos = endOffset;
            }
            return range;
        }
        catch ( NumberFormatException exp )
        {
            NLogger.warn(NLoggerNames.Download_Engine, exp, exp);
            throw new WrongHTTPHeaderException(
                "Number error while parsing content range: " + contentRangeLine );
        }
        catch ( IndexOutOfBoundsException exp )
        {
            throw new WrongHTTPHeaderException(
                "Error while parsing content range: " + contentRangeLine );
        }
    }
    
    private void updateKeepAliveSupport( HTTPResponse response )
    {
        // check if Keep-Alive connection is accepted
        HTTPHeader header = response.getHeader( HTTPHeaderNames.CONNECTION );
        if ( header != null )
        {
            if ( header.getValue().equalsIgnoreCase( "close" ) )
            {
                isKeepAliveSupported = false;
                return;
            }
            else if ( header.getValue().equalsIgnoreCase( "keep-alive" ) )
            {
                isKeepAliveSupported = true;
                return;
            }
        }
        // missing or unknown connection header do the HTTP method default.
        if ( response.getHTTPVersion().equals("HTTP/1.1") )
        {
            isKeepAliveSupported = true;
        }
        else
        {
            isKeepAliveSupported = false;
        }
    }
    
    public void handlePushProxyHeaders( HTTPHeader[] headers )
    {
        if ( headers == null || headers.length == 0 )
        {
            return;
        }
        List proxyList = new ArrayList();
        StringTokenizer tokenizer;
        for ( int i = 0; i < headers.length; i++ )
        {
            HTTPHeader header = headers[i];
            tokenizer = new StringTokenizer( header.getValue(), ",");
            while( tokenizer.hasMoreTokens() )
            {
                String addressStr = tokenizer.nextToken().trim();
                DestAddress address;
                try
                {
                    address = AddressUtils.parseAndValidateAddress( 
                        addressStr, false );
                    proxyList.add( address );
                }
                catch (MalformedDestAddressException exp)
                {
                    NLogger.debug(NLoggerNames.Download_Engine,
                        "Malformed alt-location URL: " + exp.getMessage() );
                }
            }
        }
        if ( proxyList.size() == 0 )
        {
            return;
        }
        DestAddress[] pushProxyAddresses = new DestAddress[ proxyList.size() ];
        proxyList.toArray( pushProxyAddresses );
        candidate.setPushProxyAddresses( pushProxyAddresses );
    }
    
    private class ContentRange
    {
        /**
         * startPos of -1 indicates '*' (free to choose)
         */
        long startPos;
        long endPos;
        long totalLength;
    }

}
