/*
 * Created on 21-Jan-2005
 * 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.dht.transport.udp.impl;

import java.io.*;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.*;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.ipfilter.IpFilter;
import org.gudy.azureus2.core3.ipfilter.IpFilterManagerFactory;
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.AEThread2;
import org.gudy.azureus2.core3.util.Average;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.DelayedEvent;
import org.gudy.azureus2.core3.util.RandomUtils;
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 com.aelitis.azureus.core.dht.DHT;
import com.aelitis.azureus.core.dht.DHTLogger;
import com.aelitis.azureus.core.dht.netcoords.DHTNetworkPosition;
import com.aelitis.azureus.core.dht.netcoords.DHTNetworkPositionManager;
import com.aelitis.azureus.core.dht.netcoords.DHTNetworkPositionProvider;
import com.aelitis.azureus.core.dht.netcoords.DHTNetworkPositionProviderListener;
import com.aelitis.azureus.core.dht.transport.*;
import com.aelitis.azureus.core.dht.transport.udp.*;
import com.aelitis.azureus.core.dht.transport.udp.impl.packethandler.DHTUDPPacketHandler;
import com.aelitis.azureus.core.dht.transport.udp.impl.packethandler.DHTUDPPacketHandlerException;
import com.aelitis.azureus.core.dht.transport.udp.impl.packethandler.DHTUDPPacketHandlerFactory;
import com.aelitis.azureus.core.dht.transport.udp.impl.packethandler.DHTUDPPacketHandlerStub;
import com.aelitis.azureus.core.dht.transport.udp.impl.packethandler.DHTUDPPacketReceiver;
import com.aelitis.azureus.core.dht.transport.udp.impl.packethandler.DHTUDPRequestHandler;
import com.aelitis.azureus.core.dht.transport.util.DHTTransferHandler;
import com.aelitis.azureus.core.dht.transport.util.DHTTransportRequestCounter;
import com.aelitis.azureus.core.dht.transport.util.DHTTransferHandler.Packet;
import com.aelitis.azureus.core.util.average.AverageFactory;
import com.aelitis.azureus.core.util.average.MovingImmediateAverage;
import com.aelitis.azureus.core.util.bloom.BloomFilter;
import com.aelitis.azureus.core.util.bloom.BloomFilterFactory;
import com.aelitis.azureus.core.versioncheck.VersionCheckClient;
import com.aelitis.net.udp.uc.PRUDPPacketHandler;

/**
 * @author parg
 *
 */

