/*
 * Created on Jun 21, 2010
 * Created by Paul Gardner
 * 
 * Copyright 2010 Vuze, Inc.  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; version 2 of the License only.
 * 
 * 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.
 */


package com.aelitis.azureus.core.download;

import java.util.*;
import java.lang.reflect.Method;
import java.net.URL;


import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.download.DownloadManager;
import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.AESemaphore;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.AsyncDispatcher;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.DisplayFormatters;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimeFormatter;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.PluginListener;
import org.gudy.azureus2.plugins.PluginManager;
import org.gudy.azureus2.plugins.disk.DiskManagerFileInfo;
import org.gudy.azureus2.plugins.download.Download;
import org.gudy.azureus2.plugins.download.DownloadStats;
import org.gudy.azureus2.plugins.torrent.TorrentAttribute;
import org.gudy.azureus2.pluginsimpl.local.PluginCoreUtils;
import org.gudy.azureus2.pluginsimpl.local.PluginInitializer;

import com.aelitis.azureus.core.AzureusCoreFactory;
import com.aelitis.azureus.core.devices.Device;
import com.aelitis.azureus.core.devices.DeviceManager;
import com.aelitis.azureus.core.devices.DeviceManagerFactory;
import com.aelitis.azureus.core.devices.DeviceMediaRenderer;
import com.aelitis.azureus.core.devices.TranscodeAnalysisListener;
import com.aelitis.azureus.core.devices.TranscodeException;
import com.aelitis.azureus.core.devices.TranscodeJob;
import com.aelitis.azureus.core.devices.TranscodeManager;
import com.aelitis.azureus.core.devices.TranscodeProfile;
import com.aelitis.azureus.core.devices.TranscodeProviderAnalysis;
import com.aelitis.azureus.core.devices.TranscodeQueue;
import com.aelitis.azureus.ui.UIFunctions;
import com.aelitis.azureus.ui.swt.plugininstall.SimplePluginInstaller;

