/*
 * Created on May 27, 2008
 * 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.plugins.net.buddy.tracker;

import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.*;

import org.gudy.azureus2.core3.global.GlobalManager;
import org.gudy.azureus2.core3.global.GlobalManagerAdapter;
import org.gudy.azureus2.core3.peer.PEPeerManager;
import org.gudy.azureus2.core3.util.AddressUtils;
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.HashWrapper;
import org.gudy.azureus2.core3.util.LightHashMap;
import org.gudy.azureus2.core3.util.SHA1;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;
import org.gudy.azureus2.core3.util.TimerEventPeriodic;
import org.gudy.azureus2.plugins.download.Download;
import org.gudy.azureus2.plugins.download.DownloadAnnounceResult;
import org.gudy.azureus2.plugins.download.DownloadListener;
import org.gudy.azureus2.plugins.download.DownloadManagerListener;
import org.gudy.azureus2.plugins.download.DownloadPeerListener;
import org.gudy.azureus2.plugins.download.DownloadScrapeResult;
import org.gudy.azureus2.plugins.download.DownloadTrackerListener;
import org.gudy.azureus2.plugins.peers.Peer;
import org.gudy.azureus2.plugins.peers.PeerEvent;
import org.gudy.azureus2.plugins.peers.PeerListener2;
import org.gudy.azureus2.plugins.peers.PeerManager;
import org.gudy.azureus2.plugins.peers.PeerManagerListener;
import org.gudy.azureus2.plugins.peers.PeerStats;
import org.gudy.azureus2.plugins.torrent.Torrent;
import org.gudy.azureus2.plugins.ui.config.BooleanParameter;
import org.gudy.azureus2.plugins.ui.config.Parameter;
import org.gudy.azureus2.plugins.ui.config.ParameterListener;
import org.gudy.azureus2.plugins.ui.model.BasicPluginConfigModel;
import org.gudy.azureus2.pluginsimpl.local.PluginCoreUtils;

import com.aelitis.azureus.core.AzureusCoreFactory;
import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.core.util.CopyOnWriteSet;
import com.aelitis.azureus.plugins.net.buddy.*;

public class 
BuddyPluginTracker 
	implements BuddyPluginListener, DownloadManagerListener, BuddyPluginAZ2TrackerListener, DownloadPeerListener
{
	private  static final Object	PEER_KEY		= new Object();		// maps to Download object
	
	private static final Object	PEER_STATS_KEY	= new Object();
	
	public static final int BUDDY_NETWORK_IDLE		= 1;
	public static final int BUDDY_NETWORK_OUTBOUND	= 2;
	public static final int BUDDY_NETWORK_INBOUND	= 3;
	
	private static final int	TRACK_CHECK_PERIOD		= 15*1000;
	private static final int	TRACK_CHECK_TICKS		= TRACK_CHECK_PERIOD/BuddyPlugin.TIMER_PERIOD;

	private static final int	PEER_CHECK_PERIOD		= 60*1000;
	private static final int	PEER_CHECK_TICKS		= PEER_CHECK_PERIOD/BuddyPlugin.TIMER_PERIOD;
	
	private static final int	PEER_RECHECK_PERIOD		= 120*1000;
	private static final int	PEER_RECHECK_TICKS		= PEER_RECHECK_PERIOD/BuddyPlugin.TIMER_PERIOD;

	private static final int	TRACK_INTERVAL			= 10*60*1000;
	
	private static final int	SHORT_ID_SIZE			= 4;
	private static final int	FULL_ID_SIZE			= 20;
	
	private static final int	REQUEST_TRACKER_SUMMARY	= 1;
	private static final int	REPLY_TRACKER_SUMMARY	= 2;
	private static final int	REQUEST_TRACKER_STATUS	= 3;
	private static final int	REPLY_TRACKER_STATUS	= 4;
	private static final int	REQUEST_TRACKER_CHANGE	= 5;
	private static final int	REPLY_TRACKER_CHANGE	= 6;
	private static final int	REQUEST_TRACKER_ADD		= 7;
	private static final int	REPLY_TRACKER_ADD		= 8;
	
	private static final int	RETRY_SEND_MIN			= 5*60*1000;
	private static final int	RETRY_SEND_MAX			= 60*60*1000;
	
	private static final int	BUDDY_NO		= 0;
	private static final int	BUDDY_MAYBE		= 1;
	private static final int	BUDDY_YES		= 2;
	
	private BuddyPlugin		plugin;
	
	private boolean			plugin_enabled;
	private boolean			tracker_enabled;
	private boolean			seeding_only;
	
	private boolean			old_plugin_enabled;
	private boolean			old_tracker_enabled;
	private boolean			old_seeding_only;
	
	private int				network_status = BUDDY_NETWORK_IDLE;
	
	private Set				online_buddies 			= new HashSet();
	private Map				online_buddy_ips		= new HashMap();
	
	private Set				tracked_downloads		= new HashSet();
	private int				download_set_id;
	
	private Set				last_processed_download_set;
	private int				last_processed_download_set_id;
	
	private Map				short_id_map	= new HashMap();
	private Map				full_id_map		= new HashMap();
	
	private Set				actively_tracking	= new HashSet();
		
	private CopyOnWriteSet<Peer>	buddy_peers	= new CopyOnWriteSet<Peer>( true );
	
	private CopyOnWriteList	listeners = new CopyOnWriteList();
	
	private TimerEventPeriodic	buddy_stats_timer;
	
	private Average buddy_receive_speed = Average.getInstance(1000, 10);
	
	private Average buddy_send_speed 	= Average.getInstance(1000, 10);
	
	public
	BuddyPluginTracker(
		BuddyPlugin					_plugin,
		final BooleanParameter 		tracker_enable	)
	{
		plugin		= _plugin;	
		
		tracker_enabled = tracker_enable.getValue();
		
		tracker_enable.addListener(
			new ParameterListener()
			{
				public void 
				parameterChanged(
					Parameter param )
				{
					tracker_enabled = tracker_enable.getValue();
					
					checkEnabledState();
				}
			});
		
		// Assumed if we already have a plugin reference, that the
		// Azureus Core is available
		GlobalManager gm = AzureusCoreFactory.getSingleton().getGlobalManager();
		
		gm.addListener(
			new GlobalManagerAdapter()
			{
				public void 
				seedingStatusChanged( 
					boolean seeding_only_mode,
					boolean potentially_seeding_only )
				{
					seeding_only = potentially_seeding_only;
					
					checkEnabledState();
				}
			}, false );
		
		seeding_only = gm.isPotentiallySeedingOnly();
		
		checkEnabledState();
	}
	
	public void
	initialise()
	{
		plugin_enabled = plugin.isClassicEnabled();
		
		checkEnabledState();
		
		List buddies = plugin.getBuddies();
		
		for (int i=0;i<buddies.size();i++){
			
			buddyAdded((BuddyPluginBuddy)buddies.get(i));
		}
		
		plugin.addListener( this );
		
		plugin.getAZ2Handler().addTrackerListener( this );

		plugin.getPluginInterface().getDownloadManager().addListener( this, true );
	}
	
	public void
	tick(
		int		tick_count )
	{
		if ( tick_count % TRACK_CHECK_TICKS == 0 ){
			
			checkTracking();
		}
		
		if ( ( tick_count-1 ) % TRACK_CHECK_TICKS == 0 ){
			
			doTracking();
		}
		
		if ( tick_count % PEER_CHECK_TICKS == 0 ){
			
			checkPeers();
		}
		
		if ( tick_count % PEER_RECHECK_TICKS == 0 ){
			
			recheckPeers();
		}
	}
	
	public int
	getNetworkStatus()
	{
		return( network_status );
	}
	
	public long
	getNetworkReceiveBytesPerSecond()
	{
		return( buddy_receive_speed.getAverage());
	}
	
	public long
	getNetworkSendBytesPerSecond()
	{
		return( buddy_send_speed.getAverage());
	}
	
	protected void
	doTracking()
	{
		if ( !( plugin_enabled && tracker_enabled )){
			
			return;
		}

		Map	to_do = new HashMap();
		
		Set active_set = new HashSet();
		
		synchronized( online_buddies ){

			Iterator it = online_buddies.iterator();
				
			while( it.hasNext()){
				
				BuddyPluginBuddy	buddy = (BuddyPluginBuddy)it.next();
				
				buddyData buddy_data = getBuddyData( buddy );
				
				Map active = buddy_data.getDownloadsToTrack();
				
				if ( active.size() > 0 ){
					
					Iterator it2 = active.entrySet().iterator();
					
					List track_now = new ArrayList();
					
					while( it2.hasNext()){
						
						Map.Entry entry = (Map.Entry)it2.next();
						
						Download 	dl 	= (Download)entry.getKey();
						boolean		now = ((Boolean)entry.getValue()).booleanValue();
						
						if ( now ){
							
							track_now.add( dl );
						}
						
						active_set.add( dl );
					}
					
					if( track_now.size() > 0 ){
					
						to_do.put( buddy, track_now );
					}
				}
			}
		}
		
		synchronized( actively_tracking ){
			
			Iterator it = active_set.iterator();
			
			while( it.hasNext()){
				
				Download dl = (Download)it.next();
				
				if ( !actively_tracking.contains( dl )){
					
					actively_tracking.add( dl );
					
					trackPeers( dl );
				}
			}
			
			it = actively_tracking.iterator();
			
			while( it.hasNext()){
				
				Download dl = (Download)it.next();
				
				if ( !active_set.contains( dl )){
					
					it.remove();
					
					untrackPeers( dl );
				}
			}
		}
		
		Iterator it = to_do.entrySet().iterator();
		
		while( it.hasNext()){
			
			Map.Entry	entry = (Map.Entry)it.next();
					
			BuddyPluginBuddy buddy = (BuddyPluginBuddy)entry.getKey();
			
			if ( !buddy.isOnline( false )){
				
				continue;
			}
			
			InetAddress ip 			= buddy.getAdjustedIP();
			
			if ( ip == null ){
				
				continue;
			}
			
			int			tcp_port	= buddy.getTCPPort();
			int			udp_port	= buddy.getUDPPort();
			
			List	downloads = (List)entry.getValue();
			
			for (int i=0;i<downloads.size();i++){
				
				Download	download = (Download)downloads.get(i);
				
				PeerManager pm = download.getPeerManager();
				
				if ( pm == null ){
					
					continue;
				}
				
				Peer[] existing_peers = pm.getPeers( ip.getHostAddress());
			
				boolean	connected = false;
				
				for (int j=0;j<existing_peers.length;j++){
					
					Peer peer = existing_peers[j];
					
					if ( 	peer.getTCPListenPort() == tcp_port ||
							peer.getUDPListenPort() == udp_port ){
						
						connected = true;
						
						break;
					}	
				}
				
				if ( connected ){
					
					log( download.getName() + " - peer " + ip.getHostAddress() + " already connected" );
					
					continue;
				}
				
				log( download.getName() + " - connecting to peer " + ip.getHostAddress());

				PEPeerManager c_pm = PluginCoreUtils.unwrap( pm ); 
				
				Map	user_data = new LightHashMap();
				
				user_data.put( PEER_KEY, download );
				
				user_data.put( Peer.PR_PRIORITY_CONNECTION, new Boolean( true ));
				
				c_pm.addPeer( ip.getHostAddress(), tcp_port, udp_port, true, user_data );
			}
		}
	}
	
	protected void
	checkTracking()
	{
		if ( !( plugin_enabled && tracker_enabled )){
			
			return;
		}
		
		List	online;
		
		synchronized( online_buddies ){

			online = new ArrayList( online_buddies );
		}
		
		Set			downloads;
		int			downloads_id;
		
		synchronized( tracked_downloads ){
			
			boolean downloads_changed = last_processed_download_set_id != download_set_id;
			
			if ( downloads_changed ){
				
				last_processed_download_set 	= new HashSet( tracked_downloads );
				last_processed_download_set_id	= download_set_id;
			}
			
			downloads 		= last_processed_download_set;
			downloads_id	= last_processed_download_set_id;
		}
		
		Map	diff_map = new HashMap();
		
		for (int i=0;i<online.size();i++){
			
			BuddyPluginBuddy	buddy = (BuddyPluginBuddy)online.get(i);
			
			buddyData buddy_data = getBuddyData( buddy );
			
			buddy_data.updateLocal( downloads, downloads_id, diff_map );
		}
	}		
	
	public void
	initialised(
		boolean		available )
	{	
	}
	
	public void
	buddyAdded(
		BuddyPluginBuddy	buddy )
	{
		buddyChanged( buddy );
	}
	
	public void
	buddyRemoved(
		BuddyPluginBuddy	buddy )
	{
		buddyChanged( buddy );
	}

	public void
	buddyChanged(
		BuddyPluginBuddy	buddy )
	{	
		if ( buddy.isOnline( false )){
			
			addBuddy( buddy );
			
		}else{
			
			removeBuddy( buddy );
		}
	}
	
	protected buddyData
	getBuddyData(
		BuddyPluginBuddy		buddy )
	{
		synchronized( online_buddies ){
			
			buddyData buddy_data = (buddyData)buddy.getUserData( BuddyPluginTracker.class );

			if ( buddy_data == null ){
				
				buddy_data = new buddyData( buddy );
				
				buddy.setUserData( BuddyPluginTracker.class, buddy_data );
			}
			
			return( buddy_data );
		}
	}
	
	protected buddyData
	addBuddy(
		BuddyPluginBuddy		buddy )
	{
		synchronized( online_buddies ){
			
			if ( !online_buddies.contains( buddy )){
				
				online_buddies.add( buddy );
			}
			
			buddyData bd = getBuddyData( buddy );
			
			if ( bd.hasIPChanged()){
				
				String	ip = bd.getIP();
				
				if ( ip != null ){
					
					List	l = (List)online_buddy_ips.get( ip );
					
					if ( l != null ){
						
						l.remove( buddy );
						
						if ( l.size() == 0 ){
							
							online_buddy_ips.remove( ip );
						}
					}
				}
				
				bd.updateIP();
				
				ip = bd.getIP();
				
				if ( ip != null ){
					
					List l = (List)online_buddy_ips.get( ip );
					
					if ( l == null ){
						
						l = new ArrayList();
						
						online_buddy_ips.put( ip, l );
					}
				
					l.add( buddy );
				}
			}
			
			return( bd );
		}
	}
		
	protected void
	removeBuddy(
		BuddyPluginBuddy		buddy )
	{		
		synchronized( online_buddies ){

			if ( online_buddies.contains( buddy )){
				
				buddyData bd = getBuddyData( buddy );

				online_buddies.remove( buddy );
				
				String	ip = bd.getIP();
				
				if ( ip != null ){
					
					List	l = (List)online_buddy_ips.get( ip );
					
					if ( l != null ){
						
						l.remove( buddy );
						
						if ( l.size() == 0 ){
							
							online_buddy_ips.remove( ip );
						}
					}
				}
			}
		}
	}
	
	protected int
	isBuddy(
		Peer		peer )
	{
		String	peer_ip = peer.getIp();
		
		List ips = AddressUtils.getLANAddresses( peer_ip );

		synchronized( online_buddies ){
			
			int	result = BUDDY_NO;
		
			String	tested = "";
outer:	
			for (int i=0;i<ips.size();i++){

				String ip = (String)ips.get(i);
				
				tested += ip;
				
				List buddies =(List)online_buddy_ips.get( ip  );
			
				if ( buddies != null ){
									
					if ( peer.getTCPListenPort() == 0 && peer.getUDPListenPort() == 0 ){
						
						result = BUDDY_MAYBE;
						
					}else{
					
						for (int j=0;j<buddies.size();j++){
								
							BuddyPluginBuddy	buddy = (BuddyPluginBuddy)buddies.get(j);
								
							if (	buddy.getTCPPort() == peer.getTCPListenPort() &&
									buddy.getTCPPort() != 0 ){
								
								result =  BUDDY_YES;
								
								break outer;
							}
									
							if (	buddy.getUDPPort() == peer.getUDPListenPort() &&
									buddy.getUDPPort() != 0 ){
								
								result =  BUDDY_YES;
								
								break outer;
							}
						}
					}
				}
			}
			
			// log( "isBuddy: " + peer_ip + " -> " + result + ",tested=" + tested );
			
			return( result );
		}
	}
	
	public void
	messageLogged(
		String		str,
		boolean		error )
	{	
	}
	
	public void
	enabledStateChanged(
		boolean 	_enabled )
	{
		plugin_enabled = _enabled;
		
		checkEnabledState();
	}
	
	public void 
	updated() 
	{
	}
	
	public boolean
	isEnabled()
	{
		synchronized( this ){
			
			return( plugin_enabled && tracker_enabled );
		}
	}
	
	protected void
	checkEnabledState()
	{
		boolean	seeding_change 	= false;
		boolean	enabled_change	= false;
		
		synchronized( this ){
			
			boolean	old_enabled = old_plugin_enabled && old_tracker_enabled;
			
			if ( plugin_enabled != old_plugin_enabled ){
				
				log( "Plugin enabled state changed to " + plugin_enabled );
				
				old_plugin_enabled = plugin_enabled;
			}
			
			if ( tracker_enabled != old_tracker_enabled ){
				
				log( "Tracker enabled state changed to " + tracker_enabled );
				
				old_tracker_enabled = tracker_enabled;
			}
			
			if ( seeding_only != old_seeding_only ){
				
				log( "Seeding-only state changed to " + seeding_only );
				
				old_seeding_only = seeding_only;
				
				seeding_change = true;
			}
			
			enabled_change = old_enabled != ( plugin_enabled && tracker_enabled );
		}
		
		if ( seeding_change ){
			
			updateSeedingMode();
		}
		
		if ( enabled_change ){
			
			fireEnabledChanged( isEnabled());
		}
	}
	
	protected void
	updateSeedingMode()
	{
		updateNetworkStatus();
		
		List	online;
		
		synchronized( online_buddies ){

			online = new ArrayList( online_buddies );
		}
		
		for (int i=0;i<online.size();i++){
			
			buddyData buddy_data = getBuddyData((BuddyPluginBuddy)online.get(i));
			
			if ( buddy_data.hasDownloadsInCommon()){
				
				buddy_data.updateStatus();
			}
		}
	}
	
	public void
	downloadAdded(
		final Download	download )
	{
		Torrent t = download.getTorrent();
		
		if ( t == null ){
			
			return;
		}
		
		if ( t.isPrivate()){
			
			download.addTrackerListener(
				new DownloadTrackerListener()
				{
					public void 
					scrapeResult(
						DownloadScrapeResult result )
					{					
					}
	
					public void 
					announceResult(
						DownloadAnnounceResult result)
					{
						if ( okToTrack( download )){
							
							trackDownload( download );
							
						}else{
							
							untrackDownload( download );
						}						
					}
				}, 
				false );
		}
		
		if ( okToTrack( download )){
			
			trackDownload( download );
		}
		
		download.addListener(
			new DownloadListener()
			{
				public void
				stateChanged(
					Download		download,
					int				old_state,
					int				new_state )
				{
					if ( okToTrack( download )){
						
						trackDownload( download );
						
					}else{
						
						untrackDownload( download );
					}
				}

				public void
				positionChanged(
					Download	download, 
					int 		oldPosition,
					int 		newPosition )
				{
				}
			});
	}
	
	public void
	downloadRemoved(
		Download	download )
	{
		untrackDownload( download );
	}
	
	protected void
	trackDownload(
		Download	download )
	{
		synchronized( tracked_downloads ){
			
			if ( tracked_downloads.contains( download )){
				
				return;
			}
							
			downloadData download_data = new downloadData( download );
				
			download.setUserData( BuddyPluginTracker.class, download_data );
			
			HashWrapper	full_id		= download_data.getID();
			
			HashWrapper short_id 	= new HashWrapper( full_id.getHash(), 0, 4 );
			
			full_id_map.put( full_id, download );
			
			List	dls = (List)short_id_map.get( short_id );
			
			if ( dls == null ){
				
				dls = new ArrayList();
				
				short_id_map.put( short_id, dls );
			}
			
			dls.add( download );
			
			tracked_downloads.add( download );
			
			download_set_id++;
		}
	}
	
	protected void
	untrackDownload(
		Download	download )
	{
		synchronized( tracked_downloads ){
			
			if ( tracked_downloads.remove( download )){
				
				download_set_id++;
				
				downloadData download_data = (downloadData)download.getUserData( BuddyPluginTracker.class );
								
				download.setUserData( BuddyPluginTracker.class, null );
				
				HashWrapper	full_id		= download_data.getID();
				
				full_id_map.remove( full_id );
				
				HashWrapper short_id 	= new HashWrapper( full_id.getHash(), 0, SHORT_ID_SIZE );
				
				List	dls = (List)short_id_map.get( short_id );

				if ( dls != null ){
					
					dls.remove( download );
					
					if ( dls.size() == 0 ){
						
						short_id_map.remove( short_id );
					}
				}
			}
		}
		
		synchronized( online_buddies ){

			Iterator it = online_buddies.iterator();
				
			while( it.hasNext()){
				
				BuddyPluginBuddy	buddy = (BuddyPluginBuddy)it.next();
				
				buddyData buddy_data = getBuddyData( buddy );
				
				buddy_data.resetTracking( download );
			}
		}
		
		synchronized( actively_tracking ){

			actively_tracking.remove( download );
		}
	}
	
	protected void
	trackPeers(
		final Download		download )
	{
		PeerManager pm = download.getPeerManager();
		
			// not running
		
		if ( pm == null ){

			synchronized( actively_tracking ){

				actively_tracking.remove( download );
			}
		}else{
			
			log( "Tracking peers for " + download.getName());
						
			download.addPeerListener( this );
		}
	}
	
	public void
	peerManagerAdded(
		Download		download,
		PeerManager		peer_manager )
	{
		trackPeers( download, peer_manager );
	}
	
	public void
	peerManagerRemoved(
		Download		download,
		PeerManager		peer_manager )
	{
		synchronized( actively_tracking ){

			actively_tracking.remove( download );
		}
		
		download.removePeerListener( this );
	}
	
	protected void
	trackPeers(
		final Download	download,
		PeerManager		pm )
	{
		pm.addListener(
			new PeerManagerListener()
			{
				public void
				peerAdded(
					PeerManager		manager,
					Peer			peer )
				{
					synchronized( actively_tracking ){
						
						if ( !actively_tracking.contains( download )){
							
							manager.removeListener( this );
							
							return;
						}
					}
				
					trackPeer( download, peer );
				}
				
				public void
				peerRemoved(
					PeerManager	manager,
					Peer		peer )
				{
				}
			});
		
		Peer[] peers = pm.getPeers();
		
		for (int i=0;i<peers.length;i++){
			
			trackPeer( download, peers[i] );
		}
	}
	
	protected void
	trackPeer(
		final Download	download,
		final Peer		peer )
	{
		int type = isBuddy( peer );
		
		if ( type == BUDDY_YES ){
			
			markBuddyPeer( download, peer );
			
		}else if ( type == BUDDY_MAYBE ){
			
				// mark as peer early so that we get optimistic disconnect if needed
			
			markBuddyPeer( download, peer );
			
			PeerListener2 listener = 
				new PeerListener2()
				{
					public void 
					eventOccurred(
						PeerEvent event )
					{
						if ( event.getType() == PeerEvent.ET_STATE_CHANGED ){
							
							if (((Integer)event.getData()).intValue() == Peer.TRANSFERING ){
								
								peer.removeListener( this );
								
									// withdraw buddy marker if it turns out our earlier optimism
									// was misplaced
								
								if ( isBuddy( peer ) != BUDDY_YES ){
									
									unmarkBuddyPeer( peer );
								}
							}
						}
					}
				};
				
			peer.addListener( listener );

			if ( peer.getState() == Peer.TRANSFERING ){
				
				peer.removeListener( listener );
				
					// withdraw buddy marker if it turns out our earlier optimism
					// was misplaced
			
				if ( isBuddy( peer ) != BUDDY_YES ){
					
					unmarkBuddyPeer( peer );
				}
			}
		}
	}
	
	protected void
	untrackPeers(
		Download		download )
	{
		log( "Not tracking peers for " + download.getName());
		
		download.removePeerListener( this );
		
		PeerManager pm = download.getPeerManager();
		
		if ( pm != null ){

			Peer[] peers = pm.getPeers();
			
			for (int i=0;i<peers.length;i++){
				
				Peer	peer = peers[i];
				
				unmarkBuddyPeer( peer );
			}
		}
	}
	
	protected void
	markBuddyPeer(
		final Download		download,
		final Peer			peer )
	{
		boolean	state_changed 	= false;
		
		synchronized( buddy_peers ){
			
			if ( !buddy_peers.contains( peer )){
				
				log( "Adding buddy peer " + peer.getIp());
				
				if ( buddy_peers.size() == 0 ){
					
					if ( buddy_stats_timer == null ){
						
						buddy_stats_timer = 
							SimpleTimer.addPeriodicEvent(
								"BuddyTracker:stats",
								1000,
								new TimerEventPerformer()
								{
									public void 
									perform(
										TimerEvent event )
									{
										Iterator it = buddy_peers.iterator();
										
										long	total_sent		= 0;
										long	total_received	= 0;
										
										while( it.hasNext()){
											
											Peer	p = (Peer)it.next();
											
											PeerStats ps = p.getStats();
											
											long sent		= ps.getTotalSent();
											long received 	= ps.getTotalReceived();
											
											long[]	last = (long[])p.getUserData( PEER_STATS_KEY );
											
											if ( last != null ){
												
												total_sent 		+= sent - last[0];
												total_received	+= received - last[1];
											}
											
											p.setUserData( PEER_STATS_KEY, new long[]{ sent, received });
										}
										
										buddy_receive_speed.addValue( total_received );
										buddy_send_speed.addValue( total_sent );
									}
								});
					}
					
					state_changed 	= true;
				}
				
				buddy_peers.add( peer );
				
				peer.setUserData( PEER_KEY, download );
				
				peer.setPriorityConnection( true );
								
				log( download.getName() + ": adding buddy peer " + peer.getIp());

				peer.addListener(
					new PeerListener2()
					{ 
						public void 
						eventOccurred(
							PeerEvent event )
						{
							if ( event.getType() == PeerEvent.ET_STATE_CHANGED ){
								
								int	state = ((Integer)event.getData()).intValue();
								
								if ( state == Peer.CLOSING || state == Peer.DISCONNECTED ){
									
									peer.removeListener( this );
									
									unmarkBuddyPeer( peer );
								}
							}	
						}
					});
			}
		}
		
		if ( peer.getState() == Peer.CLOSING || peer.getState() == Peer.DISCONNECTED ){
			
			unmarkBuddyPeer( peer );
		}
		
		if ( state_changed ){
			
			updateNetworkStatus();
		}
	}
	
	protected void
	unmarkBuddyPeer(
		Peer		peer )
	{
		boolean	state_changed = false;
		
		synchronized( buddy_peers ){

			Download download = (Download)peer.getUserData( PEER_KEY );
				
			if ( download == null ){
				
				return;
			}

			if ( buddy_peers.remove( peer )){
				
				if ( buddy_peers.size() == 0 ){
					
					state_changed = true;
					
					if ( buddy_stats_timer != null ){
						
						buddy_stats_timer.cancel();
						
						buddy_stats_timer = null;
					}
				}
				
				log( download.getName() + ": removing buddy peer " + peer.getIp());
			}
			
			peer.setUserData( PEER_KEY, null );
			
			peer.setPriorityConnection( false );
		}	
		
		if ( state_changed ){
			
			updateNetworkStatus();
		}
	}
	
	protected void
	checkPeers()
	{
		List	to_unmark = new ArrayList();
		
		synchronized( buddy_peers ){

			Iterator	it = buddy_peers.iterator();
			
			while( it.hasNext()){
				
				Peer	peer = (Peer)it.next();
				
				if ( peer.getState() == Peer.CLOSING || peer.getState() == Peer.DISCONNECTED ){
					
					to_unmark.add( peer );
				}
			}
		}
		
		for (int i=0;i<to_unmark.size();i++){
			
			unmarkBuddyPeer((Peer)to_unmark.get(i));
		}
	}
	
	protected void
	recheckPeers()
	{
			// go over peers for active torrents to see if we've missed and. can really only
			// happen with multi-homed LAN setups where a new (and utilised) route is found
			// after we start tracking 
		
		synchronized( actively_tracking ){
			
			Iterator it = actively_tracking.iterator();
			
			while( it.hasNext()){
				
				Download download = (Download)it.next();
				
				PeerManager pm = download.getPeerManager();
				
				if ( pm != null ){
					
					Peer[] peers = pm.getPeers();
					
					for (int i=0;i<peers.length;i++){
						
						trackPeer( download, peers[i] );
					}
				}
			}
		}
	}
	
	protected void
	updateNetworkStatus()
	{	
		int		new_status;
		boolean	changed = false;
		
		synchronized( buddy_peers ){
	
			if ( buddy_peers.size() == 0 ){
				
				new_status 	= BUDDY_NETWORK_IDLE;
				
			}else{
				
				new_status	= seeding_only?BUDDY_NETWORK_OUTBOUND:BUDDY_NETWORK_INBOUND;
			}
			
			if ( new_status != network_status ){
				
				network_status	= new_status;
				
				changed	= true;
			}
		}
		
		if ( changed ){
		
			fireStateChange( new_status );
		}
	}
	
	public void
	addListener(
		BuddyPluginTrackerListener		l )
	{
		listeners.add( l );
	}
	
	public void
	removeListener(
		BuddyPluginTrackerListener		l )
	{
		listeners.remove( l );
	}
	
	protected void
	fireStateChange(
		int		state )
	{
		Iterator	it = listeners.iterator();
		
		while( it.hasNext()){
			
			try{
				((BuddyPluginTrackerListener)it.next()).networkStatusChanged( this, state );
				
			}catch( Throwable e ){
				
				Debug.out( e );
			}
		}
	}
	
	protected void
	fireEnabledChanged(
		boolean	enabled )
	{
		Iterator	it = listeners.iterator();
		
		while( it.hasNext()){
			
			try{
				((BuddyPluginTrackerListener)it.next()).enabledStateChanged( this, enabled );
				
			}catch( Throwable e ){
				
				Debug.out( e );
			}
		}
	}
	
	protected void
	sendMessage(
		BuddyPluginBuddy	buddy,
		int					type,
		Map					body )
	{
		Map	msg = new HashMap();
		
		msg.put( "type", new Long( type ));
		msg.put( "msg", body );
		
		plugin.getAZ2Handler().sendAZ2TrackerMessage(
				buddy, 
				msg, 
				BuddyPluginTracker.this );
	}
	
	public Map
	messageReceived(
		BuddyPluginBuddy	buddy,
		Map					message )
	{
		buddyData buddy_data = buddyAlive( buddy );
		
		int type = ((Long)message.get( "type" )).intValue();
		
		Map msg = (Map)message.get( "msg" );
		
		return( buddy_data.receiveMessage( type, msg ));
	}
	
	public void
	messageFailed(
		BuddyPluginBuddy	buddy,
		Throwable			cause )
	{
		log( "Failed to send message to " + buddy.getName(), cause );
		
		buddyDead( buddy );
	}
	
	protected buddyData
	buddyAlive(
		BuddyPluginBuddy		buddy )
	{
		buddyData buddy_data = addBuddy( buddy );
		
		buddy_data.setAlive( true );
		
		return( buddy_data );
	}
	
	protected void
	buddyDead(
		BuddyPluginBuddy		buddy )
	{
		buddyData buddy_data = getBuddyData( buddy );

		if ( buddy_data != null ){
			
			buddy_data.setAlive( false );
		}
	}
	
	protected boolean
	okToTrack(
		Download	d )
	{
		Torrent	t = d.getTorrent();
		
		if ( t == null ){
			
			return( false );
		}
		
			// only track private torrents if we have successfully received peers from tracker
			// which means we have the torrent legitimately. As this rule is enforced by both
			// ends of the tracking operation it means we will only track between peers that
			// both have a legitimate copy of the torrent.
				
		if ( t.isPrivate()){
			
			DownloadAnnounceResult announce = d.getLastAnnounceResult();

			if ( 	announce == null ||
					announce.getResponseType() != DownloadAnnounceResult.RT_SUCCESS ||
					announce.getPeers().length < 2 ){
				
				return( false );
			}
		}
		
		int state = d.getState();
		
		return( 	state != Download.ST_ERROR && 
					state != Download.ST_STOPPING && 
					state != Download.ST_STOPPED );
	}
	
	protected void
	log(
		String		str )
	{
		plugin.log( "Tracker: " + str );
	}
	
	protected void
	log(
		String		str,
		boolean		verbose )
	{
		if ( verbose ){
			
			if ( Constants.isCVSVersion()){
			
				log( str );
			}
		}else{
			
			log( str );
		}
	}
	
	protected void
	log(
		String		str,
		Throwable 	e )
	{
		plugin.log( "Tracker: " + str, e );
	}
	
	private class
	buddyData
	{
		private BuddyPluginBuddy		buddy;
		
		private Set	downloads_sent;
		private int	downloads_sent_id;
		
		private Map		downloads_in_common;
		private boolean	buddy_seeding_only;
		
		private int		consecutive_fails;
		private long	last_fail;
		
		private String	current_ip;
		
		protected
		buddyData(
			BuddyPluginBuddy		_buddy )
		{
			buddy	= _buddy;
		}
		
		protected void
		updateIP()
		{
			InetAddress	latest_ip = buddy.getAdjustedIP();
			
			if ( latest_ip != null ){
			
				current_ip	= latest_ip.getHostAddress();
			
				log( "IP set to " + current_ip );
			}
		}
			
		protected boolean
		hasIPChanged()
		{
			InetAddress	latest_ip = buddy.getAdjustedIP();
			
			if ( latest_ip == null && current_ip == null ){
				
				return( false );
				
			}else if ( latest_ip == null || current_ip == null ){
				
				return( true );
				
			}else{
			
				return(	!current_ip.equals( latest_ip.getHostAddress()));
			}
		}
		
		protected String
		getIP()
		{
			return( current_ip );
		}
		
		protected boolean
		hasDownloadsInCommon()
		{
			synchronized( this ){
			
				return( downloads_in_common != null );
			}
		}
		
		protected void
		setAlive(
			boolean		alive )
		{
			synchronized( this ){
				
				if ( alive ){
					
					consecutive_fails		= 0;
					last_fail				= 0;

				}else{
					
					consecutive_fails++;
					
					last_fail	= SystemTime.getMonotonousTime();
				}
			}
		}
		
		protected void
		updateLocal(
			Set		downloads,
			int		id,
			Map		diff_map )
		{
			if ( consecutive_fails > 0 ){
				
				long	retry_millis = RETRY_SEND_MIN;
				
				for (int i=0;i<consecutive_fails-1;i++){
					
					retry_millis <<= 2;
					
					if ( retry_millis > RETRY_SEND_MAX ){
						
						retry_millis = RETRY_SEND_MAX;
						
						break;
					}
				}
				
				long	now = SystemTime.getMonotonousTime();
				
				if ( now - last_fail >= retry_millis ){
					
					last_fail			= now;	// assume we're going to fail so we avoid
												// falling through here multiple times before
												// actuallt failing again
					
					downloads_sent 		= null;
					downloads_sent_id	= 0;
				}
			}
			
				// first check to see if completion state changed for any common downloads
			
			List	comp_changed = new ArrayList();
			
			synchronized( this ){
				
				if ( downloads_in_common != null ){
					
					Iterator it = downloads_in_common.entrySet().iterator();
					
					while( it.hasNext()){
						
						Map.Entry	entry = (Map.Entry)it.next();
						
						Download d = (Download)entry.getKey();

						buddyDownloadData	bdd = (buddyDownloadData)entry.getValue();
						
						boolean	local_complete = d.isComplete( false );
						
						if ( local_complete != bdd.isLocalComplete()){
							
							bdd.setLocalComplete( local_complete );
							
							comp_changed.add( d );
						}
					}
				}
			}
			
			if ( comp_changed.size() > 0 ){
				
				byte[][] change_details = exportFullIDs( comp_changed );
				
				if( change_details[0].length > 0 ){
					
					Map	msg = new HashMap();
										
					msg.put( "seeding", new Long( seeding_only?1:0 ));
					
					msg.put( "change", 		change_details[0] );
					msg.put( "change_s", 	change_details[1] );

					sendMessage( buddy, REQUEST_TRACKER_CHANGE, msg );
				}
			}
			
			if ( id == downloads_sent_id ){
				
				return;
			}
			
			Long	key = new Long(((long)id) << 32 | (long)downloads_sent_id);
				
			Object[]	diffs = (Object[])diff_map.get( key );
			
			boolean	incremental = downloads_sent != null;
			
			byte[]	added_bytes;
			byte[]	removed_bytes;
			
			if ( diffs == null ){
				
				List	added;
				List	removed	= new ArrayList();
				

				if ( downloads_sent == null ){
					
					added 	= new ArrayList( downloads );
					
				}else{
					
					added	= new ArrayList();

					Iterator	it1 = downloads.iterator();
					
					while( it1.hasNext()){
					
						Download download = (Download)it1.next();
						
						if ( okToTrack( download )){
							
							if ( !downloads_sent.contains( download )){
								
								added.add( download );
							}
						}
					}
					
					Iterator	it2 = downloads_sent.iterator();
					
					while( it2.hasNext()){
					
						Download download = (Download)it2.next();
						
						if ( !downloads.contains( download )){
							
							removed.add( download );
						}
					}
				}
				
				added_bytes 	= exportShortIDs( added );
				removed_bytes	= exportFullIDs( removed )[0];
				
				diff_map.put( key, new Object[]{ added_bytes, removed_bytes });
			}else{
				
				added_bytes 	= (byte[])diffs[0];
				removed_bytes 	= (byte[])diffs[1];
			}
				
			downloads_sent 		= downloads;
			downloads_sent_id	= id;
			
			if ( added_bytes.length == 0 && removed_bytes.length == 0 ){
				
				return;
			}
			
			Map	msg = new HashMap();
			
			if ( added_bytes.length > 0 ){
				
				msg.put( "added", 	added_bytes );
			}
			
			if ( removed_bytes.length > 0 ){
				
				msg.put( "removed", removed_bytes );
			}
			
			msg.put( "inc", 	new Long( incremental?1:0 ));
			msg.put( "seeding", new Long( seeding_only?1:0 ));
			
			sendMessage( buddy, REQUEST_TRACKER_SUMMARY, msg );
		}	
		
		protected Map
		updateRemote(
			Map		msg )
		{			
			List	added 	= importShortIDs((byte[])msg.get( "added" ));
			
			Map	reply = new HashMap();
			
			byte[][] add_details = exportFullIDs( added );
			
			if( add_details[0].length > 0 ){
			
				reply.put( "added", 	add_details[0] );
				reply.put( "added_s", 	add_details[1] );
			}
			
			synchronized( this ){

				if ( downloads_in_common != null ){

					Map removed = importFullIDs( (byte[])msg.get( "removed" ), null );
					
					Iterator it = removed.keySet().iterator();
					
					while( it.hasNext()){
						
						Download d = (Download)it.next();
						
						if ( downloads_in_common.remove( d ) != null ){
							
							log( "Removed " + d.getName() + " common download", false, true );
						}
					}
				
					if ( downloads_in_common.size() == 0 ){
						
						downloads_in_common = null;
					}
				}
			}
			
			return( reply );
		}
		
		protected void
		updateCommonDownloads(
			Map			downloads,
			boolean		incremental )
		{
			synchronized( this ){

				if ( downloads_in_common == null ){
					
					downloads_in_common = new HashMap();
					
				}else{
					
						// if not incremental then remove any downloads that no longer
						// are in common
					
					if ( !incremental ){
						
						Iterator it = downloads_in_common.keySet().iterator();
						
						while( it.hasNext()){
							
							Download download = (Download)it.next();
							
							if ( !downloads.containsKey( download )){
								
								log( "Removing " + download.getName() + " from common downloads", false, true );

								it.remove();
							}
						}
					}
				}
				
				Iterator it = downloads.entrySet().iterator();
				
				while( it.hasNext()){
					
					Map.Entry	entry = (Map.Entry)it.next();
			
					Download d = (Download)entry.getKey();

					buddyDownloadData	bdd = (buddyDownloadData)entry.getValue();
					
					buddyDownloadData existing = (buddyDownloadData)downloads_in_common.get( d );
					
					if ( existing == null ){
						
						log( "Adding " + d.getName() + " to common downloads (bdd=" + bdd.getString() + ")", false, true );
						
						downloads_in_common.put( d, bdd );
						
					}else{
						
						boolean	old_rc = existing.isRemoteComplete();
						boolean	new_rc = bdd.isRemoteComplete();
						
						if ( old_rc != new_rc ){
						
							existing.setRemoteComplete( new_rc ); 
							
							log( "Changing " + d.getName() + " common downloads (bdd=" + existing.getString() + ")", false, true );
						}				
					}
				}
				
				if ( downloads_in_common.size() == 0 ){
					
					downloads_in_common = null;
				}
			}
		}
		
		protected void
		updateStatus()
		{
			Map	msg = new HashMap();
			
			msg.put( "seeding", new Long( seeding_only?1:0 ));
			
			sendMessage( buddy, REQUEST_TRACKER_STATUS, msg );
		}
		
		protected Map
		receiveMessage(
			int			type,
			Map			msg_in )
		{
			int	reply_type	= -1;
			Map	msg_out		= null;

			Long	l_seeding = (Long)msg_in.get( "seeding" );
			
			if( l_seeding != null ){
				
				boolean old = buddy_seeding_only;
				
				buddy_seeding_only = l_seeding.intValue() == 1;
				
				if ( old != buddy_seeding_only ){
					
					log( "Seeding only changed to " + buddy_seeding_only );
				}
			}
			
			if ( type == REQUEST_TRACKER_SUMMARY ){
		
				reply_type	= REPLY_TRACKER_SUMMARY;
				
				msg_out = updateRemote( msg_in );
				
				msg_out.put( "inc", msg_in.get( "inc" ));
								
			}else if ( type == REQUEST_TRACKER_STATUS ){
				
				reply_type	= REPLY_TRACKER_STATUS;
				
			}else if ( type == REQUEST_TRACKER_CHANGE ){

				reply_type	= REPLY_TRACKER_STATUS;
									
				Map downloads = importFullIDs( (byte[])msg_in.get( "changed" ), (byte[])msg_in.get( "changed_s" ) );

				updateCommonDownloads( downloads, true );
								
			}else if ( type == REQUEST_TRACKER_ADD ){

				reply_type	= REPLY_TRACKER_ADD;
										
				Map downloads = importFullIDs( (byte[])msg_in.get( "added" ), (byte[])msg_in.get( "added_s" ) );

				updateCommonDownloads( downloads, true );

			}else if ( type == REPLY_TRACKER_SUMMARY ){
				
					// full hashes on reply
				
				byte[]	possible_matches 		= (byte[])msg_in.get( "added" );
				byte[]	possible_match_states 	= (byte[])msg_in.get( "added_s" );

				boolean	incremental = ((Long)msg_in.get( "inc" )).intValue() == 1;
				
				if ( possible_matches != null && possible_match_states != null ){
							
					Map downloads = importFullIDs( possible_matches, possible_match_states );
						
					if ( downloads.size() > 0 ){
												
						updateCommonDownloads( downloads, incremental );
						
						byte[][] common_details = exportFullIDs( new ArrayList( downloads.keySet()));
						
						if( common_details[0].length > 0 ){
							
							Map	msg = new HashMap();
												
							msg.put( "seeding", new Long( seeding_only?1:0 ));
							
							msg.put( "added", 	common_details[0] );
							msg.put( "added_s", common_details[1] );

							sendMessage( buddy, REQUEST_TRACKER_ADD, msg );
						}
					}
				}
								
			}else if ( 	type == REPLY_TRACKER_CHANGE ||
						type == REPLY_TRACKER_STATUS ||
						type == REPLY_TRACKER_ADD ){
				
					// nothing interesting in reply for these
			}else{
				
				log( "Unrecognised type " + type );
			}
			
			if ( reply_type != -1 ){
				
				Map	reply = new HashMap();
			
				reply.put( "type", new Long( reply_type ));
		
				if ( msg_out == null ){
					
					msg_out = new HashMap();
				}
				
				msg_out.put( "seeding", new Long( seeding_only?1:0 ));
			
				reply.put( "msg", msg_out );

				return( reply );
			}
			
			return( null );
		}
		
		protected byte[]
		exportShortIDs(
			List	downloads )
		{
			byte[]	res = new byte[ SHORT_ID_SIZE * downloads.size() ];
			
			for (int i=0;i<downloads.size();i++ ){
				
				Download download = (Download)downloads.get(i);
				
				downloadData download_data = (downloadData)download.getUserData( BuddyPluginTracker.class );
				
				if ( download_data != null ){

					System.arraycopy(
						download_data.getID().getBytes(),
						0,
						res,
						i * SHORT_ID_SIZE,
						SHORT_ID_SIZE );
				}
			}
			
			return( res );
		}
		
		protected List
		importShortIDs(
			byte[]		ids )
		{
			List	res = new ArrayList();
			
			if ( ids != null ){
				
				synchronized( tracked_downloads ){

					for (int i=0;i<ids.length;i+= SHORT_ID_SIZE ){
					
						List dls = (List)short_id_map.get( new HashWrapper( ids, i, SHORT_ID_SIZE ));
						
						if ( dls != null ){
							
							res.addAll( dls );
						}
					}
				}
			}
			
			return( res );
		}
		
		protected byte[][]
   		exportFullIDs(
   			List	downloads )
   		{
   			byte[]	hashes 	= new byte[ FULL_ID_SIZE * downloads.size() ];
   			byte[] 	states	= new byte[ downloads.size()];
   			
   			for (int i=0;i<downloads.size();i++ ){
   				
   				Download download = (Download)downloads.get(i);
   				
   				downloadData download_data = (downloadData)download.getUserData( BuddyPluginTracker.class );
   				
   				if ( download_data != null ){

   					System.arraycopy(
   						download_data.getID().getBytes(),
   						0,
   						hashes,
   						i * FULL_ID_SIZE,
   						FULL_ID_SIZE );
   					
   					states[i] = download.isComplete( false )?(byte)0x01:(byte)0x00;
   				}
   			}
   			
   			return( new byte[][]{ hashes, states });
   		}
		
		protected Map
		importFullIDs(
			byte[]		ids,
			byte[]		states )
		{
			Map	res = new HashMap();
			
			if ( ids != null ){
				
				synchronized( tracked_downloads ){

					for (int i=0;i<ids.length;i+= FULL_ID_SIZE ){
					
						Download dl = (Download)full_id_map.get( new HashWrapper( ids, i, FULL_ID_SIZE ));
						
						if ( dl != null ){
							
							buddyDownloadData bdd = new buddyDownloadData( dl );
							
							if ( states != null ){
							
								bdd.setRemoteComplete(( states[i/FULL_ID_SIZE] & 0x01 ) != 0 );
							}
							
							res.put( dl, bdd );
						}
					}
				}
			}
			
			return( res );
		}
		
		protected Map
		getDownloadsToTrack()
		{
			Map	res = new HashMap();

			if ( seeding_only == buddy_seeding_only ){
				
				log( "Not tracking, buddy and me both " + (seeding_only?"seeding":"downloading"), true, false );

				return( res );
			}			
		
			long	now = SystemTime.getMonotonousTime();
			
			synchronized( this ){

				if ( downloads_in_common == null ){
					
					log( "Not tracking, buddy has nothing in common", true, false );

					return( res );
				}
				
				Iterator it = downloads_in_common.entrySet().iterator();
				
				while( it.hasNext()){
					
					Map.Entry	entry = (Map.Entry)it.next();
			
					Download d = (Download)entry.getKey();

					buddyDownloadData	bdd = (buddyDownloadData)entry.getValue();
					
					if ( d.isComplete( false ) && bdd.isRemoteComplete()){
						
							// both complete, nothing to do!
						
						log( d.getName() + " - not tracking, both complete", true, true );
						
					}else{
						
						long	last_track = bdd.getTrackTime();
						
						if ( 	last_track == 0 || 
								now - last_track >= TRACK_INTERVAL ){
							
							log( d.getName() + " - tracking", false, true );

							bdd.setTrackTime( now );
							
							res.put( d, new Boolean( true ));
							
						}else{
							
							res.put( d, new Boolean( false ));
						}
					}
				}
			}
			
			return( res );
		}
		
		protected void
		resetTracking(
			Download		download )
		{
			synchronized( this ){
				
				if ( downloads_in_common == null ){
					
					return;
				}
				
				buddyDownloadData bdd = (buddyDownloadData)downloads_in_common.get( download );
				
				if ( bdd != null ){
					
					bdd.resetTrackTime();
				}
			}
		}
		
		protected void
		log(
			String	str )
		{
			BuddyPluginTracker.this.log( buddy.getName() + ": " + str );
		}
		
		protected void
		log(
			String	str,
			boolean	verbose,
			boolean	no_buddy )
		{
			BuddyPluginTracker.this.log( (no_buddy?"":( buddy.getName() + ": ")) + str, verbose );
		}
	}
	
	private static class
	buddyDownloadData
	{
		private boolean	local_is_complete;
		private boolean	remote_is_complete;
		private long	last_track;
		
		protected
		buddyDownloadData(
			Download		download )
		{
			local_is_complete = download.isComplete( false );
		}
		
		protected void
		setLocalComplete(
			boolean		b )
		{
			local_is_complete	= b;
		}
	
		protected boolean
		isLocalComplete()
		{
			return( local_is_complete );
		}
		
		protected void
		setRemoteComplete(
			boolean		b )
		{
			remote_is_complete	= b;
		}
	
		protected boolean
		isRemoteComplete()
		{
			return( remote_is_complete );
		}
		
		protected void
		setTrackTime(
			long	time )
		{
			last_track	= time;
		}
		
		protected long
		getTrackTime()
		{
			return( last_track );
		}
		
		protected void
		resetTrackTime()
		{
			last_track	= 0;
		}
		
		protected String
		getString()
		{
			return( "lic=" + local_is_complete + ",ric=" + remote_is_complete + ",lt=" + last_track );
		}
	}
	
	private static class
	downloadData
	{
		private static final byte[]	IV = {(byte)0x7A, (byte)0x7A, (byte)0xAD, (byte)0xAB, (byte)0x8E, (byte)0xBF, (byte)0xCD, (byte)0x39, (byte)0x87, (byte)0x0, (byte)0xA4, (byte)0xB8, (byte)0xFE, (byte)0x40, (byte)0xA2, (byte)0xE8 }; 
			
		private HashWrapper	id;
		
		protected
		downloadData(
			Download	download )
		{
			Torrent t = download.getTorrent();
			
			if ( t != null ){
				
				byte[]	hash = t.getHash();
				
				SHA1	sha1 = new SHA1();
			
				sha1.update( ByteBuffer.wrap( IV ));
				sha1.update( ByteBuffer.wrap( hash ));
				
				id = new HashWrapper( sha1.digest() );
			}
		}
		
		protected HashWrapper
		getID()
		{
			return( id );
		}
	}
}