public class 
DHTTransportUDPImpl 
	implements DHTTransportUDP, DHTUDPRequestHandler
{
	public static boolean TEST_EXTERNAL_IP	= false;
		
	public static final int		MIN_ADDRESS_CHANGE_PERIOD_INIT_DEFAULT	= 5*60*1000;
	public static final int		MIN_ADDRESS_CHANGE_PERIOD_NEXT_DEFAULT	= 10*60*1000;
	
	public static final int		STORE_TIMEOUT_MULTIPLIER = 2;
		
	
	private String				external_address;
	private int					min_address_change_period = MIN_ADDRESS_CHANGE_PERIOD_INIT_DEFAULT;
	
	private byte				protocol_version;
	private int					network;
	private boolean				v6;
	private String				ip_override;
	private int					port;
	private int					max_fails_for_live;
	private int					max_fails_for_unknown;
	private long				request_timeout;
	private long				store_timeout;
	private boolean				reachable;
	private boolean				reachable_accurate;
	private int					dht_send_delay;
	private int					dht_receive_delay;

	private DHTLogger			logger;
		
	private DHTUDPPacketHandler			packet_handler;
	
	private DHTTransportRequestHandler	request_handler;
	
	private DHTTransportUDPContactImpl		local_contact;
		
	private long last_address_change;
	
	private List listeners	= new ArrayList();
	
	private IpFilter	ip_filter	= IpFilterManagerFactory.getSingleton().getIPFilter();

	
	private DHTTransportUDPStatsImpl	stats;

	private boolean		bootstrap_node	= false;
	
	private byte		generic_flags	= DHTTransportUDP.GF_NONE;
	private byte		generic_flags2	= VersionCheckClient.getSingleton().getDHTFlags();
	
	private static final int CONTACT_HISTORY_MAX 		= 32;
	private static final int CONTACT_HISTORY_PING_SIZE	= 24;
	
	private Map<InetSocketAddress,DHTTransportContact>	contact_history = 
		new LinkedHashMap<InetSocketAddress,DHTTransportContact>(CONTACT_HISTORY_MAX,0.75f,true)
		{
			protected boolean 
			removeEldestEntry(
		   		Map.Entry<InetSocketAddress,DHTTransportContact> eldest) 
			{
				return size() > CONTACT_HISTORY_MAX;
			}
		};
		
	private static final int ROUTABLE_CONTACT_HISTORY_MAX 		= 128;

	private Map<InetSocketAddress,DHTTransportContact>	routable_contact_history = 
		new LinkedHashMap<InetSocketAddress,DHTTransportContact>(ROUTABLE_CONTACT_HISTORY_MAX,0.75f,true)
		{
			protected boolean 
			removeEldestEntry(
		   		Map.Entry<InetSocketAddress,DHTTransportContact> eldest) 
			{
				return size() > ROUTABLE_CONTACT_HISTORY_MAX;
			}
		};
			
		
	private long					other_routable_total;
	private long					other_non_routable_total;
	private MovingImmediateAverage	routeable_percentage_average = AverageFactory.MovingImmediateAverage(8);
	
	private static final int RECENT_REPORTS_HISTORY_MAX = 32;

	private Map	recent_reports = 
			new LinkedHashMap(RECENT_REPORTS_HISTORY_MAX,0.75f,true)
			{
				protected boolean 
				removeEldestEntry(
			   		Map.Entry eldest) 
				{
					return size() > RECENT_REPORTS_HISTORY_MAX;
				}
			};
		
			
	private static final int	STATS_PERIOD		= 60*1000;
	private static final int 	STATS_DURATION_SECS	= 600;			// 10 minute average
	private static final long	STATS_INIT_PERIOD	= 15*60*1000;	// bit more than 10 mins to allow average to establish
	
	private long	stats_start_time	= SystemTime.getCurrentTime();
	private long	last_alien_count;
	private long	last_alien_fv_count;
	
	private Average	alien_average 		= Average.getInstance(STATS_PERIOD,STATS_DURATION_SECS);
	private Average	alien_fv_average 	= Average.getInstance(STATS_PERIOD,STATS_DURATION_SECS);
		
	private Random				random;
	
	private static final int	BAD_IP_BLOOM_FILTER_SIZE	= 32000;
	private BloomFilter			bad_ip_bloom_filter;
	
	private static AEMonitor	class_mon	= new AEMonitor( "DHTTransportUDP:class" );
	
	private AEMonitor	this_mon	= new AEMonitor( "DHTTransportUDP" );

	private boolean		initial_address_change_deferred;
	private boolean		address_changing;
	
	private DHTTransferHandler xfer_handler;
	
	public
	DHTTransportUDPImpl(
		byte			_protocol_version,
		int				_network,
		boolean			_v6,
		String			_ip,
		String			_default_ip,
		int				_port,
		int				_max_fails_for_live,
		int				_max_fails_for_unknown,
		long			_timeout,
		int				_dht_send_delay,
		int				_dht_receive_delay,
		boolean			_bootstrap_node,
		boolean			_initial_reachability,
		DHTLogger		_logger )
	
		throws DHTTransportException
	{
		protocol_version		= _protocol_version;
		network					= _network;
		v6						= _v6;
		ip_override				= _ip;
		port					= _port;
		max_fails_for_live		= _max_fails_for_live;
		max_fails_for_unknown	= _max_fails_for_unknown;
		request_timeout			= _timeout;
		dht_send_delay			= _dht_send_delay;
		dht_receive_delay		= _dht_receive_delay;
		bootstrap_node			= _bootstrap_node;
		reachable				= _initial_reachability;
		logger					= _logger;
				
		store_timeout			= request_timeout * STORE_TIMEOUT_MULTIPLIER;
		
		try{
			random = RandomUtils.SECURE_RANDOM;
			
		}catch( Throwable e ){
			
			random	= new Random();
			
			logger.log( e );
		}
		
		xfer_handler = 
			new DHTTransferHandler(
				new DHTTransferHandler.Adapter() {
					
					public void 
					sendRequest(
						DHTTransportContact 	_contact, 
						Packet 					packet	) 
					{
						DHTTransportUDPContactImpl	contact = (DHTTransportUDPContactImpl)_contact;
						
						DHTUDPPacketData	request = 
								new DHTUDPPacketData(
									DHTTransportUDPImpl.this, 
									packet.getConnectionId(), 
									local_contact, 
									contact );
						
						request.setDetails( 
							packet.getPacketType(), 
							packet.getTransferKey(), 
							packet.getRequestKey(), 
							packet.getData(), 
							packet.getStartPosition(),
							packet.getLength(),
							packet.getTotalLength());
								
						try{
							checkAddress( contact );
										
							stats.dataSent( request );
							
							packet_handler.send(
								request,
								contact.getTransportAddress());
							
						}catch( Throwable e ){
							
						}
					}
					
					public long 
					getConnectionID() 
					{
						return( DHTTransportUDPImpl.this.getConnectionID());
					}
				}, 
				DHTUDPPacketData.MAX_DATA_SIZE, 
				1,
				logger);
			
		int last_pct = COConfigurationManager.getIntParameter( "dht.udp.net" + network + ".routeable_pct", -1 );
		
		if ( last_pct > 0 ){
			
			routeable_percentage_average.update( last_pct );
		}
		
		DHTUDPUtils.registerTransport( this );
		
		createPacketHandler();
		
		SimpleTimer.addPeriodicEvent(
			"DHTUDP:stats",
			STATS_PERIOD,
			new TimerEventPerformer()
			{
				private int tick_count;
				
				public void
				perform(
					TimerEvent	event )
				{
					updateStats( tick_count++);
					
					checkAltContacts();
				}
			});
		
		String	default_ip = _default_ip==null?(v6?"::1":"127.0.0.1"):_default_ip;
				
		getExternalAddress( default_ip, logger );
		
		InetSocketAddress	address = new InetSocketAddress( external_address, port );

		DHTNetworkPositionManager.addProviderListener(
			new DHTNetworkPositionProviderListener()
			{
				public void
				providerAdded(
					DHTNetworkPositionProvider		provider )
				{
					if ( local_contact != null ){
						
						local_contact.createNetworkPositions( true );
						
						try{
							this_mon.enter();

							for ( DHTTransportContact c: contact_history.values()){
								
								c.createNetworkPositions( false );
							}

							for ( DHTTransportContact c: routable_contact_history.values()){
								
								c.createNetworkPositions( false );
							}
						}finally{
							
							this_mon.exit();
						}
						
						for (int i=0;i<listeners.size();i++){
							
							try{
								((DHTTransportListener)listeners.get(i)).resetNetworkPositions();
								
							}catch( Throwable e ){
								
								Debug.printStackTrace(e);
							}
						}
					}
				}
				
				public void
				providerRemoved(
					DHTNetworkPositionProvider		provider )
				{				
				}
			});
		
		logger.log( "Initial external address: " + address );
		
		local_contact = new DHTTransportUDPContactImpl( true, this, address, address, protocol_version, random.nextInt(), 0, (byte)0 );
	}
	
	protected void
	createPacketHandler()
	
		throws DHTTransportException
	{
		DHTUDPPacketHelper.registerCodecs();

		// DHTPRUDPPacket relies on the request-handler being an instanceof THIS so watch out
		// if you change it :)
	
		try{
			if ( packet_handler != null && !packet_handler.isDestroyed()){
				
				packet_handler.destroy();
			}
			
			packet_handler = DHTUDPPacketHandlerFactory.getHandler( this, this );
			
		}catch( Throwable e ){
			
			throw( new DHTTransportException( "Failed to get packet handler", e ));
		}
	
			// limit send and receive rates. Receive rate is lower as we want a stricter limit
			// on the max speed we generate packets than those we're willing to process.
		
		// logger.log( "send delay = " + _dht_send_delay + ", recv = " + _dht_receive_delay );
		
		packet_handler.setDelays( dht_send_delay, dht_receive_delay, (int)request_timeout );
		
		stats_start_time	= SystemTime.getCurrentTime();
		
		if ( stats == null ){
			
			stats =  new DHTTransportUDPStatsImpl( this, protocol_version, packet_handler.getStats());
			
		}else{
			
			stats.setStats( packet_handler.getStats());
		}
	}
	
	public DHTUDPRequestHandler 
	getRequestHandler() 
	{
		return( this );
	}
	
	public DHTUDPPacketHandler
	getPacketHandler()
	{
		return( packet_handler );
	}
	
	public void
	setSuspended(
		boolean			susp )
	{
		if ( susp ){
			
			if ( packet_handler != null ){
				
				packet_handler.destroy();
			}
		}else{
			if ( packet_handler == null || packet_handler.isDestroyed()){
				
				try{
					createPacketHandler();
					
				}catch( Throwable e ){
					
					Debug.out( e );
				}
			}
		}
	}
	
	protected void
	updateStats(
		int	tick_count )
	{
			// pick up latest value
		
		generic_flags2	= VersionCheckClient.getSingleton().getDHTFlags();
		
		long	alien_count	= 0;
		
		long[]	aliens = stats.getAliens();
		
		for (int i=0;i<aliens.length;i++){
			
			alien_count	+= aliens[i];
		}
		
		long	alien_fv_count = aliens[ DHTTransportStats.AT_FIND_VALUE ];
		
		alien_average.addValue( (alien_count-last_alien_count)*STATS_PERIOD/1000);
		alien_fv_average.addValue( (alien_fv_count-last_alien_fv_count)*STATS_PERIOD/1000);

		last_alien_count	= alien_count;
		last_alien_fv_count	= alien_fv_count;
		
		long	now = SystemTime.getCurrentTime();
		
		if ( now < 	stats_start_time ){
			
			stats_start_time	= now;
			
		}else{
			
				// only fiddle with the initial view of reachability when things have had
				// time to stabilise
			
			if ( Constants.isCVSVersion()){
							
				long fv_average 		= alien_fv_average.getAverage();
				long all_average 		= alien_average.getAverage();
			
				logger.log( "Aliens for net " + network + ": " + fv_average + "/" + all_average );
			}
			
			if ( now - stats_start_time > STATS_INIT_PERIOD ){
				
				reachable_accurate	= true;
				
				boolean	old_reachable	= reachable;
				
				if ( alien_fv_average.getAverage() > 1 ){
					
					reachable	= true;
					
				}else if ( alien_average.getAverage() > 3 ){
					
					reachable	= true;
					
				}else{
					
					reachable	= false;
				}
				
				if ( old_reachable != reachable ){
					
					for (int i=0;i<listeners.size();i++){
						
						try{
							((DHTTransportListener)listeners.get(i)).reachabilityChanged( reachable );
							
						}catch( Throwable e ){
							
							Debug.printStackTrace(e);
						}
					}	
				}
			}
		}
		
		int	pct = getRouteablePercentage();
			
		if ( pct > 0 ){
			
			COConfigurationManager.setParameter("dht.udp.net" + network + ".routeable_pct", pct );
		}
		
		// System.out.println( "routables=" + other_routable_total + ", non=" + other_non_routable_total );
		
		// System.out.println( "net " + network + ": aliens = " + alien_average.getAverage() + ", alien fv = " + alien_fv_average.getAverage());
	}
	
	protected void
	recordSkew(
		InetSocketAddress	originator_address,
		long				skew )
	{
		if ( stats != null ){
		
			stats.recordSkew( originator_address, skew );
		}
	}
	
	protected int
	getNodeStatus()
	{
		if ( bootstrap_node ){
		
				// bootstrap node is special case and not generally routable
			
			return( 0 );
		}
		
		if ( reachable_accurate ){
			
			int	status = reachable?DHTTransportUDPContactImpl.NODE_STATUS_ROUTABLE:0;
			
			return( status );
			
		}else{
			
			return( DHTTransportUDPContactImpl.NODE_STATUS_UNKNOWN );
		}
	}
	
	public boolean
	isReachable()
	{
		return( reachable );
	}
	
	public byte
	getProtocolVersion()
	{
		return( protocol_version );
	}
	
	public byte
	getMinimumProtocolVersion()
	{
		return( getNetwork()==DHT.NW_CVS?DHTTransportUDP.PROTOCOL_VERSION_MIN_CVS:DHTTransportUDP.PROTOCOL_VERSION_MIN );
	}
	
	public int
	getPort()
	{
		return( port );
	}
	
	public void
	setPort(
		int	new_port )
	
		throws DHTTransportException
	{
		if ( new_port == port ){
			
			return;
		}
		
		port	= new_port;
		
		createPacketHandler();
		
		setLocalContact();
	}
	
	public long
	getTimeout()
	{
		return( request_timeout );
	}
	
	public void
	setTimeout(
		long		timeout )
	{
		if ( request_timeout == timeout ){
			
			return;
		}
		
		request_timeout = timeout;
		store_timeout   = request_timeout * STORE_TIMEOUT_MULTIPLIER;
		
		packet_handler.setDelays( dht_send_delay, dht_receive_delay, (int)request_timeout );
	}
	
	public int
	getNetwork()
	{
		return( network );
	}
	
	public byte
	getGenericFlags()
	{
		return( generic_flags );
	}
	
	public byte
	getGenericFlags2()
	{
		return( generic_flags2 );
	}
	
	public void
	setGenericFlag(
		byte		flag,
		boolean		value )
	{
		synchronized( this ){
			
			if ( value ){
				
				generic_flags |= flag;
				
			}else{
				
				generic_flags &= ~flag;
			}
		}
	}
	
	public boolean 
	isIPV6() 
	{
		return( v6 );
	}
	
	public void
	testInstanceIDChange()
	
		throws DHTTransportException
	{
		local_contact = new DHTTransportUDPContactImpl( true, this, local_contact.getTransportAddress(), local_contact.getExternalAddress(), protocol_version, random.nextInt(), 0, (byte)0 );		
	}
	
	public void
	testTransportIDChange()
	
		throws DHTTransportException
	{
		if ( external_address.equals("127.0.0.1")){
			
			external_address = "192.168.0.2";
		}else{
			
			external_address = "127.0.0.1";
		}
		
		InetSocketAddress	address = new InetSocketAddress( external_address, port );
		
		local_contact = new DHTTransportUDPContactImpl( true, this, address, address, protocol_version, local_contact.getInstanceID(), 0, (byte)0 );		

		for (int i=0;i<listeners.size();i++){
			
			try{
				((DHTTransportListener)listeners.get(i)).localContactChanged( local_contact );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}
	
	public void
	testExternalAddressChange()
	{
		try{
			Iterator	it = contact_history.values().iterator();
			
			DHTTransportUDPContactImpl c1 = (DHTTransportUDPContactImpl)it.next();
			DHTTransportUDPContactImpl c2 = (DHTTransportUDPContactImpl)it.next();
			
			externalAddressChange( c1, c2.getExternalAddress(), true );
			//externalAddressChange( c, new InetSocketAddress( "192.168.0.7", 6881 ));
			
		}catch( Throwable e ){
			
			Debug.printStackTrace(e);
		}
	}
	
	public void
	testNetworkAlive(
		boolean		alive )
	{
		packet_handler.testNetworkAlive( alive );
	}
	
	protected void
	getExternalAddress(
		String				default_address,
		final DHTLogger		log )
	{
			// class level synchronisation is for testing purposes when running multiple UDP instances
			// in the same VM
		
		try{
			class_mon.enter();
			
			String new_external_address = null;
			
			try{				
				log.log( "Obtaining external address" );
				
				if ( TEST_EXTERNAL_IP ){
					
					new_external_address	= v6?"::1":"127.0.0.1";
					
					log.log( "    External IP address obtained from test data: " + new_external_address );
				}
					
				if ( ip_override != null ){
					
					new_external_address	= ip_override;
					
					log.log( "    External IP address explicitly overridden: " + new_external_address );
				}
				
				if ( new_external_address == null ){

						// First attempt is via other contacts we know about. Select three
					
					List	contacts;
					
					try{
						this_mon.enter();
						
						contacts = new ArrayList( contact_history.values());
						
					}finally{
						
						this_mon.exit();
					}
					
						// randomly select a number of entries to ping until we 
						// get three replies
					
					String	returned_address 	= null;
					int		returned_matches	= 0;
					
					int		search_lim = Math.min( CONTACT_HISTORY_PING_SIZE, contacts.size());
					
					log.log( "    Contacts to search = " + search_lim );
					
					for (int i=0;i<search_lim;i++){
						
						DHTTransportUDPContactImpl	contact = (DHTTransportUDPContactImpl)contacts.remove(RandomUtils.nextInt(contacts.size()));
													
						InetSocketAddress a = askContactForExternalAddress( contact );
						
						if ( a != null && a.getAddress() != null ){
							
							String	ip = a.getAddress().getHostAddress();
							
							if ( returned_address == null ){
								
								returned_address = ip;
																	
								log.log( "    : contact " + contact.getString() + " reported external address as '" + ip + "'" );
								
								returned_matches++;
								
							}else if ( returned_address.equals( ip )){
								
								returned_matches++;
								
								log.log( "    : contact " + contact.getString() + " also reported external address as '" + ip + "'" );
								
								if ( returned_matches == 3 ){
									
									new_external_address	= returned_address;
									
									log.log( "    External IP address obtained from contacts: "  + returned_address );
									
									break;
								}
							}else{
								
								log.log( "    : contact " + contact.getString() + " reported external address as '" + ip + "', abandoning due to mismatch" );
								
									// mismatch - give up
								
								break;
							}
						}else{
							
							log.log( "    : contact " + contact.getString() + " didn't reply" );
						}
					}

				}
				
				if ( new_external_address == null ){
			
					InetAddress public_address = logger.getPluginInterface().getUtilities().getPublicAddress( v6 );
											
					if ( public_address != null ){
							
						new_external_address = public_address.getHostAddress();
						
						log.log( "    External IP address obtained: " + new_external_address );
					}
				}
				
			}catch( Throwable e ){
				
				Debug.printStackTrace( e );
			}
		
			if ( new_external_address == null ){
			
				new_external_address =	default_address;
			
				log.log( "    External IP address defaulted:  " + new_external_address );
			}
			
			if ( external_address == null || !external_address.equals( new_external_address )){
				
				informLocalAddress( new_external_address );
			}
			
			external_address = new_external_address;
			
		}finally{
			
			class_mon.exit();
		}
	}
	
	protected void
	informLocalAddress(
		String	address )
	{
		for (int i=0;i<listeners.size();i++){
			
			try{
				((DHTTransportListener)listeners.get(i)).currentAddress( address );
				
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}		
	}
		
	protected void
	externalAddressChange(
		final DHTTransportUDPContactImpl	reporter,
		final InetSocketAddress				new_address,
		boolean								force )
	
		throws DHTTransportException
	{
			/*
			 * A node has reported that our external address and the one he's seen a 
			 * message coming from differ. Natural explanations are along the lines of
			 * 1) my address is dynamically allocated by my ISP and it has changed
			 * 2) I have multiple network interfaces 
			 * 3) there's some kind of proxy going on
			 * 4) this is a DOS attempting to stuff me up
			 * 
			 * We assume that our address won't change more frequently than once every
			 * 5 minutes
			 * We assume that if we can successfully obtain an external address by
			 * using the above explicit check then this is accurate
			 * Only in the case where the above check fails do we believe the address
			 * that we've been told about
			 */
			
		InetAddress	ia = new_address.getAddress();
		
		if ( ia == null ){
			
			Debug.out( "reported new external address '" + new_address + "' is unresolved" );
			
			throw( new DHTTransportException( "Address '" + new_address + "' is unresolved" ));
		}
		
			// dump addresses incompatible with our protocol
		
		if ( 	( ia instanceof Inet4Address && v6  ) ||
				( ia instanceof Inet6Address && !v6 )){
			
				// reduce debug spam, just return
			
			// throw( new DHTTransportException( "Address " + new_address + " is incompatible with protocol family for " + external_address ));
			
			return;
		}
		
		final String	new_ip = ia.getHostAddress();
		
		if ( new_ip.equals( external_address )){
			
				// probably just be a second notification of an address change, return
				// "ok to retry" as it should now work
							
			return;
		}
		
		try{
			this_mon.enter();
	
			long	now = SystemTime.getCurrentTime();
	
			if ( now - last_address_change < min_address_change_period ){
				
				return;
			}
						
			if ( contact_history.size() < CONTACT_HISTORY_MAX && !force ){
				
				if ( !initial_address_change_deferred ){
					
					initial_address_change_deferred = true;
					
					logger.log( "Node " + reporter.getString() + " has reported that the external IP address is '" + new_address + "': deferring new checks" );

					new DelayedEvent(
						"DHTTransportUDP:delayAC",
						30*1000,
						new AERunnable()
						{
							public void
							runSupport()
							{
								try{
									externalAddressChange( reporter, new_address, true );
									
								}catch( Throwable e ){
									
								}
							}
						});
				}
				
				return;
			}
			
			logger.log( "Node " + reporter.getString() + " has reported that the external IP address is '" + new_address + "'" );
	
				// check for dodgy addresses that shouldn't appear as an external address!
			
			if ( invalidExternalAddress( ia )){
				
				logger.log( "     This is invalid as it is a private address." );
	
				return;
			}
	
				// another situation to ignore is where the reported address is the same as
				// the reporter (they must be seeing it via, say, socks connection on a local
				// interface
			
			if ( reporter.getExternalAddress().getAddress().getHostAddress().equals( new_ip )){
				
				logger.log( "     This is invalid as it is the same as the reporter's address." );
	
				return;		
			}
			
			last_address_change	= now;
			
				// bump up min period for subsequent changes 
			
			if ( min_address_change_period == MIN_ADDRESS_CHANGE_PERIOD_INIT_DEFAULT ){
				
				min_address_change_period = MIN_ADDRESS_CHANGE_PERIOD_NEXT_DEFAULT;
			}
		}finally{
			
			this_mon.exit();
		}
		
		final String	old_external_address = external_address;
		
			// we need to perform this test on a separate thread otherwise we'll block in the UDP handling
			// code because we're already running on the "process" callback from the UDP handler
			// (the test attempts to ping contacts)
			
		
		new AEThread2( "DHTTransportUDP:getAddress", true )
		{
			public void
			run()
			{
				try{
					this_mon.enter();

					if ( address_changing ){
						
						return;
					}
					
					address_changing	= true;
					
				}finally{
					
					this_mon.exit();
				}
					
				try{
					getExternalAddress( new_ip, logger );
					
					if ( old_external_address.equals( external_address )){
						
							// address hasn't changed, notifier must be perceiving different address
							// due to proxy or something
										
						return;
					}
					
					setLocalContact();
					
				}finally{
					
					try{
						this_mon.enter();

						address_changing	= false;
						
					}finally{
						
						this_mon.exit();
					}
				}
			}
		}.start();
	}
	
	protected void
	contactAlive(
		DHTTransportUDPContactImpl	contact )
	{
		try{
			this_mon.enter();
			
			contact_history.put( contact.getTransportAddress(), contact );
			
		}finally{
			
			this_mon.exit();
		}
	}
	
	public DHTTransportContact[]
   	getReachableContacts()
   	{
		try{
			this_mon.enter();
			
			Collection<DHTTransportContact> vals = routable_contact_history.values();
			
			DHTTransportContact[]	res = new DHTTransportContact[vals.size()];
			
			vals.toArray( res );
			
			return( res );
			
		}finally{
			
			this_mon.exit();
		}
   	}
	
	public DHTTransportContact[]
   	getRecentContacts()
   	{
		try{
			this_mon.enter();
			
			Collection<DHTTransportContact> vals = contact_history.values();
			
			DHTTransportContact[]	res = new DHTTransportContact[vals.size()];
			
			vals.toArray( res );
			
			return( res );
			
		}finally{
			
			this_mon.exit();
		}
   	}
	
	protected void
	updateContactStatus(
		DHTTransportUDPContactImpl	contact,
		int							status,
		boolean						incoming )
	{
		try{
			this_mon.enter();
				
			contact.setNodeStatus( status );

			if ( contact.getProtocolVersion() >= DHTTransportUDP.PROTOCOL_VERSION_XFER_STATUS ){
								
				if ( status != DHTTransportUDPContactImpl.NODE_STATUS_UNKNOWN ){
					
					boolean	other_routable = (status & DHTTransportUDPContactImpl.NODE_STATUS_ROUTABLE) != 0;
					
						// only maintain stats on incoming requests so we get a fair sample. 
						// in general we'll only get replies from routable contacts so if we
						// take this into account then everything gets skewed
					
					if ( other_routable ){
						
						if ( incoming ){
							
							synchronized( routeable_percentage_average ){
								
								other_routable_total++;
							}
						}
												
						routable_contact_history.put( contact.getTransportAddress(), contact );
						
					}else{
						
						if ( incoming ){
							
							synchronized( routeable_percentage_average ){
							
								other_non_routable_total++;
							}
						}
					}
				}
			}
			
		}finally{
			
			this_mon.exit();
		}
	}
	
	public int 
	getRouteablePercentage() 
	{
		synchronized( routeable_percentage_average ){
		
			double	average = routeable_percentage_average.getAverage();
			
			long	both_total = other_routable_total + other_non_routable_total;
			
			int	current_percent;
			
			if ( both_total == 0 ){
				
				current_percent = 0;
				
			}else{
				
				current_percent = (int)(( other_routable_total * 100 )/ both_total );
			}
			 
			if ( both_total >= 300 ){
				
					// add current to average and reset
				
				if ( current_percent > 0 ){
				
					average = routeable_percentage_average.update( current_percent );
					
					other_routable_total = other_non_routable_total = 0;
				}
			}else if ( both_total >= 100 ){
				
					// if we have enough samples and no existing average then use current
				
				if ( average == 0 ){
					
					average = current_percent;
					
				}else{
					
						// factor in current percantage
					
					int samples = routeable_percentage_average.getSampleCount();
					
					if ( samples > 0 ){
						
						average = (( samples * average ) + current_percent ) / ( samples + 1 );
					}
				}
			}
			
			int result = (int)average;
			
			if ( result == 0 ){
				
					// -1 indicates we have no usable value
				
				result = -1;
			}
			
			return( result );
		}
	}
	
	protected boolean
	invalidExternalAddress(
		InetAddress	ia )
	{
		return(	ia.isLinkLocalAddress() ||
				ia.isLoopbackAddress() ||
				ia.isSiteLocalAddress()); 
	}
	
	protected int
	getMaxFailForLiveCount()
	{
		return( max_fails_for_live );
	}
	
	protected int
	getMaxFailForUnknownCount()
	{
		return( max_fails_for_unknown );
	}
	
	public DHTTransportContact
	getLocalContact()
	{
		return( local_contact );
	}
	
	protected void
	setLocalContact()
	{
		InetSocketAddress	s_address = new InetSocketAddress( external_address, port );
		
		try{
			local_contact = new DHTTransportUDPContactImpl( true, DHTTransportUDPImpl.this, s_address, s_address, protocol_version, random.nextInt(), 0, (byte)0 );
	
			logger.log( "External address changed: " + s_address );
			
			Debug.out( "DHTTransport: address changed to " + s_address );
			
			for (int i=0;i<listeners.size();i++){
				
				try{
					((DHTTransportListener)listeners.get(i)).localContactChanged( local_contact );
					
				}catch( Throwable e ){
					
					Debug.printStackTrace(e);
				}
			}
		}catch( Throwable e ){
			
			Debug.printStackTrace(e);
		}
	}
	
	public DHTTransportContact
	importContact(
		DataInputStream		is,
		boolean				is_bootstrap )
	
		throws IOException, DHTTransportException
	{
		DHTTransportUDPContactImpl	contact = DHTUDPUtils.deserialiseContact( this, is );
				
		importContact( contact, is_bootstrap );
			
		return( contact );
	}
	
	public DHTTransportUDPContact
	importContact(
		InetSocketAddress	_address,
		byte				_protocol_version,
		boolean				is_bootstrap )
	
		throws DHTTransportException
	{
			// instance id of 0 means "unknown"
		
		DHTTransportUDPContactImpl	contact = new DHTTransportUDPContactImpl( false, this, _address, _address, _protocol_version, 0, 0, (byte)0 );
		
		importContact( contact, is_bootstrap );

		return( contact );
	}
	
	protected void
	importContact(
		DHTTransportUDPContactImpl	contact,
		boolean						is_bootstrap )
	{
		try{
			this_mon.enter();
			
				// consider newly imported contacts as potential contacts for IP address queries if we've
				// got space (in particular, at start of day we may be able to get an address off these if
				// they're still alive )
			
			if ( contact_history.size() < CONTACT_HISTORY_MAX ){
				
				contact_history.put( contact.getTransportAddress(), contact );
			}
			
		}finally{
			
			this_mon.exit();
		}
		
		request_handler.contactImported( contact, is_bootstrap );
		
		// logger.log( "Imported contact " + contact.getString());
	}
	
	public void
	exportContact(
		DHTTransportContact	contact,
		DataOutputStream	os )
	
		throws IOException, DHTTransportException
	{
		DHTUDPUtils.serialiseContact( os, contact );
	}
	
	public Map<String,Object>
	exportContactToMap(
		DHTTransportContact	contact )
	{
		Map<String,Object>		result = new HashMap<String, Object>();
		
		result.put( "v",contact.getProtocolVersion());
		
		InetSocketAddress address = contact.getExternalAddress();
		
		result.put( "p", address.getPort());
		
		InetAddress	ia = address.getAddress();
		
		if ( ia == null ){
			
	
			result.put( "h", address.getHostName());
			
		}else{
		
			result.put( "a", ia.getAddress());
		}
		
		return( result );
	}
	
	public DHTTransportUDPContact
	importContact(
		Map<String,Object>		map )
	{
		int version = ((Number)map.get( "v" )).intValue();
		
		int port = ((Number)map.get( "p" )).intValue();
		
		byte[]	a = (byte[])map.get( "a" );
				
		InetSocketAddress address;
		
		try{
			if ( a == null ){
				
				address = InetSocketAddress.createUnresolved( new String((byte[])map.get("h"), "UTF-8" ), port );
			}else{
			
				address = new InetSocketAddress( InetAddress.getByAddress( a ), port );
			}
			
			DHTTransportUDPContactImpl contact = new DHTTransportUDPContactImpl( false, this, address, address, (byte)version, 0, 0, (byte)0 );
			
			importContact( contact, false );
				
			return( contact );
			
		}catch( Throwable e ){
			
			Debug.out( e );
			
			return( null );
		}
	}
	
	public void
	removeContact(
		DHTTransportContact	contact )
	{
		request_handler.contactRemoved( contact );
	}
	
	public void
	setRequestHandler(
		DHTTransportRequestHandler	_request_handler )
	{
		request_handler = new DHTTransportRequestCounter( _request_handler, stats );
	}
	
	public DHTTransportStats
	getStats()
	{
		return( stats );
	}
	
	//protected HashMap	port_map = new HashMap();
	//protected long		last_portmap_dump	= SystemTime.getCurrentTime();
	
	protected void
	checkAddress(
		DHTTransportUDPContactImpl		contact )
	
		throws DHTUDPPacketHandlerException
	{
		/*
		int	port = contact.getExternalAddress().getPort();
		
		try{
			this_mon.enter();
		
			int	count;
			
			Integer i = (Integer)port_map.get(new Integer(port));
			
			if ( i != null ){
				
				count 	= i.intValue() + 1;
				
			}else{
				
				count	= 1;
			}
			
			port_map.put( new Integer(port), new Integer(count));
			
			long	now = SystemTime.getCurrentTime();
			
			if ( now - last_portmap_dump > 60000 ){
				
				last_portmap_dump	= now;
				
				Iterator	it = port_map.keySet().iterator();
				
				Map	rev = new TreeMap();
				
				while( it.hasNext()){
					
					Integer	key = (Integer)it.next();
					
					Integer	val = (Integer)port_map.get(key);
					
					rev.put( val, key );
				}
				
				it = rev.keySet().iterator();
				
				while( it.hasNext()){
					
					Integer	val = (Integer)it.next();
					
					Integer	key = (Integer)rev.get(val);
					
					System.out.println( "port:" + key + "->" + val );
				}
			}
			
		}finally{
			
			this_mon.exit();
		}
		*/
		
		if ( ip_filter.isEnabled()){
			
				// don't need to synchronize access to the bloom filter as it works fine
				// without protection (especially as its add only)
			
			InetAddress ia = contact.getTransportAddress().getAddress();
			
			if ( ia != null ){
				
					// allow unresolved addresses through (e.g. ipv6 seed) as handled later
				
				byte[]	addr = ia.getAddress();
				
				if ( bad_ip_bloom_filter == null ){
					
					bad_ip_bloom_filter = BloomFilterFactory.createAddOnly( BAD_IP_BLOOM_FILTER_SIZE );
					
				}else{
					
					if ( bad_ip_bloom_filter.contains( addr )){
						
						throw( new DHTUDPPacketHandlerException( "IPFilter check fails (repeat)" ));
					}
				}
				
				if ( ip_filter.isInRange( 
						contact.getTransportAddress().getAddress(), "DHT", null,
						logger.isEnabled( DHTLogger.LT_IP_FILTER ))){
					
						// don't let an attacker deliberately fill up our filter so we start
						// rejecting valid addresses
					
					if ( bad_ip_bloom_filter.getEntryCount() >= BAD_IP_BLOOM_FILTER_SIZE/10 ){
						
						bad_ip_bloom_filter = BloomFilterFactory.createAddOnly( BAD_IP_BLOOM_FILTER_SIZE );
					}
					
					bad_ip_bloom_filter.add( addr );
					
					throw( new DHTUDPPacketHandlerException( "IPFilter check fails" ));
				}
			}
		}
	}
	
	protected void
	sendPing(
		final DHTTransportUDPContactImpl	contact,
		final DHTTransportReplyHandler		handler,
		long								timeout,
		int									priority )
	{
		try{
			checkAddress( contact );
			
			final long	connection_id = getConnectionID();			

			final DHTUDPPacketRequestPing	request = 
				new DHTUDPPacketRequestPing( this, connection_id, local_contact, contact );
				
			requestAltContacts( request );
			
			stats.pingSent( request );

			requestSendRequestProcessor( contact, request );
			
			packet_handler.sendAndReceive(
				request,
				contact.getTransportAddress(),
				new DHTUDPPacketReceiver()
				{
					public void
					packetReceived(
						DHTUDPPacketReply	packet,
						InetSocketAddress	from_address,
						long				elapsed_time )
					{
						try{							
							if ( packet.getConnectionId() != connection_id ){
								
								throw( new Exception( "connection id mismatch" ));
							}
							
							contact.setInstanceIDAndVersion( packet.getTargetInstanceID(), packet.getProtocolVersion());
							   
							requestSendReplyProcessor( contact, handler, packet, elapsed_time );							
								
							receiveAltContacts((DHTUDPPacketReplyPing)packet );
							
							stats.pingOK();
							
							handler.pingReply( contact, (int)elapsed_time );
						
						}catch( DHTUDPPacketHandlerException e ){
							
							error( e );
							
						}catch( Throwable e ){
							
							Debug.printStackTrace(e);
							
							error( new DHTUDPPacketHandlerException( "ping failed", e ));
						}
					}
					
					public void
					error(
						DHTUDPPacketHandlerException	e )
					{
						stats.pingFailed();
						
						handler.failed( contact,e );
					}
				},
				timeout, priority );
			
		}catch( Throwable e ){
			
			stats.pingFailed();
			
			handler.failed( contact,e );
		}
	}
	
	protected void
	sendPing(
		final DHTTransportUDPContactImpl	contact,
		final DHTTransportReplyHandler		handler )
	{
		sendPing( contact, handler, request_timeout, PRUDPPacketHandler.PRIORITY_MEDIUM );
	}

	protected void
	sendImmediatePing(
		final DHTTransportUDPContactImpl	contact,
		final DHTTransportReplyHandler		handler,
		long								timeout )
	{
		sendPing( contact, handler, timeout, PRUDPPacketHandler.PRIORITY_IMMEDIATE );
	}
	
	protected void
	sendKeyBlockRequest(
		final DHTTransportUDPContactImpl	contact,
		final DHTTransportReplyHandler		handler,
		byte[]								block_request,
		byte[]								block_signature )
	{
		try{
			checkAddress( contact );
			
			final long	connection_id = getConnectionID();			

			final DHTUDPPacketRequestKeyBlock	request = 
				new DHTUDPPacketRequestKeyBlock( this, connection_id, local_contact, contact );
			
			request.setKeyBlockDetails( block_request, block_signature );
			
			stats.keyBlockSent( request );

			request.setRandomID( contact.getRandomID());

			requestSendRequestProcessor( contact, request );
			
			packet_handler.sendAndReceive(
				request,
				contact.getTransportAddress(),
				new DHTUDPPacketReceiver()
				{
					public void
					packetReceived(
						DHTUDPPacketReply	packet,
						InetSocketAddress	from_address,
						long				elapsed_time )
					{
						try{							
							if ( packet.getConnectionId() != connection_id ){
								
								throw( new Exception( "connection id mismatch" ));
							}
							
							contact.setInstanceIDAndVersion( packet.getTargetInstanceID(), packet.getProtocolVersion());
							
							requestSendReplyProcessor( contact, handler, packet, elapsed_time );							
								
							stats.keyBlockOK();
							
							handler.keyBlockReply( contact );
						
						}catch( DHTUDPPacketHandlerException e ){
							
							error( e );
							
						}catch( Throwable e ){
							
							Debug.printStackTrace(e);
							
							error( new DHTUDPPacketHandlerException( "send key block failed", e ));
						}
					}
					
					public void
					error(
						DHTUDPPacketHandlerException	e )
					{
						stats.keyBlockFailed();
						
						handler.failed( contact,e );
					}
				},
				request_timeout, PRUDPPacketHandler.PRIORITY_MEDIUM );
			
		}catch( Throwable e ){
			
			stats.keyBlockFailed();
			
			handler.failed( contact,e );
		}
	}
	
		// stats
	
	protected void
	sendStats(
		final DHTTransportUDPContactImpl	contact,
		final DHTTransportReplyHandler		handler )
	{
		try{
			checkAddress( contact );
			
			final long	connection_id = getConnectionID();
			
			final DHTUDPPacketRequestStats	request = 
				new DHTUDPPacketRequestStats( this, connection_id, local_contact, contact );
				
			// request.setStatsType( DHTUDPPacketRequestStats.STATS_TYPE_NP_VER2 );
			
			stats.statsSent( request );

			requestSendRequestProcessor( contact, request );

			packet_handler.sendAndReceive(
				request,
				contact.getTransportAddress(),
				new DHTUDPPacketReceiver()
				{					
					public void
					packetReceived(
						DHTUDPPacketReply	packet,
						InetSocketAddress	from_address,
						long				elapsed_time )
					{
						try{							
							if ( packet.getConnectionId() != connection_id ){
								
								throw( new Exception( "connection id mismatch" ));
							}
							
							contact.setInstanceIDAndVersion( packet.getTargetInstanceID(), packet.getProtocolVersion());
							
							requestSendReplyProcessor( contact, handler, packet, elapsed_time );
										
							DHTUDPPacketReplyStats	reply = (DHTUDPPacketReplyStats)packet;

							stats.statsOK();
							
							if ( reply.getStatsType() == DHTUDPPacketRequestStats.STATS_TYPE_ORIGINAL ){
								
								handler.statsReply( contact, reply.getOriginalStats());
							}else{
								
								// currently no handler for new stats
								
								System.out.println( "new stats reply:" + reply.getString());
							}
						
						}catch( DHTUDPPacketHandlerException e ){
							
							error( e );
							
						}catch( Throwable e ){
							
							Debug.printStackTrace(e);
							
							error( new DHTUDPPacketHandlerException( "stats failed", e ));
						}
					}
					
					public void
					error(
						DHTUDPPacketHandlerException	e )
					{
						stats.statsFailed();
						
						handler.failed( contact, e );
					}
				},
				request_timeout, PRUDPPacketHandler.PRIORITY_LOW );
			
		}catch( Throwable e ){
			
			stats.statsFailed();
			
			handler.failed( contact, e );
		}
	}
	
		// PING for deducing external IP address
	
	protected InetSocketAddress
	askContactForExternalAddress(
		DHTTransportUDPContactImpl	contact )
	{
		try{
			checkAddress( contact );
		
			final long	connection_id = getConnectionID();
			
			final DHTUDPPacketRequestPing	request = 
					new DHTUDPPacketRequestPing( this, connection_id, local_contact, contact );
				
			stats.pingSent( request );

			final AESemaphore	sem = new AESemaphore( "DHTTransUDP:extping" );

			final InetSocketAddress[]	result = new InetSocketAddress[1];
			
			packet_handler.sendAndReceive(
				request,
				contact.getTransportAddress(),
				new DHTUDPPacketReceiver()
				{
					public void
					packetReceived(
						DHTUDPPacketReply	_packet,
						InetSocketAddress	from_address,
						long				elapsed_time )
					{
						try{
							
							if ( _packet instanceof DHTUDPPacketReplyPing ){
								
								// ping was OK so current address is OK
								
								result[0] = local_contact.getExternalAddress();
								
							}else if ( _packet instanceof DHTUDPPacketReplyError ){
								
								DHTUDPPacketReplyError	packet = (DHTUDPPacketReplyError)_packet;
								
								if ( packet.getErrorType() == DHTUDPPacketReplyError.ET_ORIGINATOR_ADDRESS_WRONG ){
									
									result[0] = packet.getOriginatingAddress();
								}
							}
						}finally{
							
							sem.release();
						}
					}
					
					public void
					error(
						DHTUDPPacketHandlerException	e )
					{
						try{
							stats.pingFailed();
							
						}finally{
						
							sem.release();
						}
					}
				},
				5000, PRUDPPacketHandler.PRIORITY_HIGH );
			
			sem.reserve( 5000 );
			
			return( result[0] );
			
		}catch( Throwable e ){
			
			stats.pingFailed();

			return( null );
		}
	}
	
		// STORE
	
	public void
	sendStore(
		final DHTTransportUDPContactImpl	contact,
		final DHTTransportReplyHandler		handler,
		byte[][]							keys,
		DHTTransportValue[][]				value_sets,
		int									priority )
	{
		final long	connection_id = getConnectionID();
		
		if ( false ){
			int	total_values = 0;
			for (int i=0;i<keys.length;i++){
				total_values += value_sets[i].length;
			}
			System.out.println( "store: keys = " + keys.length +", values = " + total_values );
		}
		
			// only report to caller the outcome of the first packet
		
		int	packet_count	= 0;
		
		try{
			checkAddress( contact );
			
			int		current_key_index	= 0;
			int		current_value_index	= 0;
			
			while( current_key_index < keys.length ){
			
				packet_count++;
								
				int	space = DHTUDPPacketHelper.PACKET_MAX_BYTES - DHTUDPPacketRequest.DHT_HEADER_SIZE;
								
				List	key_list	= new ArrayList();
				List	values_list	= new ArrayList();
								
				key_list.add( keys[current_key_index]);
				
				space -= ( keys[current_key_index].length + 1 );	// 1 for length marker
				
				values_list.add( new ArrayList());
				
				while( 	space > 0 &&  
						current_key_index < keys.length ){
					
					if ( current_value_index == value_sets[current_key_index].length ){
						
							// all values from the current key have been processed
						
						current_key_index++;
						
						current_value_index	= 0;
						
						if ( key_list.size() == DHTUDPPacketRequestStore.MAX_KEYS_PER_PACKET ){
							
								// no more keys allowed in this packet
								
							break;
						}
							
						if ( current_key_index == keys.length ){
						
								// no more keys left, job done
							
							break;
						}
						
						key_list.add( keys[current_key_index]);
						
						space -= ( keys[current_key_index].length + 1 );	// 1 for length marker
						
						values_list.add( new ArrayList());
					}
					
					DHTTransportValue	value = value_sets[current_key_index][current_value_index];
					
					int	entry_size = DHTUDPUtils.DHTTRANSPORTVALUE_SIZE_WITHOUT_VALUE + value.getValue().length + 1;
					
					List	values = (List)values_list.get(values_list.size()-1);
					
					if ( 	space < entry_size || 
							values.size() == DHTUDPPacketRequestStore.MAX_VALUES_PER_KEY ){
						
							// no space left or we've used up our limit on the
							// number of values permitted per key
						
						break;
					}
					
					values.add( value );
					
					space -= entry_size;
					
					current_value_index++;
				}
				
				int	packet_entries = key_list.size();

				if ( packet_entries > 0 ){
				
						// if last entry has no values then ignore it
					
					if ( ((List)values_list.get( packet_entries-1)).size() == 0 ){
						
						packet_entries--;
					}
				}
				
				if ( packet_entries == 0 ){
					
					break;
				}
	
				byte[][]				packet_keys 		= new byte[packet_entries][];
				DHTTransportValue[][]	packet_value_sets 	= new DHTTransportValue[packet_entries][];
				
				//int	packet_value_count = 0;
				
				for (int i=0;i<packet_entries;i++){
					
					packet_keys[i] = (byte[])key_list.get(i);
					
					List	values = (List)values_list.get(i);
					
					packet_value_sets[i] = new DHTTransportValue[values.size()];
					
					for (int j=0;j<values.size();j++){
						
						packet_value_sets[i][j] = (DHTTransportValue)values.get(j);
						
						//packet_value_count++;
					}
				}
				
				// System.out.println( "    packet " + packet_count + ": keys = " + packet_entries + ", values = " + packet_value_count );
								
				
				final DHTUDPPacketRequestStore	request = 
					new DHTUDPPacketRequestStore( this, connection_id, local_contact, contact );
			
				stats.storeSent( request );

				request.setRandomID( contact.getRandomID());
				
				request.setKeys( packet_keys );
				
				request.setValueSets( packet_value_sets );
				
				final int f_packet_count	= packet_count;
				
				requestSendRequestProcessor( contact, request );

				packet_handler.sendAndReceive(
					request,
					contact.getTransportAddress(),
					new DHTUDPPacketReceiver()
					{
						public void
						packetReceived(
							DHTUDPPacketReply	packet,
							InetSocketAddress	from_address,
							long				elapsed_time )
						{
							try{								
								if ( packet.getConnectionId() != connection_id ){
									
									throw( new Exception( "connection id mismatch: sender=" + from_address + ",packet=" + packet.getString()));
								}
								
								contact.setInstanceIDAndVersion( packet.getTargetInstanceID(), packet.getProtocolVersion());
								
								requestSendReplyProcessor( contact, handler, packet, elapsed_time );

								DHTUDPPacketReplyStore	reply = (DHTUDPPacketReplyStore)packet;
									
								stats.storeOK();
									
								if ( f_packet_count == 1 ){
										
									handler.storeReply( contact, reply.getDiversificationTypes());
								}
								
							}catch( DHTUDPPacketHandlerException e ){
								
								error( e );
								
							}catch( Throwable e ){
								
								Debug.printStackTrace(e);
								
								error( new DHTUDPPacketHandlerException( "store failed", e ));
							}
						}
						
						public void
						error(
							DHTUDPPacketHandlerException	e )
						{
							stats.storeFailed();
							
							if ( f_packet_count == 1 ){
								
								handler.failed( contact, e );
							}
						}
					},
					store_timeout,
					priority );

			}
		}catch( Throwable e ){
										
			stats.storeFailed();
			
			if ( packet_count <= 1 ){
								
				handler.failed( contact, e );
			}
		}
	}
	
		// QUERY STORE
	
	public void 
	sendQueryStore(
		final DHTTransportUDPContactImpl	contact,
		final DHTTransportReplyHandler 		handler,
		int									header_size,
		List<Object[]>						key_details )
	{
		try{
			checkAddress( contact );
			
			final long	connection_id = getConnectionID();
			
			Iterator<Object[]> it = key_details.iterator();
			
			byte[]				current_prefix			= null;
			Iterator<byte[]>	current_suffixes 		= null;
						
			List<DHTUDPPacketRequestQueryStorage> requests = new ArrayList<DHTUDPPacketRequestQueryStorage>();
	
outer:	
			while( it.hasNext()){

				int	space = DHTUDPPacketRequestQueryStorage.SPACE;

				DHTUDPPacketRequestQueryStorage	request = 
					new DHTUDPPacketRequestQueryStorage( this, connection_id, local_contact, contact );

				List<Object[]> packet_key_details = new ArrayList<Object[]>();
				
				while( space > 0 && it.hasNext()){
					
					if ( current_prefix == null ){
					
						Object[] entry = it.next();
					
						current_prefix = (byte[])entry[0];
					
						List<byte[]> l = (List<byte[]>)entry[1];
						
						current_suffixes = l.iterator();
					}
					
					if ( current_suffixes.hasNext()){
						
						int	min_space = header_size + 3;	// 1 byte prefix len, 2 byte num suffix
						
						if ( space < min_space ){
							
							request.setDetails( header_size, packet_key_details );
							
							requests.add( request );
							
							continue outer ;
						}
						
						List<byte[]> s = new ArrayList<byte[]>();
						
						packet_key_details.add( new Object[]{ current_prefix, s });
						
						int	prefix_size = current_prefix.length;
						int	suffix_size = header_size - prefix_size;

						space -= ( 3 + prefix_size );
						
						while( space >= suffix_size && current_suffixes.hasNext()){
							
							s.add( current_suffixes.next());
						}
						
					}else{
						
						current_prefix = null;
					}
				}
				
				if ( !it.hasNext()){
					
					request.setDetails( header_size, packet_key_details );
					
					requests.add( request );
				}
			}

			final Object[] replies = new Object[ requests.size() ];
							
			for ( int i=0;i<requests.size();i++){
				
				DHTUDPPacketRequestQueryStorage request = requests.get(i);
				
				final int f_i = i;
					
				stats.queryStoreSent( request );
							
				requestSendRequestProcessor( contact, request );
	
				packet_handler.sendAndReceive(
					request,
					contact.getTransportAddress(),
					new DHTUDPPacketReceiver()
					{					
						public void
						packetReceived(
							DHTUDPPacketReply	packet,
							InetSocketAddress	from_address,
							long				elapsed_time )
						{
							try{														
								if ( packet.getConnectionId() != connection_id ){
									
									throw( new Exception( "connection id mismatch" ));
								}
	
								contact.setInstanceIDAndVersion( packet.getTargetInstanceID(), packet.getProtocolVersion());
								
								requestSendReplyProcessor( contact, handler, packet, elapsed_time );
									
								DHTUDPPacketReplyQueryStorage	reply = (DHTUDPPacketReplyQueryStorage)packet;
								
									// copy out the random id in preparation for a possible subsequent
									// store operation
								
								contact.setRandomID( reply.getRandomID());
																							
								stats.queryStoreOK();
									
								synchronized( replies ){
									
									replies[f_i] = reply;
									
									checkComplete();
								}
								
							}catch( DHTUDPPacketHandlerException e ){
								
								error( e );
								
							}catch( Throwable e ){
								
								Debug.printStackTrace(e);
								
								error( new DHTUDPPacketHandlerException( "queryStore failed", e ));
							}
						}
						
						public void
						error(
							DHTUDPPacketHandlerException	e )
						{
							stats.queryStoreFailed();
							
							synchronized( replies ){
								
								replies[f_i] = e;
								
								checkComplete();
							}
						}
						
						protected void
						checkComplete()
						{
							DHTUDPPacketHandlerException last_error = null;
							
							for ( int i=0;i<replies.length;i++ ){
							
								Object o = replies[i];
								
								if ( o == null ){
									
									return;
								}
								
								if ( o instanceof DHTUDPPacketHandlerException ){
									
									last_error = (DHTUDPPacketHandlerException)o;
								}
							}
							
							if ( last_error != null ){
							
								handler.failed( contact, last_error );
								
							}else{
							
								if ( replies.length == 1 ){
									
									handler.queryStoreReply( contact, ((DHTUDPPacketReplyQueryStorage)replies[0]).getResponse());
									
								}else{
									
									List<byte[]> response = new ArrayList<byte[]>();
							
									for ( int i=0;i<replies.length;i++ ){
									
										response.addAll(((DHTUDPPacketReplyQueryStorage)replies[0]).getResponse());
									}
								
									handler.queryStoreReply( contact, response );
								}
							}
						}
					},
					request_timeout, PRUDPPacketHandler.PRIORITY_MEDIUM );
			}
			
		}catch( Throwable e ){
			
			stats.queryStoreFailed();
			
			handler.failed( contact, e );
		}
	}
	
		// FIND NODE
	
	public void
	sendFindNode(
		final DHTTransportUDPContactImpl	contact,
		final DHTTransportReplyHandler		handler,
		byte[]								nid )
	{
		try{
			checkAddress( contact );
			
			final long	connection_id = getConnectionID();
			
			final DHTUDPPacketRequestFindNode	request = 
				new DHTUDPPacketRequestFindNode( this, connection_id, local_contact, contact );
			
			stats.findNodeSent( request );
			
			request.setID( nid );
			
			request.setNodeStatus( getNodeStatus());
			
			request.setEstimatedDHTSize( request_handler.getTransportEstimatedDHTSize());
			
			requestSendRequestProcessor( contact, request );

			packet_handler.sendAndReceive(
				request,
				contact.getTransportAddress(),
				new DHTUDPPacketReceiver()
				{					
					public void
					packetReceived(
						DHTUDPPacketReply	packet,
						InetSocketAddress	from_address,
						long				elapsed_time )
					{
						try{														
							if ( packet.getConnectionId() != connection_id ){
								
								throw( new Exception( "connection id mismatch" ));
							}

							contact.setInstanceIDAndVersion( packet.getTargetInstanceID(), packet.getProtocolVersion());
							
							requestSendReplyProcessor( contact, handler, packet, elapsed_time );
								
							DHTUDPPacketReplyFindNode	reply = (DHTUDPPacketReplyFindNode)packet;
							
								// copy out the random id in preparation for a possible subsequent
								// store operation
							
							contact.setRandomID( reply.getRandomID());
														
							updateContactStatus( contact, reply.getNodeStatus(), false );
														
							request_handler.setTransportEstimatedDHTSize( reply.getEstimatedDHTSize());
							
							stats.findNodeOK();
								
							DHTTransportContact[] contacts = reply.getContacts();
							
								// scavenge any contacts here to help bootstrap process
								// when ip wrong and no import history
							
							try{
								this_mon.enter();
								
								for (int i=0; contact_history.size() < CONTACT_HISTORY_MAX && i<contacts.length;i++){
								
									DHTTransportUDPContact c = (DHTTransportUDPContact)contacts[i];
								
										contact_history.put( c.getTransportAddress(), c );
								}
							}finally{
									
								this_mon.exit();
							}
							
							handler.findNodeReply( contact, contacts );
							
						}catch( DHTUDPPacketHandlerException e ){
							
							error( e );
							
						}catch( Throwable e ){
							
							Debug.printStackTrace(e);
							
							error( new DHTUDPPacketHandlerException( "findNode failed", e ));
						}
					}
					
					public void
					error(
						DHTUDPPacketHandlerException	e )
					{
						stats.findNodeFailed();
						
						handler.failed( contact, e );
					}
				},
				request_timeout, PRUDPPacketHandler.PRIORITY_MEDIUM );
			
		}catch( Throwable e ){
			
			stats.findNodeFailed();
			
			handler.failed( contact, e );
		}
	}
	
		// FIND VALUE
	
	public void
	sendFindValue(
		final DHTTransportUDPContactImpl	contact,
		final DHTTransportReplyHandler		handler,
		byte[]								key,
		int									max_values,
		short								flags )
	{
		try{
			checkAddress( contact );
			
			final long	connection_id = getConnectionID();
			
			final DHTUDPPacketRequestFindValue	request = 
				new DHTUDPPacketRequestFindValue( this, connection_id, local_contact, contact );
			
			stats.findValueSent( request );

			request.setID( key );
			
			request.setMaximumValues( max_values );
			
			request.setFlags((byte)flags );
			
			requestSendRequestProcessor( contact, request );

			packet_handler.sendAndReceive(
				request,
				contact.getTransportAddress(),
				new DHTUDPPacketReceiver()
				{
					public void
					packetReceived(
						DHTUDPPacketReply	packet,
						InetSocketAddress	from_address,
						long				elapsed_time )
					{
						try{							
							if ( packet.getConnectionId() != connection_id ){
								
								throw( new Exception( "connection id mismatch" ));
							}
							
							contact.setInstanceIDAndVersion( packet.getTargetInstanceID(), packet.getProtocolVersion());
							
							requestSendReplyProcessor( contact, handler, packet, elapsed_time );
								
							DHTUDPPacketReplyFindValue	reply = (DHTUDPPacketReplyFindValue)packet;
								
							stats.findValueOK();
								
							DHTTransportValue[]	res = reply.getValues();
								
							if ( res != null ){
									
								boolean	continuation = reply.hasContinuation();
																
								handler.findValueReply( contact, res, reply.getDiversificationType(), continuation);
									
							}else{
									
								handler.findValueReply( contact, reply.getContacts());
							}
						}catch( DHTUDPPacketHandlerException e ){
							
							error( e );
							
						}catch( Throwable e ){
							
							Debug.printStackTrace(e);
							
							error( new DHTUDPPacketHandlerException( "findValue failed", e ));
						}
					}
					
					public void
					error(
						DHTUDPPacketHandlerException	e )
					{
						stats.findValueFailed();
						
						handler.failed( contact, e );
					}
				},
				request_timeout, PRUDPPacketHandler.PRIORITY_HIGH );
			
		}catch( Throwable e ){
			
			if ( !(e instanceof DHTUDPPacketHandlerException )){
				
				stats.findValueFailed();
			
				handler.failed( contact, e );
			}
		}
	}
	
	protected DHTTransportFullStats
	getFullStats(
		DHTTransportUDPContactImpl	contact )
	{
		if ( contact == local_contact ){
			
			return( request_handler.statsRequest( contact ));
		}
		
		final DHTTransportFullStats[] res = { null };
		
		final AESemaphore	sem = new AESemaphore( "DHTTransportUDP:getFullStats");
		
		sendStats(	contact,
					new DHTTransportReplyHandlerAdapter()
					{
						public void
						statsReply(
							DHTTransportContact 	_contact,
							DHTTransportFullStats	_stats )
						{
							res[0]	= _stats;
							
							sem.release();
						}
						
						public void
						failed(
							DHTTransportContact 	_contact,
							Throwable				_error )
						{
							sem.release();
						}
				
					});
		
		sem.reserve();

		return( res[0] );
	}
	
	public void
	registerTransferHandler(
		byte[]						handler_key,
		DHTTransportTransferHandler	handler )
	{
		xfer_handler.registerTransferHandler(handler_key, handler);
	}
	
	public void
	registerTransferHandler(
		byte[]						handler_key,
		DHTTransportTransferHandler	handler,
		Map<String,Object>			options )
	{
		xfer_handler.registerTransferHandler( handler_key, handler, options );
	}
	
	public void
	unregisterTransferHandler(
		byte[]						handler_key,
		DHTTransportTransferHandler	handler )
	{
		xfer_handler.unregisterTransferHandler(handler_key, handler);
	}
	
	public byte[]
	readTransfer(
		DHTTransportProgressListener	listener,
		DHTTransportContact				target,
		byte[]							handler_key,
		byte[]							key,
		long							timeout )
	
		throws DHTTransportException
	{
		return( xfer_handler.readTransfer( listener, target, handler_key, key, timeout ));
	}
	
	public void
	writeTransfer(
		DHTTransportProgressListener	listener,
		DHTTransportContact				target,
		byte[]							handler_key,
		byte[]							key,
		byte[]							data,
		long							timeout )
	
		throws DHTTransportException
	{
		xfer_handler.writeTransfer(listener, target, handler_key, key, data, timeout);
	}
	
	public byte[]
	writeReadTransfer(
		DHTTransportProgressListener	listener,
		DHTTransportContact				target,
		byte[]							handler_key,
		byte[]							data,
		long							timeout )	
	
		throws DHTTransportException
	{
		return( xfer_handler.writeReadTransfer( listener, target, handler_key, data, timeout ));
	}
	
	protected void
	dataRequest(
		final DHTTransportUDPContactImpl	originator,
		final DHTUDPPacketData				req )
	{
		stats.dataReceived();
		
		xfer_handler.receivePacket( 
			originator, 
			new DHTTransferHandler.Packet(
				req.getConnectionId(),
				req.getPacketType(),
				req.getTransferKey(),
				req.getRequestKey(),
				req.getData(),
				req.getStartPosition(),
				req.getLength(),
				req.getTotalLength()));
	}
		
	public void
	process(
		DHTUDPPacketRequest	request,
		boolean				alien )
	{
		process( packet_handler, request, alien  );
	}
	
	public void
	process(
		DHTUDPPacketHandlerStub		packet_handler_stub,
		DHTUDPPacketRequest			request,
		boolean						alien )
	{
		if ( request_handler == null ){
			
			logger.log( "Ignoring packet as not yet ready to process" );
			
			return;
		}
		
		try{			
			stats.incomingRequestReceived( request, alien );
			
			InetSocketAddress	transport_address = request.getAddress();
			
			DHTTransportUDPContactImpl	originating_contact = 
				new DHTTransportUDPContactImpl( 
						false,
						this, 
						transport_address, 
						request.getOriginatorAddress(), 
						request.getOriginatorVersion(),
						request.getOriginatorInstanceID(),
						request.getClockSkew(),
						request.getGenericFlags());
			
			try{
				checkAddress( originating_contact );
					
			}catch( DHTUDPPacketHandlerException e ){
				
				return;
			}

			requestReceiveRequestProcessor( originating_contact, request );
			
			boolean	bad_originator = !originating_contact.addressMatchesID();
			
				// bootstrap node returns details regardless of whether the originator ID matches
				// as the details will help the sender discover their correct ID (hopefully)
			
			if ( bad_originator && !bootstrap_node ){
				
				String	contact_string = originating_contact.getString();

				if ( recent_reports.get(contact_string) == null ){
					
					recent_reports.put( contact_string, "" );
					
					logger.log( "Node " + contact_string + " has incorrect ID, reporting it to them" );
				}
				
				DHTUDPPacketReplyError	reply = 
					new DHTUDPPacketReplyError(
							this,
							request.getTransactionId(),
							request.getConnectionId(),
							local_contact,
							originating_contact );
				
				reply.setErrorType( DHTUDPPacketReplyError.ET_ORIGINATOR_ADDRESS_WRONG );
				
				reply.setOriginatingAddress( originating_contact.getTransportAddress());
				
				requestReceiveReplyProcessor( originating_contact, reply );
				
				packet_handler_stub.send( reply, request.getAddress());

			}else{
				
				if ( bad_originator ){
				
						// we need to patch the originator up otherwise we'll be populating our
						// routing table with crap
					
					originating_contact = 
						new DHTTransportUDPContactImpl( 
								false,
								this, 
								transport_address, 
								transport_address, 		// set originator address to transport
								request.getOriginatorVersion(),
								request.getOriginatorInstanceID(),
								request.getClockSkew(),
								request.getGenericFlags());

				}else{
					
					contactAlive( originating_contact );
				}
				
				if ( request instanceof DHTUDPPacketRequestPing ){
					
					if ( !bootstrap_node ){
						
						request_handler.pingRequest( originating_contact );
						
						DHTUDPPacketRequestPing ping = (DHTUDPPacketRequestPing)request;
												
						DHTUDPPacketReplyPing	reply = 
							new DHTUDPPacketReplyPing(
									this,
									request.getTransactionId(),
									request.getConnectionId(),
									local_contact,
									originating_contact );
													
						sendAltContacts( ping, reply );
						
						requestReceiveReplyProcessor( originating_contact, reply );

						packet_handler_stub.send( reply, request.getAddress());
					}
				}else if ( request instanceof DHTUDPPacketRequestKeyBlock ){
						
					if ( !bootstrap_node ){
						
						DHTUDPPacketRequestKeyBlock	kb_request = (DHTUDPPacketRequestKeyBlock)request;

						originating_contact.setRandomID( kb_request.getRandomID());

						request_handler.keyBlockRequest( 
								originating_contact, 
								kb_request.getKeyBlockRequest(),
								kb_request.getKeyBlockSignature());
						
						DHTUDPPacketReplyKeyBlock	reply = 
							new DHTUDPPacketReplyKeyBlock(
									this,
									request.getTransactionId(),
									request.getConnectionId(),
									local_contact,
									originating_contact );
						
						requestReceiveReplyProcessor( originating_contact, reply );

						packet_handler_stub.send( reply, request.getAddress());
					}
				}else if ( request instanceof DHTUDPPacketRequestStats ){
					
					DHTUDPPacketRequestStats	stats_request = (DHTUDPPacketRequestStats)request;
										
					DHTUDPPacketReplyStats	reply = 
						new DHTUDPPacketReplyStats(
								this,
								request.getTransactionId(),
								request.getConnectionId(),
								local_contact,
								originating_contact );
					
					int	type = stats_request.getStatsType();
					
					if ( type == DHTUDPPacketRequestStats.STATS_TYPE_ORIGINAL ){
						
						DHTTransportFullStats	full_stats = request_handler.statsRequest( originating_contact );

						reply.setOriginalStats( full_stats );
						
					}else if ( type == DHTUDPPacketRequestStats.STATS_TYPE_NP_VER2 ){
						
						DHTNetworkPositionProvider prov = DHTNetworkPositionManager.getProvider( DHTNetworkPosition.POSITION_TYPE_VIVALDI_V2 );
						
						byte[]	data = new byte[0];
						
						if ( prov != null ){
							
							ByteArrayOutputStream	baos = new ByteArrayOutputStream();
							
							DataOutputStream	dos = new DataOutputStream( baos );
							
							prov.serialiseStats( dos );
							
							dos.flush();
							
							data = baos.toByteArray();
						}
						
						reply.setNewStats( data, DHTNetworkPosition.POSITION_TYPE_VIVALDI_V2 );
						
					}else{
						
						throw( new IOException( "Uknown stats type '" + type + "'" ));
					}
					
					requestReceiveReplyProcessor( originating_contact, reply );

					packet_handler_stub.send( reply, request.getAddress());

				}else if ( request instanceof DHTUDPPacketRequestStore ){
					
					if ( !bootstrap_node ){

						DHTUDPPacketRequestStore	store_request = (DHTUDPPacketRequestStore)request;
						
						originating_contact.setRandomID( store_request.getRandomID());
						
						DHTTransportStoreReply	res = 
							request_handler.storeRequest(
								originating_contact, 
								store_request.getKeys(), 
								store_request.getValueSets());
						
						if ( res.blocked()){
							
							if ( originating_contact.getProtocolVersion() >= DHTTransportUDP.PROTOCOL_VERSION_BLOCK_KEYS ){
								
								DHTUDPPacketReplyError	reply = 
									new DHTUDPPacketReplyError(
										this,
										request.getTransactionId(),
										request.getConnectionId(),
										local_contact,
										originating_contact );
								
								reply.setErrorType( DHTUDPPacketReplyError.ET_KEY_BLOCKED );
								
								reply.setKeyBlockDetails( res.getBlockRequest(), res.getBlockSignature() );
								
								requestReceiveReplyProcessor( originating_contact, reply );

								packet_handler_stub.send( reply, request.getAddress());
							}else{
								
								DHTUDPPacketReplyStore	reply = 
									new DHTUDPPacketReplyStore(
											this,
											request.getTransactionId(),
											request.getConnectionId(),
											local_contact,
											originating_contact );
								
								reply.setDiversificationTypes( new byte[store_request.getKeys().length] );
								
								requestReceiveReplyProcessor( originating_contact, reply );
		
								packet_handler_stub.send( reply, request.getAddress());
							}
						}else{
							
							DHTUDPPacketReplyStore	reply = 
								new DHTUDPPacketReplyStore(
										this,
										request.getTransactionId(),
										request.getConnectionId(),
										local_contact,
										originating_contact );
							
							reply.setDiversificationTypes( res.getDiversificationTypes());
							
							requestReceiveReplyProcessor( originating_contact, reply );
	
							packet_handler_stub.send( reply, request.getAddress());
						}
					}
				}else if ( request instanceof DHTUDPPacketRequestQueryStorage ){
					
					DHTUDPPacketRequestQueryStorage	query_request = (DHTUDPPacketRequestQueryStorage)request;
					
					DHTTransportQueryStoreReply	res = 
						request_handler.queryStoreRequest(
									originating_contact,
									query_request.getHeaderLength(),
									query_request.getKeys());		
					
					DHTUDPPacketReplyQueryStorage	reply = 
						new DHTUDPPacketReplyQueryStorage(
								this,
								request.getTransactionId(),
								request.getConnectionId(),
								local_contact,
								originating_contact );
							
					reply.setRandomID( originating_contact.getRandomID());
						
					reply.setResponse( res.getHeaderSize(), res.getEntries());
					
					requestReceiveReplyProcessor( originating_contact, reply );

					packet_handler_stub.send( reply, request.getAddress());
					
				}else if ( request instanceof DHTUDPPacketRequestFindNode ){
					
					DHTUDPPacketRequestFindNode	find_request = (DHTUDPPacketRequestFindNode)request;
					
					boolean	acceptable;
					
						// as a bootstrap node we only accept find-node requests for the originator's
						// ID
					
					if ( bootstrap_node ){
						
							// log( originating_contact );
						
							// let bad originators through to aid bootstrapping with bad IP
						
						acceptable = bad_originator || Arrays.equals( find_request.getID(), originating_contact.getID());
												
					}else{
						
						acceptable	= true;
					}
					
					if ( acceptable ){
						
						if ( find_request.getProtocolVersion() >= DHTTransportUDP.PROTOCOL_VERSION_MORE_NODE_STATUS ){
						
							updateContactStatus( originating_contact, find_request.getNodeStatus(), true );
											
							request_handler.setTransportEstimatedDHTSize( find_request.getEstimatedDHTSize());
						}
						
						DHTTransportContact[]	res = 
							request_handler.findNodeRequest(
										originating_contact,
										find_request.getID());
						
						DHTUDPPacketReplyFindNode	reply = 
							new DHTUDPPacketReplyFindNode(
									this,
									request.getTransactionId(),
									request.getConnectionId(),
									local_contact,
									originating_contact );
								
						reply.setRandomID( originating_contact.getRandomID());
						
						reply.setNodeStatus( getNodeStatus());
						
						reply.setEstimatedDHTSize( request_handler.getTransportEstimatedDHTSize());
						
						reply.setContacts( res );
						
						requestReceiveReplyProcessor( originating_contact, reply );

						packet_handler_stub.send( reply, request.getAddress());
					}
					
				}else if ( request instanceof DHTUDPPacketRequestFindValue ){
					
					if ( !bootstrap_node ){

						DHTUDPPacketRequestFindValue	find_request = (DHTUDPPacketRequestFindValue)request;
					
						DHTTransportFindValueReply res = 
							request_handler.findValueRequest(
										originating_contact,
										find_request.getID(),
										find_request.getMaximumValues(),
										find_request.getFlags());
					
						if ( res.blocked()){
							
							if ( originating_contact.getProtocolVersion() >= DHTTransportUDP.PROTOCOL_VERSION_BLOCK_KEYS ){
								
								DHTUDPPacketReplyError	reply = 
									new DHTUDPPacketReplyError(
										this,
										request.getTransactionId(),
										request.getConnectionId(),
										local_contact,
										originating_contact );
								
								reply.setErrorType( DHTUDPPacketReplyError.ET_KEY_BLOCKED );
								
								reply.setKeyBlockDetails( res.getBlockedKey(), res.getBlockedSignature() );
								
								requestReceiveReplyProcessor( originating_contact, reply );

								packet_handler_stub.send( reply, request.getAddress());
								
							}else{
								
								DHTUDPPacketReplyFindValue	reply = 
									new DHTUDPPacketReplyFindValue(
										this,
										request.getTransactionId(),
										request.getConnectionId(),
										local_contact,
										originating_contact );
								
								reply.setValues( new DHTTransportValue[0], DHT.DT_NONE, false );
								
								requestReceiveReplyProcessor( originating_contact, reply );
	
								packet_handler_stub.send( reply, request.getAddress());
							}
							
						}else{
							DHTUDPPacketReplyFindValue	reply = 
								new DHTUDPPacketReplyFindValue(
									this,
									request.getTransactionId(),
									request.getConnectionId(),
									local_contact,
									originating_contact );
							
							if ( res.hit()){
								
								DHTTransportValue[]	res_values = res.getValues();
								
								int		max_size = DHTUDPPacketHelper.PACKET_MAX_BYTES - DHTUDPPacketReplyFindValue.DHT_FIND_VALUE_HEADER_SIZE;
															
								List	values 		= new ArrayList();
								int		values_size	= 0;
								
								int	pos = 0;
								
								while( pos < res_values.length ){
							
									DHTTransportValue	v = res_values[pos];
									
									int	v_len = v.getValue().length + DHTUDPPacketReplyFindValue.DHT_FIND_VALUE_TV_HEADER_SIZE;
									
									if ( 	values_size > 0 && // if value too big, cram it in anyway 
											values_size + v_len > max_size ){
										
											// won't fit, send what we've got
										
										DHTTransportValue[]	x = new DHTTransportValue[values.size()];
										
										values.toArray( x );
																			
										reply.setValues( x, res.getDiversificationType(), true );	// continuation = true
																		
										packet_handler_stub.send( reply, request.getAddress());
										
										values_size	= 0;
										
										values		= new ArrayList();
										
									}else{
										
										values.add(v);
										
										values_size	+= v_len;
										
										pos++;
									}
								}
								
									// send the remaining (possible zero length) non-continuation values
									
								DHTTransportValue[]	x = new DHTTransportValue[values.size()];
									
								values.toArray( x );
									
								reply.setValues( x, res.getDiversificationType(), false );
									
								requestReceiveReplyProcessor( originating_contact, reply );
	
								packet_handler_stub.send( reply, request.getAddress());
							
							}else{
								
								reply.setContacts(res.getContacts());
								
								requestReceiveReplyProcessor( originating_contact, reply );
	
								packet_handler_stub.send( reply, request.getAddress());
							}
						}
					}
				}else if ( request instanceof DHTUDPPacketData ){
					
					if ( !bootstrap_node ){
						
						dataRequest(originating_contact, (DHTUDPPacketData)request );
					}
				}else{
					
					Debug.out( "Unexpected packet:" + request.toString());
				}
			}
		}catch( DHTUDPPacketHandlerException e ){
			
			// not interesting, send packet fail or something
			
		}catch( Throwable e ){
			
			Debug.printStackTrace(e);
		}
	}
	
		// the _state networks are populated via ping requests to other peers
	
	private Map<Integer, DHTTransportAlternativeNetworkImpl>	alt_net_states 		= new HashMap<Integer, DHTTransportAlternativeNetworkImpl>();
	
		// the _providers represent a local source of contacts that are used as a primary
		// source of contacts for replying to other peers ping requests
		
	private volatile Map<Integer, DHTTransportAlternativeNetwork>		alt_net_providers	= new HashMap<Integer, DHTTransportAlternativeNetwork>();
	
	private Object	alt_net_providers_lock = new Object();
	
	{
		for ( Integer net: DHTTransportAlternativeNetwork.AT_ALL ){
			
			alt_net_states.put( net, new DHTTransportAlternativeNetworkImpl( net ));
		}
	}
	
	public DHTTransportAlternativeNetwork
	getAlternativeNetwork(
		int		network_type )
	{
		return( alt_net_states.get( network_type ));
	}
	
	public void
	registerAlternativeNetwork(
		DHTTransportAlternativeNetwork		network )
	{
		synchronized( alt_net_providers_lock ){
			
			Map<Integer, DHTTransportAlternativeNetwork> new_providers = new HashMap<Integer, DHTTransportAlternativeNetwork>( alt_net_providers );
			
			new_providers.put( network.getNetworkType(), network );
			
			alt_net_providers = new_providers;
		}
	}
	
	public void
	unregisterAlternativeNetwork(
		DHTTransportAlternativeNetwork		network )
	{
		synchronized( alt_net_providers_lock ){
			
			Map<Integer, DHTTransportAlternativeNetwork> new_providers = new HashMap<Integer, DHTTransportAlternativeNetwork>( alt_net_providers );
			
			Iterator< Map.Entry<Integer, DHTTransportAlternativeNetwork>> it = new_providers.entrySet().iterator();
			
			while( it.hasNext()){
				
				if ( it.next().getValue() == network ){
					
					it.remove();
				}
			}
			
			alt_net_providers = new_providers;
		}
	}
	
	private void
	checkAltContacts()
	{
		int	total_required = 0;
		
		for ( DHTTransportAlternativeNetworkImpl net: alt_net_states.values()){
			
			total_required += net.getRequiredContactCount();
		}
		
		if ( total_required > 0 ){
			
			List<DHTTransportContact> targets = new ArrayList<DHTTransportContact>( ROUTABLE_CONTACT_HISTORY_MAX );
			
			try{
				this_mon.enter();
				
				for ( DHTTransportContact contact: routable_contact_history.values()){
				
					if ( contact.getProtocolVersion() >= DHTTransportUDP.PROTOCOL_VERSION_ALT_CONTACTS ){
						
						targets.add( contact );
					}
				}
				
			}finally{
				
				this_mon.exit();
			}
			
			if ( targets.size() > 0 ){
				
				targets.get( RandomUtils.nextInt( targets.size())).sendPing( 
					new DHTTransportReplyHandlerAdapter()
					{
						public void
						pingReply(
							DHTTransportContact _contact )
						{
							
						}
						
						public void
						failed(
							DHTTransportContact 	_contact,
							Throwable				_error )
						{
							
						}
					});
			}
		}
	}
	
	private void
	sendAltContacts(
		DHTUDPPacketRequestPing		request,
		DHTUDPPacketReplyPing		reply )
	{		
		if ( request.getProtocolVersion() >= DHTTransportUDP.PROTOCOL_VERSION_ALT_CONTACTS ){

			int[]	alt_nets 	= request.getAltNetworks();
			int[] 	counts 		= request.getAltNetworkCounts();

			if ( alt_nets.length > 0 ){
				
				List<DHTTransportAlternativeContact>	alt_contacts = new ArrayList<DHTTransportAlternativeContact>();
								
				Map<Integer, DHTTransportAlternativeNetwork> providers = alt_net_providers;
				
				for ( int i=0; i<alt_nets.length;i++){
				
					int	count = counts[i];
					
					if ( count == 0 ){
						
						continue;
					}
					
					int	net = alt_nets[i];
					
					DHTTransportAlternativeNetworkImpl local = alt_net_states.get( net );
					
					if ( local == null ){
						
						continue;
					}
					
					int wanted = local.getRequiredContactCount();
					
					if ( wanted > 0 ){
						
						DHTTransportAlternativeNetwork provider = providers.get( net );
						
						if ( provider != null ){
							
							local.addContactsForSend( provider.getContacts( wanted ));
						}
					}
					
						// need to limit response for large serialisations
					
					if ( net == DHTTransportAlternativeNetwork.AT_I2P ){
						
						count = Math.min( 2, count );
					}
					
					alt_contacts.addAll( local.getContacts( count, true ));
				}
				
				if ( alt_contacts.size() > 0 ){
					
					reply.setAltContacts( alt_contacts );
				}
			}
		}
	}
	
	private void
	requestAltContacts(
		DHTUDPPacketRequestPing		request )
	{
		if ( request.getProtocolVersion() >= DHTTransportUDP.PROTOCOL_VERSION_ALT_CONTACTS ){
			
				// see if we could do with any more alt contacts
			
			List<int[]> wanted = null;
			
			for ( DHTTransportAlternativeNetworkImpl net: alt_net_states.values()){
				
				int req = net.getRequiredContactCount();
				
				if ( req > 0 ){
					
					int net_type = net.getNetworkType();
					
					if ( net_type == DHTTransportAlternativeNetwork.AT_I2P ){
						
						req = Math.min( 2, req );
					}

					if ( wanted == null ){
						
						wanted = new ArrayList<int[]>( alt_net_states.size());
					}
					
					wanted.add( new int[]{ net_type, req } );
				}
			}
			
			if ( wanted != null ){
				
				int[] networks 	= new int[wanted.size()];
				int[] counts	= new int[networks.length];
				
				for ( int i=0;i<networks.length;i++ ){
					
					int[] 	entry = wanted.get( i );
					
					networks[i] = entry[0];
					counts[i]	= entry[1];
				}
				
					// doesn't matter how many we request in total, the reply will be
					// limited to whatever the max is
				
				request.setAltContactRequest( networks, counts );
			}
		}
	}
	
	private void
	receiveAltContacts(
		DHTUDPPacketReplyPing		reply )
	{		
		if ( reply.getProtocolVersion() >= DHTTransportUDP.PROTOCOL_VERSION_ALT_CONTACTS ){

			for ( DHTTransportAlternativeContact contact: reply.getAltContacts()){
				
				DHTTransportAlternativeNetworkImpl net = alt_net_states.get( contact.getNetworkType());
				
				if ( net != null ){
					
					net.addContactFromReply( contact );
				}
			}
		}
	}
	
	protected void
	requestReceiveRequestProcessor( 
		DHTTransportUDPContactImpl	contact, 
		DHTUDPPacketRequest			request )
	{
		// called when request received
	}

	protected void
	requestReceiveReplyProcessor( 
		DHTTransportUDPContactImpl	contact, 
		DHTUDPPacketReply			reply )
	{
		// called before sending reply to request 
		
		int	action = reply.getAction();
		
		if ( 	action == DHTUDPPacketHelper.ACT_REPLY_PING ||
				action == DHTUDPPacketHelper.ACT_REPLY_FIND_NODE ||
				action == DHTUDPPacketHelper.ACT_REPLY_FIND_VALUE ){
						
			reply.setNetworkPositions( local_contact.getNetworkPositions());
		}
	}
	
	protected void
	requestSendRequestProcessor(
		DHTTransportUDPContactImpl	contact,
		DHTUDPPacketRequest 		request )
	{
		// called before sending request
		
	}
	
		/**
		 * Returns false if this isn't an error reply, true if it is and a retry can be
		 * performed, throws an exception otherwise
		 * @param reply
		 * @return
		 */
	
	protected void
	requestSendReplyProcessor(
		DHTTransportUDPContactImpl	remote_contact,
		DHTTransportReplyHandler	handler,
		DHTUDPPacketReply			reply,
		long						elapsed_time )
	
		throws DHTUDPPacketHandlerException
	{
			// called after receiving reply to request
		
		// System.out.println( "request:" + contact.getAddress() + " = " + elapsed_time );
		
		DHTNetworkPosition[]	remote_nps = reply.getNetworkPositions();
		
		if ( remote_nps != null ){
			
				// save current position of target
			
			remote_contact.setNetworkPositions( remote_nps );

				// update local positions
			
			DHTNetworkPositionManager.update( local_contact.getNetworkPositions(), remote_contact.getID(), remote_nps, (float)elapsed_time );						
		}
		
		remote_contact.setGenericFlags( reply.getGenericFlags());
		
		if ( reply.getAction() == DHTUDPPacketHelper.ACT_REPLY_ERROR ){
			
			DHTUDPPacketReplyError	error = (DHTUDPPacketReplyError)reply;
						
			switch( error.getErrorType()){
			
				case DHTUDPPacketReplyError.ET_ORIGINATOR_ADDRESS_WRONG:
				{
					try{
						externalAddressChange( remote_contact, error.getOriginatingAddress(), false );
						
					}catch( DHTTransportException e ){
						
						Debug.printStackTrace(e);
					}
											
					throw( new DHTUDPPacketHandlerException( "address changed notification" ));
				}
				case DHTUDPPacketReplyError.ET_KEY_BLOCKED:
				{
					handler.keyBlockRequest( remote_contact, error.getKeyBlockRequest(), error.getKeyBlockSignature());
					
					contactAlive( remote_contact );
					
					throw( new DHTUDPPacketHandlerException( "key blocked" ));
				}
			}
				
			throw( new DHTUDPPacketHandlerException( "unknown error type " + error.getErrorType()));
			
		}else{
			
			contactAlive( remote_contact );
		}
	}
	
	protected long
	getConnectionID()
	{
			// unfortunately, to reuse the UDP port with the tracker protocol we 
			// have to distinguish our connection ids by setting the MSB. This allows
			// the decode to work as there is no common header format for the request
			// and reply packets
		
			// note that tracker usage of UDP via this handler is only for outbound
			// messages, hence for that use a request will never be received by the
			// handler
		
		return( 0x8000000000000000L | random.nextLong());
	}
	
	public boolean
	supportsStorage()
	{
		return( !bootstrap_node );
	}
	
	public void
	addListener(
		DHTTransportListener	l )
	{
		listeners.add(l);
		
		if ( external_address != null ){
			
			l.currentAddress( external_address );
		}
	}
	
	public void
	removeListener(
		DHTTransportListener	l )
	{
		listeners.remove(l);
	}

	
	/*
	private PrintWriter	contact_log;
	private int			contact_log_entries;
	private SimpleDateFormat	contact_log_format = new SimpleDateFormat( "dd/MM/yyyy HH:mm:ss");
	{
		contact_log_format.setTimeZone( TimeZone.getTimeZone( "UTC" ));
	}
	
	protected void
	log(
		DHTTransportUDPContactImpl		contact )
	{
		if ( network == DHT.NW_MAIN ){
			
			synchronized( this ){
		
				try{
					if ( contact_log == null ){
						
						contact_log = new PrintWriter( new FileWriter( new File( SystemProperties.getUserPath(), "contact_log" )));
					}
					
					contact_log_entries++;
					
					InetSocketAddress address = contact.getAddress();
					
					contact_log.println( contact_log_format.format( new Date()) + ", " + address.getAddress().getHostAddress() + ", " + address.getPort());
					
					if ( contact_log_entries % 1000 == 0 ){
						
						System.out.println( "contact-log: " + contact_log_entries );
						
						contact_log.flush();
					}
					
				}catch( Throwable e ){
					
					Debug.out( e );
				}
			}
		}
	}
	*/
}