public class 
StreamManager 
{
	private static final int BUFFER_SECS 		= 30;
	private static final int BUFFER_MIN_SECS	= 3;
	

	private static StreamManager		singleton = new StreamManager();
	
	public static StreamManager
	getSingleton()
	{
		return( singleton );
	}
	
	private TorrentAttribute	mi_ta;
	
	private AsyncDispatcher	dispatcher = new AsyncDispatcher();
	
	private List<SMDImpl>		streamers = new ArrayList<SMDImpl>();
	
	private
	StreamManager()
	{
		PluginInterface default_pi = PluginInitializer.getDefaultInterface();

		mi_ta = default_pi.getTorrentManager().getPluginAttribute( "sm_metainfo" );
		
		default_pi.addListener(
			new PluginListener()
			{
				public void
				initializationComplete()
				{
					
				}
				
				public void
				closedownInitiated()
				{
					dispatcher.dispatch(
						new AERunnable()
						{
							public void 
							runSupport()
							{
								List<SMDImpl>	to_cancel;
								
								synchronized( StreamManager.this ){
									
									to_cancel = new ArrayList<SMDImpl>( streamers );
									
									streamers.clear();
								}
								
								for ( SMDImpl s: to_cancel ){
									
									s.cancel();
								}
							}
						});
				}
				
				public void
				closedownComplete()
				{
					
				}
			});
	}
	
	public boolean
	isStreamingUsable()
	{
			// need win or osx 10.5+
		
		if ( !( Constants.isWindows || Constants.isOSX_10_5_OrHigher )){
			
			return( false );
		}
		
		try{
			PluginManager plug_man = AzureusCoreFactory.getSingleton().getPluginManager();
			
			PluginInterface xcode_pi = plug_man.getPluginInterfaceByID( "vuzexcode", false );
	
			if ( xcode_pi != null && !xcode_pi.getPluginState().isOperational()){
				
					// can't use if xcode borked
				
				return( false );
			}
			
				// otherwise xcode will be installed on demand
			
			PluginInterface emp_pi = plug_man.getPluginInterfaceByID( "azemp", false );
	
			if ( emp_pi == null ){
				
					// will be installed on demand
				
				return( true );
			}
			
			if ( !emp_pi.getPluginState().isOperational()){
				
					// can't use if emp borked
				
				return( false );
			}
			
				// emp installed but need version with prepareWindow, wait for update
			
			Class<?> epwClass = emp_pi.getPlugin().getClass().getClassLoader().loadClass( "com.azureus.plugins.azemp.ui.swt.emp.EmbeddedPlayerWindowSWT" );
			
			Method method = epwClass.getMethod( "prepareWindow", new Class[] { String.class });
			
			return( method != null );
			
		}catch( Throwable e ){
			
			return( false );
		}
	}
	
	public StreamManagerDownload
	stream(
		DownloadManager					dm,
		int								file_index,
		URL								url,
		boolean							preview_mode,
		StreamManagerDownloadListener	listener )
	{
		SMDImpl	result = new SMDImpl( dm, file_index, url, preview_mode, listener );
			
		synchronized( StreamManager.this ){
			
			streamers.add( result );
		}
		
		return( result );
	}


	private class
	SMDImpl
		extends AERunnable
		implements StreamManagerDownload
	{
		private DownloadManager						dm;
		private int									file_index;
		private URL									url;
		private StreamManagerDownloadListener		listener;
			
		private int						existing_dl_limit;
		
		private boolean					preview_mode;
		private long					preview_mode_last_change = 0;
		
		private AESemaphore				active_sem;
		private TranscodeJob			active_job;
		
		private EnhancedDownloadManager	active_edm;
		private boolean					active_edm_activated;

		private volatile boolean		cancelled;
		
		private 
		SMDImpl(
			DownloadManager					_dm,
			int								_file_index,
			URL								_url,
			boolean							_preview_mode,
			StreamManagerDownloadListener	_listener )
		{
			dm				= _dm;
			file_index		= _file_index;
			url				= _url;
			preview_mode	= _preview_mode;
			listener		= _listener;
			
			dispatcher.dispatch( this );
		}
		
		public DownloadManager
		getDownload()
		{
			return( dm );
		}
		
		public int
		getFileIndex()
		{
			return( file_index );
		}
		
		public URL
		getURL()
		{
			return( url );
		}
		
		public boolean
		getPreviewMode()
		{
			return( preview_mode );
		}
		
		public void
		setPreviewMode(
			boolean	_preview_mode )
		{
			long	now = SystemTime.getMonotonousTime();
			
			if ( 	preview_mode_last_change == 0 || 
					now - preview_mode_last_change > 500 ){
				
				preview_mode_last_change = now;
				
				preview_mode = _preview_mode;
				
				listener.updateActivity( "Preview mode changed to " + preview_mode );
			}
		}
		
		public void
		runSupport()
		{
			try{
				synchronized( StreamManager.this ){
					
					if ( cancelled ){
						
						throw( new Exception( "Cancelled" ));
					}

					active_edm = DownloadManagerEnhancer.getSingleton().getEnhancedDownload( dm );
				}

				final long stream_start = SystemTime.getMonotonousTime();
				
				final Download download = PluginCoreUtils.wrap( dm );
				
				final DiskManagerFileInfo file = download.getDiskManagerFileInfo( file_index );
								
				PluginInterface emp_pi = checkPlugin( "azemp", "media player" );

				checkPlugin( "vuzexcode", "media analyser" );

				Class<?> epwClass = emp_pi.getPlugin().getClass().getClassLoader().loadClass( "com.azureus.plugins.azemp.ui.swt.emp.EmbeddedPlayerWindowSWT" );
				
				Method method = epwClass.getMethod( "prepareWindow", new Class[] { String.class });
				
				final Object player = method.invoke(null, new Object[] { file.getFile( true ).getName() });
			
				final Method buffering_method	= player.getClass().getMethod( "bufferingPlayback", new Class[] { Map.class });
				final Method is_active_method	= player.getClass().getMethod( "isActive", new Class[] {});

				final StreamManagerDownloadListener original_listener = listener;
				
				listener = 
					new StreamManagerDownloadListener()
					{
						public void
						updateActivity(
							String		str )
						{
							original_listener.updateActivity(str);
						}
						
						public void
						updateStats(
							int			secs_until_playable,
							int			buffer_secs,
							long		buffer_bytes,
							int			target_buffer_secs )
						{
							original_listener.updateStats(secs_until_playable, buffer_secs, buffer_bytes, target_buffer_secs);
						}
						
						public void
						ready()
						{
							original_listener.ready();
						}
						
						public void
						failed(
							Throwable 	error )
						{
							try{
								original_listener.failed(error);
								
								Map<String,Object> b_map = new HashMap<String,Object>();
								
								b_map.put( "state", new Integer( 3 ));
								b_map.put( "msg", Debug.getNestedExceptionMessage( error ));
								
								try{
									buffering_method.invoke(player, new Object[] { b_map });
	
								}catch( Throwable e ){
									
									Debug.out( e );
								}
							}finally{
								
								cancel();
							}
						}
					};
				
				Map<String,Map<String,Object>>	map = (Map<String,Map<String,Object>>)download.getMapAttribute( mi_ta );
				
				Long	l_duration 		= null;
				Long	l_video_width 	= null;
				Long	l_video_height 	= null;
				
				if ( map != null ){
					
					Map<String,Object> file_map = map.get( String.valueOf( file_index ));
					
					if ( file_map != null ){
						
						l_duration 		= (Long)file_map.get( "duration" );
						l_video_width 	= (Long)file_map.get( "video_width" );
						l_video_height 	= (Long)file_map.get( "video_height" );
					}
				}
				
				final long duration;
				long video_width;
				long video_height;
				
				if ( l_duration == null ){
						
					active_edm.prepareForProgressiveMode( true );
					
					try{
						DeviceManager dm = DeviceManagerFactory.getSingleton();
						
						TranscodeManager tm = dm.getTranscodeManager();
						
						DeviceMediaRenderer dmr = 
							(DeviceMediaRenderer)dm.addVirtualDevice(
								Device.DT_MEDIA_RENDERER,
								"18a0b53a-a466-6795-1d0f-cf38c830ca0e", 
								"generic",
								"Media Analyser" );
	
						dmr.setHidden(true);
	
						TranscodeQueue queue = tm.getQueue();
						
						TranscodeJob[] jobs = queue.getJobs();
						
						for ( TranscodeJob job: jobs ){
							
							if ( job.getTarget() == dmr ){
								
								job.removeForce();
							}
						}
						
						TranscodeProfile[] profiles = dmr.getTranscodeProfiles();
						
						TranscodeProfile profile = null;
						
						for (TranscodeProfile p : profiles) {
							
							if ( p.getName().equals( "Generic MP4" )){
								
								profile = p;
	
								break;
							}
						}
						
						if ( profile == null ){
							
							throw( new Exception( "Analyser transcode profile not found" ));
						}
						
						listener.updateActivity( "Analysing media" );
						
						final Map<String,Object> b_map = new HashMap<String,Object>();
						
						b_map.put( "state", new Integer( 1 ));
						b_map.put( "msg", MessageText.getString( "stream.analysing.media" ));
						
						buffering_method.invoke(player, new Object[] { b_map });

						final TranscodeJob tj = queue.add( dmr, profile, file, true );
											
						try{
							final AESemaphore sem = new AESemaphore( "analyserWait" );

							synchronized( StreamManager.this ){
								
								if ( cancelled ){
									
									throw( new Exception( "Cancelled" ));
								}
								
								active_sem	= sem;
								active_job 	= tj;
							}
								
							final long[] properties = new long[3];
							
							final Throwable[] error = { null };
							
							tj.analyseNow(
								new TranscodeAnalysisListener()
								{
									public void
									analysisComplete(
										TranscodeJob					file,
										TranscodeProviderAnalysis		analysis )
									{
										try{											
											properties[0] = analysis.getLongProperty( TranscodeProviderAnalysis.PT_DURATION_MILLIS );
											properties[1] = analysis.getLongProperty( TranscodeProviderAnalysis.PT_VIDEO_WIDTH );
											properties[2] = analysis.getLongProperty( TranscodeProviderAnalysis.PT_VIDEO_HEIGHT );
											
											tj.removeForce();
											
										}finally{
											
											sem.releaseForever();
										}
									}
									
									public void
									analysisFailed(
										TranscodeJob		file,
										TranscodeException	e )
									{
										try{
											error[0] = e;
										
											tj.removeForce();
											
										}finally{
											
											sem.releaseForever();
										}
									}
								});
							
							new AEThread2( "SM:anmon" )
								{
									public void
									run()
									{
										boolean	last_preview_mode = preview_mode;
										
										while( !sem.isReleasedForever() && !cancelled ){
											
											if ( !sem.reserve( 250 )){
											
												if ( cancelled ){
													
													return;
												}
													
												try{
													Boolean b = (Boolean)is_active_method.invoke( player, new Object[0] );
													
													if ( !b ){
														
														cancel();
														
														break;
													}
												}catch( Throwable e ){	
												}
												
												if ( last_preview_mode != preview_mode ){
													
													last_preview_mode = preview_mode;
													
													b_map.put( "msg", MessageText.getString( last_preview_mode?"stream.analysing.media.preview":"stream.analysing.media" ));

												}
												DownloadStats stats = download.getStats();
												
												b_map.put( "dl_rate", stats.getDownloadAverage());
												b_map.put( "dl_size", stats.getDownloaded());
												b_map.put( "dl_time", SystemTime.getMonotonousTime() - stream_start );
												
												try{
													buffering_method.invoke(player, new Object[] { b_map });

												}catch( Throwable e ){
													
												}
											}
										}
									}
								}.start();
								
							sem.reserve();
							
							synchronized( StreamManager.this ){
								
								if ( cancelled ){
										
									throw( new Exception( "Cancelled" ));
								}
								
								active_job 	= null;
								active_sem	= null;
							}
							
							if ( error[0] != null ){
								
								throw( error[0] );
							}
							
							duration 		= properties[0];
							video_width		= properties[1];
							video_height	= properties[2];
							
							if ( duration > 0 ){
								
								if ( map == null ){
									
									map = new HashMap<String, Map<String,Object>>();
									
								}else{
									
									map = new HashMap<String, Map<String,Object>>( map );
								}
								
								Map<String,Object> file_map = map.get( String.valueOf( file_index ));
								
								if ( file_map == null ){
								
									file_map = new HashMap<String, Object>();
									
									map.put( String.valueOf( file_index ), file_map );
								}
								
								file_map.put( "duration", duration );
								file_map.put( "video_width", video_width );
								file_map.put( "video_height", video_height );
								
								download.setMapAttribute( mi_ta, map );
							}
							
						}catch( Throwable e ){
							
							tj.removeForce();
							
							throw( e );
						}
						
					}catch( Throwable e ){
						
						throw( new Exception( "Media analysis failed", e ));
						
					}finally{
						
					}
				}else{
						
					duration 		= l_duration;
					video_width		= l_video_width==null?0:l_video_width;
					video_height	= l_video_height==null?0:l_video_height;
				}
					
				if ( video_width == 0 || video_height == 0){
					
					throw( new Exception( "Media analysis failed - video stream not found" ));
				}
				
				if ( duration == 0 ){
					
					throw( new Exception( "Media analysis failed - duration unknown" ));
				}
				
				listener.updateActivity( "MetaData read: duration=" + TimeFormatter.formatColon( duration/1000) + ", width=" + video_width + ", height=" + video_height );
				
				Method smd_method = player.getClass().getMethod( "setMetaData", new Class[] { Map.class });
				
				Map<String,Object>	md_map = new HashMap<String,Object>();
				
				md_map.put( "duration", duration );
				md_map.put( "width", video_width );
				md_map.put( "height", video_height );
				
				smd_method.invoke( player, new Object[] { md_map });

				final long	bytes_per_sec = file.getLength() / (duration/1000);
			
				long	dl_lim_max 		= COConfigurationManager.getIntParameter( "Plugin.azemp.azemp.config.dl_lim_max" ) * 1024L;
				long	dl_lim_extra 	= COConfigurationManager.getIntParameter( "Plugin.azemp.azemp.config.dl_lim_extra" ) * 1024L;

				existing_dl_limit = download.getDownloadRateLimitBytesPerSecond();
				
				long	required_limit = Math.max( dl_lim_max, bytes_per_sec + dl_lim_extra );

				if ( required_limit > 0 ){
				
					download.setDownloadRateLimitBytesPerSecond((int)required_limit );
				}
				
				listener.updateActivity( "Average rate=" + DisplayFormatters.formatByteCountToKiBEtcPerSec( bytes_per_sec ) + ", applied dl limit=" + DisplayFormatters.formatByteCountToKiBEtcPerSec( required_limit ));

				synchronized( StreamManager.this ){
					
					if ( cancelled ){
						
						throw( new Exception( "Cancelled" ));
					}
																										
					active_edm.setExplicitProgressive( BUFFER_SECS, bytes_per_sec, file_index );
					
					if ( !active_edm.setProgressiveMode( true )){
						
						throw( new Exception( "Failed to set download as progressive" ));
					}
					
					active_edm_activated = true;
				}
				
				new AEThread2( "streamMon" )
				{
					
					public void
					run()
					{	
						final int TIMER_PERIOD 		= 250;
						final int PLAY_STATS_PERIOD	= 5000;
						final int PLAY_STATS_TICKS	= PLAY_STATS_PERIOD / TIMER_PERIOD;
						
						final int DL_STARTUP_PERIOD	= 5000;
						final int DL_STARTUP_TICKS	= DL_STARTUP_PERIOD / TIMER_PERIOD;
			
						boolean playback_started 	= false;
						boolean	playback_paused		= false;

						boolean	error_reported = false;
						
						try{
							Method start_method 		= player.getClass().getMethod( "startPlayback", new Class[] { URL.class });
							Method pause_method 		= player.getClass().getMethod( "pausePlayback", new Class[] {});
							Method resume_method 		= player.getClass().getMethod( "resumePlayback", new Class[] {});
							Method buffering_method		= player.getClass().getMethod( "bufferingPlayback", new Class[] { Map.class });
							Method play_stats_method	= player.getClass().getMethod( "playStats", new Class[] { Map.class });

							int tick_count = 0;
							
							while( !cancelled ){
									
								tick_count++;
								
								int dm_state = dm.getState();
								
								boolean complete = file.getLength() == file.getDownloaded();

								if ( !complete ){
									
									if ( 	dm_state == DownloadManager.STATE_ERROR || 
											dm_state == DownloadManager.STATE_STOPPED ||
											dm_state == DownloadManager.STATE_QUEUED ){
										
										if ( tick_count >= DL_STARTUP_TICKS ){
										
											throw( new Exception( "Streaming abandoned, download isn't running" ));
										}
									}
	
									if ( !active_edm.getProgressiveMode()){
									
										complete = file.getLength() == file.getDownloaded();
										
										if ( !complete ){
										
											throw( new Exception( "Streaming mode abandoned for download" ));
										}
									}
								}
							
								long[] details = updateETA( active_edm );
								
								int		eta 		= (int)details[0];
								int		buffer_secs	= (int)details[1];
								long	buffer		= details[2];
								
								listener.updateStats( eta, buffer_secs, buffer, BUFFER_SECS );

								boolean playable;
								
								int	buffer_to_use = playback_started?BUFFER_MIN_SECS:BUFFER_SECS;
								
								if ( complete ){
									
									playable = true;
									
								}else{
									
									playable = buffer_secs > buffer_to_use;
								
									playable = playable && ( eta <= 0  || (playback_started && !playback_paused ) || preview_mode );
								}
								
								if ( playback_started ){
									
									if ( playable ){
										
										if ( playback_paused ){
											
											listener.updateActivity( "Resuming playback" );
											
											resume_method.invoke(player, new Object[] {});

											playback_paused = false;
										}
									}else{
											
										if ( !playback_paused ){
											
											listener.updateActivity( "Pausing playback to prevent stall" );
																							
											pause_method.invoke(player, new Object[] {});
	
											playback_paused = true;
										}
									}
								}else{
									
									if ( playable ){
										
										listener.ready();
											
										start_method.invoke(player, new Object[] { url });
										
										playback_started = true;
									}
								}
							
								if ( playable ){
									
									if ( tick_count % PLAY_STATS_TICKS == 0 ){
											
										long contiguous_done = active_edm.getContiguousAvailableBytes( active_edm.getPrimaryFileIndex(), 0, 0 );
									
										Map<String,Object> map = new HashMap<String,Object>();
										
										map.put( "buffer_min", new Long( BUFFER_SECS ));
										map.put( "buffer_secs", new Integer( buffer_secs ));
										map.put( "buffer_bytes", new Long( buffer ));
										
										map.put( "stream_rate", bytes_per_sec );
										
										DownloadStats stats = download.getStats();
										
										map.put( "dl_rate", stats.getDownloadAverage());
										map.put( "dl_size", stats.getDownloaded());
										map.put( "dl_time", SystemTime.getMonotonousTime() - stream_start );
	
										map.put( "duration", duration );
										map.put( "file_size", file.getLength());
										map.put( "cont_done", contiguous_done );
										
										play_stats_method.invoke(player, new Object[] { map });
									}
								}else{
									
									DownloadStats stats = download.getStats();

									Map<String,Object> map = new HashMap<String,Object>();
									
									map.put( "state", new Integer( 2 ));

									if ( preview_mode && !complete ){
										
										long rate = stats.getDownloadAverage();
										
										int	preview_eta;
										
										if ( rate <= 0 ){
											
											preview_eta = Integer.MAX_VALUE;
											
										}else{
											
											double secs_per_sec = ((double)bytes_per_sec)/rate;
										
											preview_eta = (int)(( buffer_to_use - buffer_secs ) * secs_per_sec);
										}
										
										map.put( "eta", new Integer( preview_eta ));
										
										map.put( "preview", 1 );
										
									}else{
										
										map.put( "eta", new Integer( eta ));
										
										map.put( "preview", 0 );
									}
									
									map.put( "buffer_min", new Long( BUFFER_SECS ));
									map.put( "buffer_secs", new Integer( buffer_secs ));
									map.put( "buffer_bytes", new Long( buffer ));
									
									map.put( "stream_rate", bytes_per_sec );
									
									map.put( "dl_rate", stats.getDownloadAverage());
									map.put( "dl_size", stats.getDownloaded());
									map.put( "dl_time", SystemTime.getMonotonousTime() - stream_start );
									
									buffering_method.invoke(player, new Object[] { map });
								}
								
								Thread.sleep( TIMER_PERIOD );
								
								try{
									Boolean b = (Boolean)is_active_method.invoke( player, new Object[0] );
									
									if ( !b ){
										
										cancel();
										
										break;
									}
								}catch( Throwable e ){	
								}
							}
						}catch( Throwable e ){
							
							error_reported = true;
							
							listener.failed( e );
							
						}finally{
							
							if ( !( error_reported || cancelled )){
								
								if ( !playback_started ){
								
									listener.failed( new Exception( "Streaming failed, reason unknown" ));
								}
							}
						}
					}
				}.start();
								
			}catch( Throwable e ){
						
				try{
					listener.failed( e );
					
				}finally{
					
					cancel();
				}
			}
		}
		
		private long[]
		updateETA(
			EnhancedDownloadManager 	edm )
		{
			long _eta = edm.getProgressivePlayETA();
					
			int	eta = _eta>=Integer.MAX_VALUE?Integer.MAX_VALUE:(int)_eta;
			
			EnhancedDownloadManager.progressiveStats stats = edm.getProgressiveStats();
			
			long provider_pos = stats.getCurrentProviderPosition( false );
			
			long buffer = edm.getContiguousAvailableBytes( edm.getPrimaryFileIndex(), provider_pos, 0 );
			
			long bps = stats.getStreamBytesPerSecondMin();
						
			int	buffer_secs = bps<=0?Integer.MAX_VALUE:(int)(buffer/bps);
											
			return( new long[]{ eta, buffer_secs, buffer });
		}
		
		public void
		cancel()
		{
			TranscodeJob	job;
			
			EnhancedDownloadManager	edm;
			boolean					edm_activated;
			
			synchronized( StreamManager.this ){
				
				cancelled = true;
			
				job = active_job;
				
				if ( active_sem != null ){
					
					active_sem.releaseForever();
				}
				
				edm 			= active_edm;
				edm_activated	= active_edm_activated;
				
				streamers.remove( this );
			}
			
			if ( job != null ){
				
				job.removeForce();
			}
			
			if ( edm != null ){
				
				if ( edm_activated ){
				
					edm.setProgressiveMode( false );
					
				}else{
					
					edm.prepareForProgressiveMode( false );
				}
			}
			
			final Download download = PluginCoreUtils.wrap( dm );

			download.setDownloadRateLimitBytesPerSecond( existing_dl_limit );
		}
		
		public boolean
		isCancelled()
		{
			return( cancelled );
		}
		
		private PluginInterface
		checkPlugin(
			String		id,
			String		name )
		
			throws Throwable
		{
			PluginManager plug_man = AzureusCoreFactory.getSingleton().getPluginManager();
			
			PluginInterface pi = plug_man.getPluginInterfaceByID( id, false );
			
			if ( pi == null ){
				
				listener.updateActivity( "Installing " + name );
				
				final AESemaphore sem = new AESemaphore( "analyserWait" );

				synchronized( StreamManager.this ){
					
					if ( cancelled ){
						
						throw( new Exception( "Cancelled" ));
					}
					
					active_sem	= sem;
				}
				
				final Throwable[] error = { null };
				
				new SimplePluginInstaller(
						id,
	    				"dlg.install." + id,
	    				new UIFunctions.actionListener()
						{
							public void
							actionComplete(
								Object		result )
							{
								try{
									if ( result instanceof Boolean ){
										
									}else{
										
										error[0] = (Throwable)result;
									}
								}finally{
									
									sem.release();
								}
							}
						});
				
				sem.reserve();
				
				synchronized( StreamManager.this ){
					
					if ( cancelled ){
							
						throw( new Exception( "Cancelled" ));
					}
					
					active_sem	= null;
				}

				if( error[0] != null ){
					
					throw( error[0] );
				}
				
				long start = SystemTime.getMonotonousTime();
				
				listener.updateActivity( "Waiting for plugin initialisation" );
				
				while( true ){
					
					if ( cancelled ){
						
						throw( new Exception( "Cancelled" ));
					}
					
					if ( SystemTime.getMonotonousTime() - start >= 30*1000 ){
						
						throw( new Exception( "Timeout waiting for " + name + " to initialise" ));
					}
					
					pi = plug_man.getPluginInterfaceByID( id, false );

					if ( pi != null && pi.getPluginState().isOperational()){
						
						return( pi );
					}
					
					Thread.sleep(250);
				}
			}else if ( !pi.getPluginState().isOperational()){
				
				throw( new Exception( name + " not operational" ));
				
			}else{
				
				return( pi );
			}
		}
	}		
}
