/*
 *  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: SWDownloadSegment.java,v 1.22 2004/09/15 04:44:41 nxf Exp $
 */
package phex.download.swarming;

import java.io.File;

import javax.xml.bind.JAXBException;

import phex.common.FileHandlingException;
import phex.common.ServiceManager;
import phex.common.bandwidth.BandwidthManager;
import phex.download.IDownloadSegment;
import phex.utils.FileUtils;
import phex.utils.Logger;
import phex.xml.ObjectFactory;
import phex.xml.XJBSWDownloadSegment;
import phex.http.*;

/**
 * Defines a download segment. A download segment is one piece of the download file
 * the segment describes the start position of the segment and the length. Also
 * the segment has a link to it's following segement. This link is used to be able
 * to merge following segments after donwload. Also the files that correspond
 * to these segments will be merged once we have a sequence of downloaded segments.
 * This merging strategy is used to save hard drive space and to be able to present
 * the user a slowly growing file that is already runnable.
 */
public class SWDownloadSegment implements IDownloadSegment, SWDownloadConstants
{
    /**
     * Classification of this segment. The lower its rating, the better it is to get it first.
     * This value will be used to sort segments.
     */
    public PriorityRating rating;

    /**
     * Defines the start position of the segment.
     */
    private Long startPos;

    /**
     * Defines the total length to download.
     */
    private long transferDataSize;

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

    /**
     * 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;

    /**
     * The segment number is used to indentify the segment in the list. It's
     * also used as the file extension for the segment on the file system.
     * For a segment number of 1234 the file name starts with sg1234*
     * The numbers might not be ordered according to the order of segments.
     */
    private int segmentNumber;

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

    /**
     * Time and size of the last speed transfer check
     */
    private long transferSpeedCheckTime;
    private long transferSpeedCheckSize;

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

    /**
     * Holds the last messured data transfer rate.
     */
    private int transferRate;

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

    /**
     * The incomplete file of the download data for this segment.
     * The filename contains the localFilename of the download file and the
     * segment code. The first segment of the file should not modify the
     * original file extension. This allows applications to open the file.
     * All other segments should modify the extension since it should not be
     * possible to open a segment that is not the first one.
     * For performance reasons the segment keeps hold to its File reference.
     */
    private File incompleteFile;

    /**
     * Link to the next segment.
     */
    private SWDownloadSegment nextSegment;

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

    /**
     * The download worker that is currently working on downloading this segment.
     * Or null if no download worker is active.
     */
    private SWDownloadWorker worker;

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

    /**
     * Transfer stop time
     */
    private long transferStopTime;
    
