/*
 *  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: SWDownloadWorker.java,v 1.47 2004/09/27 11:01:47 nxf Exp $
 */
package phex.download.swarming;

import java.io.File;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;

import phex.common.ServiceManager;
import phex.common.ThreadPool;
import phex.connection.ConnectionFailedException;
import phex.connection.NetworkManager;
import phex.download.*;
import phex.host.UnusableHostException;
import phex.http.HTTPMessageException;
import phex.statistic.UploadDownloadCountStatistic;
import phex.utils.Logger;

public class SWDownloadWorker implements Runnable
{
    /**
     * A temporary worker indicates a worker that is used to wait for a valid
     * download set. Only one temporary worker should be in the system. Once
     * a valid download set is found the worker will lose its temporary status.
     * This flag will help to limit the worker count and only hold as many
     * workers as required and necessary.
     */
    private boolean isTemporaryWorker;

    private boolean isRunning;

    private DownloadEngine downloadEngine;

    private boolean cleanUp = true; // should we merge segments, etc. when we're finishing?

    public SWDownloadWorker()
    {
    }

    /**
     * Sets the temporary worker status.
     * @param state
     * @see isTemporaryWorker
     */
    public void setTemporaryWorker(boolean state)
    {
        isTemporaryWorker = state;
    }

    /**
     * Returns the temporary worker status.
     * @return the temporary worker status.
     */
    public boolean isTemporaryWorker()
    {
        return isTemporaryWorker;
    }

    public void run()
    {
        SwarmingManager swarmingMgr = SwarmingManager.getInstance();
        try
        {
            SWDownloadSet downloadSet;
            while (isRunning)
            {
                boolean isStopped = swarmingMgr.checkToStopWorker(this);
                if ( isStopped )
                {
                    break;
                }
                //Logger.logMessage( Logger.FINE, Logger.DOWNLOAD,
                //    toString() + " - SWDownloadWorker Allocating DownloadSet." );
                downloadSet = swarmingMgr.allocateDownloadSet(this);
                if ( downloadSet == null )
                {
                    if ( isTemporaryWorker )
                    {
                        swarmingMgr.waitForNotify();
                        continue;
                    }
                    else
                    {
                        // no download set aquired after handling last download...
                        // break away from further trying...
                        break;
                    }
                }
                Logger.logMessage(Logger.FINE, Logger.DOWNLOAD, toString()
                    + " - SWDownloadWorker Allocated DownloadSet "
                    + downloadSet.toString());

                try
                {
                    handleDownload(downloadSet);
                }
                finally
                {
                    if ( downloadSet != null )
                    {
                        Logger.logMessage(Logger.FINE, Logger.DOWNLOAD,
                            toString() + " - Releasing DownloadSet "
                                + downloadSet.toString());
                        downloadSet.releaseDownloadSet();
                    }
                }
            }
        }
        finally
        {
            if ( isRunning )
            {// the worker should run... give notice about crash
                swarmingMgr.notifyWorkerShoutdown(this);
            }
            Logger.logMessage(Logger.FINE, Logger.DOWNLOAD,
                "Download worker finished: " + this);
        }
    }

    public void startWorker()
    {
        isRunning = true;
        ThreadPool.getInstance().addJob(this,
            "SWDownloadWorker-" + Integer.toHexString(hashCode()));
        Logger.logMessage(Logger.FINE, Logger.DOWNLOAD,
            "Started SWDownloadWorker " + this);
    }

    public void stopWorker()
    {
        Logger.logMessage(Logger.FINE, Logger.DOWNLOAD,
            "Download worker has been instructed to stop running: " + this);
        cleanUp = false;
        isRunning = false;
    }

    public boolean isRunning()
    {
        return isRunning;
    }

    public void stopDownload()
    {
        Logger.logMessage(Logger.FINE, Logger.DOWNLOAD,
            "Download worker has been instructed to stop downloading: " + this);
        if ( downloadEngine != null )
        {
            downloadEngine.stopDownload();
            downloadEngine = null;
        }
    }

