/*
 * Created on 1 Nov 2006
 * Created by Paul Gardner
 * Copyright (C) 2006 Aelitis, All Rights Reserved.
 *
 * 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.
 * 
 * AELITIS, SAS au capital de 63.529,40 euros
 * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
 *
 */


package com.aelitis.azureus.core.download;

import java.util.List;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.config.impl.TransferSpeedValidator;
import org.gudy.azureus2.core3.disk.DiskManager;
import org.gudy.azureus2.core3.disk.DiskManagerFileInfo;
import org.gudy.azureus2.core3.disk.DiskManagerPiece;
import org.gudy.azureus2.core3.download.DownloadManager;
import org.gudy.azureus2.core3.download.DownloadManagerListener;
import org.gudy.azureus2.core3.download.DownloadManagerPeerListener;
import org.gudy.azureus2.core3.download.impl.DownloadManagerAdapter;
import org.gudy.azureus2.core3.global.GlobalManager;
import org.gudy.azureus2.core3.peer.PEPeer;
import org.gudy.azureus2.core3.peer.PEPeerManager;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.util.*;

import com.aelitis.azureus.core.peermanager.piecepicker.PiecePicker;
import com.aelitis.azureus.core.peermanager.piecepicker.PieceRTAProvider;
import com.aelitis.azureus.core.torrent.PlatformTorrentUtils;
import com.aelitis.azureus.core.util.average.Average;
import com.aelitis.azureus.core.util.average.AverageFactory;
import com.aelitis.azureus.util.ConstantsVuze;


