/*
 * Created on Dec 4, 2009
 * 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 org.gudy.azureus2.core3.tracker.client.impl;

import java.net.URL;
import java.util.*;

import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.peer.PEPeerSource;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.torrent.TOTorrentAnnounceURLSet;
import org.gudy.azureus2.core3.torrent.TOTorrentException;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncer;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerDataProvider;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerException;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerFactory;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerResponse;
import org.gudy.azureus2.core3.tracker.client.impl.bt.TRTrackerBTAnnouncerImpl;
import org.gudy.azureus2.core3.tracker.client.impl.dht.TRTrackerDHTAnnouncerImpl;
import org.gudy.azureus2.core3.util.AEThread2;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.IndentWriter;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;
import org.gudy.azureus2.core3.util.TorrentUtils;
import org.gudy.azureus2.plugins.download.DownloadAnnounceResult;

import com.aelitis.azureus.core.tracker.TrackerPeerSource;
import com.aelitis.azureus.core.util.CopyOnWriteList;

public class 
TRTrackerAnnouncerMuxer
	extends TRTrackerAnnouncerImpl
{
	private static final int ACT_CHECK_INIT_DELAY			= 2500;
	private static final int ACT_CHECK_INTERIM_DELAY		= 10*1000;
	private static final int ACT_CHECK_IDLE_DELAY			= 30*1000;
	private static final int ACT_CHECK_SEEDING_SHORT_DELAY		= 60*1000;
	private static final int ACT_CHECK_SEEDING_LONG_DELAY		= 3*60*1000;
	
	
	private TRTrackerAnnouncerFactory.DataProvider		f_provider;
	private boolean										is_manual;
	
	private long				create_time = SystemTime.getMonotonousTime();
	
	private CopyOnWriteList<TRTrackerAnnouncerHelper>	announcers 	= new CopyOnWriteList<TRTrackerAnnouncerHelper>();
	private Set<TRTrackerAnnouncerHelper>				activated	= new HashSet<TRTrackerAnnouncerHelper>();
	private long										last_activation_time;
	private Set<String>									failed_urls	= new HashSet<String>();
	
	private volatile TimerEvent					event;
	
	private TRTrackerAnnouncerDataProvider		provider;
	private String								ip_override;
	private boolean								complete;
	private boolean								stopped;
	private boolean								destroyed;
	
	private TRTrackerAnnouncerHelper			last_best_active;
	private long								last_best_active_set_time;
	
	private Map<String,StatusSummary>			recent_responses = new HashMap<String,StatusSummary>();
	
	private TRTrackerAnnouncerResponse			last_response_informed;

	
	protected
	TRTrackerAnnouncerMuxer(
		TOTorrent								_torrent,
		TRTrackerAnnouncerFactory.DataProvider	_f_provider,
		boolean									_manual )
	
		throws TRTrackerAnnouncerException
	{
		super( _torrent );
		
		try{	
			last_response_informed = new TRTrackerAnnouncerResponseImpl( null, _torrent.getHashWrapper(), TRTrackerAnnouncerResponse.ST_OFFLINE, TRTrackerAnnouncer.REFRESH_MINIMUM_SECS, "Initialising" );
			
		}catch( TOTorrentException e ){
			
			Logger.log(new LogEvent( _torrent, LOGID, "Torrent hash retrieval fails", e));
			
			throw( new TRTrackerAnnouncerException( "TRTrackerAnnouncer: URL encode fails"));	
		}

		is_manual 	= _manual;
		f_provider	= _f_provider;
		
		split();
	}
	
	protected void
	split()
	
		throws TRTrackerAnnouncerException
	{
		String[]	networks = f_provider==null?null:f_provider.getNetworks();
		
		TRTrackerAnnouncerHelper to_activate = null;
		
		synchronized( this ){
			
			if ( stopped || destroyed ){
				
				return;
			}
			
			TOTorrent torrent = getTorrent();
					
			TOTorrentAnnounceURLSet[]	sets = torrent.getAnnounceURLGroup().getAnnounceURLSets();
			
				// sanitise dht entries
			
			if ( sets.length == 0 ){
				
				sets = new TOTorrentAnnounceURLSet[]{ torrent.getAnnounceURLGroup().createAnnounceURLSet( new URL[]{ torrent.getAnnounceURL()})};
				
			}else{
				
				boolean	found_decentralised = false;
				boolean	modified			= false;
				
				for ( int i=0;i<sets.length;i++ ){
					
					TOTorrentAnnounceURLSet set = sets[i];
					
					URL[] urls = set.getAnnounceURLs().clone();
					
					for (int j=0;j<urls.length;j++){
						
						URL u = urls[j];
						
						if ( u != null && TorrentUtils.isDecentralised( u )){
														
							if ( found_decentralised ){
								
								modified = true;
								
								urls[j] = null;
								
							}else{
								
								found_decentralised = true;
							}
						}
					}
				}
				
				if ( modified ){
					
					List<TOTorrentAnnounceURLSet> s_list = new ArrayList<TOTorrentAnnounceURLSet>();
					
					for ( TOTorrentAnnounceURLSet set: sets ){
						
						URL[] urls = set.getAnnounceURLs();
						
						List<URL> u_list = new ArrayList<URL>( urls.length );
						
						for ( URL u: urls ){
							
							if ( u != null ){
								
								u_list.add( u );
							}
						}
						
						if ( u_list.size() > 0 ){
							
							s_list.add( torrent.getAnnounceURLGroup().createAnnounceURLSet( u_list.toArray( new URL[ u_list.size() ])));
						}
					}
					
					sets = s_list.toArray( new TOTorrentAnnounceURLSet[ s_list.size() ]);
				}
			}
			
			List<TOTorrentAnnounceURLSet[]>	new_sets = new ArrayList<TOTorrentAnnounceURLSet[]>();
			
			if ( is_manual || sets.length < 2 ){
					
				new_sets.add( sets );
				
			}else{
				
				List<TOTorrentAnnounceURLSet> list = new ArrayList<TOTorrentAnnounceURLSet>( Arrays.asList( sets ));
				
					// often we have http:/xxxx/ and udp:/xxxx/ as separate groups - keep these together
								
				while( list.size() > 0 ){
					
					TOTorrentAnnounceURLSet set1 = list.remove(0);
					
					boolean	done = false;
					
					URL[] urls1 = set1.getAnnounceURLs();
					
					if ( urls1.length == 1 ){
						
						URL url1 = urls1[0];
						
						String prot1 = url1.getProtocol().toLowerCase();
						String host1	= url1.getHost();
						
						for (int i=0;i<list.size();i++){
							
							TOTorrentAnnounceURLSet set2 = list.get(i);
							
							URL[] urls2 = set2.getAnnounceURLs();
							
							if ( urls2.length == 1 ){
								
								URL url2 = urls2[0];
								
								String prot2 = url2.getProtocol().toLowerCase();
								String host2 = url2.getHost();
				
								if ( host1.equals( host2 )){
									
									if (	( prot1.equals( "udp" ) && prot2.startsWith( "http" )) ||
											( prot2.equals( "udp" ) && prot1.startsWith( "http" ))){
										
										list.remove( i );
										
										new_sets.add( new TOTorrentAnnounceURLSet[]{ set1, set2 });
										
										done	= true;
									}
								}
							}
						}
					}
					
					if ( !done ){
						
						new_sets.add( new TOTorrentAnnounceURLSet[]{ set1 });
					}
				}
			}
			
				// work out the difference
			
			Iterator<TOTorrentAnnounceURLSet[]> ns_it = new_sets.iterator();
			
				// need to copy list as we modify it and returned list ain't thread safe
			
			List<TRTrackerAnnouncerHelper> existing_announcers 	= new ArrayList<TRTrackerAnnouncerHelper>( announcers.getList());
			
			List<TRTrackerAnnouncerHelper> new_announcers 		= new ArrayList<TRTrackerAnnouncerHelper>();
			
				// first look for unchanged sets
			
			while( ns_it.hasNext()){
				
				TOTorrentAnnounceURLSet[] ns = ns_it.next();
				
				Iterator<TRTrackerAnnouncerHelper> a_it = existing_announcers.iterator();
					
				while( a_it.hasNext()){
					
					TRTrackerAnnouncerHelper a = a_it.next();
					
					TOTorrentAnnounceURLSet[] os = a.getAnnounceSets();
					
					if ( same( ns, os )){
						
						ns_it.remove();
						a_it.remove();
						
						new_announcers.add( a );
						
						break;
					}
				}
			}
								
				// first remove dht ones from the equation
			
			TRTrackerAnnouncerHelper 	existing_dht_announcer 	= null;
			TOTorrentAnnounceURLSet[]	new_dht_set				= null;

			ns_it = new_sets.iterator();
			
			while( ns_it.hasNext()){
				
				TOTorrentAnnounceURLSet[] x = ns_it.next();
				
				if ( TorrentUtils.isDecentralised( x[0].getAnnounceURLs()[0])){
					
					new_dht_set = x;
						
					ns_it.remove();
					
					break;
				}
			}
			
			Iterator<TRTrackerAnnouncerHelper>	an_it = existing_announcers.iterator();
			
			while( an_it.hasNext()){
				
				TRTrackerAnnouncerHelper a = an_it.next();
				
				TOTorrentAnnounceURLSet[] x = a.getAnnounceSets();
				
				if ( TorrentUtils.isDecentralised( x[0].getAnnounceURLs()[0])){
					
					existing_dht_announcer = a;
						
					an_it.remove();
					
					break;
				}
			}
	
			if ( existing_dht_announcer != null && new_dht_set != null ){
				
				new_announcers.add( existing_dht_announcer );
				
			}else if ( existing_dht_announcer != null ){
				
				activated.remove( existing_dht_announcer );
				
				existing_dht_announcer.destroy();
				
			}else if ( new_dht_set != null ){
				
				TRTrackerAnnouncerHelper a = create( torrent, networks, new_dht_set );
				
				new_announcers.add( a );
			}
			
				// create any new ones required
			
			ns_it = new_sets.iterator();
			
			while( ns_it.hasNext()){
				
				TOTorrentAnnounceURLSet[] s = ns_it.next();
				
				TRTrackerAnnouncerHelper a = create( torrent, networks, s );
				
				new_announcers.add( a );
			}
			
				// finally fix up the announcer list to represent the new state
			
			Iterator<TRTrackerAnnouncerHelper>	a_it = announcers.iterator();
				
			while( a_it.hasNext()){
				
				TRTrackerAnnouncerHelper a = a_it.next();
				
				if ( !new_announcers.contains( a )){
					
					a_it.remove();
					
					try{
						if ( 	activated.contains( a ) &&
								torrent.getPrivate() && 
								a instanceof TRTrackerBTAnnouncerImpl ){
							
							URL url = a.getTrackerURL();
						
							if ( url != null ){
								
								forceStop((TRTrackerBTAnnouncerImpl)a, networks, url );
							}
						}
					}finally{
						
						if (Logger.isEnabled()) {
							Logger.log(new LogEvent(getTorrent(), LOGID, "Deactivating " + getString( a.getAnnounceSets())));
						}

						activated.remove( a );
												
						a.destroy();
					}
				}
			}
			
			a_it = new_announcers.iterator();
			
			while( a_it.hasNext()){
				
				TRTrackerAnnouncerHelper a = a_it.next();
				
				if ( !announcers.contains( a )){
					
					announcers.add( a );
				}
			}
			
			if ( !is_manual && announcers.size() > 0 ){
				
				if ( activated.size() == 0 ){
					
					TRTrackerAnnouncerHelper a = announcers.get(0);
					
					if (Logger.isEnabled()) {
						Logger.log(new LogEvent(getTorrent(), LOGID, "Activating " + getString( a.getAnnounceSets())));
					}

					activated.add( a );
					
					last_activation_time = SystemTime.getMonotonousTime();
					
					if ( provider != null ){
						
						to_activate = a;
					}
				}
				
				setupActivationCheck( ACT_CHECK_INIT_DELAY );
			}
		}
		
		if ( to_activate != null ){
			
			if ( complete ){
				
				to_activate.complete( true );
				
			}else{
				
				to_activate.update( false );
			}
		}
	}
	
	protected void
	setupActivationCheck(
		int		delay )
	{
		if ( announcers.size() > activated.size()){
			
			event = SimpleTimer.addEvent(
				"TRMuxer:check",
				SystemTime.getOffsetTime( delay ),
				new TimerEventPerformer()
				{
					public void 
					perform(
						TimerEvent event )
					{
						checkActivation( false );
					}
				});
		}
	}
	
	protected void
	checkActivation(
		boolean		force )
	{
		synchronized( this ){
			
			int	next_check_delay;
			
			if ( 	destroyed || 
					stopped || 
					announcers.size() <= activated.size()){

				return;
			}
			
			if ( provider == null ){
				
				next_check_delay = ACT_CHECK_INIT_DELAY;
				
			}else{

				boolean	activate = force;
														
				boolean	seeding = provider.getRemaining() == 0;

				if ( seeding && activated.size() > 0 ){
				
						// when seeding we only activate on tracker fail or major lack of connections
						// as normally we rely on downloaders rotating and finding us

					int	connected	= provider.getConnectedConnectionCount();

					if ( connected < 1 ){
						
						activate = SystemTime.getMonotonousTime() - last_activation_time >= 60*1000;
						
						next_check_delay = ACT_CHECK_SEEDING_SHORT_DELAY;
						
					}else if ( connected < 3 ){
						
						next_check_delay = ACT_CHECK_SEEDING_LONG_DELAY;
						
					}else{
						
						next_check_delay = 0;
					}
				}else{
					
					int	allowed		= provider.getMaxNewConnectionsAllowed();	
					int	pending		= provider.getPendingConnectionCount();
					int	connected	= provider.getConnectedConnectionCount();
					
					int	online = 0;
					
					for ( TRTrackerAnnouncerHelper a: activated ){
						
						TRTrackerAnnouncerResponse response = a.getLastResponse();
						
						if ( 	response != null && 
								response.getStatus() == TRTrackerAnnouncerResponse.ST_ONLINE ){
							
							online++;
						}
					}
					
					/*
					System.out.println( 
						"checkActivation: announcers=" + announcers.size() + 
						", active=" + activated.size() +
						", online=" + online +
						", allowed=" + allowed +
						", pending=" + pending +
						", connected=" + connected +
						", seeding=" + seeding );
					*/
					
					if ( online == 0 ){
						
						activate = true;
						
							// no trackers online, start next and recheck soon
						
						next_check_delay = ACT_CHECK_INIT_DELAY;
						
					}else{
						
						int	potential = connected + pending;
						
						if ( potential < 10 ){
							
								// minimal connectivity 
							
							activate = true;
							
							next_check_delay = ACT_CHECK_INIT_DELAY;

						}else if ( allowed >= 5 && pending < 3*allowed/4 ){
							
								// not enough to fulfill our needs
							
							activate = true;
							
							next_check_delay = ACT_CHECK_INTERIM_DELAY;
							
						}else{
								// things look good, recheck in a bit
							
							next_check_delay = ACT_CHECK_IDLE_DELAY;
						}
					}
				}
					
				if ( activate ){
					
					for ( TRTrackerAnnouncerHelper a: announcers ){
						
						if ( !activated.contains( a )){
							
							if (Logger.isEnabled()) {
								Logger.log(new LogEvent(getTorrent(), LOGID, "Activating " + getString( a.getAnnounceSets())));
							}
							
							activated.add( a );
							
							last_activation_time = SystemTime.getMonotonousTime();
							
							if ( complete ){
								
								a.complete( true );
								
							}else{
								
								a.update( false );
							}
							
							break;
						}
					}
				}
			}
			
			if ( next_check_delay > 0 ){
			
				setupActivationCheck( next_check_delay );
			}
		}
	}
	
	private String
	getString(
		TOTorrentAnnounceURLSet[]	sets )
	{
		StringBuffer str = new StringBuffer();
		
		str.append( "[" );
		
		int	num1 = 0;
		
		for ( TOTorrentAnnounceURLSet s: sets ){
			
			if ( num1++ > 0 ){
				str.append( ", ");
			}
			
			str.append( "[" );

			URL[]	urls = s.getAnnounceURLs();
			
			int	num2 = 0;
			
			for ( URL u: urls ){
				
				if ( num2++ > 0 ){
					str.append( ", ");
				}
				
				str.append( u.toExternalForm());
			}
			
			str.append( "]" );
		}
		
		str.append( "]" );
		
		return( str.toString());
	}
	
	private boolean
	same(
		TOTorrentAnnounceURLSet[]	s1,
		TOTorrentAnnounceURLSet[]	s2 )
	{
		boolean	res = sameSupport( s1, s2 );
		
		// System.out.println( "same->" + res + ": " + getString(s1) + "/" + getString(s2));
		
		return( res );
	}
	
	private boolean
	sameSupport(
		TOTorrentAnnounceURLSet[]	s1,
		TOTorrentAnnounceURLSet[]	s2 )
	{
		if ( s1.length != s2.length ){
			
			return( false );
		}
		
		for (int i=0;i<s1.length;i++){
			
			URL[] u1 = s1[i].getAnnounceURLs();
			URL[] u2 = s2[i].getAnnounceURLs();
			
			if ( u1.length != u2.length ){
				
				return( false );
			}
			
			if ( u1.length == 1 ){
				
				return( u1[0].toExternalForm().equals( u2[0].toExternalForm()));
			}
			
			Set<String> set1 = new HashSet<String>();
			
			for ( URL u: u1 ){
				
				set1.add( u.toExternalForm());
			}
			
			Set<String> set2 = new HashSet<String>();
			
			for ( URL u: u2 ){
				
				set2.add( u.toExternalForm());
			}
			
			if ( !set1.equals( set2 )){
				
				return( false );
			}
		}
		
		return( true );
	}
	
	protected void
	forceStop(
		final TRTrackerBTAnnouncerImpl		announcer,
		final String[]						networks,
		final URL							url )
	{
		if (Logger.isEnabled()) {
			Logger.log(new LogEvent(getTorrent(), LOGID, "Force stopping " + url + " as private torrent" ));
		}
		
		new AEThread2( "TRMux:fs", true )
		{
			public void
			run()
			{
				try{
					TRTrackerBTAnnouncerImpl an = 
						new TRTrackerBTAnnouncerImpl( getTorrent(), new TOTorrentAnnounceURLSet[0], networks, true, getHelper());
					
					an.cloneFrom( announcer );
					
					an.setTrackerURL( url );
					
					an.stop( false );
					
					an.destroy();
					
				}catch( Throwable e ){
					
				}
			}
		}.start();
	}
	
	private TRTrackerAnnouncerHelper
	create(
		TOTorrent						torrent,
		String[]						networks,
		TOTorrentAnnounceURLSet[]		sets )
	
		throws TRTrackerAnnouncerException
	{
		TRTrackerAnnouncerHelper announcer;
		
		boolean	decentralised;
		
		if ( sets.length == 0 ){
			
			decentralised = TorrentUtils.isDecentralised( torrent.getAnnounceURL());
			
		}else{
			
			decentralised = TorrentUtils.isDecentralised( sets[0].getAnnounceURLs()[0]);
		}
		
		if ( decentralised ){
			
			announcer	= new TRTrackerDHTAnnouncerImpl( torrent, networks, is_manual, getHelper());
			
		}else{
			
			announcer = new TRTrackerBTAnnouncerImpl( torrent, sets, networks, is_manual, getHelper());
		}
		
		for ( TOTorrentAnnounceURLSet set: sets ){
			
			URL[] urls = set.getAnnounceURLs();
			
			for ( URL u: urls ){
				
				String key = u.toExternalForm();
				
				StatusSummary summary = recent_responses.get( key );
				
				if ( summary == null ){
					
					summary = new StatusSummary( announcer, u );
										
					recent_responses.put( key, summary );
					
				}else{
					
					summary.setHelper( announcer );
				}
			}
		}
		
		if ( provider != null ){
			
			announcer.setAnnounceDataProvider( provider );
		}
		
		if ( ip_override != null ){
			
			announcer.setIPOverride( ip_override );
		}
		
		return( announcer );
	}
	
	
	public TRTrackerAnnouncerResponse
	getLastResponse()
	{
		TRTrackerAnnouncerResponse	result = null;
		
		TRTrackerAnnouncerHelper best = getBestActive();
		
		if ( best != null ){
			
			result = best.getLastResponse();
		}
		
		if ( result == null ){
			
			result = last_response_informed;
		}
		
		return( result );
	}
	

	@Override
	protected void
	informResponse(
		TRTrackerAnnouncerHelper		helper,
		TRTrackerAnnouncerResponse		response )
	{
		URL	url = response.getURL();
		
			// can be null for external plugins (e.g. mldht...)
		
		if ( url != null ){
			
			synchronized( this ){
				
				String key = url.toExternalForm();
				
				StatusSummary summary = recent_responses.get( key );
			
				if ( summary != null ){
				
					summary.updateFrom( response );
				}
			}
		}
		
		last_response_informed = response;
		
			// force recalc of best active next time
		
		last_best_active_set_time = 0;
		
		super.informResponse( helper, response );
		
		if ( response.getStatus() != TRTrackerAnnouncerResponse.ST_ONLINE ){
			
			URL	u = response.getURL();
			
			if ( u != null ){
				
				String s = u.toExternalForm();
				
				synchronized( failed_urls ){
					
					if ( failed_urls.contains( s )){
						
						return;
					}
					
					failed_urls.add( s );
				}
			}
			
			checkActivation( true );
		}
	}
	
	public boolean
	isManual()
	{
		return( is_manual );
	}

	public void
	setAnnounceDataProvider(
		TRTrackerAnnouncerDataProvider		_provider )
	{
		List<TRTrackerAnnouncerHelper>	to_set;
		
		synchronized( this ){
			
			provider	= _provider;
			
			to_set = announcers.getList();
		}
		
		for ( TRTrackerAnnouncer announcer: to_set ){
		
			announcer.setAnnounceDataProvider( provider );
		}
	}
	
	protected TRTrackerAnnouncerHelper
	getBestActive()
	{
		long	now = SystemTime.getMonotonousTime();
		
		if ( now - last_best_active_set_time < 1000 ){
			
			return( last_best_active );
		}
		
		last_best_active = getBestActiveSupport();
		
		last_best_active_set_time = now;
		
		return( last_best_active );
	}
	
	protected TRTrackerAnnouncerHelper
	getBestActiveSupport()
	{
		List<TRTrackerAnnouncerHelper> x = announcers.getList();
		
		TRTrackerAnnouncerHelper error_resp = null;
		
		for ( TRTrackerAnnouncerHelper announcer: x ){
			
			TRTrackerAnnouncerResponse response = announcer.getLastResponse();
			
			if ( response != null ){
				
				int	resp_status = response.getStatus();
				
				if ( resp_status == TRTrackerAnnouncerResponse.ST_ONLINE ){
					
					return( announcer );
					
				}else if ( error_resp == null && resp_status == TRTrackerAnnouncerResponse.ST_REPORTED_ERROR ){
					
					error_resp = announcer;
				}
			}
		}
		
		if ( error_resp != null ){
			
			return( error_resp );
		}
		
		if ( x.size() > 0 ){
			
			return( x.get(0));
		}
		
		return( null );
	}
	
	public URL
	getTrackerURL()
	{
		TRTrackerAnnouncerHelper	active = getBestActive();
		
		if ( active != null ){
			
			return( active.getTrackerURL());
		}
		
		return( null );
	}
	
	public void
	setTrackerURL(
		URL		url )
	{
		List<List<String>> groups = new ArrayList<List<String>>();
		
		List<String> group = new ArrayList<String>();
		
		group.add( url.toExternalForm());
		
		groups.add( group );
		
		TorrentUtils.listToAnnounceGroups( groups, getTorrent());
		
		resetTrackerUrl( false );
	}
	
	public void
	resetTrackerUrl(
		boolean	shuffle )
	{
		try{
			split();
			
		}catch( Throwable e ){
			
			Debug.out( e );
		}
		
		for ( TRTrackerAnnouncer announcer: announcers ){
		
			announcer.resetTrackerUrl( shuffle );
		}
	}
	
	public void
	setIPOverride(
		String		override )
	{
		List<TRTrackerAnnouncerHelper>	to_set;
		
		synchronized( this ){
			
			to_set	= announcers.getList();
			
			ip_override	= override;
		}
		
		for ( TRTrackerAnnouncer announcer: to_set ){
		
			announcer.setIPOverride( override );
		}
	}
	
	public void
	clearIPOverride()
	{
		List<TRTrackerAnnouncerHelper>	to_clear;
		
		synchronized( this ){
			
			to_clear	= announcers.getList();
			
			ip_override	= null;
		}
		
		for ( TRTrackerAnnouncer announcer: to_clear ){
		
			announcer.clearIPOverride();
		}
	}
	
	public void
	setRefreshDelayOverrides(
		int		percentage )
	{
		for ( TRTrackerAnnouncer announcer: announcers ){
		
			announcer.setRefreshDelayOverrides( percentage );
		}
	}
	
	public int
	getTimeUntilNextUpdate()
	{
		TRTrackerAnnouncerHelper	active = getBestActive();

		if ( active != null ){
			
			return( active.getTimeUntilNextUpdate());
		}
		
		return( Integer.MAX_VALUE );
	}
	
	public int
	getLastUpdateTime()
	{
		TRTrackerAnnouncerHelper	active = getBestActive();

		if ( active != null ){
			
			return( active.getLastUpdateTime());
		}
		
		return( 0 );
	}	
	
	public void
	update(
		boolean	force )
	{
		List<TRTrackerAnnouncerHelper> to_update;
		
		synchronized( this ){
						
			to_update = is_manual?announcers.getList():new ArrayList<TRTrackerAnnouncerHelper>( activated );
		}
		
		for ( TRTrackerAnnouncer announcer: to_update ){
		
			announcer.update(force);
		}
	}
	
	public void
	complete(
		boolean	already_reported )
	{
		List<TRTrackerAnnouncerHelper> to_complete;
		
		synchronized( this ){
			
			complete	= true;
			
			to_complete = is_manual?announcers.getList():new ArrayList<TRTrackerAnnouncerHelper>( activated );
		}
		
		for ( TRTrackerAnnouncer announcer: to_complete ){
		
			announcer.complete( already_reported );
		}
	}
	
	public void
	stop(
		boolean	for_queue )
	{
		List<TRTrackerAnnouncerHelper> to_stop;
		
		synchronized( this ){
			
			stopped	= true;
			
			to_stop = is_manual?announcers.getList():new ArrayList<TRTrackerAnnouncerHelper>( activated );
			
			activated.clear();
		}
		
		for ( TRTrackerAnnouncer announcer: to_stop ){
		
			announcer.stop( for_queue );
		}
	}
	
	public void
	destroy()
	{
		TRTrackerAnnouncerFactoryImpl.destroy( this );

		List<TRTrackerAnnouncerHelper> to_destroy;
		
		synchronized( this ){
			
			destroyed = true;
			
			to_destroy = announcers.getList();
		}
		
		for ( TRTrackerAnnouncer announcer: to_destroy ){
		
			announcer.destroy();
		}
		
		TimerEvent	ev = event;
		
		if ( ev != null ){
			
			ev.cancel();
		}
	}
	
	public int
	getStatus()
	{
		TRTrackerAnnouncer	max_announcer = getBestAnnouncer();
		
		return( max_announcer==null?-1:max_announcer.getStatus());
	}
		
	public String
	getStatusString()
	{		
		TRTrackerAnnouncer	max_announcer = getBestAnnouncer();
		
		return( max_announcer==null?"":max_announcer.getStatusString());
	}
	
	public TRTrackerAnnouncer
	getBestAnnouncer()
	{
		int	max = -1;
		
		TRTrackerAnnouncer	max_announcer = null;
		
		for ( TRTrackerAnnouncer announcer: announcers ){
			
			int	status = announcer.getStatus();
			
			if ( status > max ){
				
				max_announcer 	= announcer;
				max				= status;
			}
		}
		
		return( max_announcer==null?this:max_announcer );
	}
	
	public void
	refreshListeners()
	{
		informURLRefresh();
	}
	
	public void
	setAnnounceResult(
		DownloadAnnounceResult	result )
	{
			// this is only used for setting DHT results
		
		for ( TRTrackerAnnouncer announcer: announcers ){
			
			if ( announcer instanceof TRTrackerDHTAnnouncerImpl ){
				
				announcer.setAnnounceResult( result );
				
				return;
			}
		}
		
			// TODO: we should always create a DHT entry and have it denote DHT tracking for all circustances
			// have the DHT plugin set it to offline if disabled
		
		List<TRTrackerAnnouncerHelper> x = announcers.getList();
		
		if ( x.size() > 0 ){
			
			x.get(0).setAnnounceResult( result );
		}
	}
		
	protected int
	getPeerCacheLimit()
	{
		synchronized( this ){
			
			if ( activated.size() < announcers.size()){
				
				return( 0 );
			}
		}
		
		if ( SystemTime.getMonotonousTime() - create_time < 15*1000 ){
			
			return( 0 );
		}
		
		TRTrackerAnnouncer active = getBestActive();
		
		if ( active != null && provider != null && active.getStatus() == TRTrackerAnnouncerResponse.ST_ONLINE ){
			
			if ( 	provider.getMaxNewConnectionsAllowed() > 0 &&
					provider.getPendingConnectionCount() == 0 ){
				
				return( 5 );
				
			}else{
				
				return( 0 );
			}
		}
		
		return( 10 );
	}
	
	public TrackerPeerSource 
	getTrackerPeerSource(
		final TOTorrentAnnounceURLSet		set )
	{
		URL[]	urls = set.getAnnounceURLs();
		
		final String[] url_strs = new String[ urls.length ];
		
		for ( int i=0;i<urls.length;i++ ){
			
			url_strs[i] = urls[i].toExternalForm();
		}
		
		return( 
			new TrackerPeerSource()
			{
				private StatusSummary		_summary;
				private boolean				enabled;
				private long				fixup_time;
				
				private StatusSummary
				fixup()
				{
					long now = SystemTime.getMonotonousTime();
					
					if ( now - fixup_time > 1000 ){
												
						long			most_recent	= 0;
						StatusSummary	summary	 	= null;
						
						synchronized( TRTrackerAnnouncerMuxer.this ){
						
							for ( String str: url_strs ){
							
								StatusSummary s = recent_responses.get( str );
								
								if ( s != null ){
									
									if ( summary == null || s.getTime() > most_recent ){
										
										summary		= s;
										most_recent	= s.getTime();
									}
								}
							}	
						}
						
						if ( provider != null ){
						
							enabled = provider.isPeerSourceEnabled( PEPeerSource.PS_BT_TRACKER );
						}
						
						if ( summary != null ){
							
							_summary = summary;
						}
						
						fixup_time = now;
					}
					
					return( _summary );
				}
				
				public int
				getType()
				{
					return( TrackerPeerSource.TP_TRACKER );
				}
				
				public String
				getName()
				{
					StatusSummary summary = fixup();
					
					if ( summary != null ){
						
						String str =summary.getURL().toExternalForm();
						
						int pos = str.indexOf( '?' );
						
						if ( pos != -1 ){
							
							str = str.substring( 0, pos );
						}
						
						return( str );
					}
					
					return( url_strs[0] );
				}
				
				public int
				getStatus()
				{
					StatusSummary summary = fixup();
					
					if ( !enabled ){
						
						return( ST_DISABLED );
					}
					
					if ( summary != null ){
						
						return( summary.getStatus());
					}
					
					return( ST_QUEUED );
				}
				
				public String
				getStatusString()
				{
					StatusSummary summary = fixup();
					
					if ( summary != null && enabled ){
						
						return( summary.getStatusString());
					}
					
					return( null );
				}
						
				public int
				getSeedCount()
				{
					StatusSummary summary = fixup();
					
					if ( summary != null ){
						
						return( summary.getSeedCount());
					}
					
					return( -1 );
				}
				
				public int
				getLeecherCount()
				{
					StatusSummary summary = fixup();
					
					if ( summary != null ){
						
						return( summary.getLeecherCount());
					}
					
					return( -1 );
				}			
				
				public int
				getCompletedCount()
				{
					StatusSummary summary = fixup();
					
					if ( summary != null ){
						
						return( summary.getCompletedCount());
					}
					
					return( -1 );
				}			
				
				public int
				getPeers()
				{
					StatusSummary summary = fixup();
					
					if ( summary != null ){
						
						return( summary.getPeers());
					}
					
					return( -1 );
				}			

				public int 
				getLastUpdate() 
				{
					StatusSummary summary = fixup();
					
					if ( summary != null ){
						
						long time = summary.getTime();
						
						if ( time == 0 ){
							
							return( 0 );
						}
						
						long elapsed = SystemTime.getMonotonousTime() - time;
						
						return((int)( (SystemTime.getCurrentTime() - elapsed ) / 1000 ));
					}
					
					return( 0 );
				}
				
				public int
				getSecondsToUpdate()
				{
					StatusSummary summary = fixup();
					
					if ( summary != null ){
						
						return( summary.getSecondsToUpdate());
					}
					
					return( -1 );
				}
				
				public int
				getInterval()
				{
					StatusSummary summary = fixup();
					
					if ( summary != null ){
						
						return( summary.getInterval());
					}
					
					return( -1 );
				}
				
				public int
				getMinInterval()
				{
					StatusSummary summary = fixup();
					
					if ( summary != null && enabled ){
						
						return( summary.getMinInterval());
					}
					
					return( -1 );
				}
				
				public boolean
				isUpdating()
				{
					StatusSummary summary = fixup();
					
					if ( summary != null && enabled  ){
						
						return( summary.isUpdating());
					}
					
					return( false );
				}
				
				public boolean
				canManuallyUpdate()
				{
					StatusSummary summary = fixup();
					
					if ( summary == null ){
						
						return( false );
					}
					
					return( summary.canManuallyUpdate());
				}
				
				public void
				manualUpdate()
				{
					StatusSummary summary = fixup();
					
					if ( summary != null ){
						
						summary.manualUpdate();
					}
				}
				
				public boolean
				canDelete()
				{
					return( false );
				}
				
				public void
				delete()
				{
					Debug.out( "derp" );
				}
			});
	}
	
	public void 
	generateEvidence(
		IndentWriter writer )
	{
		for ( TRTrackerAnnouncer announcer: announcers ){
		
			announcer.generateEvidence(writer);
		}
	}
	
	private static class
	StatusSummary
	{
		private TRTrackerAnnouncerHelper		helper;
		
		private long		time;
		private URL			url;
		private int			status;
		private String		status_str;
		private int			seeds		= -1;
		private int			leechers	= -1;
		private int			peers		= -1;
		private int			completed	= -1;
		
		private int			interval;
		private int			min_interval;
		
		protected 
		StatusSummary(
			TRTrackerAnnouncerHelper		_helper,
			URL								_url )
		{
			helper	= _helper;
			url		= _url;
			
			status = TrackerPeerSource.ST_QUEUED;
		}
		
		protected void
		setHelper(
			TRTrackerAnnouncerHelper		_helper )
		{
			helper	= _helper;
		}
		
		protected void
		updateFrom(
			TRTrackerAnnouncerResponse		response )
		{			
			time	= SystemTime.getMonotonousTime();
			
			int	state = response.getStatus();
			
			if ( state == TRTrackerAnnouncerResponse.ST_ONLINE ){
				
				status = TrackerPeerSource.ST_ONLINE;
			
				seeds		= response.getScrapeCompleteCount();
				leechers	= response.getScrapeIncompleteCount();
				completed	= response.getScrapeDownloadedCount();
				peers		= response.getPeers().length;
				
			}else{
				
				status = TrackerPeerSource.ST_ERROR;
			}
			
			status_str = response.getStatusString();
			
			interval 		= (int)helper.getInterval();
			min_interval 	= (int)helper.getMinInterval();
		}
		
		public long
		getTime()
		{
			return( time );
		}
		
		public URL
		getURL()
		{
			return( url );
		}
		
		public int
		getStatus()
		{
			return( status );
		}
		
		public String
		getStatusString()
		{
			return( status_str );
		}
		
		public int
		getSeedCount()
		{
			return( seeds );
		}
		
		public int
		getLeecherCount()
		{
			return( leechers );
		}
		
		public int
		getCompletedCount()
		{
			return( completed );
		}
		
		public int
		getPeers()
		{
			return( peers );
		}
		
		public boolean
		isUpdating()
		{
			return( helper.isUpdating());
		}
		
		public int
		getInterval()
		{
			return( interval );
		}
		
		public int
		getMinInterval()
		{
			return( min_interval );
		}
		
		public int
		getSecondsToUpdate()
		{
			return( helper.getTimeUntilNextUpdate());
		}
		
		public boolean
		canManuallyUpdate()
		{
			return( ((SystemTime.getCurrentTime() / 1000 - helper.getLastUpdateTime() >= TRTrackerAnnouncer.REFRESH_MINIMUM_SECS)));
		}
		
		public void
		manualUpdate()
		{
			helper.update( true );
		}
	}
}