    /**
     * Handles a specific SWDownloadSet to start the download for.
     * @param downloadSet the download set containing the download configuration.
     */
    private void handleDownload(SWDownloadSet downloadSet)
    {
        SWDownloadFile downloadFile = downloadSet.getDownloadFile();
        SWDownloadCandidate downloadCandidate = downloadSet
            .getDownloadCandidate();
        if ( downloadCandidate.isPushNeeded() )
        {
            connectDownloadEngineViaPush(downloadSet);
        }
        else
        {
            connectDownloadEngine(downloadSet);
        }

        if ( downloadEngine == null ) { return; }
        try
        {
            startDownload(downloadSet);
        }
        catch (IOException exp)
        {
            // this is only temporary for testing to let the download sleep for a while...
            // downloadCandidate.setStatus( SWDownloadConstants.STATUS_CANDIDATE_BUSY );
            downloadCandidate
                .setStatus(SWDownloadConstants.STATUS_CANDIDATE_WAITING);
            Logger.logMessage(Logger.FINE, Logger.DOWNLOAD, exp);
            Logger.logMessage(Logger.FINE, Logger.DOWNLOAD, downloadCandidate);
        }
        finally
        {
            stopDownload();
            downloadSet.releaseDownloadSegment();

            // check for completed files merge...
            if (cleanUp)
            {
                downloadFile.sortSegments();
                downloadFile.mergeSegments();
            }
            // segment download completed
            downloadFile.verifyStatus();
            if ( downloadFile.isDownloadCompleted() )
            {
                downloadFile.moveToDestinationFile();
            }
        }

    }

    /**
     * Connects the download engine to the host with a direct connection.
     */
    private void connectDownloadEngine(SWDownloadSet downloadSet)
    {
        SWDownloadCandidate downloadCandidate = downloadSet
            .getDownloadCandidate();
        SWDownloadFile downloadFile = downloadSet.getDownloadFile();

        downloadCandidate
            .setStatus(SWDownloadConstants.STATUS_CANDIDATE_CONNECTING);

        // invalidate the download engine
        downloadEngine = null;

        downloadEngine = new DownloadEngine(downloadFile, downloadCandidate);

        try
        {
            downloadEngine.connect( ServiceManager.sCfg.mSocketTimeout );
        }
        catch (ConnectionFailedException exp)
        {
            // indicates a general communication error while connecting
            Logger.logMessage(Logger.FINEST, Logger.DOWNLOAD, exp.toString());
            // trying a push
            connectDownloadEngineViaPush(downloadSet);
            return;
        }
        catch (SocketTimeoutException exp)
        {
            // indicates a general communication error while connecting
            Logger.logMessage(Logger.FINEST, Logger.DOWNLOAD, exp.toString());
            // trying a push
            connectDownloadEngineViaPush(downloadSet);
            return;
        }
        catch (IOException exp)
        {
            // stop and set to null so that download is not starting.
            if ( downloadEngine != null )
            {
                downloadEngine.stopDownload();
                downloadEngine = null;
            }
            // unknown error trying a push
            connectDownloadEngineViaPush(downloadSet);
            // TODO3 handle different cases on some try again on others remove
            Logger.logMessage(Logger.SEVERE, Logger.DOWNLOAD, exp,
                "Error at Host: "
                    + downloadCandidate.getHostAddress().getFullHostName()
                    + " Vendor: " + downloadCandidate.getVendor());
            return;
        }
    }

    /**
     * Connectes the download engine via a push request.
     */
    private void connectDownloadEngineViaPush(SWDownloadSet downloadSet)
    {
        SWDownloadCandidate downloadCandidate = downloadSet
            .getDownloadCandidate();
        SWDownloadFile downloadFile = downloadSet.getDownloadFile();

        // invalidate the download engine
        downloadEngine = null;

        // if we are behind a firewall there is no chance to successfully push
        // when we are not connected to a LAN and the host has a private address.
        if ( !NetworkManager.getInstance().hasConnectedIncoming()
            && (!ServiceManager.sCfg.connectedToLAN || downloadCandidate
                .getHostAddress().isPrivateIP()) )
        {
            Logger
                .logMessage(
                    Logger.FINEST,
                    Logger.DOWNLOAD,
                    this.toString()
                        + downloadCandidate.toString()
                        + " Cant PUSH->me.isFirewalled||(!me.connectedToLAN&&candidate.isPrivateIP)");

            downloadCandidate
                .setStatus(SWDownloadConstants.STATUS_CANDIDATE_CONNECTION_FAILED);
            // candidate must have push. can't directly connect
            if ( downloadCandidate.isPushNeeded() )
            {
                downloadFile.markCandidateBad(downloadCandidate, true);
                // no bad alt loc in this case... others might connect correct...
            }
            return;
        }

        downloadCandidate
            .setStatus(SWDownloadConstants.STATUS_CANDIDATE_PUSH_REQUEST);
        UploadDownloadCountStatistic.pushDownloadAttempts.increment(1);
        Socket socket = PushHandler.requestSocketViaPush(downloadCandidate);
        if ( socket == null )
        {
            downloadCandidate
                .setStatus(SWDownloadConstants.STATUS_CANDIDATE_CONNECTION_FAILED);
            // candidate must have push. cant directly connect
            if ( downloadCandidate.isPushNeeded() )
            {
                downloadFile.markCandidateBad(downloadCandidate, true);
                // no bad alt loc in this case... others might connect corrent...
            }
            Logger.logMessage(Logger.FINE, Logger.DOWNLOAD,
                "Push request fails for candidate: " + downloadCandidate);
            UploadDownloadCountStatistic.pushDownloadFailure.increment(1);
            return;
        }
        UploadDownloadCountStatistic.pushDownloadSuccess.increment(1);
        downloadEngine = new DownloadEngine(socket, downloadFile,
            downloadCandidate);

        try
        {
            downloadEngine.connect( ServiceManager.sCfg.mSocketTimeout );
        }
        catch (IOException exp)
        {
            downloadCandidate
                .setStatus(SWDownloadConstants.STATUS_CANDIDATE_CONNECTION_FAILED);
            // TODO3 handle different cases on some try again on others remove
            Logger.logMessage(Logger.SEVERE, Logger.DOWNLOAD, exp,
                "Error at Host: "
                    + downloadCandidate.getHostAddress().getFullHostName()
                    + " Vendor: " + downloadCandidate.getVendor());
            // stop and set to null so that download is not starting.
            downloadEngine.stopDownload();
            downloadEngine = null;
            return;
        }
    }