public class 
EnhancedDownloadManager 
{
	public static  int	DEFAULT_MINIMUM_INITIAL_BUFFER_SECS_FOR_ETA	= 30;
		
		// number of seconds of buffer required before we fall back to normal bt mode
	
	public static  int	MINIMUM_INITIAL_BUFFER_SECS;
	
	static{
		COConfigurationManager.addAndFireParameterListeners(
			new String[]{
				"filechannel.rt.buffer.millis"	
			},
			new ParameterListener()
			{
				public void 
				parameterChanged(
					String parameterName )
				{
					int channel_buffer_millis = COConfigurationManager.getIntParameter( "filechannel.rt.buffer.millis" );
					
					MINIMUM_INITIAL_BUFFER_SECS = (2 * channel_buffer_millis )/1000;
				}
			});
	}
	
	public static final int REACTIVATE_PROVIDER_PERIOD			= 5*1000;
	public static final int REACTIVATE_PROVIDER_PERIOD_TICKS	= REACTIVATE_PROVIDER_PERIOD/DownloadManagerEnhancer.TICK_PERIOD;

	public static final int LOG_PROG_STATS_PERIOD	= 10*1000;
	public static final int LOG_PROG_STATS_TICKS	= LOG_PROG_STATS_PERIOD/DownloadManagerEnhancer.TICK_PERIOD;

	private static final int content_stream_bps_min_increase_ratio	= 5;
	private static final int content_stream_bps_max_increase_ratio	= 8;
	
	
	private DownloadManagerEnhancer		enhancer;
	private DownloadManager				download_manager;
	
	private boolean						explicit_progressive;
	
	private volatile PiecePicker		current_piece_pickler;
	
	
	
	private volatile boolean	progressive_active	= false;
	
	private long	content_min_delivery_bps;
		
	private int		minimum_initial_buffer_secs_for_eta;
	
	private bufferETAProvider	buffer_provider	= new bufferETAProvider();

	private progressiveStats	progressive_stats;

		
	private boolean	marked_active;
	private boolean	destroyed;

	private DownloadManagerListener dmListener;
				
	private EnhancedDownloadManagerFile[]	enhanced_files;




	
	protected
	EnhancedDownloadManager(
		DownloadManagerEnhancer		_enhancer,
		DownloadManager				_download_manager )
	{
		enhancer			= _enhancer;
		download_manager	= _download_manager;

		DiskManagerFileInfo[] files = download_manager.getDiskManagerFileInfo();
		
		minimum_initial_buffer_secs_for_eta = DEFAULT_MINIMUM_INITIAL_BUFFER_SECS_FOR_ETA;
		
		enhanced_files = new EnhancedDownloadManagerFile[files.length];
					
		long	offset = 0;
		
		for (int i=0;i<files.length;i++){
			
			DiskManagerFileInfo f = files[i];
							
			enhanced_files[i] = new EnhancedDownloadManagerFile( f, offset );
			
			offset += f.getLength();
		}
		
		
		int primary_index = getPrimaryFileIndex();
		
		EnhancedDownloadManagerFile primary_file = enhanced_files[primary_index==-1?0:primary_index];
				
		progressive_stats	= createProgressiveStats( download_manager, primary_file );
		
		download_manager.addPeerListener(
			new DownloadManagerPeerListener()
			{
       			public void
    			peerManagerWillBeAdded(
    				PEPeerManager	peer_manager )
       			{
       			}
       			
				public void
				peerManagerAdded(
					PEPeerManager	manager )
				{
					synchronized( EnhancedDownloadManager.this ){
											
						current_piece_pickler = manager.getPiecePicker();
						
						if ( progressive_active && current_piece_pickler != null ){
							
							buffer_provider.activate( current_piece_pickler );
						}
					}
				}
				
				public void
				peerManagerRemoved(
					PEPeerManager	manager )
				{
					synchronized( EnhancedDownloadManager.this ){

						progressive_active		= false;
						
						if ( current_piece_pickler != null ){
					
							buffer_provider.deactivate(  current_piece_pickler );
							
							current_piece_pickler	= null;	
						}
					}
				}
				
				public void
				peerAdded(
					PEPeer 	peer )
				{
				}
					
				public void
				peerRemoved(
					PEPeer	peer )
				{
				}
			});
	}

	public int getPrimaryFileIndex() {
		DiskManagerFileInfo info = download_manager.getDownloadState().getPrimaryFile();
		
		if ( info == null ){
			return( -1 );
		}
		return( info.getIndex());
	}

	public void
	setExplicitProgressive(
		int		min_initial_buffer_secs,
		long	min_bps,
		int		file_index )
	{
		if ( file_index >= 0 && file_index < enhanced_files.length ){
			
			explicit_progressive = true;

			minimum_initial_buffer_secs_for_eta = min_initial_buffer_secs;
			
			content_min_delivery_bps = min_bps;
											
			EnhancedDownloadManagerFile primary_file = enhanced_files[file_index];
				
			progressive_stats	= createProgressiveStats( download_manager, primary_file );		
		}
	}
	
	public String
	getName()
	{
		return( download_manager.getDisplayName());
	}
	
	public byte[]
	getHash()
	{
		TOTorrent t = download_manager.getTorrent();
		
		if ( t == null ){
			
			return( null );
		}
		
		try{
			
			return( t.getHash());
			
		}catch( Throwable e ){
		
			return( null );
		}
	}
	
	public boolean
	isPlatform()
	{
		TOTorrent	torrent = download_manager.getTorrent();
		
		if ( torrent != null ){
			
			return( PlatformTorrentUtils.isContent( torrent, true ));
		}

		return( false );
	}
	
	public EnhancedDownloadManagerFile[]
	getFiles()
	{
		return( enhanced_files );
	}
	
	protected long
	getTargetSpeed()
	{
		long	target_speed = progressive_active?progressive_stats.getStreamBytesPerSecondMax():content_min_delivery_bps;
		
		if ( target_speed < content_min_delivery_bps ){
			
			target_speed = content_min_delivery_bps;
		}
			
		return( target_speed );
	}
	
	protected boolean
	updateStats(
		int		tick_count )
	{
		return( updateProgressiveStats( tick_count ));
	}
	

	
	public boolean
	supportsProgressiveMode()
	{
		TOTorrent	torrent = download_manager.getTorrent();
		
		if ( torrent == null ){
			
			return( false );
		}
		
		return( enhancer.isProgressiveAvailable() && 
				( PlatformTorrentUtils.isContentProgressive( torrent ) || explicit_progressive ));
	}
	
	public void
	prepareForProgressiveMode(
		boolean		active )
	{
		enhancer.prepareForProgressiveMode( download_manager, active );
	}
	
	public boolean
	setProgressiveMode(
		boolean		active )
	{
		return( setProgressiveMode( active, false ));
	}
		
	protected boolean
	setProgressiveMode(
		boolean		active,
		boolean		switching_progressive_downloads )
	{
		TOTorrent	torrent = download_manager.getTorrent();
		
		if ( torrent == null || download_manager.getDownloadState().getPrimaryFile() == null ){

			return( false );
		}
		
		DiskManagerFileInfo primaryFile = download_manager.getDownloadState().getPrimaryFile();
		EnhancedDownloadManagerFile enhanced_file = enhanced_files[primaryFile.getIndex()];
		
		synchronized( this ){

			if ( progressive_active == active ){
				
				return( true );
			}			

			if (active && !supportsProgressiveMode()) {
				
				Debug.out( "Attempt to set progress mode on non-progressible content - " + getName());
				
				return( false );
			}
			
			log( "Progressive mode changed to " + active );

			final GlobalManager gm = download_manager.getGlobalManager(); 
			if (active) {
				if (dmListener == null) {
					dmListener = new DownloadManagerAdapter() {
						public void downloadComplete(DownloadManager manager) {
							enhancer.resume();
						}
					};
				}
				download_manager.addListener(dmListener);
				
				// Check existing downloading torrents and turn off any
				// existing progressive/downloading
				Object[] dms = gm.getDownloadManagers().toArray();
				for (int i = 0; i < dms.length; i++) {
					DownloadManager dmCheck = (DownloadManager) dms[i];
					if (dmCheck.equals(download_manager)) {
						continue;
					}

					if (!dmCheck.isDownloadComplete(false)) {
						int state = dmCheck.getState();
						if (state == DownloadManager.STATE_DOWNLOADING
								|| state == DownloadManager.STATE_QUEUED) {
							enhancer.pause( dmCheck );
						}
						EnhancedDownloadManager edmCheck = enhancer.getEnhancedDownload(dmCheck);
						if (edmCheck != null && edmCheck.getProgressiveMode()) {
							edmCheck.setProgressiveMode(false, true);
						}
					}
				}
				if (download_manager.isPaused()) {
					enhancer.resume( download_manager );
				}

				// Make sure download can start by moving out of stop state
				// and putting at top
				if (download_manager.getState() == DownloadManager.STATE_STOPPED) {
					download_manager.setStateWaiting();
				}

				if (download_manager.getPosition() != 1) {
					download_manager.getGlobalManager().moveTo(download_manager, 1);
				}
			} else {
				download_manager.removeListener(dmListener);
				if ( !switching_progressive_downloads ){
					enhancer.resume();
				}
			}
			
			progressive_active	= active;

			if ( progressive_active ){
				
				enhancer.progressiveActivated();
			}
			
			if ( current_piece_pickler != null ){
		
				if ( progressive_active ){
					
					buffer_provider.activate( current_piece_pickler );
										
					progressive_stats.update( 0 );
					
				}else{
					
					buffer_provider.deactivate( current_piece_pickler );
										
					progressive_stats = createProgressiveStats( download_manager, enhanced_file );
				}
			}else{
				
				progressive_stats = createProgressiveStats( download_manager, enhanced_file );
			}
			
			if ( !switching_progressive_downloads ){
				
				if ( active ){
					
					RealTimeInfo.setProgressiveActive(  progressive_stats.getStreamBytesPerSecondMax());
					
				}else{
					
					RealTimeInfo.setProgressiveInactive();
				}
			}
		}
		
		return( true );
	}


	public boolean
	getProgressiveMode()
	{
		return( progressive_active );
	}
	
	public long
	getProgressivePlayETA()
	{
		progressiveStats stats = getProgressiveStats();
		
		if ( stats == null ){
			
			return( Long.MAX_VALUE );
		}
		
		long	eta = stats.getETA();
				
		return( eta );
	}
	
	protected progressiveStats
	getProgressiveStats()
	{
		synchronized( this ){
			
			if ( progressive_stats == null ){
				
				return( null );
			}
			
			return( progressive_stats.getCopy());
		}
	}
	
	protected progressiveStats
	createProgressiveStats(
		DownloadManager					dm,
		EnhancedDownloadManagerFile		file )
	{
		return( new progressiveStatsCommon( dm, file ));
	}
	
	protected boolean
	updateProgressiveStats(
		int		tick_count )
	{
		if ( !progressive_active ){
			
			return( false );
		}
					
		synchronized( this ){
			
			if ( !progressive_active || progressive_stats == null ){
				
				return( false );
			}			

			if ( tick_count % REACTIVATE_PROVIDER_PERIOD_TICKS == 0 ){
				
				PiecePicker piece_picker = current_piece_pickler;
				
				if ( piece_picker != null ){
				
					buffer_provider.checkActivation( piece_picker );
				}
			}
			
			progressive_stats.update( tick_count );
			
			long	current_max = progressive_stats.getStreamBytesPerSecondMax();
			
			if ( RealTimeInfo.getProgressiveActiveBytesPerSec() != current_max ){
				
				RealTimeInfo.setProgressiveActive( current_max );
			}
		}
		
		return( true );
	}
	
	protected void
	setRTA(
		boolean	active )
	{
		synchronized( this ){

			if ( marked_active && !active ){
								
				marked_active = false;

				RealTimeInfo.removeRealTimeTask();
			}
			
			if ( destroyed ){
				
				return;
			}
			
			if ( !marked_active && active ){
				
				marked_active = true;

				RealTimeInfo.addRealTimeTask();
			}
		}
	}

	public long
	getContiguousAvailableBytes(
		int						file_index,
		long					file_start_offset,
		long					stop_counting_after )
	{
		if ( file_index < 0 || file_index >= enhanced_files.length ) {

			return( -1 );
		}

		EnhancedDownloadManagerFile	efile = enhanced_files[ file_index ];
		
		DiskManagerFileInfo file = efile.getFile();
		
		DiskManager dm = download_manager.getDiskManager();
		
		if ( dm == null ){
			
			if ( file.getDownloaded() == file.getLength()){
				
				return( file.getLength() - file_start_offset );
			}
			
			return( -1 );
		}
		
		int	piece_size = dm.getPieceLength();
		
		DiskManagerFileInfo[]	 files = dm.getFiles();
		
		long	start_index = efile.getByteOffestInTorrent() + file_start_offset;
	
		
		int	first_piece_index 	= (int)( start_index / piece_size );
		int	first_piece_offset	= (int)( start_index % piece_size );
		int	last_piece_index	= file.getLastPieceNumber();
		
		DiskManagerPiece[]	pieces = dm.getPieces();
		
		DiskManagerPiece	first_piece = pieces[first_piece_index];
				
		long	available = 0;
		
		if ( !first_piece.isDone()){
			
			boolean[] blocks = first_piece.getWritten();
						
			if ( blocks == null ){
				
				if ( first_piece.isDone()){
					
					available = first_piece.getLength() - first_piece_offset;
				}
			}else{
				
				int	piece_offset = 0;
				
				for (int j=0;j<blocks.length;j++){
				
					if ( blocks[j] ){
					
						int	block_size = first_piece.getBlockSize( j );
						
						piece_offset = piece_offset + block_size;
						
						if ( available == 0 ){
						
							if ( piece_offset > first_piece_offset ){
								
								available = piece_offset - first_piece_offset;
							}
						}else{
							
							available += block_size;
						}						
					}else{
						
						break;
					}
				}
			}	
		}else{
		
			available = first_piece.getLength() - first_piece_offset; 
		
			for (int i=first_piece_index+1;i<=last_piece_index;i++){
				
				if ( stop_counting_after > 0 && available >= stop_counting_after ){
					
					break;
				}
				
				DiskManagerPiece piece = pieces[i];
				
				if ( piece.isDone()){
					
					available += piece.getLength();
					
				}else{
				
					boolean[] blocks = piece.getWritten();
							
					if ( blocks == null ){
						
						if ( piece.isDone()){
					
							available += piece.getLength();
							
						}else{
							
							break;
						}
					}else{
						
						for (int j=0;j<blocks.length;j++){
						
							if ( blocks[j] ){
							
								available += piece.getBlockSize( j );
								
							}else{
								
								break;
							}
						}
					}
					
					break;
				}
			}
		}
		
		long	max_available = file.getLength() - file_start_offset;
		
		if ( available > max_available ){
		
			available = max_available;
		}
		
		return( available );
	}
	
	
	public DownloadManager 
	getDownloadManager() 
	{
		return download_manager;
	}
	
	protected void 
	destroy()
	{
		synchronized( this ){
			
			setRTA( false );

			destroyed = true;
		}
	}
	
	protected void
	log(
		String	str )
	{
		log( str, true );
	}
	
	protected void
	log(
		String	str,
		boolean	to_file )
	{
		log( download_manager, str, to_file );
	}
	
	protected void
	log(
		DownloadManager	dm,
		String			str,
		boolean			to_file )
	{
		str = dm.toString() + ": " + str;
		
		if ( to_file ){
			
			AEDiagnosticsLogger diag_logger = AEDiagnostics.getLogger("v3.Stream");
			
			diag_logger.log(str);
		}
		
		if ( ConstantsVuze.DIAG_TO_STDOUT ) {
			
			System.out.println(Thread.currentThread().getName() + "|"
					+ System.currentTimeMillis() + "] " + str);
		}
	}
	
	protected class
	bufferETAProvider
		implements PieceRTAProvider
	{
		private boolean		is_buffering	= true;
		
		private long[]		piece_rtas;
				
		private long				last_buffer_size;
		private long				last_buffer_size_time;
		
		private boolean		active;
		private long		last_recalc;
		
		protected void
		activate(	
			PiecePicker		picker )
		{
			synchronized( EnhancedDownloadManager.this ){

				if ( !active ){

					log( "Activating RTA provider" );
				
					active = true;
				
					picker.addRTAProvider( this );
				}
			}
		}
		
		protected void
		deactivate(
			PiecePicker		picker )
		{			
			synchronized( EnhancedDownloadManager.this ){
		
				if ( active ){
					
	   				log( "Deactivating RTA provider" );

	   				picker.removeRTAProvider( this );
				}
				
				piece_rtas	= null;
				
				active = false;
			}
		}
		
		protected void
		checkActivation(
			PiecePicker		picker )
		{
				// might need to re-enable the buffer provider if speeds change
			
   			if ( getProgressivePlayETA() > 0 ){
  
    			synchronized( EnhancedDownloadManager.this ){
    					
    				if ( piece_rtas == null ){
    						
    					activate( picker );
     				}
    			}
    		}
		}
		
		public long[]
    	updateRTAs(
    		PiecePicker		picker )
    	{		
			long	mono_now = SystemTime.getMonotonousTime();
			
			if ( mono_now - last_recalc < 500 ){
				
				return( piece_rtas );
			}
			
			last_recalc	= mono_now;
								
    		DiskManager	disk_manager = download_manager.getDiskManager();

    		progressiveStats stats = progressive_stats;

    		EnhancedDownloadManagerFile file = stats.getFile();

    		if ( disk_manager == null || stats == null || file.isComplete()){
     				
    			deactivate( picker );
    			
    			return( null );
    		}
    		
			long	abs_provider_pos = stats.getCurrentProviderPosition( true );
 			long	rel_provider_pos = stats.getCurrentProviderPosition( false );

			long	buffer_bytes = stats.getBufferBytes();
 			
  			boolean buffering = getProgressivePlayETA() >= 0;
  			
  			if ( buffering ){
  				
				long	 buffer_size = getContiguousAvailableBytes( file.getIndex(), rel_provider_pos, buffer_bytes );
				
				if ( buffer_size == buffer_bytes ){
					
					buffering = false;
				}
  			}
  				
  			if ( buffering != is_buffering ){
  			
  				if ( buffering ){
  					
  					log( "Switching to buffer mode" );
  					
  				}else{
  					
  					log( "Switching to RTA mode" );
  				}
  				
  				is_buffering = buffering;
  			}
  			
			long	piece_size = disk_manager.getPieceLength();
			 			  		  
			int		start_piece = (int)( abs_provider_pos / piece_size );

			int		end_piece	= file.getFile().getLastPieceNumber();
						
			piece_rtas = new long[ picker.getNumberOfPieces()];

			long	now = SystemTime.getCurrentTime();

			if ( is_buffering ){
								
				for (int i=start_piece;i<=end_piece;i++){
					
						// not bothered about times here but need to use large increments to ensure
						// that pieces are picked in order even for slower peers
					
					piece_rtas[i] = now+(i*60000);
				}
	
				long	 buffer_size = getContiguousAvailableBytes( file.getIndex(), rel_provider_pos, 0 );
     			    			
    			if ( last_buffer_size != buffer_size ){
    				
    				last_buffer_size = buffer_size;
    				
    				last_buffer_size_time = now;
    				
    			}else{
    				
    				if ( now < last_buffer_size_time ){
    					
    					last_buffer_size_time = now;
    					
    				}else{
    					
    					long	stalled_for = now - last_buffer_size_time;
    					
   						long	dl_speed = progressive_stats.getDownloadBytesPerSecond();
   					 
   						if ( dl_speed > 0 ){
   							
   							long	block_time = (DiskManager.BLOCK_SIZE * 1000) / dl_speed;
   							
   							if ( stalled_for > Math.max( 5000, 5*block_time )){
    						
   								long	target_rta = now + block_time;
   								
   								int	blocked_piece_index = (int)((abs_provider_pos + buffer_size ) / disk_manager.getPieceLength());
   								
   								DiskManagerPiece[] pieces = disk_manager.getPieces();
   								  								
   								if ( blocked_piece_index < pieces.length ){
   									  									
   									if ( pieces[blocked_piece_index].isDone()){
   										
   										blocked_piece_index++;
   										
   										if ( blocked_piece_index < pieces.length ){
   											
   											if ( pieces[blocked_piece_index].isDone()){
   												
   												blocked_piece_index = -1;
   											}
   										}else{
   											
   											blocked_piece_index = -1;
   										}
   									}
   								}
   								
   								if ( blocked_piece_index >= 0 ){
   									  										
   									piece_rtas[blocked_piece_index] = target_rta;
   										
   									log( "Buffer provider: reprioritising lagging piece " + blocked_piece_index + " with rta " + block_time );
   								}
   							}
   						}
    				}
    			}
    		}else{
    			
				long	bytes_offset = 0;
				
				long	max_bps = stats.getStreamBytesPerSecondMax();
								
				for (int i=start_piece;i<=end_piece;i++){
										
					piece_rtas[i] = now + ( 1000 * ( bytes_offset / max_bps ));
					
					bytes_offset += piece_size;		
					
					if ( bytes_offset > buffer_bytes ){
							
						break;
					}
				}
    		}
    	
    		return( piece_rtas );
    	}
    	
    	public long
    	getCurrentPosition()
    	{
    		return( 0 );
    	}
    	
      	public long
       	getStartTime()
       	{
       		return( 0 );
       	}
       	
       	public long
       	getStartPosition()
       	{
       		return( 0 );
       	}
       	
    	public long
		getBlockingPosition()
		{
			return( 0 );
		}
		
    	public void
    	setBufferMillis(
			long	millis,
			long	delay_millis )
		{
		}
    	
		public String
		getUserAgent()
		{
			return( null );
		}
	}
	
	protected abstract class
	progressiveStats
		implements Cloneable
	{
		protected abstract EnhancedDownloadManagerFile 
		getFile();
		
		protected abstract boolean
		isProviderActive();
			
		protected abstract long
		getCurrentProviderPosition(
			boolean		absolute );
		
		protected abstract long
		getStreamBytesPerSecondMax();
		
		protected abstract long
		getStreamBytesPerSecondMin();
		
		protected abstract long
		getDownloadBytesPerSecond();
		
		protected abstract long
		getETA();
			
		public abstract long
		getBufferBytes();
		
		protected abstract long
		getSecondsToDownload();
		
		protected abstract long
		getSecondsToWatch();

		protected abstract void
		update(
			int	tick_count );
				
		protected progressiveStats
		getCopy()
		{
			try{
				return((progressiveStats)clone());
				
			}catch( CloneNotSupportedException e ){
				
				Debug.printStackTrace(e);
				
				return( null );
			}
		}
		
		protected String
		formatBytes(
			long	l )
		{
			return( DisplayFormatters.formatByteCountToKiBEtc( l ));
		}
		
		protected String
		formatSpeed(
			long	l )
		{
			return( DisplayFormatters.formatByteCountToKiBEtcPerSec( l ));
		}

	}
	
	protected class
	progressiveStatsCommon
		extends progressiveStats
	{
		private EnhancedDownloadManagerFile	primary_file;
		
		private PieceRTAProvider	current_provider;
		private String				current_user_agent;
		
		private long	content_stream_bps_min;
		private long	content_stream_bps_max;


		private Average		capped_download_rate_average 	= AverageFactory.MovingImmediateAverage( 10 );
		private Average		discard_rate_average 			= AverageFactory.MovingImmediateAverage( 10 );
		private long		last_discard_bytes				= download_manager.getStats().getDiscarded();
		
		private long		actual_bytes_to_download;
		private long		weighted_bytes_to_download;		// gives less weight to incomplete pieces
		
		private long		provider_life_secs;
		private long		provider_initial_position;
		private long		provider_byte_position;
		private long		provider_last_byte_position	= -1;
		private long		provider_blocking_byte_position;
		private Average		provider_speed_average	= AverageFactory.MovingImmediateAverage( 10 );
				
		protected
		progressiveStatsCommon(
			DownloadManager					_dm,
			EnhancedDownloadManagerFile		_primary_file )
		{		
			primary_file = _primary_file;
			
			TOTorrent	torrent = download_manager.getTorrent();
						
			content_stream_bps_min = explicit_progressive?content_min_delivery_bps:PlatformTorrentUtils.getContentStreamSpeedBps( torrent );
			
			if ( content_stream_bps_min == 0 ){
			
					// hack in some test values for torrents that don't have a bps in them yet
				
				long	size = torrent.getSize();
				
				if ( size < 200*1024*1024 ){
				
					content_stream_bps_min = 30*1024;
					
				}else if ( size < 1000*1024*1024L ){
					
					content_stream_bps_min = 200*1024;
					
				}else{

					content_stream_bps_min = 400*1024;
				}
			}
				
				// bump it up by a bit to be conservative to deal with fluctuations, discards etc.
				
			content_stream_bps_min += content_stream_bps_min / content_stream_bps_min_increase_ratio;
			
			content_stream_bps_max = content_stream_bps_min + ( content_stream_bps_min / content_stream_bps_max_increase_ratio );
			
			setRTA( false );
			
			log( 	download_manager,
					"content_stream_bps=" + getStreamBytesPerSecondMin() +
					",primary=" + primary_file.getFile().getIndex(),
					true );
		}
	
						
		protected void
		updateCurrentProvider(
			PieceRTAProvider	provider )
		{
			long	file_start = primary_file.getByteOffestInTorrent();

			if ( current_provider != provider || provider == null ){
				
				current_provider 	= provider;
				current_user_agent	= null;
								
				provider_speed_average	= AverageFactory.MovingImmediateAverage( 10 );
				
				if ( current_provider == null ){
					
					provider_life_secs					= 0;
					provider_initial_position			= file_start;
					provider_byte_position				= file_start;
					provider_blocking_byte_position		= -1;
					provider_last_byte_position 		= -1;
					
				}else{
					
					provider_initial_position	= Math.max( file_start, current_provider.getStartPosition());
					
					provider_byte_position 		= provider_initial_position;
					provider_last_byte_position	= provider_initial_position;
					
					provider_blocking_byte_position		= current_provider.getBlockingPosition();
					
					provider_life_secs = ( SystemTime.getCurrentTime() - current_provider.getStartTime()) / 1000;
					
					if ( provider_life_secs < 0 ){
						
						provider_life_secs = 0;
					}
				}
				
				setRTA( current_provider != null );
				
			}else{
				
				provider_life_secs++;
					
				if ( current_user_agent == null ){
				
					current_user_agent = current_provider.getUserAgent();
					
					if ( current_user_agent != null ){
						
						log( "Provider user agent = " + current_user_agent );
					}
				}
				
				provider_byte_position			= Math.max( file_start, current_provider.getCurrentPosition());
				provider_blocking_byte_position	= current_provider.getBlockingPosition();
				
				long bytes_read = provider_byte_position - provider_last_byte_position;
					
				provider_speed_average.update( bytes_read );
	
				provider_last_byte_position = provider_byte_position;
			}
		}
		
		protected boolean
		isProviderActive()
		{
			return( current_provider != null );
		}
		
		protected long
		getInitialProviderPosition()
		{
			return( provider_initial_position );
		}
		
		protected long
		getCurrentProviderPosition(
			boolean		absolute )
		{
			long	res = provider_byte_position;
			
			if ( absolute ){
				
				if ( res == 0 ){
					
					res = primary_file.getByteOffestInTorrent();
				}
			}else{
				
				res -= primary_file.getByteOffestInTorrent();
				
				if ( res < 0 ){
					
					res = 0;
				}
			}
			
			return( res );
		}
		
		protected long
		getProviderLifeSecs()
		{
			return( provider_life_secs );
		}
		
		protected void
		update(
			int		tick_count )
		{
			long download_rate = download_manager.getStats().getDataReceiveRate();
			
			capped_download_rate_average.update( download_rate );
			
			long	discards = download_manager.getStats().getDiscarded();
			
			discard_rate_average.update( discards - last_discard_bytes );
			
			last_discard_bytes = discards;
			
			DiskManager	disk_manager = download_manager.getDiskManager();
			
			PiecePicker	picker = current_piece_pickler;

			if ( getStreamBytesPerSecondMin() > 0 && disk_manager != null && picker != null ){
				
				List	providers = picker.getRTAProviders();
				
				long	max_cp	= 0;
				
				PieceRTAProvider	best_provider = null;
				
				for (int i=0;i<providers.size();i++){
					
					PieceRTAProvider	provider = (PieceRTAProvider)providers.get(i);
					
					if ( provider.getStartTime() > 0 ){
						
						long	cp = provider.getCurrentPosition();
						
						if ( cp >= max_cp ){
							
							best_provider = provider;
							
							max_cp	= cp;
						}
					}
				}

				updateCurrentProvider( best_provider );
												
				if ( best_provider != null ){
								
						// the file channel provider will try best-effort-RTA based which will result
						// in high discard - back it off based on how much slack we have 
					
					long relative_pos = getCurrentProviderPosition( false );
					
					long buffer_bytes = getContiguousAvailableBytes( primary_file.getIndex(), relative_pos, getStreamBytesPerSecondMin() * 60 );
					
					long buffer_secs = buffer_bytes / getStreamBytesPerSecondMin();
					
						// don't be too aggresive with small buffers
					
					buffer_secs = Math.max( 10, buffer_secs );
					
					best_provider.setBufferMillis( 15*1000, buffer_secs * 1000 );
				}
				
				DiskManagerPiece[] pieces = disk_manager.getPieces();
				
				actual_bytes_to_download 	= 0;
				weighted_bytes_to_download	= 0;
				
				int	first_incomplete_piece = -1;
				
				int	piece_size = disk_manager.getPieceLength();
				
				int	last_piece_number = primary_file.getFile().getLastPieceNumber();
				
				for (int i=(int)(provider_byte_position/piece_size);i<=last_piece_number;i++){
					
					DiskManagerPiece piece = pieces[i];
					
					if ( piece.isDone()){
						
						continue;
					}
					
					if ( first_incomplete_piece == -1 ){
						
						first_incomplete_piece = i;
					}
					
					boolean[] blocks = piece.getWritten();
					
					int	bytes_this_piece = 0;
					
					if ( blocks == null ){
						
						bytes_this_piece = piece.getLength();
						
					}else{
						for (int j=0;j<blocks.length;j++){
							
							if ( !blocks[j] ){
								
								bytes_this_piece += piece.getBlockSize( j );
							}
						}
					}
					
					if ( bytes_this_piece > 0 ){
						
						actual_bytes_to_download += bytes_this_piece;
						
						int	diff = i - first_incomplete_piece;
						
						if ( diff == 0 ){
							
							weighted_bytes_to_download += bytes_this_piece;
							
						}else{
														
							int	weighted_bytes_done =  piece.getLength() - bytes_this_piece;
						
							weighted_bytes_done = ( weighted_bytes_done * ( pieces.length - i )) / (pieces.length - first_incomplete_piece);
						
							weighted_bytes_to_download += piece.getLength() - weighted_bytes_done;
						}
					}
				}
			}
			
			log( getString(), tick_count % LOG_PROG_STATS_TICKS == 0 );
		}
						
		protected long
		getETA()
		{
			DiskManagerFileInfo file = primary_file.getFile();
			
			if ( file.getLength() == file.getDownloaded()){
				
				return( 0 );
			}
			
			long download_rate = getDownloadBytesPerSecond();
			
			if ( download_rate <= 0 ){
				
				return( Long.MAX_VALUE );
			}
			
			long	buffer_bytes	= getBufferBytes();
			
			long	buffer_done		= getContiguousAvailableBytes( file.getIndex(), getCurrentProviderPosition( false ), buffer_bytes );
			
			long 	rem_buffer = buffer_bytes - buffer_done;	// ok as initial dl is forced in order byte buffer-rta
			
			long 	rem_secs = (rem_buffer<=0)?0:(rem_buffer / download_rate);
			
			long	secs_to_download = getSecondsToDownload();
									
			long	secs_to_watch = getSecondsToWatch();
			
			long eta = secs_to_download - secs_to_watch;
			
			if ( rem_secs > eta && rem_secs > 0 ){
				
				eta = rem_secs;
			}
			
			return( eta );
		}
		
		protected long
		getStreamBytesPerSecondMax()
		{
			return( content_stream_bps_max );
		}

		protected long
		getStreamBytesPerSecondMin()
		{
			return( content_stream_bps_min );
		}

		public long
		getBufferBytes()
		{			
			long	min_dl = minimum_initial_buffer_secs_for_eta * getStreamBytesPerSecondMax();
			
			return( min_dl );
		}
			
		protected EnhancedDownloadManagerFile 
		getFile() 
		{
			return( primary_file );
		}
		
		protected long 
		getDownloadBytesPerSecond() 
		{
			long	original = (long)capped_download_rate_average.getAverage();
			
			long	current	= original;
			
			int	dl_limit = download_manager.getStats().getDownloadRateLimitBytesPerSecond();
			
			if ( dl_limit > 0 ){
				
				current = Math.min( current, dl_limit );
			}
			
			int global_limit = TransferSpeedValidator.getGlobalDownloadRateLimitBytesPerSecond();
			
			if ( global_limit > 0 ){
				
				current = Math.min( current, global_limit );
			}
						
			return( current );
		}
		
		protected long
		getSecondsToDownload()
		{
			long download_rate = getDownloadBytesPerSecond();

			if ( download_rate == 0 ){
				
				return( Long.MAX_VALUE );
			}
			
			return( weighted_bytes_to_download / download_rate );
		}
		
		public long
		getSecondsToWatch()
		{
			return(( primary_file.getLength() - getCurrentProviderPosition( false )) / getStreamBytesPerSecondMin());
		}
										
		protected String
		getString()
		{
			long	dl_rate = getDownloadBytesPerSecond();
				
			long	buffer_bytes	= getBufferBytes();
			
			long	buffer_done		= getContiguousAvailableBytes( primary_file.getIndex(), getCurrentProviderPosition( false ), buffer_bytes );

			return( "play_eta=" + getETA() + "/d=" + getSecondsToDownload() + "/w=" + getSecondsToWatch()+ 
					", dl_rate=" + formatSpeed(dl_rate)+ ", download_rem=" + formatBytes(weighted_bytes_to_download) + "/" + formatBytes(actual_bytes_to_download) +
					", discard_rate=" + formatSpeed((long)discard_rate_average.getAverage()) +
					", buffer: " + buffer_bytes + "/" + buffer_done +
					", prov: byte=" + formatBytes( provider_byte_position ) + " secs=" + ( provider_byte_position/getStreamBytesPerSecondMin()) + " speed=" + formatSpeed((long)provider_speed_average.getAverage()) +
					" block= " + formatBytes( provider_blocking_byte_position ));
		}
	}
}
