/*
 * Created on 20-Dec-2005
 * Created by Paul Gardner
 * Copyright (C) 2005, 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 46,603.30 euros
 * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
 *
 */

package com.aelitis.azureus.core.instancemanager.impl;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.util.*;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.download.DownloadManager;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.AESemaphore;
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.Debug;
import org.gudy.azureus2.core3.util.DelayedEvent;
import org.gudy.azureus2.core3.util.SHA1Simple;
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.plugins.download.Download;
import org.gudy.azureus2.pluginsimpl.local.download.DownloadManagerImpl;

import com.aelitis.azureus.core.AzureusCore;
import com.aelitis.azureus.core.AzureusCoreLifecycleAdapter;
import com.aelitis.azureus.core.instancemanager.AZInstance;
import com.aelitis.azureus.core.instancemanager.AZInstanceManager;
import com.aelitis.azureus.core.instancemanager.AZInstanceManagerListener;
import com.aelitis.azureus.core.instancemanager.AZInstanceTracked;
import com.aelitis.azureus.core.util.NetUtils;
import com.aelitis.net.udp.mc.MCGroup;
import com.aelitis.net.udp.mc.MCGroupAdapter;
import com.aelitis.net.udp.mc.MCGroupFactory;

public class 
AZInstanceManagerImpl 
	implements AZInstanceManager, MCGroupAdapter
{
	private static final boolean DISABLE_LAN_LOCAL_STUFF	= false;
	
	static{
		if ( DISABLE_LAN_LOCAL_STUFF ){
			System.out.println( "**** LAN LOCAL STUFF DISABLED ****" );
		}
	}
	
	private static final LogIDs LOGID = LogIDs.NET;
	
	private String				MC_GROUP_ADDRESS 	= "239.255.067.250";	// 239.255.000.000-239.255.255.255 
	private int					MC_GROUP_PORT		= 16680;				//
	private int					MC_CONTROL_PORT		= 0;

	private static final int	MT_VERSION		= 1;
	
	private static final int	MT_ALIVE		= 1;
	private static final int	MT_BYE			= 2;
	private static final int	MT_REQUEST		= 3;
	private static final int	MT_REPLY		= 4;
	
	private static final int	MT_REQUEST_SEARCH	= 1;
	private static final int	MT_REQUEST_TRACK	= 2;
	
	private static final long	ALIVE_PERIOD	= 30*60*1000;
	
	private static AZInstanceManagerImpl	singleton;
	
	private List	listeners	= new ArrayList();
	
	private static AEMonitor	class_mon = new AEMonitor( "AZInstanceManager:class" );
	
	private static String	socks_proxy	= null;
	
	static{
		COConfigurationManager.addAndFireParameterListeners(
			new String[]{ "Proxy.Data.Enable", "Proxy.Host", "Proxy.Data.Same", "Proxy.Data.Host" },
			new ParameterListener()
			{
				public void 
				parameterChanged(
					String parameterName )
				{
					if ( !COConfigurationManager.getBooleanParameter("Proxy.Data.Enable")){
						
						socks_proxy = null;
						
						return;
					}
					
					if ( COConfigurationManager.getBooleanParameter("Proxy.Data.Same")){
						
						socks_proxy = COConfigurationManager.getStringParameter( "Proxy.Host" );
						
					}else{
						
						socks_proxy = COConfigurationManager.getStringParameter( "Proxy.Data.Host" );

					}
					
					if ( socks_proxy != null ){
						
						socks_proxy	= socks_proxy.trim();
					}
				}
			});
	}
	
	public static AZInstanceManager
	getSingleton(
		AzureusCore	core )
	{
		try{
			class_mon.enter();
			
			if ( singleton == null ){
				
				singleton = new AZInstanceManagerImpl( core );
			}
		}finally{
			
			class_mon.exit();
		}
		
		return( singleton );
	}
	
	private AzureusCore	core;
	private MCGroup	 	mc_group;
	private long		search_id_next;
	private List		requests = new ArrayList();
	
	private AZMyInstanceImpl		my_instance;
	private Map						other_instances	= new HashMap();
	
	private volatile Map			tcp_lan_to_ext	= new HashMap();
	private volatile Map			udp_lan_to_ext	= new HashMap();
	private volatile Map			udp2_lan_to_ext	= new HashMap();
	private volatile Map			tcp_ext_to_lan	= new HashMap();
	private volatile Map			udp_ext_to_lan	= new HashMap();
	private volatile Map			udp2_ext_to_lan	= new HashMap();
	
	private volatile Set			lan_addresses	= new HashSet();
	private volatile Set			ext_addresses	= new HashSet();
	
	private volatile List			lan_subnets		= new ArrayList();
	private volatile List			explicit_peers 	= new ArrayList();
	
	private volatile boolean		include_well_known_lans	= true;
	
	private AESemaphore	initial_search_sem	= new AESemaphore( "AZInstanceManager:initialSearch" );
	
	private AEMonitor	this_mon = new AEMonitor( "AZInstanceManager" );

	private boolean		closing;
	
	protected
	AZInstanceManagerImpl(
		AzureusCore	_core )
	{
		core			= _core;
		
		my_instance	= new AZMyInstanceImpl( core, this );
		
		new AZPortClashHandler( this );
	}
	
	public void
	initialize()
	{		
		try{
			mc_group = 
				MCGroupFactory.getSingleton(
					this,
					MC_GROUP_ADDRESS,
					MC_GROUP_PORT,
					MC_CONTROL_PORT,
					null );
					
			core.addLifecycleListener(
				new AzureusCoreLifecycleAdapter()
				{
					public void
					stopping(
						AzureusCore		core )
					{
						closing	= true;
						
						sendByeBye();
					}
				});
			
			SimpleTimer.addPeriodicEvent(
				"InstManager:timeouts",
				ALIVE_PERIOD,
				new TimerEventPerformer()
				{
					public void
					perform(
						TimerEvent	event )
					{
						checkTimeouts();
													
						sendAlive();				
					}
				});
		
		}catch( Throwable e ){
			
			initial_search_sem.releaseForever();
			
			Debug.printStackTrace(e);
		}
		
		new AEThread( "AZInstanceManager:initialSearch", true )
		{
			public void
			runSupport()
			{
				try{
					search();
					
						// pick up our own details as soon as we can
					
					addAddresses( my_instance );
					
				}finally{
					
					initial_search_sem.releaseForever();
				}
			}
		}.start();
	}
	
	public void
	trace(
		String	str )
	{
		if ( Logger.isEnabled()){
				
			Logger.log(new LogEvent( LOGID, str )); 
		}
	}
	
	public void
	log(
		Throwable e )
	{
		Debug.printStackTrace(e);
	}
	
	public boolean
	isInitialized()
	{
		return( initial_search_sem.isReleasedForever());
	}
	
	protected boolean
	isClosing()
	{
		return( closing );
	}
	
	protected void
	sendAlive()
	{
		sendMessage( MT_ALIVE );
	}
	
	protected void
	sendAlive(
		InetSocketAddress	target )
	{
		sendMessage( MT_ALIVE, target );
	}
	
	protected void
	sendByeBye()
	{
		sendMessage( MT_BYE );
	}
	
	protected void
	sendByeBye(
		InetSocketAddress	target )
	{
		sendMessage( MT_BYE, target );
	}
	
	protected void
	sendMessage(
		int		type )
	{
		sendMessage( type, (Map)null );
	}
	
	protected void
	sendMessage(
		int					type,
		InetSocketAddress	target )
	{
		sendMessage( type, null, target );
	}
	
	protected void
	sendMessage(
		int		type,
		Map		body )
	{
		sendMessage( type, body, null );
	}
	
	protected void
	sendMessage(
		int					type,
		Map					body,
		InetSocketAddress	member )
	{
		Map	map = new HashMap();
		
		map.put( "ver", new Long(MT_VERSION));
		map.put( "type", new Long(type));
		
		Map	originator = new HashMap();
		
		map.put( "orig", originator );
		
		my_instance.encode( originator );
		
		if ( body != null ){
			
			map.put( "body", body );
		}
				
		try{
			
			if ( member == null ){
				
				byte[]	data = BEncoder.encode( map );

				mc_group.sendToGroup( data );
				
				if ( explicit_peers.size() > 0 ){
					
					map.put( "explicit", new Long(1));
				
					byte[]	explicit_data = BEncoder.encode( map );
					
					Iterator	it = explicit_peers.iterator();
				
					while( it.hasNext()){
					
						mc_group.sendToMember((InetSocketAddress)it.next(), explicit_data );
					}
				}
			}else{
				
				if ( explicit_peers.contains( member )){
					
					map.put( "explicit", new Long(1));
				}
				
				byte[]	explicit_data = BEncoder.encode( map );

				mc_group.sendToMember( member, explicit_data );
			}
		}catch( Throwable e ){
			
		}
	}

	public void
	received(
		NetworkInterface	network_interface,
		InetAddress			local_address,
		InetSocketAddress	originator,
		byte[]				data,
		int					length )
	{
		try{
			Map	map = BDecoder.decode(new BufferedInputStream(new ByteArrayInputStream( data, 0, length )));
			
			long	version = ((Long)map.get( "ver" )).longValue();
			long	type	= ((Long)map.get( "type" )).longValue();
			
			InetAddress	originator_address = originator.getAddress();
			
			if ( map.get( "explicit" ) != null ){
				
				addInstanceSupport( originator_address, false );
			}
			
			AZOtherInstanceImpl	instance = AZOtherInstanceImpl.decode( originator_address, (Map)map.get( "orig" ));
			
			if ( type == MT_ALIVE ){
				
				checkAdd( instance );
				
			}else if ( type == MT_BYE ){
				
				checkRemove( instance );
				
			}else{
				
				checkAdd( instance );
				
				Map	body = (Map)map.get( "body" );
				
				if ( type == MT_REQUEST ){
					
					String	originator_id	= instance.getID();
					
					if ( !originator_id.equals( my_instance.getID())){
						
						Map	reply = requestReceived( instance, body );
					
						if ( reply != null ){
						
							reply.put( "oid", originator_id.getBytes());
							reply.put( "rid", body.get( "rid" ));
							
							sendMessage( MT_REPLY, reply, originator );
						}
					}
				}else if ( 	type == MT_REPLY ){
					
					String	originator_id	= new String((byte[])body.get( "oid" ));
					
					if ( originator_id.equals( my_instance.getID())){
						
						long req_id = ((Long)body.get("rid")).longValue();
						
						try{
							this_mon.enter();
							
							for (int i=0;i<requests.size();i++){
								
								request	req = (request)requests.get(i);
								
								if ( req.getID() == req_id ){
									
									req.addReply( instance, body );
								}
							}
						}finally{
							
							this_mon.exit();
						}
					}
				}
			}
		}catch( Throwable e ){
			
			Debug.out( "Invalid packet received from " + originator, e );
		}
	}
	

	protected Map
	requestReceived(
		AZInstance		instance,
		Map				body )
	{
		// System.out.println( "received result: " + ST + "/" + AL );
		

		long	type = ((Long)body.get( "type")).longValue();
		
		if ( type == MT_REQUEST_SEARCH ){
			
			return( new HashMap());
			
		}else if ( type == MT_REQUEST_TRACK ){
							
			byte[]	hash = (byte[])body.get( "hash" );
			
			boolean	seed = ((Long)body.get( "seed" )).intValue() == 1;
			
			List	dms = core.getGlobalManager().getDownloadManagers();
			
			Iterator	it = dms.iterator();
			
			DownloadManager	matching_dm = null;
			
			try{
				while( it.hasNext()){
					
					DownloadManager	dm = (DownloadManager)it.next();
					
					TOTorrent	torrent = dm.getTorrent();
					
					if ( torrent == null ){
						
						continue;
					}
					
					byte[]	sha1_hash = (byte[])dm.getData( "AZInstanceManager::sha1_hash" );
					
					if ( sha1_hash == null ){			

						sha1_hash	= new SHA1Simple().calculateHash( torrent.getHash());
						
						dm.setData( "AZInstanceManager::sha1_hash", sha1_hash );
					}
					
					if ( Arrays.equals( hash, sha1_hash )){
						
						matching_dm	= dm;
						
						break;
					}
				}
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
			
			if ( matching_dm == null ){
				
				return( null );
			}
			
			int	dm_state = matching_dm.getState();
			
			if ( dm_state == DownloadManager.STATE_ERROR || dm_state == DownloadManager.STATE_STOPPED ){
				
				return( null );
			}
							
			try{		
				informTracked( 
					new trackedInstance( instance, DownloadManagerImpl.getDownloadStatic( matching_dm ), seed ));
					
			}catch( Throwable e ){
					
				Debug.printStackTrace(e);
			}
			
			Map	reply = new HashMap();
			
			// XXX include DND?  I don't know
			reply.put( "seed", new Long( matching_dm.isDownloadComplete(true)?1:0));		
			
			return( reply );
			
		}else{
			
			return( null );
		}
	}
	
	public void
	interfaceChanged(
		NetworkInterface	network_interface )
	{
		sendAlive();
	}
	
	protected AZOtherInstanceImpl
	checkAdd(
		AZOtherInstanceImpl	inst )
	{
		if ( inst.getID().equals( my_instance.getID())){
			
			return( inst );
		}
		
		boolean	added 	= false;
		boolean	changed	= false;
		
		try{
			this_mon.enter();
			
			AZOtherInstanceImpl	existing = (AZOtherInstanceImpl)other_instances.get( inst.getID());
			
			if ( existing == null ){
				
				added	= true;
			
				other_instances.put( inst.getID(), inst );
								
			}else{
								
				changed = existing.update( inst );

				inst	= existing;
			}
		}finally{
			
			this_mon.exit();
		}
		
		if ( added ){
			
			informAdded( inst );
			
		}else if ( changed ){
			
			informChanged( inst );
		}
		
		return( inst );
	}
	
	protected void
	checkRemove(
		AZOtherInstanceImpl	inst )
	{
		if ( inst.getID().equals( my_instance.getID())){
			
			return;
		}
		
		boolean	removed = false;
		
		try{
			this_mon.enter();
			
			removed = other_instances.remove( inst.getID()) != null;
			
		}finally{
			
			this_mon.exit();
		}
		
		if ( removed ){
			
			informRemoved( inst );
		}
	}
	
	public AZInstance
	getMyInstance()
	{
		return( my_instance );
	}
	
	protected void
	search()
	{
		sendRequest( MT_REQUEST_SEARCH );
	}
	
	public AZInstance[]
	getOtherInstances()
	{
		initial_search_sem.reserve();
		
		try{
			this_mon.enter();

			return((AZInstance[])other_instances.values().toArray( new AZInstance[other_instances.size()]));
			
		}finally{
			
			this_mon.exit();
		}
	}
	
	protected void
	addAddresses(
		AZInstance	inst )
	{
		InetAddress	internal_address 	= inst.getInternalAddress();
		InetAddress	external_address	= inst.getExternalAddress();
		int			tcp					= inst.getTCPListenPort();
		int			udp					= inst.getUDPListenPort();
		int			udp2				= inst.getUDPNonDataListenPort();
		
		modifyAddresses( internal_address, external_address, tcp, udp, udp2, true );
	}
	
	protected void
	removeAddresses(
		AZOtherInstanceImpl	inst )
	{
		List		internal_addresses 	= inst.getInternalAddresses();
		InetAddress	external_address	= inst.getExternalAddress();
		int			tcp					= inst.getTCPListenPort();
		int			udp					= inst.getUDPListenPort();
		int			udp2				= inst.getUDPNonDataListenPort();

		for (int i=0;i<internal_addresses.size();i++){
			
			modifyAddresses( (InetAddress)internal_addresses.get(i), external_address, tcp, udp, udp2, false );
		}
	}
	
	protected void
	modifyAddresses(
		InetAddress		internal_address,
		InetAddress		external_address,
		int				tcp,
		int				udp,
		int				udp2,
		boolean			add )	
	{
		if ( internal_address.isAnyLocalAddress()){
			
			try{
				internal_address = NetUtils.getLocalHost();
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
		
		try{
			this_mon.enter();
 
			InetSocketAddress	int_tcp = new InetSocketAddress(internal_address, tcp);
			InetSocketAddress	ext_tcp = new InetSocketAddress(external_address, tcp);
			InetSocketAddress	int_udp = new InetSocketAddress(internal_address, udp);
			InetSocketAddress	ext_udp = new InetSocketAddress(external_address, udp);
			InetSocketAddress	int_udp2 = new InetSocketAddress(internal_address, udp2);
			InetSocketAddress	ext_udp2 = new InetSocketAddress(external_address, udp2);

				// not the most efficient code in the world this... will need rev
			
			tcp_ext_to_lan 	= modifyAddress( tcp_ext_to_lan, ext_tcp, int_tcp, add );
			tcp_lan_to_ext 	= modifyAddress( tcp_lan_to_ext, int_tcp, ext_tcp, add );
			udp_ext_to_lan 	= modifyAddress( udp_ext_to_lan, ext_udp, int_udp, add );
			udp_lan_to_ext 	= modifyAddress( udp_lan_to_ext, int_udp, ext_udp, add );
			udp2_ext_to_lan = modifyAddress( udp2_ext_to_lan, ext_udp2, int_udp2, add );
			udp2_lan_to_ext = modifyAddress( udp2_lan_to_ext, int_udp2, ext_udp2, add );

			if ( !lan_addresses.contains( internal_address )){
				
				Set	new_lan_addresses = new HashSet( lan_addresses );
				
				new_lan_addresses.add( internal_address );
				
				lan_addresses	= new_lan_addresses;
			}
			
			if ( !ext_addresses.contains( external_address )){
				
				Set	new_ext_addresses = new HashSet( ext_addresses );
				
				new_ext_addresses.add( external_address );
				
				ext_addresses	= new_ext_addresses;
			}
		}finally{
			
			this_mon.exit();
		}
	}
		
	protected Map
	modifyAddress(
		Map					map,
		InetSocketAddress	key,
		InetSocketAddress	value,
		boolean				add )
	{
		// System.out.println( "ModAddress: " + key + " -> " + value + " - " + (add?"add":"remove"));
		
		InetSocketAddress	old_value = (InetSocketAddress)map.get(key);

		boolean	same = old_value != null && old_value.equals( value );
		
		Map	new_map = map;
		
		if ( add ){
			
			if ( !same ){
				
				new_map	= new HashMap( map );
	
				new_map.put( key, value );
			}
		}else{
			
			if ( same ){
				
				new_map	= new HashMap( map );
				
				new_map.remove( key );
			}
		}	
		
		return( new_map );
	}
	
	public InetSocketAddress
	getLANAddress(
		InetSocketAddress	external_address,
		int					address_type )
	{
		Map	map;
		
		if ( address_type == AT_TCP ){
			map = tcp_ext_to_lan;
		}else if ( address_type == AT_UDP ){
			map = udp_ext_to_lan;
		}else{
			map = udp2_ext_to_lan;
		}
		
		if ( map.size() == 0 ){
			
			return( null );
		}
		
		return((InetSocketAddress)map.get( external_address ));
	}
	
	public InetSocketAddress
	getExternalAddress(
		InetSocketAddress	lan_address,
		int					address_type )
	{
		Map	map;
		
		if ( address_type == AT_TCP ){
			map = tcp_lan_to_ext;
		}else if ( address_type == AT_UDP ){
			map = udp_lan_to_ext;
		}else{
			map = udp2_lan_to_ext;
		}
		
		if ( map.size() == 0 ){
			
			return( null );
		}
		
		return((InetSocketAddress)map.get( lan_address ));	
	}
	
	public boolean
	isLANAddress(
		InetAddress			address )
	{
		if ( DISABLE_LAN_LOCAL_STUFF ){
			
			return( false );
		}
		
		if ( address == null ){
			
			return( false );
		}
		
		String	sp = socks_proxy;
		
		if ( sp != null ){
			
			if ( sp.equals( address.getHostAddress())){
				
				return( false );
			}
		}
		
		if ( include_well_known_lans ){
		
			if ( 	address.isLoopbackAddress() || 
					address.isLinkLocalAddress() ||
					address.isSiteLocalAddress()){
					
				return( true );
			}
		}
		
		String	host_address = address.getHostAddress();
		
		for (int i=0;i<lan_subnets.size();i++){
			
			Pattern	p = (Pattern)lan_subnets.get(i);
			
			if ( p.matcher( host_address ).matches()){
								
				return( true );
			}
		}
		
		if ( lan_addresses.contains( address )){
			
			return( true );
		}
		
		if ( explicit_peers.size() > 0 ){
			
			Iterator	it = explicit_peers.iterator();
			
			while( it.hasNext()){

				if (((InetSocketAddress)it.next()).getAddress().equals( address )){
					
					return( true );
				}
			}
		}
		
		return( false );
	}
	
	public boolean
	addLANSubnet(
		String	subnet )
	
		throws PatternSyntaxException
	{
		String	str = "";
		
		for (int i=0;i<subnet.length();i++){
			
			char	c = subnet.charAt(i);
			
			if ( c == '*' ){
				
				str += ".*?";
				
			}else if ( c == '.' ){
				
				str += "\\.";
			
			}else{
				
				str += c;
			}
		}
		
		Pattern pattern = Pattern.compile( str );
		
		for (int i=0;i<lan_subnets.size();i++){
			
			if ( pattern.pattern().equals(((Pattern)lan_subnets.get(i)).pattern())){
				
				return( false );
			}
		}
		
		try{
			this_mon.enter();
			
			List	new_nets = new ArrayList( lan_subnets );
			
			new_nets.add( pattern );
			
			lan_subnets	= new_nets;
			
		}finally{
			
			this_mon.exit();
		}
		
		return( true );
	}
	
	public void
	setIncludeWellKnownLANs(
		boolean	include )
	{
		include_well_known_lans	= include;
	}
	
	public boolean
	getIncludeWellKnownLANs()
	{
		return( include_well_known_lans );
	}
	
	public boolean
	addInstance(
		InetAddress			explicit_address )
	{
		return( addInstanceSupport( explicit_address, true ));
	}
	
	protected boolean
	addInstanceSupport(
		InetAddress			explicit_address,
		boolean				force_send_alive )
	{
		final InetSocketAddress	sad = new InetSocketAddress( explicit_address, MC_GROUP_PORT );
		
		boolean	new_peer = false;
		
		if ( !explicit_peers.contains( sad )){
			
			try{
				this_mon.enter();
	
				List	new_peers = new ArrayList( explicit_peers );
				
				new_peers.add( sad );
				
				explicit_peers	= new_peers;
				
			}finally{
				
				this_mon.exit();
			}
							
			new_peer = true;

		}
		
		if ( force_send_alive || new_peer ){
			
				// take this off the current thread as there are potential deadlock issues
				// regarding this during initialisation as sending the event attempts to
				// get the external address, this may hit DHT and the current thread
				// maybe initialising the DHT...
			
			new DelayedEvent(
					"AZInstanceManagerImpl:delaySendAlive", 
					0,
					new AERunnable()
					{
						public void 
						runSupport()
						{
							sendAlive( sad );
						}
					});
		}
		
		return( new_peer );
	}
	
	public boolean
	isExternalAddress(
		InetAddress			address )
	{
		return( ext_addresses.contains( address ));
	}
	
	public AZInstanceTracked[]
	track(
		Download		download )
	{
		if ( mc_group == null || download.getTorrent() == null || getOtherInstances().length == 0 ){
			
			return( new AZInstanceTracked[0]);
		}
		
		Map	body = new HashMap();
		
		body.put( "hash", new SHA1Simple().calculateHash(download.getTorrent().getHash()));
		
		body.put( "seed", new Long( download.isComplete()?1:0 ));
		
		Map	replies = sendRequest( MT_REQUEST_TRACK, body ); 
				
		AZInstanceTracked[]	res = new AZInstanceTracked[replies.size()];
		
		Iterator	it = replies.entrySet().iterator();
		
		int	pos = 0;
		
		while( it.hasNext()){
			
			Map.Entry	entry = (Map.Entry)it.next();
			
			AZInstance	inst 	= (AZInstance)entry.getKey();
			Map			reply	= (Map)entry.getValue();
	
			boolean	seed = ((Long)reply.get( "seed" )).intValue() == 1;
	
			res[ pos++ ] = new trackedInstance( inst, download, seed );
		}
		
		return( res );
	}
	
	protected void
	checkTimeouts()
	{
		long	now = SystemTime.getCurrentTime();
	
		List	removed = new ArrayList();
		
		try{
			this_mon.enter();

			Iterator	it = other_instances.values().iterator();
			
			while( it.hasNext()){
				
				AZOtherInstanceImpl	inst = (AZOtherInstanceImpl)it.next();
	
				if ( now - inst.getAliveTime() > ALIVE_PERIOD * 2.5 ){
					
					removed.add( inst );
					
					it.remove();
				}
			}
		}finally{
			
			this_mon.exit();
		}
		
		for (int i=0;i<removed.size();i++){
			
			AZOtherInstanceImpl	inst = (AZOtherInstanceImpl)removed.get(i);
			
			informRemoved( inst );
		}
	}
	
	protected void
	informRemoved(
		AZOtherInstanceImpl	inst )
	{
		removeAddresses( inst );
		
		for (int i=0;i<listeners.size();i++){
			
			try{
				((AZInstanceManagerListener)listeners.get(i)).instanceLost( inst );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}
	
	protected void
	informAdded(
		AZInstance	inst )
	{
		addAddresses( inst );

		for (int i=0;i<listeners.size();i++){
			
			try{
				((AZInstanceManagerListener)listeners.get(i)).instanceFound( inst );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}
	
	protected void
	informChanged(
		AZInstance	inst )
	{
		addAddresses( inst );
		
		if ( inst == my_instance ){
			
			sendAlive();
		}
		
		for (int i=0;i<listeners.size();i++){
			
			try{
				((AZInstanceManagerListener)listeners.get(i)).instanceChanged( inst );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}
	
	protected void
	informTracked(
		AZInstanceTracked	inst )
	{
		for (int i=0;i<listeners.size();i++){
			
			try{
				((AZInstanceManagerListener)listeners.get(i)).instanceTracked( inst );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}
	
	protected Map
	sendRequest(
		int		type )
	{
		return( new request( type, new HashMap()).getReplies());
	}
	
	protected Map
	sendRequest(
		int		type,
		Map		body )
	{
		return( new request( type, body ).getReplies());
	}
	
	protected class
	request
	{
		private long	id;
		
		private Set	reply_instances	= new HashSet();
		
		private Map	replies			= new HashMap();
		
		protected
		request(
			int			type,
			Map			body  )
		{
			try{
				this_mon.enter();

				id	= search_id_next++;
						
				requests.add( this );
	
			}finally{
				
				this_mon.exit();
			}
			
			body.put( "type", new Long( type ));
			
			body.put( "rid", new Long( id ));
			
			sendMessage( MT_REQUEST, body );
		}
		
		protected long
		getID()
		{
			return( id );
		}
		
		protected void
		addReply(
			AZInstance	instance,
			Map			body )
		{
			try{
				this_mon.enter();
				
				if ( !reply_instances.contains( instance.getID())){
						
					reply_instances.add( instance.getID());
					
					replies.put( instance, body );
				}
						
			}finally{
				
				this_mon.exit();
			}
		}
		
		protected Map
		getReplies()
		{
			try{
				Thread.sleep( 2500 );
				
			}catch( Throwable e ){
				
			}
			
			try{
				this_mon.enter();

				requests.remove( this );
				
				return( replies );	
				
			}finally{
				
				this_mon.exit();
			}
		}
	}

	public void
	addListener(
		AZInstanceManagerListener	l )
	{
		listeners.add( l );
	}
	
	public void
	removeListener(
		AZInstanceManagerListener	l )
	{
		listeners.remove( l );
	}
	
	protected static class
	trackedInstance
		implements AZInstanceTracked
	{
		private AZInstance		instance;
		private Download		download;
		private boolean			seed;
		
		protected
		trackedInstance(
			AZInstance		_instance,
			Download		_download,
			boolean			_seed )
		{
			instance		= _instance;
			download		= _download;
			seed			= _seed;
		}
		public AZInstance
		getInstance()
		{
			return( instance );
		}
		
		public Download
		getDownload()
		{
			return( download );
		}
		
		public boolean
		isSeed()
		{
			return( seed );
		}
	}
}