    private void exchangeHTTPHandshake(SWDownloadSet downloadSet)
    {
        SWDownloadCandidate downloadCandidate = downloadSet
            .getDownloadCandidate();
        SWDownloadFile downloadFile = downloadSet.getDownloadFile();
        SWDownloadSegment downloadSegment;
        try
        {
            downloadCandidate
                .setStatus(SWDownloadConstants.STATUS_CANDIDATE_REQUESTING);
            downloadSegment = downloadSet.allocateDownloadSegment(this);
            if ( downloadSegment == null )
            {// no more segments found...
                Logger.logMessage(Logger.FINE, Logger.DOWNLOAD,
                    "No segment to allocate found");
                downloadCandidate
                    .setStatus(SWDownloadConstants.STATUS_CANDIDATE_WAITING);
                // set to null so that download is not starting.
                downloadEngine = null;
                return;
            }
            downloadEngine.exchangeHTTPHandshake(downloadSegment);
        }
        catch (RemotelyQueuedException exp)
        {
            // dont set download engine to null... we need it to stay connected.

            // release download segment... for others... we will get a new one on
            // next try.
            downloadSet.releaseDownloadSegment();
            // must first set queue parameters to update waiting time when settings
            // status.
            downloadCandidate.updateXQueueParameters(exp.getXQueueParameters());
            downloadCandidate
                .setStatus(SWDownloadConstants.STATUS_CANDIDATE_REMOTLY_QUEUED);
        }
        catch (RangeUnavailableException exp)
        {// TODO2 416 error maybe we should stop requesting from this candidate!!
            // TODO2 in case of keep-alive supported connection we can 
            // adjust and rerequest immediatly (job of SWDownloadWorker)

            // stop and set to null so that download is not starting.
            downloadEngine.stopDownload();
            downloadEngine = null;
            downloadCandidate
                .setStatus(SWDownloadConstants.STATUS_CANDIDATE_RANGE_UNAVAILABLE);
            Logger.logMessage(Logger.FINEST, Logger.DOWNLOAD, exp);
            return;
        }
        catch (HostBusyException exp)
        {
            Logger.logMessage(Logger.FINEST, Logger.DOWNLOAD, downloadCandidate
                + " " + exp.getMessage());
            // stop and set to null so that download is not starting.
            downloadEngine.stopDownload();
            downloadEngine = null;
            downloadCandidate.setStatus(
                SWDownloadConstants.STATUS_CANDIDATE_BUSY, exp
                    .getWaitTimeInSeconds());
            return;
        }
        catch (UnusableHostException exp)
        {
            // stop and set to null so that download is not starting.
            downloadEngine.stopDownload();
            downloadEngine = null;
            // file not available or wrong http header.
            Logger.logMessage(Logger.FINE, Logger.DOWNLOAD, exp);
            Logger.logMessage(Logger.FINE, Logger.DOWNLOAD,
                "Removing download candidate: " + downloadCandidate);
            downloadFile.markCandidateBad(downloadCandidate, true);
            downloadFile.addBadAltLoc(downloadCandidate);
            return;
        }
        catch (HTTPMessageException exp)
        {
            // stop and set to null so that download is not starting.
            downloadEngine.stopDownload();
            downloadEngine = null;
            // file not available or wrong http header.
            Logger.logMessage(Logger.WARNING, Logger.DOWNLOAD, exp);
            downloadFile.markCandidateBad(downloadCandidate, true);
            downloadFile.addBadAltLoc(downloadCandidate);
            return;
        }
        catch (SocketTimeoutException exp)
        {
            Logger.logMessage(Logger.FINE, Logger.DOWNLOAD, exp);
            downloadCandidate
                .setStatus(SWDownloadConstants.STATUS_CANDIDATE_CONNECTION_FAILED);
            downloadEngine.stopDownload();
            downloadEngine = null;
            return;
        }
        catch (SocketException exp)
        {
            Logger.logMessage(Logger.FINE, Logger.DOWNLOAD, exp);
            downloadCandidate
                .setStatus(SWDownloadConstants.STATUS_CANDIDATE_CONNECTION_FAILED);
            if (downloadEngine != null)
            {
                downloadEngine.stopDownload();
                downloadEngine = null;
            }
            return;
        }
        catch (IOException exp)
        {
            downloadCandidate
                .setStatus(SWDownloadConstants.STATUS_CANDIDATE_CONNECTION_FAILED);
            // TODO3 handle different cases on some try again on others remove
            Logger.logMessage(Logger.WARNING, Logger.DOWNLOAD, exp,
                "Error at Host: "
                    + downloadCandidate.getHostAddress().getFullHostName()
                    + " Vendor: " + downloadCandidate.getVendor());
            // stop and set to null so that download is not starting.

            // TODO1 error check
            if ( downloadEngine == null )
            {// why can it happen that download engine is null here???
                Thread.dumpStack();
                Logger.logError(Logger.DOWNLOAD,
                    "DownloadEngine is null, why??"
                        + downloadCandidate.getHostAddress().getFullHostName()
                        + " Vendor: " + downloadCandidate.getVendor());
            }
            downloadEngine.stopDownload();
            downloadEngine = null;
            return;
        }
    }