    /**
     * 
     * @param aDownloadFile
     * @param aSegmentNumber
     * @param aStartPos
     * @param aLength
     */
    public SWDownloadSegment( SWDownloadFile aDownloadFile, int aSegmentNumber,
        long aStartPos, long aLength )
    {
        rating = new PriorityRating(WORST_RATING, 0);
        downloadFile = aDownloadFile;
        segmentNumber = aSegmentNumber;
        startPos = new Long( aStartPos );
        transferDataSize = aLength;
        transferredDataSize = 0;
        transferSpeedCheckTime = 0;
        transferSpeedCheckSize = 0;
        currentProgress = new Integer( 0 );
        initTransferredDataSize();

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

    public SWDownloadSegment( SWDownloadFile aDownloadFile,
        XJBSWDownloadSegment xjbSegment )
    {
        this( aDownloadFile, xjbSegment.getSegmentNumber(), xjbSegment.getStartPosition(),
            xjbSegment.getLength() );
        String incompleteFileName = xjbSegment.getIncompleteFileName();
        if ( incompleteFileName != null )
        {
            incompleteFile = new File( xjbSegment.getIncompleteFileName() );
            initTransferredDataSize();
        }
    }

    /**
     * Returns the start position of the transfer. The start position depends on
     * the segment start position plus the already transferred data size.
     * During a transfer this start position will move. So if you like to know
     * the start position of a running transfer you need to get it before the
     * transfer starts.
     */
    public long getTransferStartPosition()
    {
        return startPos.longValue() + transferredDataSize;
    }

    /**
     * Returns the start position of the segment inside the download file.
     */
    public long getStartOffset()
    {
        return startPos.longValue();
    }

    /**
     * Returns the start position of the segment.
     */
    public Long getStartOffsetObject()
    {
        return startPos;
    }

    /**
     * Returns the stop position of the segment.
     * Note: this is actually the start offset of the NEXT segment,
     * not the last byte of THIS segment!!!! 
     */
    public long getEndOffset()
    {
        return startPos.longValue() + transferDataSize;
    }

    /**
     * Returns the data size that has already be transferred.
     */
    public long getTransferredDataSize()
    {
        return transferredDataSize;
    }

    /**
     * Returns the length that is left to download.
     */
    public long getTransferDataSizeLeft()
    {
        return Math.max( 0, transferDataSize - transferredDataSize );
    }

    /**
     * Returns the length of the segment.
     */
    public long getTransferDataSize()
    {
        return transferDataSize;
    }

    /**
     * This is the total size of the available data. Even if its not importend
     * for the transfer itself.
     */
    public long getTotalDataSize()
    {
        return getTransferDataSize();
    }

    /**
     * Called if the size of the segment changes. Either to make it larger or
     * smaller.
     */
    public void setTransferDataSize( long size )
    {
        transferDataSize = size;
        fireSegmentChange();
    }

    /**
     * Sets the size of the data that has been transferred.
     */
    public void setTransferredDataSize( long size )
    {
        long diff = size - transferredDataSize;
        transferRateBytes += diff;
        sessionTransferRateBytes += diff;
        if ( size < transferredDataSize || size > transferDataSize )
        {
            try
            {
                throw new Exception();
            }
            catch (Exception exp )
            {
                Logger.logError( exp );
            }
        }
        transferredDataSize = size;
        fireSegmentChange();
    }

    /**
     * Return the data transfer status.
     * It can be TRANSFER_RUNNING, TRANSFER_NOT_RUNNING, TRANSFER_COMPLETED,
     * TRANSFER_ERROR.
     */
    public short getDataTransferStatus()
    {
        // TODO wire this up with the real status!
        return TRANSFER_NOT_RUNNING;
    }

    /**
     * If the transfer has gone for more than segmentTransferTime since the last check,
     * calculate the average transfer rate. If it's less than minimumAllowedTransferRate,
     * return true.
     */
    public boolean transferTooSlow()
    {
        long currentTime = System.currentTimeMillis();
        // STT is in seconds, but timings are in millis
        if ( currentTime - transferSpeedCheckTime < ( ServiceManager.sCfg.segmentTransferTime * 1000)
                || worker == null 
                || transferSpeedCheckTime == 0 // hasn't been initialised yet by starting a download
                || transferStopTime != 0 // stopped so no activity expected
            )
            return false; // too early to tell 
        if (
                (transferredDataSize - transferSpeedCheckSize) / ((currentTime - transferSpeedCheckTime) / 1000L)
                < ServiceManager.sCfg.minimumAllowedTransferRate )
        {
            Logger.logMessage( Logger.FINE, Logger.DOWNLOAD, "Should really stop this segment! : " + this + ": time since last check is " + (currentTime - transferSpeedCheckTime) + " and bytes transferred is only " + (transferredDataSize - transferSpeedCheckSize));
            return true; // yes, it's too slow
        } else {
            // Reset time/volume markers
            transferSpeedCheckTime = currentTime;
            transferSpeedCheckSize = transferredDataSize;
            return false;
        }
    }

    public void setTransferRateTimestamp( long timestamp )
    {
        transferRateTimestamp = timestamp;
        transferRateBytes = 0;
    }

    /**
     * Returns the data transfer rate. The rate should depend on the transfer
     * rate timestamp.
     */
    public int getShortTermTransferRate()
    {
        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;
    }

    /**
     * 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
            transferTime = (System.currentTimeMillis() - transferStartTime) / 1000;
        }
        return (int)( sessionTransferRateBytes / (transferTime + 1) );
    }

    /**
     * Indicate that the download is just starting.
     */
    public void downloadStartNotify()
    {
        transferStartTime = System.currentTimeMillis();
        transferRateTimestamp = transferStartTime;
        transferSpeedCheckTime = transferStartTime;
        transferSpeedCheckSize = 0;
        transferStopTime = 0;
        sessionTransferRateBytes = 0;
    }

    /**
     * Indicate that the download is no longer running.
     */
    public void downloadStopNotify()
    {
        // Ignore nested calls.
        if( transferStopTime == 0 )
        {
            transferStopTime = System.currentTimeMillis();
        }
    }

    /**
     * Returns the progress in percent. If status == completed will always be 100%.
     */
    public Integer getProgress()
    {
        int percentage;
        long transferDataSize = getTransferDataSize();
        if ( transferDataSize == 0 )
        {
            transferDataSize = 1;
        }
        percentage = (int)( getTransferredDataSize() * 100L / transferDataSize );

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

        return currentProgress;
    }

    /**
     * Sets the download segment that is following this segment.
     */
    public void setNextDownloadSegment( SWDownloadSegment segment )
    {
        nextSegment = segment;
    }

    /**
     * Gets the download segment that is following this segment.
     */
    public SWDownloadSegment getNextDownloadSegment()
    {
        return nextSegment;
    }

    /**
     * Returns if the current segment is allocated by any worker.
     */
    public boolean isAbleToBeAllocated( )
    {
        if ( ! Thread.holdsLock(this))
            throw new Error ("This segment is not locked when allocation status is being requested.");
        return worker == null && ( transferDataSize > transferredDataSize );
    }
    
    /**
     * Sets the segment to be allocated or not allocated by any worker.
     */
    public void setAllocatedByWorker( SWDownloadWorker aWorker )
    {
        if ( ! Thread.holdsLock(this))
            throw new Error ("This segment is not locked by the thread about to allocate a worker");
        worker = aWorker;
    }

    /**
     * Returns the worker who allocated this segment or null if currently not allocated by a worker.
     * @return the worker who allocated this segment or null if currently not allocated by a worker
     */
    public SWDownloadWorker getAllocatedByWorker()
    {
        return worker;
    }

    public boolean isBusy()
    {
        return worker != null;
    }

    /**
     * The incomplete file of the segment. Each segment has it's own download file.
     * The filename contains the localFilename of the download file and the
     * segment code. The first segment of the file should not modify the
     * original file extension. This allows applications to open the file.
     * All other segments should modify the extension since it should not be
     * possible to open a segment that is not the first one.
     * For performance reasons the segment keeps hold to its File reference.
     */
    public File getIncompleteFile( )
    {
        initIncompleteFile();
        return incompleteFile;
    }

    /**
     * Removes the download file of the segment.
     */
    public void removeDownloadDestinationFile()
    {
        if ( worker != null )
        {
            logMessage( Logger.WARNING,
                "Can't remove download destination file if worker is allocated.");
        }
        if ( incompleteFile != null && incompleteFile.exists() )
        {
            boolean succ = incompleteFile.delete();
            if ( !succ )
            {
                logMessage( Logger.FINE,
                    "Failed to delete " + incompleteFile + ".");
            }
        }
    }

    /**
     * Renames the segment 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.
     */
    public void renameToDestinationRoot( File destinationRoot )
        throws FileHandlingException
    {
        initIncompleteFile();
        File newIncompleteFile = createIncompleteFile( destinationRoot, this );
        if ( incompleteFile.exists() )
        {
            FileUtils.renameLocalFile( incompleteFile, newIncompleteFile );
        }
        incompleteFile = newIncompleteFile;
    }

    // new JAXB way
    public XJBSWDownloadSegment createXJBSWDownloadSegment()
        throws JAXBException
    {
        XJBSWDownloadSegment xjbSegment = ObjectFactory.createXJBSWDownloadSegment();
        xjbSegment.setSegmentNumber( segmentNumber );
        xjbSegment.setLength( transferDataSize );
        xjbSegment.setStartPosition( startPos.longValue() );
        if ( incompleteFile != null )
        {
            xjbSegment.setIncompleteFileName( incompleteFile.getAbsolutePath() );
        }
        return xjbSegment;
    }

    /**
     * The method merges this segment with the folowing segment without! doing
     * any check or any filesystem activity. Only use this with care!
     */
    protected void mergeWithNextSegment()
    {
        Logger.logMessage( Logger.FINER, Logger.DOWNLOAD, "Merging myself (" + this
            + ") with " + nextSegment );
        long newSize = transferDataSize + nextSegment.transferDataSize;
        setTransferDataSize( newSize );

        newSize = transferredDataSize + nextSegment.transferredDataSize;
        // we dont use the set method for setting the transferred data size
        // since this method will try another merge in some situations.
        transferredDataSize = newSize;

        nextSegment = nextSegment.nextSegment;
        Logger.logMessage( Logger.FINE, Logger.DOWNLOAD, "Merge result: " + this  );
    }

    /**
     * Creates the incomplete file object.
     * The filename contains the localFilename of the download file and the
     * segment code. The first segment of the file should not modify the
     * original file extension. This allows applications to open the file.
     * All other segments should modify the extension since it should not be
     * possible to open a segment that is not the first one.
     * For performance reasons the segment keeps hold to its File reference.
     */
    private void initIncompleteFile()
    {
        if ( incompleteFile != null )
        {
            return;
        }
        File destFile = downloadFile.getDestinationFile();
        incompleteFile = createIncompleteFile( destFile, this );
    }
    
    private void initTransferredDataSize()
    {
        initIncompleteFile();
        if ( incompleteFile.exists() )
        {
            transferredDataSize = incompleteFile.length();
        }
    }

    private void fireSegmentChange()
    {
        downloadFile.fireDownloadSegmentChanged( this );
    }
    
    public String toString()
    {
        StringBuffer buffer = new StringBuffer();
        buffer.append( getClass().getName() );
        buffer.append( "[#" );
        buffer.append( segmentNumber );
        buffer.append( ", start: " );
        buffer.append( startPos );
        buffer.append( ", so far: " );
        buffer.append( transferredDataSize );
        buffer.append( " of " );
        buffer.append( transferDataSize );
        buffer.append( ", " );
        buffer.append( worker );
        buffer.append( ", rating: " );
        buffer.append( rating );
        if ( nextSegment != null )
        {
            buffer.append( " -> #" );
            buffer.append( nextSegment.segmentNumber );
            buffer.append( ", " );
            buffer.append( nextSegment.startPos );
            buffer.append( ", " );
            buffer.append( nextSegment.transferredDataSize );
            buffer.append( '/' );
            buffer.append( nextSegment.transferDataSize );
            buffer.append( ", " );
            buffer.append( nextSegment.worker );
        }
        buffer.append( "]@" );
        buffer.append( hashCode() );
        buffer.append( "\n" );
        return buffer.toString();
    }

    public void logMessage( Logger.LogLevel level, String msg )
    {
        Logger.logMessage( level, Logger.DOWNLOAD,
            "Segment " + toString() + ": " + msg );
    }

    public IRating getRating()
    {
        return rating;
    }

    private static File createIncompleteFile( File destinationFile,
        SWDownloadSegment segment )
    {
        int tryCount = 0;
        File tryFile;
        do
        {
            StringBuffer fullFileNameBuf = new StringBuffer();
            fullFileNameBuf.append( ServiceManager.sCfg.incompleteDir );
            fullFileNameBuf.append( File.separatorChar );
            fullFileNameBuf.append( "sg" ).append( segment.segmentNumber );
            if ( tryCount > 0 )
            {
                fullFileNameBuf.append( '(' );
                fullFileNameBuf.append( String.valueOf( tryCount ) );
                fullFileNameBuf.append( ')' );
            }
            fullFileNameBuf.append( destinationFile.getName() );
            // if this is not the first segment... change the extension
            if ( segment.startPos.longValue() != 0 )
            {
                fullFileNameBuf.append( ".sg" );
            }
            tryFile = new File( fullFileNameBuf.toString() );
            tryCount ++;
        }
        while ( tryFile.exists() );
        return tryFile;
    }
}
