/*
 * Created on May 1, 2007
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, 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; 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.
 *
 */


package com.aelitis.azureus.core.networkmanager.admin.impl;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

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.torrent.TOTorrent;
import org.gudy.azureus2.core3.torrent.TOTorrentFactory;
import org.gudy.azureus2.core3.util.AEThread;
import org.gudy.azureus2.core3.util.BDecoder;
import org.gudy.azureus2.core3.util.BEncoder;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.FileUtil;
import org.gudy.azureus2.core3.util.SystemProperties;
import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.download.Download;
import org.gudy.azureus2.plugins.download.DownloadManagerListener;
import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloader;
import org.gudy.azureus2.pluginsimpl.local.PluginConfigImpl;
import org.gudy.azureus2.pluginsimpl.local.utils.resourcedownloader.ResourceDownloaderFactoryImpl;

import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTestScheduledTest;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTestScheduledTestListener;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTestScheduler;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTester;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTesterListener;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdminSpeedTesterResult;
import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.plugins.upnp.UPnPPlugin;

public class 
NetworkAdminSpeedTestScheduledTestImpl 
	implements NetworkAdminSpeedTestScheduledTest
{
	   //Types of requests sent to SpeedTest scheduler.
    private static final long REQUEST_TEST 		= 0;
    private static final long CHALLENGE_REPLY 	= 1;
    private static final long TEST_RESULT		= 2;
    
    private static int ZERO_DOWNLOAD_SETTING = -1;


    private PluginInterface						plugin;
 	private NetworkAdminSpeedTesterImpl			tester;
	
    private String detectedRouter;

	private SpeedTestDownloadState		preTestSettings;
	
	private byte[]		challenge_id;
	private long		delay_millis;
	private long		max_speed;
	private TOTorrent	test_torrent;
	
	private volatile boolean	aborted;
	
	private CopyOnWriteList		listeners = new CopyOnWriteList();
	
	protected
	NetworkAdminSpeedTestScheduledTestImpl(
		PluginInterface						_plugin,
		NetworkAdminSpeedTesterImpl			_tester )
	{
		plugin		= _plugin;
		tester		= _tester;
	    
        	//detect the router.
		
        PluginInterface upnp = plugin.getPluginManager().getPluginInterfaceByClass( UPnPPlugin.class );
        
        if( upnp != null ){
        	
            detectedRouter = upnp.getPluginconfig().getPluginStringParameter("plugin.info");
        }
        
		tester.addListener(
			new NetworkAdminSpeedTesterListener()
			{
				public void 
				complete(
					NetworkAdminSpeedTester 		tester, 
					NetworkAdminSpeedTesterResult 	result )
				{
					try{
						sendResult( result );
		
					}finally{
						
						reportComplete();
					}
				}

				public void 
				stage(
					NetworkAdminSpeedTester 	tester, 
					String 						step ) 
				{
				}
			});
	}
	
	public NetworkAdminSpeedTester
	getTester()
	{
		return( tester );
	}
	
	public long
	getMaxUpBytePerSec()
	{
		return( max_speed );
	}

	public long
	getMaxDownBytePerSec()
	{
		return( max_speed );
	}
	    
	public boolean
	start()
	{
		if ( schedule()){
		
			new AEThread( "NetworkAdminSpeedTestScheduledTest:delay", true )
			{
				public void
				runSupport()
				{
					long delay_ticks = delay_millis/1000;
			
					for (int i=0;i<delay_ticks;i++){

						if ( aborted ){
			            		
			            	break;
			            }
                        
                        String testScheduledIn = MessageText.getString( "SpeedTestWizard.abort.message.scheduled.in"
                                , new String[]{""+(delay_ticks - i)} );
						reportStage( testScheduledIn );
						
						try{
							Thread.sleep(1000);
							
						}catch( InterruptedException e ){
							
							e.printStackTrace();
						}
					}
					
					if ( !aborted ){
				
						setSpeedLimits();
						
						if ( tester.getTestType() == NetworkAdminSpeedTestScheduler.TEST_TYPE_BT ){
						
							((NetworkAdminSpeedTesterBTImpl)tester).start( test_torrent );
							
						}else{
							String unsupportedType = MessageText.getString("SpeedTestWizard.abort.message.unsupported.type");
							tester.abort(unsupportedType);
						}
					}
				}
			}.start();
			
			return( true );
			
		}else{
		
			return( false );
		}
	}
	
	public void
	abort()
	{
        abort( MessageText.getString("SpeedTestWizard.abort.message.manual.abort") );
	}
	
	public void
	abort(
		String	reason )
	{
		if ( !aborted ){
			
			aborted	= true;
			
			tester.abort( reason );
		}
	}
	
	/**
     * Request a test from the speed testing service, handle the "challenge" if request and then get
     * the id for the test.
     *
     * Per spec all request are BEncoded maps.
     *
     * @return boolean - true if the test has been reserved with the service.
     */
    private boolean
    schedule()
    {
        try{
            //lookup UPnP devices found. One might be a router.

            //Send "schedule test" request.
            Map request = new HashMap();
            request.put("request_type", new Long(REQUEST_TEST) );

            String id = COConfigurationManager.getStringParameter("ID","unknown");
            
            	// get jar file its version for the test
            
            File	jar_file 		= null;
            String	jar_version		= null;
            
            String	explicit_path = System.getProperty( "azureus.speed.test.challenge.jar.path", null );
            
            if ( explicit_path != null ){
            	
            	File	f = new File( explicit_path );
            	
            	if ( f.exists()){
            		
            		String v = getVersionFromJAR( f );
            		
            		if ( v != null ){
            			
             			jar_file	= f;
            			jar_version	= v;
            			
               			System.out.println( "SpeedTest: using explicit challenge jar " + jar_file.getAbsolutePath() + ", version " + jar_version );
            		}
            	}
            }
            
            if ( jar_file == null ){
            	
                String debug = System.getProperty("debug.speed.test.challenge","n");

                if( !debug.equals( "n" )){
                    
                	//over-ride the jar version, and location for debugging.

                	File f = new File( "C:\\test\\azureus\\Azureus3.0.1.2.jar" );  //ToDo: make this a -D option with this default.

                	if ( f.exists()){
                		
                		jar_file 	= f;
                		jar_version = "3.0.1.2";
                		
              			System.out.println( "SpeedTest: using old spec challenge jar " + jar_file.getAbsolutePath() + ", version " + jar_version );
                	}
                }
            }
            	
            if ( jar_file == null ){
            
            	jar_file = FileUtil.getJarFileFromClass( getClass());

            	if ( jar_file != null ){
            		
            		jar_version = Constants.AZUREUS_VERSION;
            		
          			// System.out.println( "SpeedTest: using class-based challenge jar " + jar_file.getAbsolutePath() + ", version " + jar_version );

            	}else{
            		
            		File f = new File( SystemProperties.getAzureusJarPath());
            		
            		if ( f.exists()){
            			
            			jar_version = Constants.AZUREUS_VERSION;
            			jar_file	= f;
            			
             			// System.out.println( "SpeedTest: using config-based challenge jar " + jar_file.getAbsolutePath() + ", version " + jar_version );
            		}
            	}
            }            
            
            if ( jar_file == null ){
            	
            	throw( new Exception( "Failed to locate an 'Azureus2.jar' to use for the challenge protocol" ));
            }
            
            //ToDo: remove once challenge testing is done.
   
            request.put("az-id",id); //Where to I get the AZ-ID and client version from the Configuration?
            request.put("type","both");
            request.put("jar_ver",jar_version);
 
            if ( detectedRouter != null ){
            	
            	request.put( "router", detectedRouter );
            }
            
            Map result = sendRequest( request );
            
            challenge_id = (byte[]) result.get("challenge_id");

            if( challenge_id == null ){
            	
                throw new IllegalStateException("No challenge returned from speed test scheduling service");
            }
        
            Long responseType =  (Long) result.get("reply_type");

            if( responseType.intValue()==1 ){
                	//a challenge has occured.
                result = handleChallengeFromSpeedTestService( jar_file, result );
                
                responseType = (Long) result.get("reply_type");
            }

            if( responseType == null ){
                throw new IllegalStateException("No challenge response returned from speed test scheduling service");
            }

            if( responseType.intValue()==0 ){
                	//a test has been scheduled.
                	//set the Map properly.
            	
                Long time = (Long) result.get("time");
                Long limit = (Long) result.get("limit");

                if( time==null || limit==null ){
                    throw new IllegalArgumentException("Returned time or limit parameter is null");
                }
                
                delay_millis 	= time.longValue();
                max_speed		= limit.longValue();
                                
                	// this is test-specific data
                
                Map torrentMap = (Map)result.get("torrent");
    	
                test_torrent = TOTorrentFactory.deserialiseFromMap(torrentMap);

                return( true );
            }else{
            	
                throw new IllegalStateException( "Unrecognized response from speed test scheduling servcie." );
            }

        }catch( Throwable t ){
        	       	
            Debug.printStackTrace(t);

            tester.abort( MessageText.getString("SpeedTestWizard.abort.message.scheduling.failed"), t );

            return( false );
        }
    }

    private String
    getVersionFromJAR(
    	File	jar_file )
    {
		try{
				// force the URLClassLoader to load from the URL and not delegate and find the currently
				// jar's Constants
			
			ClassLoader parent = new ClassLoader()
			{
				protected synchronized Class 
				loadClass(
					String 		name,
					boolean		resolve )
				
					throws ClassNotFoundException
				{
					if ( name.equals( "org.gudy.azureus2.core3.util.Constants")){
					
						throw( new ClassNotFoundException());
					}
					
					return( super.loadClass( name, resolve ));
				}
			};
			
			ClassLoader cl = new URLClassLoader(new URL[]{jar_file.toURI().toURL()}, parent);
			
	    	Class c = cl.loadClass( "org.gudy.azureus2.core3.util.Constants");

	    	Field	field = c.getField( "AZUREUS_VERSION" );
	    	
	    	return((String)field.get( null ));
	    	
		}catch( Throwable e){
						
			return( null );
		}
    }
 

    /**
     *
     * @param jar_file - File Azureus jar used to load classes.
     * @param result - Map from the previous response
     * @return Map - from the current response.
     */
    private Map 
    handleChallengeFromSpeedTestService(
    	File		jar_file,
    	Map 		result )
    
    	throws IOException
    {
        //verify the following items are in the response.

        //size (in bytes)
        //offset (in bytes)
        //challenge_id
        Map retVal = new HashMap();
        RandomAccessFile raf=null;
        try{

            Long size = (Long) result.get("size");
            Long offset = (Long) result.get("offset");
 
            if( size==null || offset==null  )
                throw new IllegalStateException("scheduleTestWithSpeedTestService had a null parameter.");

 
            //read the bytes
            raf = new RandomAccessFile( jar_file, "r" );
            byte[] jarBytes = new byte[size.intValue()];

            raf.seek(offset.intValue());
            raf.read( jarBytes );


            //Build the URL.
            Map request = new HashMap();
            request.put("request_type", new Long(CHALLENGE_REPLY) );
            request.put("challenge_id", challenge_id );
            request.put("data",jarBytes);
 
            retVal = sendRequest( request );

        }finally{
            //close
            try{
                if(raf!=null)
                    raf.close();
            }catch(Throwable t){
                Debug.printStackTrace(t);
            }
        }

        return retVal;
    }


    private void
  	sendResult(
  		NetworkAdminSpeedTesterResult	result )
    {
    	try{
    		if ( challenge_id != null ){
    			
	    		Map request = new HashMap();
	    		
	    		request.put("request_type", new Long(TEST_RESULT) );
	    		
	    		request.put("challenge_id", challenge_id );
	
    			request.put( "type", new Long( tester.getTestType()));
    			request.put( "mode", new Long( tester.getMode()));
    			request.put( "crypto", new Long( tester.getUseCrypto()?1:0));

	    		if ( result.hadError()){
	    			
	    			request.put( "result", new Long(0));
	    			
	    			request.put( "error", result.getLastError());
	    			
	    		}else{
	    			
	    			request.put( "result", new Long(1));

	    			request.put( "maxup", new Long(result.getUploadSpeed()));
	    			request.put( "maxdown", new Long(result.getDownloadSpeed()));
	    		}
	    		
	    		sendRequest( request );
    		}
    	}catch( Throwable e ){
    		
    		Debug.printStackTrace(e);
    	}
    }

    private Map
    sendRequest(
    	Map		request )
    
    	throws IOException
    {
        request.put( "ver", new Long(1) );//request version
        request.put( "locale",  MessageText.getCurrentLocale().toString());
        
        String speedTestServiceName = System.getProperty( "speedtest.service.ip.address", Constants.SPEED_TEST_SERVER );

        URL urlRequestTest = new URL("http://"+speedTestServiceName+":60000/scheduletest?request="
                + URLEncoder.encode( new String(BEncoder.encode(request),"ISO-8859-1"),"ISO-8859-1"));
        
        return( getBEncodedMapFromRequest( urlRequestTest ));

    }
    /**
     * Read from URL and return byte array.
     * @param url -
     * @return byte[] of the results. Max size currently 100k.
     * @throws java.io.IOException -
     */
    private Map getBEncodedMapFromRequest(URL url)
            throws IOException
    {

        ResourceDownloader rd = ResourceDownloaderFactoryImpl.getSingleton().create( url );

        InputStream is=null;
        Map reply = new HashMap();
        try
        {
            is = rd.download();
            reply = BDecoder.decode( new BufferedInputStream(is) );

            //all replys of this type contains a "result"
            Long res = (Long) reply.get("result");
            if(res==null)
                throw new IllegalStateException("No result parameter in the response!! reply="+reply);
            if(res.intValue()==0){
                StringBuffer msg = new StringBuffer("Server failed. ");
                String error = new String( (byte[]) reply.get("error") );
                String errDetail = new String( (byte[]) reply.get("error_detail") );
                msg.append("Error: ").append(error);
                
                // detail is of no interest to the user
                // msg.append(" ,error detail: ").append(errDetail);
                
                Debug.outNoStack( "SpeedCheck server returned an error: " + error + ", details=" + errDetail );
                
                throw new IOException( msg.toString() );
            }
        }catch(IOException ise){
            //rethrow this type of exception.
            throw ise;
        }catch(Throwable t){
            Debug.out(t);
            Debug.printStackTrace(t);
        }finally{
            try{
                if(is!=null)
                    is.close();
            }catch(Throwable e){
                Debug.printStackTrace(e);
            }
        }
        return reply;
    }//getBytesFromRequest

    
    /**
     * Restore all the downloads the state before the speed test started.
     */
    protected synchronized void 
    resetSpeedLimits()
    {
    	if ( preTestSettings != null ){
    	
	        preTestSettings.restoreLimits();
		        
	        preTestSettings = null;
    	}
    }

    /**
     * Preserve all the data about the downloads while the test is running.
     */
    protected synchronized void setSpeedLimits(){

    		// in case we've already saved limits
    	
    	resetSpeedLimits();
    	
        preTestSettings = new SpeedTestDownloadState();
        
        preTestSettings.saveLimits();
      }


    // ---------------    HELPER CLASSES BELOW HERE   ---------------- //

    /**
     * Preservers the state of all the downloads before the speed test started.
     */
    class 
    SpeedTestDownloadState
    	implements ParameterListener, DownloadManagerListener
    {

        private Map torrentLimits = new HashMap(); //Map <Download , Map<String,Integer> >

        public static final String TORRENT_UPLOAD_LIMIT 	= "u";
        public static final String TORRENT_DOWNLOAD_LIMIT 	= "d";

        	//global limits.
        
        int maxUploadKbs;
        int maxUploadSeedingKbs;
        int maxDownloadKbs;

        boolean autoSpeedEnabled;
        boolean autoSpeedSeedingEnabled;
        boolean LANSpeedEnabled;

        public 
        SpeedTestDownloadState()
        {
        }
        
        public void 
        parameterChanged(
        	String name )
        {
        		// add some trace so we have some clue as to what has made the change!
        	
        	String trace = Debug.getCompressedStackTrace();
        	
        	abort( "Configuration parameter '" + name + "' changed (new value=" + COConfigurationManager.getParameter( name ) + ") during test (" + trace + ")" );
        }
        
    	public void
    	downloadAdded(
    		Download	download )
    	{
    		if ( test_torrent != null ){
    			
    			try{
	    			if ( Arrays.equals( download.getTorrent().getHash(), test_torrent.getHash())){
	    				
	    				return;
	    			}
    			}catch( Throwable e ){
    				
    				Debug.printStackTrace(e);
    			}
    		}

            String downloadAdded = MessageText.getString("SpeedTestWizard.abort.message.download.added"
                    , new String[]{download.getName()});
            abort(downloadAdded);
    	}
    	
    	public void
    	downloadRemoved(
    		Download	download )
    	{
    	}
    	
        public void
        saveLimits()
        {
        		// a bunch of plugins mess with limits (AutoSpeed, Shaper, SpeedScheduler...) - disable their
        		// ability to mess with config during the test
        	
        	PluginConfigImpl.setEnablePluginCoreConfigChange( false );
        	
        	plugin.getDownloadManager().addListener( this, false );
        	
            //preserve the limits for all the downloads and set each to zero.
            Download[] d = plugin.getDownloadManager().getDownloads();
            if(d!=null){
                int len = d.length;
                for(int i=0;i<len;i++){

                    plugin.getDownloadManager().getStats();
                    int downloadLimit = d[i].getDownloadRateLimitBytesPerSecond();
                    int uploadLimit = d[i].getUploadRateLimitBytesPerSecond();
                    
                    setDownloadDetails(d[i],uploadLimit,downloadLimit);

                    d[i].setUploadRateLimitBytesPerSecond(ZERO_DOWNLOAD_SETTING);
                    d[i].setDownloadRateLimitBytesPerSecond( ZERO_DOWNLOAD_SETTING );
                }
            }

            //preserve the global limits
            
            saveGlobalLimits();

            COConfigurationManager.setParameter( "LAN Speed Enabled", false );

            COConfigurationManager.setParameter( TransferSpeedValidator.AUTO_UPLOAD_ENABLED_CONFIGKEY,false);
            COConfigurationManager.setParameter( TransferSpeedValidator.AUTO_UPLOAD_SEEDING_ENABLED_CONFIGKEY,false);

            COConfigurationManager.setParameter( TransferSpeedValidator.UPLOAD_CONFIGKEY, max_speed);
            COConfigurationManager.setParameter( TransferSpeedValidator.UPLOAD_SEEDING_CONFIGKEY, max_speed);
            COConfigurationManager.setParameter( TransferSpeedValidator.DOWNLOAD_CONFIGKEY, max_speed);
            
            String[]	params = TransferSpeedValidator.CONFIG_PARAMS;
        	
        	for (int i=0;i<params.length;i++){
        		COConfigurationManager.addParameterListener( params[i], this );
        	}
        }

        public void
        restoreLimits()
        {  		
        	String[]	params = TransferSpeedValidator.CONFIG_PARAMS;
        	
        	for (int i=0;i<params.length;i++){
        		COConfigurationManager.removeParameterListener( params[i], this );
        	}
        	
           	plugin.getDownloadManager().removeListener( this );

         	restoreGlobalLimits();
       	
        	restoreIndividualLimits();
        	
           	PluginConfigImpl.setEnablePluginCoreConfigChange( true );
        }
        
        /**
         * Get the global limits from the TransferSpeedValidator class. Call before starting a speed test.
         */
        private void saveGlobalLimits(){
            //int settings.
            maxUploadKbs = COConfigurationManager.getIntParameter( TransferSpeedValidator.UPLOAD_CONFIGKEY );
            maxUploadSeedingKbs = COConfigurationManager.getIntParameter( TransferSpeedValidator.UPLOAD_SEEDING_CONFIGKEY );
            maxDownloadKbs = COConfigurationManager.getIntParameter( TransferSpeedValidator.DOWNLOAD_CONFIGKEY );
            //boolean setting.
            autoSpeedEnabled = COConfigurationManager.getBooleanParameter( TransferSpeedValidator.AUTO_UPLOAD_ENABLED_CONFIGKEY );
            autoSpeedSeedingEnabled = COConfigurationManager.getBooleanParameter( TransferSpeedValidator.AUTO_UPLOAD_SEEDING_ENABLED_CONFIGKEY );
            
            LANSpeedEnabled = COConfigurationManager.getBooleanParameter( "LAN Speed Enabled" );
            
        }//saveGlobalLimits

        /**
         * Call this method after a speed test completes to restore the global limits.
         */
        private void restoreGlobalLimits(){
            COConfigurationManager.setParameter( "LAN Speed Enabled", LANSpeedEnabled );
            
            COConfigurationManager.setParameter(TransferSpeedValidator.AUTO_UPLOAD_ENABLED_CONFIGKEY,autoSpeedEnabled);
            COConfigurationManager.setParameter(TransferSpeedValidator.AUTO_UPLOAD_SEEDING_ENABLED_CONFIGKEY,autoSpeedSeedingEnabled);

            COConfigurationManager.setParameter(TransferSpeedValidator.UPLOAD_CONFIGKEY,maxUploadKbs);
            COConfigurationManager.setParameter(TransferSpeedValidator.UPLOAD_SEEDING_CONFIGKEY,maxUploadSeedingKbs);
            COConfigurationManager.setParameter(TransferSpeedValidator.DOWNLOAD_CONFIGKEY,maxDownloadKbs);
        }//restoreGlobalLimits

        /**
         * Call this method after the speed test is completed to restore the individual download limits
         * before the test started.
         */
        private void restoreIndividualLimits(){
            Download[] downloads = getAllDownloads();
            if(downloads!=null){
                int nDownloads = downloads.length;

                for(int i=0;i<nDownloads;i++){
                    int uploadLimit = getDownloadDetails(downloads[i], TORRENT_UPLOAD_LIMIT);
                    int downLimit = getDownloadDetails(downloads[i], TORRENT_DOWNLOAD_LIMIT);

                    downloads[i].setDownloadRateLimitBytesPerSecond(downLimit);
                    downloads[i].setUploadRateLimitBytesPerSecond(uploadLimit);
                }
            }
        }
        /**
         * Save the upload/download limits of this Download object before the test started.
         * @param d - Download
         * @param uploadLimit - int
         * @param downloadLimit - int
         */
        private void setDownloadDetails(Download d, int uploadLimit, int downloadLimit){
            if(d==null)
                throw new IllegalArgumentException("Download should not be null.");

            Map props = new HashMap();//Map<String,Integer>

            props.put(TORRENT_UPLOAD_LIMIT, new Integer(uploadLimit) );
            props.put(TORRENT_DOWNLOAD_LIMIT, new Integer(downloadLimit) );

            torrentLimits.put(d,props);
        }

        /**
         * Get the upload or download limit for this Download object before the test started.
         * @param d - Download
         * @param param - String
         * @return - limit as int.
         */
        private int getDownloadDetails(Download d, String param){
            if(d==null || param==null )
                throw new IllegalArgumentException("null inputs.");

            if(!param.equals(TORRENT_UPLOAD_LIMIT) && !param.equals(TORRENT_DOWNLOAD_LIMIT))
                throw new IllegalArgumentException("invalid param. param="+param);

            Map out = (Map) torrentLimits.get(d);
            Integer limit = (Integer) out.get(param);

            return limit.intValue();
        }

        /**
         * Get all the Download keys in this Map.
         * @return - Download[]
         */
        private Download[] getAllDownloads(){
            Download[] a = new Download[0];
            return (Download[]) torrentLimits.keySet().toArray(a);
        }

    }

	protected void
	reportStage(
		String	str )
	{
		Iterator	it = listeners.iterator();
		
		while( it.hasNext()){
			
			try{
				((NetworkAdminSpeedTestScheduledTestListener)it.next()).stage( this, str );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace( e );
			}
		}
	}
	
	protected void
	reportComplete()
	{		
		resetSpeedLimits();

		Iterator	it = listeners.iterator();
		
		while( it.hasNext()){
			
			try{
				((NetworkAdminSpeedTestScheduledTestListener)it.next()).complete( this );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace( e );
			}
		}
	}
	
	public void
	addListener(
		NetworkAdminSpeedTestScheduledTestListener	listener )
	{
		listeners.add( listener );
	}
	
	public void
	removeListener(
		NetworkAdminSpeedTestScheduledTestListener	listener )
	{
		listeners.remove( listener );
	}

}