    /**
     * Execute the actual download routine.
     */
    private void startDownload(SWDownloadSet downloadSet) throws IOException
    {
        SWDownloadFile downloadFile = downloadSet.getDownloadFile();
        SWDownloadCandidate downloadCandidate = downloadSet
            .getDownloadCandidate();
        // we came that far proves that we can successful connect to this candidate
        // we can use it as good alt loc
        // in cases where the http handshake revises this determination the
        // alt loc will be adjusted accordingly.
        downloadFile.addGoodAltLoc(downloadCandidate);
        downloadFile.markCandidateGood(downloadCandidate);
        do
        {
            do
            {
                exchangeHTTPHandshake(downloadSet);
                if ( downloadEngine == null ) { return; }
                if ( downloadCandidate.isRemotlyQueued() )
                {
                    try
                    {
                        Thread.sleep(downloadCandidate.getXQueueParameters()
                            .getRequestSleepTime());
                        if ( downloadEngine == null )
                        {// download stopped in the meantime.
                            Logger.logMessage(Logger.FINE, Logger.DOWNLOAD,
                                "Download stopped while waiting for queue.");
                            return;
                        }
                    }
                    catch (InterruptedException exp)
                    {// interrupted while sleeping
                        Logger.logMessage(Logger.FINE, Logger.DOWNLOAD,
                            "Interrupted Worker sleeping for queue.");
                        downloadCandidate
                            .setStatus(SWDownloadConstants.STATUS_CANDIDATE_CONNECTION_FAILED);
                        // stop and set to null so that download is not starting.
                        downloadEngine.stopDownload();
                        downloadEngine = null;
                        return;
                    }
                }
            }
            while (downloadCandidate.isRemotlyQueued());

            downloadFile.setStatus(SWDownloadConstants.STATUS_FILE_DOWNLOADING);
            downloadSet.getDownloadCandidate().setStatus(
                SWDownloadConstants.STATUS_CANDIDATE_DOWNLOADING);

            SWDownloadSegment downloadSegment = downloadSet
                .getDownloadSegment();
            File destFile = downloadSegment.getIncompleteFile();
            downloadEngine.startDownload(destFile);

            // segment download completed
            // release segment
            Logger.logMessage(Logger.FINE, Logger.DOWNLOAD,
                    "Completed a segment which started at " + downloadSegment.getStartOffset() + " and was downloaded at a rate of " + downloadSegment.getLongTermTransferRate());
            downloadSet.releaseDownloadSegment();

            // check for completed files merge...
            downloadFile.mergeSegments();

        }
        while (downloadEngine.isAcceptingNextSegment() && ! downloadFile.isFull() );
        downloadCandidate
            .setStatus(SWDownloadConstants.STATUS_CANDIDATE_WAITING);
    }

    public String toString()
    {
        return "[Worker:running:" + isRunning + ",tempWorker:"
            + isTemporaryWorker + ",engine:" + downloadEngine + "]";
    }
}
