/*
 * Created by Olivier Chalouhi
 * Modified Apr 13, 2004 by Alon Rohter
 * Heavily modified Sep 2005 by Joseph Bridgewater
 * Copyright (C) 2004, 2005, 2006 Aelitis, All Rights Reserved.
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * AELITIS, SAS au capital de 46,603.30 euros
 * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
 */

package org.gudy.azureus2.core3.peer.impl.control;


import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.*;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.config.ParameterListener;
import org.gudy.azureus2.core3.disk.*;
import org.gudy.azureus2.core3.disk.DiskManager.GettingThere;
import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.ipfilter.*;
import org.gudy.azureus2.core3.logging.*;
import org.gudy.azureus2.core3.peer.*;
import org.gudy.azureus2.core3.peer.impl.*;
import org.gudy.azureus2.core3.peer.util.PeerIdentityDataID;
import org.gudy.azureus2.core3.peer.util.PeerIdentityManager;
import org.gudy.azureus2.core3.peer.util.PeerUtils;
import org.gudy.azureus2.core3.torrent.TOTorrentException;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncer;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerResponse;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerResponsePeer;
import org.gudy.azureus2.core3.tracker.client.TRTrackerScraperResponse;
import org.gudy.azureus2.core3.util.*;
import org.gudy.azureus2.plugins.download.DownloadAnnounceResultPeer;
import org.gudy.azureus2.plugins.network.Connection;
import org.gudy.azureus2.plugins.network.OutgoingMessageQueue;
import org.gudy.azureus2.plugins.peers.Peer;
import org.gudy.azureus2.plugins.peers.PeerDescriptor;

import com.aelitis.azureus.core.networkmanager.LimitedRateGroup;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin;
import com.aelitis.azureus.core.networkmanager.impl.tcp.TCPConnectionManager;
import com.aelitis.azureus.core.networkmanager.impl.tcp.TCPNetworkManager;
import com.aelitis.azureus.core.networkmanager.impl.udp.UDPNetworkManager;
import com.aelitis.azureus.core.peermanager.control.PeerControlInstance;
import com.aelitis.azureus.core.peermanager.control.PeerControlScheduler;
import com.aelitis.azureus.core.peermanager.control.PeerControlSchedulerFactory;
import com.aelitis.azureus.core.peermanager.nat.PeerNATInitiator;
import com.aelitis.azureus.core.peermanager.nat.PeerNATTraversalAdapter;
import com.aelitis.azureus.core.peermanager.nat.PeerNATTraverser;
import com.aelitis.azureus.core.peermanager.peerdb.*;
import com.aelitis.azureus.core.peermanager.piecepicker.PiecePicker;
import com.aelitis.azureus.core.peermanager.piecepicker.PiecePickerFactory;
import com.aelitis.azureus.core.peermanager.unchoker.Unchoker;
import com.aelitis.azureus.core.peermanager.unchoker.UnchokerFactory;
import com.aelitis.azureus.core.peermanager.unchoker.UnchokerUtil;
import com.aelitis.azureus.core.peermanager.uploadslots.UploadHelper;
import com.aelitis.azureus.core.peermanager.uploadslots.UploadSlotManager;
import com.aelitis.azureus.core.tracker.TrackerPeerSource;
import com.aelitis.azureus.core.tracker.TrackerPeerSourceAdapter;
import com.aelitis.azureus.core.util.FeatureAvailability;
import com.aelitis.azureus.core.util.bloom.BloomFilter;
import com.aelitis.azureus.core.util.bloom.BloomFilterFactory;

/**
 * manages all peer transports for a torrent
 * 
 * @author MjrTom
 *			2005/Oct/08: Numerous changes for new piece-picking. Also
 *						a few optimizations and multi-thread cleanups
 *			2006/Jan/02: refactoring piece picking related code
 */

@SuppressWarnings("serial")
public class 
PEPeerControlImpl
extends LogRelation
implements 	PEPeerControl, ParameterListener, DiskManagerWriteRequestListener, PeerControlInstance, PeerNATInitiator,
DiskManagerCheckRequestListener, IPFilterListener
{
	private static final LogIDs LOGID = LogIDs.PEER;

	private static final boolean TEST_PERIODIC_SEEDING_SCAN_FAIL_HANDLING	= false;
	
	static{
		if ( TEST_PERIODIC_SEEDING_SCAN_FAIL_HANDLING ){
			Debug.out( "**** test periodic scan failure enabled ****" );
		}
	}
	
	private static final int WARNINGS_LIMIT = 2;

	private static final int	CHECK_REASON_DOWNLOADED		= 1;
	private static final int	CHECK_REASON_COMPLETE		= 2;
	private static final int	CHECK_REASON_SCAN			= 3;
	private static final int	CHECK_REASON_SEEDING_CHECK		= 4;
	private static final int	CHECK_REASON_BAD_PIECE_CHECK	= 5;

	private static final int	SEED_CHECK_WAIT_MARKER	= 65526;

		// config
	
	private static boolean 	disconnect_seeds_when_seeding;
	private static boolean 	enable_seeding_piece_rechecks;
	private static int		stalled_piece_timeout;
	private static boolean 	fast_unchoke_new_peers;
    private static float	ban_peer_discard_ratio;
    private static int		ban_peer_discard_min_kb;
    private static boolean	udp_fallback_for_failed_connection;
    private static boolean	udp_fallback_for_dropped_connection;  
    private static boolean	udp_probe_enabled;
    private static boolean	hide_a_piece;
    private static boolean	prefer_udp_default;
    
	static{
		
		COConfigurationManager.addAndFireParameterListeners(
			new String[]{
				"Disconnect Seed",
				"Seeding Piece Check Recheck Enable",
				"peercontrol.stalled.piece.write.timeout",
				"Peer.Fast.Initial.Unchoke.Enabled",
   				"Ip Filter Ban Discard Ratio",
   				"Ip Filter Ban Discard Min KB",
   				"peercontrol.udp.fallback.connect.fail",
   				"peercontrol.udp.fallback.connect.drop",
   				"peercontrol.udp.probe.enable",
   				"peercontrol.hide.piece",
   				"peercontrol.prefer.udp",
			},
			new ParameterListener()
			{
				public void 
				parameterChanged(
					String name )
				{
					disconnect_seeds_when_seeding 		= COConfigurationManager.getBooleanParameter("Disconnect Seed");
					enable_seeding_piece_rechecks 		= COConfigurationManager.getBooleanParameter("Seeding Piece Check Recheck Enable");
					stalled_piece_timeout				= COConfigurationManager.getIntParameter( "peercontrol.stalled.piece.write.timeout", 60*1000 );
					fast_unchoke_new_peers 				= COConfigurationManager.getBooleanParameter( "Peer.Fast.Initial.Unchoke.Enabled" );
					ban_peer_discard_ratio				= COConfigurationManager.getFloatParameter( "Ip Filter Ban Discard Ratio" );
					ban_peer_discard_min_kb				= COConfigurationManager.getIntParameter( "Ip Filter Ban Discard Min KB" );
					udp_fallback_for_failed_connection	= COConfigurationManager.getBooleanParameter( "peercontrol.udp.fallback.connect.fail" );
					udp_fallback_for_dropped_connection	= COConfigurationManager.getBooleanParameter( "peercontrol.udp.fallback.connect.drop" );				
					udp_probe_enabled					= COConfigurationManager.getBooleanParameter( "peercontrol.udp.probe.enable" );				
					hide_a_piece						= COConfigurationManager.getBooleanParameter( "peercontrol.hide.piece" );
					
					if ( hide_a_piece ){
						
						disconnect_seeds_when_seeding = false;
					}
					
					prefer_udp_default						= COConfigurationManager.getBooleanParameter( "peercontrol.prefer.udp" );
				}
			});
	}
	
	private static IpFilter ip_filter = IpFilterManagerFactory.getSingleton().getIPFilter();

	private volatile boolean	is_running 		= false;  
	private volatile boolean	is_destroyed 	= false;  

	private volatile ArrayList<PEPeer>	peer_transports_cow = new ArrayList<PEPeer>();	// Copy on write!
	private final AEMonitor     peer_transports_mon	= new AEMonitor( "PEPeerControl:PT");

	protected final PEPeerManagerAdapter	adapter;
	private final DiskManager           disk_mgr;
	private final DiskManagerPiece[]    dm_pieces;

	private PEPeerManager.StatsReceiver	stats_receiver;
	
	private final PiecePicker	piecePicker;
	private long				lastNeededUndonePieceChange;

	/** literally seeding as in 100% torrent complete */
	private boolean 			seeding_mode;	
	private boolean				restart_initiated;

	private final int       _nbPieces;     //how many pieces in the torrent
	private PEPieceImpl[]	pePieces;      //pieces that are currently in progress
	private int				nbPiecesActive;	// how many pieces are currently in progress

	private int				nbPeersSnubbed;

	private PeerIdentityDataID	_hash;
	private final byte[]        _myPeerId;
	private PEPeerManagerStats        _stats;
	//private final TRTrackerAnnouncer _tracker;
	//  private int _maxUploads;
	private int		_seeds, _peers,_remotesTCPNoLan, _remotesUDPNoLan, _remotesUTPNoLan;
	private int 	_tcpPendingConnections, _tcpConnectingConnections;
	private long last_remote_time;
	private long	_timeStarted;
	private long	_timeStarted_mono;
	private long	_timeStartedSeeding 		= -1;
	private long	_timeStartedSeeding_mono 	= -1;
	private long	_timeFinished;
	private Average	_averageReceptionSpeed;

	private long mainloop_loop_count;

	private static final int MAINLOOP_ONE_SECOND_INTERVAL = 1000 / PeerControlScheduler.SCHEDULE_PERIOD_MILLIS;
	private static final int MAINLOOP_FIVE_SECOND_INTERVAL = MAINLOOP_ONE_SECOND_INTERVAL * 5;
	private static final int MAINLOOP_TEN_SECOND_INTERVAL = MAINLOOP_ONE_SECOND_INTERVAL * 10;
	private static final int MAINLOOP_THIRTY_SECOND_INTERVAL = MAINLOOP_ONE_SECOND_INTERVAL * 30;
	private static final int MAINLOOP_SIXTY_SECOND_INTERVAL = MAINLOOP_ONE_SECOND_INTERVAL * 60;
	private static final int MAINLOOP_TEN_MINUTE_INTERVAL = MAINLOOP_SIXTY_SECOND_INTERVAL * 10;


	private volatile ArrayList<PEPeerManagerListener> peer_manager_listeners_cow = new ArrayList<PEPeerManagerListener>();  //copy on write


	private final List<Object[]>	piece_check_result_list     	= new ArrayList<Object[]>();
	private final AEMonitor	piece_check_result_list_mon  	= new AEMonitor( "PEPeerControl:PCRL");

	private boolean 			superSeedMode;
	private int 				superSeedModeCurrentPiece;
	private int 				superSeedModeNumberOfAnnounces;
	private SuperSeedPiece[]	superSeedPieces;

	private final int			hidden_piece;
	
	private final AEMonitor     this_mon = new AEMonitor( "PEPeerControl");

	private long		ip_filter_last_update_time;

	private Map<Object,Object>			user_data;

	private Unchoker		unchoker;

	private List<Object[]>	external_rate_limiters_cow;
	
	private int	bytes_queued_for_upload;
	private int	connections_with_queued_data;
	private int	connections_with_queued_data_blocked;
	private int	connections_unchoked;
	

	private List<PEPeer> sweepList = Collections.emptyList();
	private int nextPEXSweepIndex = 0;


	private final UploadHelper upload_helper = new UploadHelper() {		
		public int getPriority() {			
			return UploadHelper.PRIORITY_NORMAL;  //TODO also must call UploadSlotManager.getSingleton().updateHelper( upload_helper ); on priority change
		}

		public ArrayList<PEPeer> getAllPeers() {
			return( peer_transports_cow );
		}		

		public boolean isSeeding() {
			return seeding_mode;
		}		
	};



	private PeerDatabase	peer_database = PeerDatabaseFactory.createPeerDatabase();

	private int				bad_piece_reported		= -1;
	
	private int				next_rescan_piece		= -1;
	private long			rescan_piece_time		= -1;

	private long			last_eta;
	private long			last_eta_calculation;

	private static final int MAX_UDP_CONNECTIONS		= 16;

	private static final int PENDING_NAT_TRAVERSAL_MAX	= 32;
	private static final int MAX_UDP_TRAVERSAL_COUNT	= 3;
	
	private static final String	PEER_NAT_TRAVERSE_DONE_KEY	= PEPeerControlImpl.class.getName() + "::nat_trav_done";
	
	private Map<String,PEPeerTransport>	pending_nat_traversals = 
		new LinkedHashMap<String,PEPeerTransport>(PENDING_NAT_TRAVERSAL_MAX,0.75f,true)
	{
		protected boolean 
		removeEldestEntry(
			Map.Entry<String,PEPeerTransport> eldest) 
		{
			return size() > PENDING_NAT_TRAVERSAL_MAX;
		}
	};	

	private int udp_traversal_count;

	private static final int UDP_RECONNECT_MAX			= 16;
	
	private Map<String,PEPeerTransport>	udp_reconnects = 
		new LinkedHashMap<String,PEPeerTransport>(UDP_RECONNECT_MAX,0.75f,true)
	{
		protected boolean 
		removeEldestEntry(
			Map.Entry<String,PEPeerTransport> eldest) 
		{
			return size() > UDP_RECONNECT_MAX;
		}
	};	

	private static final int UDP_RECONNECT_MIN_MILLIS	= 10*1000;
	private long	last_udp_reconnect;
	
	private boolean	prefer_udp;

	private static final int		PREFER_UDP_BLOOM_SIZE	= 10000;
	private volatile BloomFilter	prefer_udp_bloom;
	
	private final LimitedRateGroup upload_limited_rate_group = new LimitedRateGroup() {
		public String 
		getName() 
		{
			return( "per_dl_up: " + getDisplayName());
		}
		public int getRateLimitBytesPerSecond() {
			return adapter.getUploadRateLimitBytesPerSecond();
		}
	};

	private final LimitedRateGroup download_limited_rate_group = new LimitedRateGroup() {
		public String 
		getName() 
		{
			return( "per_dl_down: " + getDisplayName());
		}
		public int getRateLimitBytesPerSecond() {
			return adapter.getDownloadRateLimitBytesPerSecond();
		}
	};

	private final int	partition_id;
	
	private final boolean	is_metadata_download;
	private int				metadata_infodict_size;
	
	private GettingThere	finish_in_progress;
	
	public 
	PEPeerControlImpl(
		byte[]					_peer_id,
		PEPeerManagerAdapter 	_adapter,
		DiskManager 			_diskManager,
		int						_partition_id )
	{
		_myPeerId		= _peer_id;
		adapter 		= _adapter;
		disk_mgr 		= _diskManager;
		partition_id	= _partition_id;
		
		is_metadata_download	= adapter.isMetadataDownload();
		
		if ( !is_metadata_download ){
			metadata_infodict_size	= adapter.getTorrentInfoDictSize();
		}
		
		_nbPieces =disk_mgr.getNbPieces();
		dm_pieces =disk_mgr.getPieces();
		
		pePieces = new PEPieceImpl[_nbPieces];

		hidden_piece = hide_a_piece?((int)(Math.abs(adapter.getRandomSeed())%_nbPieces)):-1;

		if ( hidden_piece >= 0 ){
		
			System.out.println( "Hidden piece for " + getDisplayName() + " = " + hidden_piece );
		}
		
		piecePicker = PiecePickerFactory.create( this );

		COConfigurationManager.addParameterListener("Ip Filter Enabled", this);

		ip_filter.addListener( this );

	}


	public void
	start()
	{
		//This torrent Hash
		try
		{

			_hash = PeerIdentityManager.createDataID( disk_mgr.getTorrent().getHash());

		} catch (TOTorrentException e)
		{

			// this should never happen
			Debug.printStackTrace( e );

			_hash = PeerIdentityManager.createDataID( new byte[20] ); 
		}

		// the recovered active pieces
		for (int i =0; i <_nbPieces; i++ )
		{
			final DiskManagerPiece dmPiece =dm_pieces[i];
			if (!dmPiece.isDone() &&dmPiece.getNbWritten() >0)
			{
				addPiece(new PEPieceImpl(this, dmPiece, 0), i, true, null );
			}
		}

		//The peer connections
		peer_transports_cow = new ArrayList();

		//BtManager is threaded, this variable represents the
		// current loop iteration. It's used by some components only called
		// at some specific times.
		mainloop_loop_count = 0;

		//The current tracker state
		//this could be start or update

		_averageReceptionSpeed = Average.getInstance(1000, 30);

		// the stats
		_stats =new PEPeerManagerStatsImpl(this);

		superSeedMode = (COConfigurationManager.getBooleanParameter("Use Super Seeding") && this.getRemaining() == 0);

		superSeedModeCurrentPiece = 0;

		if (superSeedMode)
		{
			initialiseSuperSeedMode();
		}

		// initial check on finished state - future checks are driven by piece check results

		// Moved out of mainLoop() so that it runs immediately, possibly changing
		// the state to seeding.

		checkFinished( true );

		UploadSlotManager.getSingleton().registerHelper( upload_helper );

		lastNeededUndonePieceChange =Long.MIN_VALUE;
		_timeStarted 		= SystemTime.getCurrentTime();
		_timeStarted_mono 	= SystemTime.getMonotonousTime();

		is_running = true;

		// activate after marked as running as we may synchronously add connections here due to pending activations

		adapter.getPeerManagerRegistration().activate( this );

		PeerNATTraverser.getSingleton().register( this );

		PeerControlSchedulerFactory.getSingleton(partition_id).register(this);
	}

	public void stopAll()
	{
		is_running =false;

		UploadSlotManager.getSingleton().deregisterHelper( upload_helper );

		PeerControlSchedulerFactory.getSingleton(partition_id).unregister(this);

		PeerNATTraverser.getSingleton().unregister( this );

		// remove legacy controller activation

		adapter.getPeerManagerRegistration().deactivate();

		closeAndRemoveAllPeers("download stopped", false);

		// clear pieces
		for (int i =0; i <_nbPieces; i++ )
		{
			if (pePieces[i] !=null)
				removePiece(pePieces[i], i);
		}

		// 5. Remove listeners
		COConfigurationManager.removeParameterListener("Ip Filter Enabled", this);

		ip_filter.removeListener(this);

		piecePicker.destroy();
		
		final ArrayList<PEPeerManagerListener> peer_manager_listeners = peer_manager_listeners_cow;

		for( int i=0; i < peer_manager_listeners.size(); i++ ) {
			
			((PEPeerManagerListener)peer_manager_listeners.get(i)).destroyed();
		}
		
		sweepList = Collections.emptyList();
		
		pending_nat_traversals.clear();
		
		udp_reconnects.clear();
		
		is_destroyed = true;
	}

	public int
	getPartitionID()
	{
		return( partition_id );
	}
	
	public boolean
	isDestroyed()
	{
		return( is_destroyed );
	}
	
	public DiskManager getDiskManager() {  return disk_mgr;   }
	public PiecePicker getPiecePicker()
	{
		return piecePicker;
	}

	public PEPeerManagerAdapter	getAdapter(){ return( adapter ); }

	public String getDisplayName(){ return( adapter.getDisplayName()); }

	public String
	getName()
	{
		return( getDisplayName());
	}
	
	public void
	schedule()
	{
		if ( finish_in_progress != null ){
			
			// System.out.println( "Finish in prog" );
			
			if ( finish_in_progress.hasGotThere()){
				
				finish_in_progress = null;
				
				// System.out.println( "Finished" );
			}else{
				
				return;
			}
		}
		
		try{
				// first off update the stats so they can be used by subsequent steps
			
			updateStats();

			updateTrackerAnnounceInterval();
			
			doConnectionChecks();
			
			processPieceChecks();

			if ( finish_in_progress != null ){
				
					// get off the scheduler thread while potentially long running operations complete
				
				return;
			}
			
			// note that seeding_mode -> torrent totally downloaded, not just non-dnd files
			// complete, so there is no change of a new piece appearing done by a means such as
			// background periodic file rescans

			if ( !seeding_mode ){

				checkCompletedPieces(); //check to see if we've completed anything else
			}

			checkBadPieces();

			checkInterested();      // see if need to recheck Interested on all peers

			piecePicker.updateAvailability();

			checkCompletionState();	// pick up changes in completion caused by dnd file changes

			if ( finish_in_progress != null ){
				
					// get off the scheduler thread while potentially long running operations complete

				return;
			}
			
			checkSeeds();

			if(!seeding_mode) {
				// if we're not finished

				checkRequests();

				piecePicker.allocateRequests();

				checkRescan();
				checkSpeedAndReserved();

				check99PercentBug();
			}


			updatePeersInSuperSeedMode();
			doUnchokes();

		}catch (Throwable e) {

			Debug.printStackTrace( e );
		}
		mainloop_loop_count++;
	}



	/**
	 * A private method that does analysis of the result sent by the tracker.
	 * It will mainly open new connections with peers provided
	 * and set the timeToWait variable according to the tracker response.
	 * @param tracker_response
	 */

	private void 
	analyseTrackerResponse(
			TRTrackerAnnouncerResponse	tracker_response )
	{
		// tracker_response.print();
		final TRTrackerAnnouncerResponsePeer[]	peers = tracker_response.getPeers();

		if ( peers != null ){
			addPeersFromTracker( tracker_response.getPeers());  
		}

		final Map extensions = tracker_response.getExtensions();

		if (extensions != null ){
			addExtendedPeersFromTracker( extensions );
		}
	}

	public void
	processTrackerResponse(
			TRTrackerAnnouncerResponse	response )
	{
		// only process new peers if we're still running
		if ( is_running ){
			analyseTrackerResponse( response );
		}
	}

	private void
	addExtendedPeersFromTracker(
			Map		extensions )
	{
		final Map	protocols = (Map)extensions.get("protocols");

		if ( protocols != null ){

			System.out.println( "PEPeerControl: tracker response contained protocol extensions");

			final Iterator protocol_it = protocols.keySet().iterator();

			while( protocol_it.hasNext()){

				final String	protocol_name = (String)protocol_it.next();

				final Map	protocol = (Map)protocols.get(protocol_name);

				final List	transports = PEPeerTransportFactory.createExtendedTransports( this, protocol_name, protocol );

				for (int i=0;i<transports.size();i++){

					final PEPeer	transport = (PEPeer)transports.get(i);

					addPeer( transport );
				}
			}
		}
	}

	public List<PEPeer>
	getPeers()
	{
		return( peer_transports_cow );
	}

	public List<PEPeer>
	getPeers(
		String	address )
	{		
		List<PEPeer>	result = new ArrayList<PEPeer>();

		Iterator<PEPeer>	it = peer_transports_cow.iterator();

		while( it.hasNext()){

			PEPeerTransport	peer = (PEPeerTransport)it.next();

			if ( peer.getIp().equals( address )){

				result.add( peer );
			}
		}

		return( result );
	}

	public int
	getPendingPeerCount()
	{
		return( peer_database.getDiscoveredPeerCount());
	}
	
	public PeerDescriptor[]
  	getPendingPeers()
  	{
  		return((PeerDescriptor[])peer_database.getDiscoveredPeers());
  	}
	
	public PeerDescriptor[]
	getPendingPeers(
		String	address )
	{
		return((PeerDescriptor[])peer_database.getDiscoveredPeers( address ));
	}

	public void
	addPeer(
			PEPeer		_transport )
	{
		if ( !( _transport instanceof PEPeerTransport )){

			throw( new RuntimeException("invalid class"));
		}

		final PEPeerTransport	transport = (PEPeerTransport)_transport;

		if (!ip_filter.isInRange(transport.getIp(), getDisplayName(), getTorrentHash())) {

			final ArrayList peer_transports = peer_transports_cow;

			if ( !peer_transports.contains(transport)){

				addToPeerTransports( transport );

				transport.start();

			}else{
				Debug.out( "addPeer():: peer_transports.contains(transport): SHOULD NEVER HAPPEN !" );
				transport.closeConnection( "already connected" );
			}
		}else{

			transport.closeConnection( "IP address blocked by filters" );
		}
	}

	protected byte[]
	getTorrentHash()
	{
		try{
			return( disk_mgr.getTorrent().getHash());

		}catch( Throwable e ){
			
			return( null );
		}
	}
	public void
	removePeer(
			PEPeer	_transport )
	{
		removePeer( _transport, "remove peer" );
	}

	public void
	removePeer(
			PEPeer	_transport,
			String	reason )
	{
		if ( !( _transport instanceof PEPeerTransport )){

			throw( new RuntimeException("invalid class"));
		}

		PEPeerTransport	transport = (PEPeerTransport)_transport;

		closeAndRemovePeer( transport, reason, true );
	}

	private void closeAndRemovePeer( PEPeerTransport peer, String reason, boolean log_if_not_found )
	{
		boolean removed =false;

		// copy-on-write semantics
		try{
			peer_transports_mon.enter();

			if ( peer_transports_cow.contains( peer )){

				final ArrayList new_peer_transports = new ArrayList( peer_transports_cow );

				new_peer_transports.remove(peer);
				peer_transports_cow =new_peer_transports;
				removed =true;
			}
		}
		finally{ 
			peer_transports_mon.exit();
		}

		if( removed ) {
			peer.closeConnection( reason );
			peerRemoved(peer);  	//notify listeners			
		}
		else {
			if ( log_if_not_found ){
				// we know this happens due to timing issues... Debug.out( "closeAndRemovePeer(): peer not removed" );
			}
		}
	}

	private void closeAndRemoveAllPeers( String reason, boolean reconnect ) {
		ArrayList peer_transports;

		try{
			peer_transports_mon.enter();

			peer_transports = peer_transports_cow;

			peer_transports_cow = new ArrayList( 0 );  
		}
		finally{
			peer_transports_mon.exit();
		}

		for( int i=0; i < peer_transports.size(); i++ ) {
			final PEPeerTransport peer = (PEPeerTransport)peer_transports.get( i );

			try{

				peer.closeConnection( reason );

			}catch( Throwable e ){

				// if something goes wrong with the close process (there's a bug in there somewhere whereby
				// we occasionally get NPEs then we want to make sure we carry on and close the rest

				Debug.printStackTrace(e);
			}

			try{
				peerRemoved( peer );  //notify listeners

			}catch( Throwable e ){

				Debug.printStackTrace(e);
			}
		}

		if( reconnect ) {
			for( int i=0; i < peer_transports.size(); i++ ) {
				final PEPeerTransport peer = (PEPeerTransport)peer_transports.get( i );

				PEPeerTransport	reconnected_peer = peer.reconnect(false, false);
			}
		}
	}




	public void 
	addPeer( 
		String 		ip_address, 
		int			tcp_port, 
		int			udp_port,
		boolean 	use_crypto,
		Map			user_data )
	{
		final byte type = use_crypto ? PeerItemFactory.HANDSHAKE_TYPE_CRYPTO : PeerItemFactory.HANDSHAKE_TYPE_PLAIN;
		final PeerItem peer_item = PeerItemFactory.createPeerItem( ip_address, tcp_port, PeerItem.convertSourceID( PEPeerSource.PS_PLUGIN ), type, udp_port, PeerItemFactory.CRYPTO_LEVEL_1, 0 );

		byte	crypto_level = PeerItemFactory.CRYPTO_LEVEL_1;

		if( !isAlreadyConnected( peer_item ) ) {

			String fail_reason;

			boolean	tcp_ok = TCPNetworkManager.TCP_OUTGOING_ENABLED && tcp_port > 0;
			boolean udp_ok = UDPNetworkManager.UDP_OUTGOING_ENABLED && udp_port > 0;
			
			if ( 	tcp_ok && !( ( prefer_udp || prefer_udp_default ) && udp_ok )){

				fail_reason = makeNewOutgoingConnection( PEPeerSource.PS_PLUGIN, ip_address, tcp_port, udp_port, true, use_crypto, crypto_level, user_data );  //directly inject the the imported peer

			}else if ( udp_ok ){

				fail_reason = makeNewOutgoingConnection( PEPeerSource.PS_PLUGIN, ip_address, tcp_port, udp_port, false, use_crypto, crypto_level, user_data );  //directly inject the the imported peer

			}else{

				fail_reason = "No usable protocol";
			}

			if( fail_reason != null )  Debug.out( "Injected peer " + ip_address + ":" + tcp_port + " was not added - " + fail_reason );
		}
	}



	private void 
	addPeersFromTracker(
		TRTrackerAnnouncerResponsePeer[]		peers )
	{

		for (int i = 0; i < peers.length; i++){
			final TRTrackerAnnouncerResponsePeer	peer = peers[i];

			final ArrayList peer_transports = peer_transports_cow;

			boolean already_connected = false;

			for( int x=0; x < peer_transports.size(); x++ ) {
				final PEPeerTransport transport = (PEPeerTransport)peer_transports.get( x );

				// allow loopback connects for co-located proxy-based connections and testing

				if( peer.getAddress().equals( transport.getIp() )){

					final boolean same_allowed = COConfigurationManager.getBooleanParameter( "Allow Same IP Peers" ) ||
					
					transport.getIp().equals( "127.0.0.1" );

					if( !same_allowed || peer.getPort() == transport.getPort() ) {
						already_connected = true;
						break;
					}
				}
			}

			if( already_connected )  continue;

			if( peer_database != null ){
				
				byte type = peer.getProtocol() == DownloadAnnounceResultPeer.PROTOCOL_CRYPT ? PeerItemFactory.HANDSHAKE_TYPE_CRYPTO : PeerItemFactory.HANDSHAKE_TYPE_PLAIN;

				byte crypto_level = peer.getAZVersion() < TRTrackerAnnouncer.AZ_TRACKER_VERSION_3?PeerItemFactory.CRYPTO_LEVEL_1:PeerItemFactory.CRYPTO_LEVEL_2;

				PeerItem item = PeerItemFactory.createPeerItem( 
						peer.getAddress(), 
						peer.getPort(), 
						PeerItem.convertSourceID( peer.getSource() ), 
						type, 
						peer.getUDPPort(), 
						crypto_level,
						peer.getUploadSpeed());

				peerDiscovered( null, item );
				
				peer_database.addDiscoveredPeer( item );
			}

			int	http_port = peer.getHTTPPort();

			if ( http_port != 0 && !seeding_mode ){

				adapter.addHTTPSeed( peer.getAddress(), http_port );
			}
		}
	}


	/**
	 * Request a new outgoing peer connection.
	 * @param address ip of remote peer
	 * @param port remote peer listen port
	 * @return null if the connection was added to the transport list, reason if rejected
	 */
	
	private String 
	makeNewOutgoingConnection( 
			String	peer_source,
			String 	address, 
			int 		tcp_port,
			int			udp_port,
			boolean		use_tcp,
			boolean 	require_crypto,
			byte		crypto_level,
			Map			user_data )
	{    
		//make sure this connection isn't filtered
   
		if( ip_filter.isInRange( address, getDisplayName(), getTorrentHash())) {
			return "IPFilter block";
		}

		//make sure we need a new connection
		final int needed = getMaxNewConnectionsAllowed();

		boolean	is_priority_connection = false;
		
		if ( user_data != null ){
			
			Boolean pc = (Boolean)user_data.get( Peer.PR_PRIORITY_CONNECTION );
			
			if ( pc != null && pc.booleanValue()){
				
				is_priority_connection = true;
			}
		}
		
		if( needed == 0 ){

			if ( 	peer_source != PEPeerSource.PS_PLUGIN ||
					!doOptimisticDisconnect(
							AddressUtils.isLANLocalAddress( address ) != AddressUtils.LAN_LOCAL_NO,
							is_priority_connection )){

				return "Too many connections";
			}
		}

		//make sure not already connected to the same IP address; allow loopback connects for co-located proxy-based connections and testing
		final boolean same_allowed = COConfigurationManager.getBooleanParameter( "Allow Same IP Peers" ) || address.equals( "127.0.0.1" );
		if( !same_allowed && PeerIdentityManager.containsIPAddress( _hash, address ) ){  
			return "Already connected to IP";
		}

		if( PeerUtils.ignorePeerPort( tcp_port ) ) {
			if (Logger.isEnabled())
				Logger.log(new LogEvent(disk_mgr.getTorrent(), LOGID,
						"Skipping connect with " + address + ":" + tcp_port
						+ " as peer port is in ignore list."));
			return "TCP port in ignore list";
		}

			//start the connection
		
		PEPeerTransport real = PEPeerTransportFactory.createTransport( this, peer_source, address, tcp_port, udp_port, use_tcp, require_crypto, crypto_level, user_data );

		addToPeerTransports( real );
		
		return null;
	}


	/**
	 * A private method that checks if PEPieces being downloaded are finished
	 * If all blocks from a PEPiece are written to disk, this method will
	 * queue the piece for hash check.
	 * Elsewhere, if it passes sha-1 check, it will be marked as downloaded,
	 * otherwise, it will unmark it as fully downloaded, so blocks can be retreived again.
	 */
	private void checkCompletedPieces() {
		if ((mainloop_loop_count %MAINLOOP_ONE_SECOND_INTERVAL) !=0)
			return;

		//for every piece
		for (int i = 0; i <_nbPieces; i++) {
			final DiskManagerPiece dmPiece =dm_pieces[i];
			//if piece is completly written, not already checking, and not Done
			if (dmPiece.isNeedsCheck())
			{
				//check the piece from the disk
				dmPiece.setChecking();

				DiskManagerCheckRequest req = 
					disk_mgr.createCheckRequest(
							i, new Integer(CHECK_REASON_DOWNLOADED));

				req.setAdHoc( false );

				disk_mgr.enqueueCheckRequest(  req, this );
			}
		}
	}

	/** Checks given piece to see if it's active but empty, and if so deactivates it.
	 * @param pieceNumber to check
	 * @return true if the piece was removed and is no longer active (pePiece ==null)
	 */ 
	private boolean 
	checkEmptyPiece(
		final int pieceNumber )
	{
        if ( piecePicker.isInEndGameMode()){
            
			return false;   // be sure to not remove pieces in EGM
        }
        
		final PEPiece pePiece =pePieces[pieceNumber];
		final DiskManagerPiece dmPiece =dm_pieces[pieceNumber];
		
		if ( pePiece == null || pePiece.isRequested())
			return false;
		
		if (dmPiece.getNbWritten() >0 ||pePiece.getNbUnrequested() < pePiece.getNbBlocks() ||pePiece.getReservedBy() !=null)
			return false;
	
			// reset in case dmpiece is in some skanky state
		
		pePiece.reset();
		
		removePiece(pePiece, pieceNumber);
		return true;
	}

	/**
	 * Check if a piece's Speed is too fast for it to be getting new data
	 * and if a reserved pieced failed to get data within 120 seconds
	 */
	private void checkSpeedAndReserved()
	{
		// only check every 5 seconds
		if(mainloop_loop_count % MAINLOOP_FIVE_SECOND_INTERVAL != 0)
			return;

		final int				nbPieces	=_nbPieces;
		final PEPieceImpl[] pieces =pePieces;
		//for every piece
		for (int i =0; i <nbPieces; i++)
		{
			// placed before null-check in case it really removes a piece
			checkEmptyPiece(i);


			final PEPieceImpl pePiece =pieces[i];
			// these checks are only against pieces being downloaded
			// yet needing requests still/again
			if (pePiece !=null)
			{
				final long timeSinceActivity =pePiece.getTimeSinceLastActivity()/1000;				

				int pieceSpeed =pePiece.getSpeed();
				// block write speed slower than piece speed
				if (pieceSpeed > 0 && timeSinceActivity*pieceSpeed*0.25 > DiskManager.BLOCK_SIZE/1024)
				{
					if(pePiece.getNbUnrequested() > 2)
						pePiece.setSpeed(pieceSpeed-1);
					else
						pePiece.setSpeed(0);
				}


				if(timeSinceActivity > 120)
				{
					pePiece.setSpeed(0);
					// has reserved piece gone stagnant?
					final String reservingPeer =pePiece.getReservedBy();
					if(reservingPeer !=null)
					{
						final PEPeerTransport pt = getTransportFromAddress(reservingPeer);
						// Peer is too slow; Ban them and unallocate the piece
						// but, banning is no good for peer types that get pieces reserved
						// to them for other reasons, such as special seed peers
						if (needsMD5CheckOnCompletion(i))
							badPeerDetected(reservingPeer, i);
						else if (pt != null)
							closeAndRemovePeer(pt, "Reserved piece data timeout; 120 seconds", true);

						pePiece.setReservedBy(null);
					}

					if (!piecePicker.isInEndGameMode()){
						pePiece.checkRequests();
					}

					checkEmptyPiece(i);
				}

			}
		}
	}

	private void
	check99PercentBug()
	{
		// there's a bug whereby pieces are left downloaded but never written. might have been fixed by
		// changes to the "write result" logic, however as a stop gap I'm adding code to scan for such
		// stuck pieces and reset them

		if ( mainloop_loop_count % MAINLOOP_SIXTY_SECOND_INTERVAL == 0 ) {

			long	now = SystemTime.getCurrentTime();

			for ( int i=0;i<pePieces.length;i++){

				PEPiece	pe_piece = pePieces[ i ];

				if ( pe_piece != null ){

					DiskManagerPiece	dm_piece = dm_pieces[i];

					if ( !dm_piece.isDone()){

						if ( pe_piece.isDownloaded()){

							if ( now - pe_piece.getLastDownloadTime(now) > stalled_piece_timeout ){

								// people with *very* slow disk writes can trigger this (I've been talking to a user
								// with a SAN that has .5 second write latencies when checking a file at the same time
								// this means that when dowloading > 32K/sec things start backing up). Eventually the
								// write controller will start blocking the network thread to prevent unlimited
								// queueing but until that time we need to handle this situation slightly better)

								// if there are any outstanding requests for this piece then leave it alone

								if ( !( disk_mgr.hasOutstandingWriteRequestForPiece( i ) || 
										disk_mgr.hasOutstandingReadRequestForPiece( i ) ||
										disk_mgr.hasOutstandingCheckRequestForPiece( i ) )){

									Debug.out( "Fully downloaded piece stalled pending write, resetting p_piece " + i );

									pe_piece.reset();
								}
							}
						}
					}
				}

			}
		}
	}

	private void checkInterested()
	{
		if ( (mainloop_loop_count %MAINLOOP_ONE_SECOND_INTERVAL) != 0 ){
			return;
		}

		if (lastNeededUndonePieceChange >=piecePicker.getNeededUndonePieceChange())
			return;

		lastNeededUndonePieceChange =piecePicker.getNeededUndonePieceChange();

		final ArrayList peer_transports = peer_transports_cow;
		int cntPeersSnubbed =0;	// recount # snubbed peers while we're at it
		for (int i =0; i <peer_transports.size(); i++)
		{
			final PEPeerTransport peer =(PEPeerTransport)peer_transports.get(i);
			peer.checkInterested();
			if (peer.isSnubbed())
				cntPeersSnubbed++;
		}
		setNbPeersSnubbed(cntPeersSnubbed);
	}


	/**
	 * Private method to process the results given by DiskManager's
	 * piece checking thread via asyncPieceChecked(..)
	 */
	private void 
	processPieceChecks() 
	{
		if ( piece_check_result_list.size() > 0 ){

			final List pieces;

			// process complete piece results

			try{
				piece_check_result_list_mon.enter();

				pieces = new ArrayList( piece_check_result_list );

				piece_check_result_list.clear();

			}finally{

				piece_check_result_list_mon.exit();
			}

			final Iterator it = pieces.iterator();

			while (it.hasNext()) {

				final Object[]	data = (Object[])it.next();

				//bah
				
				processPieceCheckResult((DiskManagerCheckRequest)data[0],((Integer)data[1]).intValue());

			}
		}
	}

	private void
	checkBadPieces()
	{
		if ( mainloop_loop_count % MAINLOOP_SIXTY_SECOND_INTERVAL == 0 ){

			if ( bad_piece_reported != -1 ){
				
				DiskManagerCheckRequest	req = 
					disk_mgr.createCheckRequest(
						bad_piece_reported, 
						new Integer( CHECK_REASON_BAD_PIECE_CHECK ));	
				
				req.setLowPriority( true );
				
			   	if ( Logger.isEnabled()){
			   		
					Logger.log(
							new LogEvent(
								disk_mgr.getTorrent(), LOGID,
								"Rescanning reported-bad piece " + bad_piece_reported ));
							
			   	}
			   	
				bad_piece_reported	= -1;

				try{
					disk_mgr.enqueueCheckRequest( req, this );
					
				}catch( Throwable e ){
					
					
					Debug.printStackTrace(e);
				}
			}
		}		
	}
	
	private void
	checkRescan()
	{
		if ( rescan_piece_time == 0 ){

			// pending a piece completion

			return;
		}

		if ( next_rescan_piece == -1 ){

			if ( mainloop_loop_count % MAINLOOP_FIVE_SECOND_INTERVAL == 0 ){

				if ( adapter.isPeriodicRescanEnabled()){

					next_rescan_piece	= 0;
				}
			}
		}else{

			if ( mainloop_loop_count % MAINLOOP_TEN_MINUTE_INTERVAL == 0 ){

				if ( !adapter.isPeriodicRescanEnabled()){

					next_rescan_piece	= -1;
				}
			}
		}

		if ( next_rescan_piece == -1 ){

			return;
		}

		// delay as required

		final long	now = SystemTime.getCurrentTime();

		if ( rescan_piece_time > now ){

			rescan_piece_time	= now;
		}

		// 250K/sec limit

		final long	piece_size = disk_mgr.getPieceLength();

		final long	millis_per_piece = piece_size / 250;

		if ( now - rescan_piece_time < millis_per_piece ){

			return;
		}

		while( next_rescan_piece != -1 ){

			int	this_piece = next_rescan_piece;

			next_rescan_piece++;

			if ( next_rescan_piece == _nbPieces ){

				next_rescan_piece	= -1;
			}
			
				// this functionality is to pick up pieces that have been downloaded OUTSIDE of
				// Azureus - e.g. when two torrents are sharing a single file. Hence the check on
				// the piece NOT being done

			if ( pePieces[this_piece] == null && !dm_pieces[this_piece].isDone() && dm_pieces[this_piece].isNeeded()){

				DiskManagerCheckRequest	req = 
					disk_mgr.createCheckRequest(
							this_piece, 
							new Integer( CHECK_REASON_SCAN ));	

				req.setLowPriority( true );

				if ( Logger.isEnabled()){

					Logger.log(
							new LogEvent(
									disk_mgr.getTorrent(), LOGID,
									"Rescanning piece " + this_piece ));

				}

				rescan_piece_time	= 0;	// mark as check piece in process

				try{
					disk_mgr.enqueueCheckRequest( req, this );

				}catch( Throwable e ){

					rescan_piece_time	= now;

					Debug.printStackTrace(e);
				}

				break;
			}
		}
	}

	public void
	badPieceReported(
		PEPeerTransport		originator,
		int					piece_number )
	{
		Debug.outNoStack( getDisplayName() + ": bad piece #" + piece_number + " reported by " + originator.getIp());
		
		if ( piece_number < 0 || piece_number >= _nbPieces ){
			
			return;
		}
		
		bad_piece_reported = piece_number;
	}
	
	public void
	setStatsReceiver(
		PEPeerManager.StatsReceiver	receiver )
	{
		stats_receiver = receiver;
	}
	
	public void 
	statsRequest(
		PEPeerTransport 	originator, 
		Map 				request )
	{
		Map		reply = new HashMap();
		
		adapter.statsRequest( originator, request, reply );
		
		if ( reply.size() > 0 ){
			
			originator.sendStatsReply( reply );
		}
	}
	
	public void 
	statsReply(
		PEPeerTransport 	originator, 
		Map 				reply )
	{	
		PEPeerManager.StatsReceiver receiver = stats_receiver;
		
		if ( receiver != null ){
			
			receiver.receiveStats( originator, reply );
		}
	}

	/**
	 * This method checks if the downloading process is finished.
	 *
	 */
	
	private void 
	checkFinished(
		final boolean start_of_day )
	{
		final boolean all_pieces_done =disk_mgr.getRemainingExcludingDND() ==0;

		if ( all_pieces_done ){
		
			seeding_mode	= true;
			
			prefer_udp_bloom = null;
			
			piecePicker.clearEndGameChunks();

			if (!start_of_day)
				adapter.setStateFinishing();

			_timeFinished = SystemTime.getCurrentTime();
			final ArrayList peer_transports = peer_transports_cow;

			//remove previous snubbing
			for (int i =0; i <peer_transports.size(); i++ )
			{
				final PEPeerTransport pc = (PEPeerTransport) peer_transports.get(i);
				pc.setSnubbed(false);
			}
			setNbPeersSnubbed(0);

			final boolean checkPieces =COConfigurationManager.getBooleanParameter( "Check Pieces on Completion" );

			//re-check all pieces to make sure they are not corrupt, but only if we weren't already complete
			if (checkPieces &&!start_of_day)
			{
				final DiskManagerCheckRequest req =disk_mgr.createCheckRequest(-1, new Integer(CHECK_REASON_COMPLETE));
				disk_mgr.enqueueCompleteRecheckRequest(req, this);
			}

			_timeStartedSeeding 		= SystemTime.getCurrentTime();
			_timeStartedSeeding_mono 	= SystemTime.getMonotonousTime();

			try{
				disk_mgr.saveResumeData(false);
				
			}catch( Throwable e ){
				Debug.out( "Failed to save resume data", e );
			}
			
			adapter.setStateSeeding( start_of_day );
			
			final AESemaphore waiting_it = new AESemaphore( "PEC:DE" );
			
			new AEThread2( "PEC:DE" ){
				public void
				run()
				{
					try{
						disk_mgr.downloadEnded(
							new DiskManager.OperationStatus()
							{
								public void 
								gonnaTakeAWhile(
									GettingThere gt ) 
								{
									boolean	async_set = false;
									
									synchronized( PEPeerControlImpl.this ){
										
										if ( finish_in_progress == null ){
											
											finish_in_progress = gt;
											
											async_set = true;
										}
									}
										
									if ( async_set ){
									
										waiting_it.release();
									}
								}
							});
					}finally{
						
						waiting_it.release();
					}
				}
			}.start();
			
			waiting_it.reserve();
						
		}else{
			
			seeding_mode = false;
		}
	}

	protected void
	checkCompletionState()
	{
		if ( mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL != 0 ){

			return;
		}

		boolean dm_done = disk_mgr.getRemainingExcludingDND() == 0;

		if ( seeding_mode ){

			if ( !dm_done ){

				seeding_mode = false;

				_timeStartedSeeding 		= -1;
				_timeStartedSeeding_mono 	= -1;
				_timeFinished				= 0;

				Logger.log(
						new LogEvent(	disk_mgr.getTorrent(), LOGID,
								"Turning off seeding mode for PEPeerManager"));
			}
						
		}else{

			if ( dm_done ){

				checkFinished( false );

				if ( seeding_mode ){

					Logger.log(
							new LogEvent(	disk_mgr.getTorrent(), LOGID,
									"Turning on seeding mode for PEPeerManager"));
				
				}
			}
		}
	}

	/**
	 * This method will locate expired requests on peers, will cancel them,
	 * and mark the peer as snubbed if we haven't received usefull data from
	 * them within the last 60 seconds
	 */
	private void checkRequests()
	{
		// to be honest I don't see why this can't be 5 seconds, but I'm trying 1 second
		// now as the existing 0.1 second is crazy given we're checking for events that occur
		// at 60+ second intervals

		if ( mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL != 0 ){

			return;
		}

		final long now =SystemTime.getCurrentTime();

		//for every connection
		final ArrayList peer_transports = peer_transports_cow;
		for (int i =peer_transports.size() -1; i >=0 ; i--)
		{
			final PEPeerTransport pc =(PEPeerTransport)peer_transports.get(i);
			if (pc.getPeerState() ==PEPeer.TRANSFERING)
			{
				final List expired = pc.getExpiredRequests();
				if (expired !=null &&expired.size() >0)
				{   // now we know there's a request that's > 60 seconds old
					final boolean isSeed =pc.isSeed();
					// snub peers that haven't sent any good data for a minute
					final long timeSinceGoodData =pc.getTimeSinceGoodDataReceived();
					if (timeSinceGoodData <0 ||timeSinceGoodData >60 *1000)
						pc.setSnubbed(true);

					//Only cancel first request if more than 2 mins have passed
					DiskManagerReadRequest request =(DiskManagerReadRequest) expired.get(0);

					final long timeSinceData =pc.getTimeSinceLastDataMessageReceived();
					final boolean noData =(timeSinceData <0) ||timeSinceData >(1000 *(isSeed ?120 :60));
					final long timeSinceOldestRequest = now - request.getTimeCreated(now);


					//for every expired request                              
					for (int j = (timeSinceOldestRequest >120 *1000 && noData)  ? 0 : 1; j < expired.size(); j++)
					{
						//get the request object
						request =(DiskManagerReadRequest) expired.get(j);
						//Only cancel first request if more than 2 mins have passed
						pc.sendCancel(request);				//cancel the request object
						//get the piece number
						final int pieceNumber = request.getPieceNumber();
						PEPiece	pe_piece = pePieces[pieceNumber];
						//unmark the request on the block
						if ( pe_piece != null )
							pe_piece.clearRequested(request.getOffset() /DiskManager.BLOCK_SIZE);
						// remove piece if empty so peers can choose something else, except in end game
						if (!piecePicker.isInEndGameMode())
							checkEmptyPiece(pieceNumber);
					}
				}
			}	
		}
	}

	private void 
	updateTrackerAnnounceInterval() 
	{
		if ( mainloop_loop_count % MAINLOOP_FIVE_SECOND_INTERVAL != 0 ){
			return;
		}

		final int WANT_LIMIT = 100;

		int num_wanted = getMaxNewConnectionsAllowed();

		final boolean has_remote = adapter.isNATHealthy();
		if( has_remote ) {
			//is not firewalled, so can accept incoming connections,
			//which means no need to continually keep asking the tracker for peers
			num_wanted = (int)(num_wanted / 1.5);
		}

		if ( num_wanted < 0 || num_wanted > WANT_LIMIT ) {
			num_wanted = WANT_LIMIT;
		}

		int current_connection_count = PeerIdentityManager.getIdentityCount( _hash );

		final TRTrackerScraperResponse tsr = adapter.getTrackerScrapeResponse();

		if( tsr != null && tsr.isValid() ) {  //we've got valid scrape info
			final int num_seeds = tsr.getSeeds();   
			final int num_peers = tsr.getPeers();

			final int swarm_size;

			if( seeding_mode ) {
				//Only use peer count when seeding, as other seeds are unconnectable.
				//Since trackers return peers randomly (some of which will be seeds),
				//backoff by the seed2peer ratio since we're given only that many peers
				//on average each announce.
				final float ratio = (float)num_peers / (num_seeds + num_peers);
				swarm_size = (int)(num_peers * ratio);
			}
			else {
				swarm_size = num_peers + num_seeds;
			}

			if( swarm_size < num_wanted ) {  //lower limit to swarm size if necessary
				num_wanted = swarm_size;
			}
		}

		if( num_wanted < 1 ) {  //we dont need any more connections
			adapter.setTrackerRefreshDelayOverrides( 100 );  //use normal announce interval
			return;
		}

		if( current_connection_count == 0 )  current_connection_count = 1;  //fudge it :)

		final int current_percent = (current_connection_count * 100) / (current_connection_count + num_wanted);

		adapter.setTrackerRefreshDelayOverrides( current_percent );  //set dynamic interval override
	}

	public boolean
	hasDownloadablePiece()
	{
		return( piecePicker.hasDownloadablePiece());
	}

	public int
	getBytesQueuedForUpload()
	{
		return( bytes_queued_for_upload );
	}
	
	public int	
	getNbPeersWithUploadQueued()
	{
		return( connections_with_queued_data );
	}
	    
	public int	
	getNbPeersWithUploadBlocked()
	{
		return( connections_with_queued_data_blocked );
	}
	
	public int
	getNbPeersUnchoked()
	{
		return( connections_unchoked );
	}
	
	public int[] getAvailability() 
	{
		return piecePicker.getAvailability();
	}

	//this only gets called when the My Torrents view is displayed
	public float getMinAvailability()
	{
		return piecePicker.getMinAvailability();
	}

	public float getAvgAvail()
	{
		return piecePicker.getAvgAvail();
	}

	public long getAvailWentBadTime()
	{
		return( piecePicker.getAvailWentBadTime());
	}
	
	public void addPeerTransport( PEPeerTransport transport ) {
    if (!ip_filter.isInRange(transport.getIp(), getDisplayName(), getTorrentHash())) {
			final ArrayList peer_transports = peer_transports_cow;

			if (!peer_transports.contains( transport )) {
				addToPeerTransports(transport);    
			}
			else{
				Debug.out( "addPeerTransport():: peer_transports.contains(transport): SHOULD NEVER HAPPEN !" );        
				transport.closeConnection( "already connected" );
			}
		}
		else {
			transport.closeConnection( "IP address blocked by filters" );
		}
	}


	/**
	 * Do all peer choke/unchoke processing.
	 */
	private void doUnchokes() {  

		// logic below is either 1 second or 10 secondly, bail out early id neither

		if( !UploadSlotManager.AUTO_SLOT_ENABLE ) {		 //manual per-torrent unchoke slot mode

			if( mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL != 0 ) { 
				return;
			}

			final int max_to_unchoke = adapter.getMaxUploads();  //how many simultaneous uploads we should consider
			final ArrayList peer_transports = peer_transports_cow;

			//determine proper unchoker
			if( seeding_mode ) {
				if( unchoker == null || !(unchoker.isSeedingUnchoker()) ) {
					unchoker = UnchokerFactory.getSingleton().getUnchoker( true );
				}
			}
			else {
				if( unchoker == null || unchoker.isSeedingUnchoker()) {
					unchoker = UnchokerFactory.getSingleton().getUnchoker( false );
				}
			}

				//do main choke/unchoke update every 10 secs
			
			if ( mainloop_loop_count % MAINLOOP_TEN_SECOND_INTERVAL == 0 ){

				final boolean refresh = mainloop_loop_count % MAINLOOP_THIRTY_SECOND_INTERVAL == 0;

				unchoker.calculateUnchokes( max_to_unchoke, peer_transports, refresh, adapter.hasPriorityConnection());

				ArrayList	chokes 		= unchoker.getChokes();
				ArrayList	unchokes	= unchoker.getUnchokes();
				
				addFastUnchokes( unchokes );
				
				UnchokerUtil.performChokes( chokes, unchokes );
				
			}else if ( mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL == 0 ) {  //do quick unchoke check every 1 sec

				ArrayList unchokes = unchoker.getImmediateUnchokes( max_to_unchoke, peer_transports );

				addFastUnchokes( unchokes );
				
				UnchokerUtil.performChokes( null, unchokes );
			}
		}
	}

	private void
	addFastUnchokes(
		ArrayList	peers_to_unchoke )
	{
		for( Iterator it=peer_transports_cow.iterator();it.hasNext();) {

			PEPeerTransport peer = (PEPeerTransport)it.next();

			if ( 	peer.getConnectionState() != PEPeerTransport.CONNECTION_FULLY_ESTABLISHED ||
					!UnchokerUtil.isUnchokable( peer, true ) ||
					peers_to_unchoke.contains( peer )){
				
				continue;
			}

			if( peer.isLANLocal()){ 
			
				peers_to_unchoke.add( peer );
				
			}else if ( 	fast_unchoke_new_peers &&
						peer.getData( "fast_unchoke_done" ) == null ){

				peer.setData( "fast_unchoke_done", "" );
				
				peers_to_unchoke.add( peer );
			}
		}
	}
	
//	send the have requests out
	private void sendHave(int pieceNumber) {
		//fo
		final ArrayList peer_transports = peer_transports_cow;

		for (int i = 0; i < peer_transports.size(); i++) {
			//get a peer connection
			final PEPeerTransport pc = (PEPeerTransport) peer_transports.get(i);
			//send the have message
			pc.sendHave(pieceNumber);
		}

	}

	// Method that checks if we are connected to another seed, and if so, disconnect from him.
	private void checkSeeds() {
		//proceed on mainloop 1 second intervals if we're a seed and we want to force disconnects
		if ((mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL) != 0)
			return;

		if (!disconnect_seeds_when_seeding ){
			return;
		}

		ArrayList to_close = null;

		final ArrayList peer_transports = peer_transports_cow;
		for (int i = 0; i < peer_transports.size(); i++) {
			final PEPeerTransport pc = (PEPeerTransport) peer_transports.get(i);

			if (pc != null && pc.getPeerState() == PEPeer.TRANSFERING && ((isSeeding() && pc.isSeed()) || pc.isRelativeSeed())) {
				if( to_close == null )  to_close = new ArrayList();
				to_close.add( pc );
			}
		}

		if( to_close != null ) {		
			for( int i=0; i < to_close.size(); i++ ) {  			
				closeAndRemovePeer( (PEPeerTransport)to_close.get(i), "disconnect other seed when seeding", false );
			}
		}
	}




	private void updateStats() {   

		if ( (mainloop_loop_count %MAINLOOP_ONE_SECOND_INTERVAL) != 0 ){
			return;
		}

		//calculate seeds vs peers
		final ArrayList<PEPeer> peer_transports = peer_transports_cow;

		int	new_pending_tcp_connections 	= 0;
		int new_connecting_tcp_connections	= 0;
		
		int	new_seeds = 0;
		int new_peers = 0;
		int new_tcp_incoming 	= 0;
		int new_udp_incoming  	= 0;
		int new_utp_incoming  	= 0;
		
		int	bytes_queued 	= 0;
		int	con_queued		= 0;
		int con_blocked		= 0;
		int con_unchoked	= 0;
		
		for ( Iterator<PEPeer> it=peer_transports.iterator();it.hasNext();){
			
			final PEPeerTransport pc = (PEPeerTransport) it.next();
			
			if ( pc.getPeerState() == PEPeer.TRANSFERING) {
				
				if ( !pc.isChokedByMe()){
					
					con_unchoked++;
				}
				
				Connection connection = pc.getPluginConnection();
				
				if ( connection != null ){
					
					OutgoingMessageQueue mq = connection.getOutgoingMessageQueue();
					
					int q = mq.getDataQueuedBytes() + mq.getProtocolQueuedBytes();
					
					bytes_queued += q;
					
					if ( q > 0 ){
						
						con_queued++;
						
						if ( mq.isBlocked()){
							
							con_blocked++;
						}
					}
				}
				
				if (pc.isSeed())
					new_seeds++;
				else
					new_peers++;

				if ( pc.isIncoming() && !pc.isLANLocal()){
										
					if ( pc.isTCP() ) {
				
						new_tcp_incoming++;
						
					}else{
						
						String protocol = pc.getProtocol();
						
						if ( protocol.equals( "UDP" )){
						
							new_udp_incoming++;
							
						}else{
							
							new_utp_incoming++;
						}
					}
				}
			}else{				
				if ( pc.isTCP()){
					
					int c_state = pc.getConnectionState();

					if ( c_state == PEPeerTransport.CONNECTION_PENDING ){
						
						new_pending_tcp_connections++;
						
					}else if ( c_state == PEPeerTransport.CONNECTION_CONNECTING ){
						
						new_connecting_tcp_connections++;
					}
				}
			}
		}

		_seeds = new_seeds;
		_peers = new_peers;
		_remotesTCPNoLan = new_tcp_incoming;
		_remotesUDPNoLan = new_udp_incoming;
		_remotesUTPNoLan = new_utp_incoming;
		_tcpPendingConnections = new_pending_tcp_connections;
		_tcpConnectingConnections = new_connecting_tcp_connections;
		
		bytes_queued_for_upload 				= bytes_queued;
		connections_with_queued_data			= con_queued;
		connections_with_queued_data_blocked	= con_blocked;
		connections_unchoked					= con_unchoked;
	}
	/**
	 * The way to unmark a request as being downloaded, or also 
	 * called by Peer connections objects when connection is closed or choked
	 * @param request a DiskManagerReadRequest holding details of what was canceled
	 */
	public void requestCanceled(DiskManagerReadRequest request)
	{
		final int pieceNumber =request.getPieceNumber();  //get the piece number
		PEPiece pe_piece = pePieces[pieceNumber];
		if (pe_piece != null ){
			pe_piece.clearRequested(request.getOffset() /DiskManager.BLOCK_SIZE);
		}
	}


	public PEPeerControl
	getControl()
	{
		return( this );
	}

	public byte[][]
	getSecrets(
		int	crypto_level )
	{
		return( adapter.getSecrets( crypto_level ));
	}

//	get the hash value
	public byte[] getHash() {
		return _hash.getDataID();
	}

	public PeerIdentityDataID
	getPeerIdentityDataID()
	{
		return( _hash );
	}

//	get the peer id value
	public byte[] getPeerId() {
		return _myPeerId;
	}

//	get the remaining percentage
	public long getRemaining() {
		return disk_mgr.getRemaining();
	}


	public void discarded(PEPeer peer, int length) {
		if (length > 0){
			_stats.discarded(peer, length);
			
				// discards are more likely during end-game-mode
			
			if ( ban_peer_discard_ratio > 0 && !( piecePicker.isInEndGameMode() || piecePicker.hasEndGameModeBeenAbandoned())){
				
				long	received 	= peer.getStats().getTotalDataBytesReceived();
				long	discarded	= peer.getStats().getTotalBytesDiscarded();
				
				long	non_discarded = received - discarded;
				
				if ( non_discarded < 0 ){
					
					non_discarded = 0;
				}
				
				if ( discarded >= ban_peer_discard_min_kb * 1024 ){
								
					if ( 	non_discarded == 0 || 
							((float)discarded) / non_discarded >= ban_peer_discard_ratio ){
												
						badPeerDetected( peer.getIp(), -1 );
					}
				}
			}
		}
	}

	public void dataBytesReceived(PEPeer peer, int length) {
		if (length > 0) {
			_stats.dataBytesReceived(peer,length);

			_averageReceptionSpeed.addValue(length);
		}
	}


	public void protocolBytesReceived(PEPeer peer,  int length ) {
		if (length > 0) {
			_stats.protocolBytesReceived(peer,length);
		}
	}

	public void dataBytesSent(PEPeer peer, int length) {
		if (length > 0) {
			_stats.dataBytesSent(peer, length );
		}
	}


	public void protocolBytesSent( PEPeer peer, int length ) {
		if (length > 0) {
			_stats.protocolBytesSent(peer,length);
		}
	}

	/** DiskManagerWriteRequestListener message
	 * @see org.gudy.azureus2.core3.disk.DiskManagerWriteRequestListener
	 */
	public void writeCompleted(DiskManagerWriteRequest request)
	{
		final int pieceNumber =request.getPieceNumber();

		DiskManagerPiece	dm_piece = dm_pieces[pieceNumber];

		if (!dm_piece.isDone()){

			final PEPiece pePiece =pePieces[pieceNumber];

			if (pePiece !=null){

				pePiece.setWritten((PEPeer)request.getUserData(), request.getOffset() /DiskManager.BLOCK_SIZE );

			}else{

				// this is a way of fixing a 99.9% bug where a dmpiece is left in a 
				// fully downloaded state with the underlying pe_piece null. Possible explanation is
				// that a slow peer sends an entire piece at around the time a pe_piece gets reset
				// due to inactivity.

				// we also get here when recovering data that has come in late after the piece has
				// been abandoned

				dm_piece.setWritten( request.getOffset() /DiskManager.BLOCK_SIZE );
			}
		}
	}

	public void 
	writeFailed( 
			DiskManagerWriteRequest 	request, 
			Throwable		 			cause )
	{
		// if the write has failed then the download will have been stopped so there is no need to try
		// and reset the piece
	}

	
		/**
		 * 
		 * @param data_offset	MUST be block aligned within the TORRENT
		 * @param data
		 * @return		buffers not yet written
		 * @throws Exception
		 */
	
	public DirectByteBuffer[]
	write(
		long					data_offset,
		DirectByteBuffer[]		data )
	
		throws Exception
	{
		if ( data_offset % DiskManager.BLOCK_SIZE != 0 ){
			
			throw( new Exception( "data must start at a block offset" ));
		}
		
		int	piece_length = disk_mgr.getPieceLength();
		
		int	data_length = 0;
		
		for( DirectByteBuffer buffer: data ){
			
			data_length += buffer.remaining( DirectByteBuffer.SS_DW );
		}
		
		long	written = 0;
		
		int		buffer_number = 0;
		
		for ( int i=0;i<data_length;i+=DiskManager.BLOCK_SIZE ){
			
			int	rem = data_length - i;
			
			if ( rem > DiskManager.BLOCK_SIZE ){
				
				rem = DiskManager.BLOCK_SIZE;
			}
			
			int	piece_number = (int)( data_offset / piece_length );
			
			DiskManagerPiece dm_piece = dm_pieces[piece_number];

			int	block_number = (int)(data_offset - ( piece_number * piece_length )) / DiskManager.BLOCK_SIZE;
			
			int block_size = dm_piece.getBlockSize( block_number );
			
			if ( rem < block_size ){
				
				break;
			}
			
			DirectByteBuffer chunk = DirectByteBufferPool.getBuffer( DirectByteBuffer.AL_BT_PIECE, block_size );
			
			
			writeBlock( piece_number, block_number*DiskManager.BLOCK_SIZE, chunk, null, true );
			
			written 		+= rem;
			data_offset		+= rem;
		}
		
		List<DirectByteBuffer>	unwritten = new ArrayList<DirectByteBuffer>();
		
		for (int i=buffer_number;i<data.length;i++){
			
			if (data[i].hasRemaining( DirectByteBuffer.SS_DW )){
				
				unwritten.add( data[i] );
				
			}else{
				
				data[i].returnToPool();
			}
		}
		
		return( unwritten.toArray( new DirectByteBuffer[ unwritten.size()] ));
	}
	
		
	/** This method will queue up a dism manager write request for the block if the block is not already written.
	 * It will send out cancels for the block to all peer either if in end-game mode, or per cancel param 
	 * @param pieceNumber to potentialy write to
	 * @param offset within piece to queue write for
	 * @param data to be writen
	 * @param sender peer that sent this data
	 * @param cancel if cancels definatly need to be sent to all peers for this request
	 */
	public void writeBlock(int pieceNumber, int offset, DirectByteBuffer data, PEPeer sender, boolean cancel)
	{
		final int blockNumber =offset /DiskManager.BLOCK_SIZE;
		final DiskManagerPiece dmPiece =dm_pieces[pieceNumber];
		if (dmPiece.isWritten(blockNumber))
		{
			data.returnToPool();
			return;
		}

		PEPiece	pe_piece = pePieces[ pieceNumber ];

		if ( pe_piece != null ){

			pe_piece.setDownloaded( offset );
		}

		final DiskManagerWriteRequest request =disk_mgr.createWriteRequest(pieceNumber, offset, data, sender);
		disk_mgr.enqueueWriteRequest(request, this );
		// In case we are in endGame mode, remove the block from the chunk list
		if (piecePicker.isInEndGameMode())
			piecePicker.removeFromEndGameModeChunks(pieceNumber, offset);
		if (cancel ||piecePicker.isInEndGameMode())
		{   // cancel any matching outstanding download requests
			//For all connections cancel the request
			final ArrayList peer_transports = peer_transports_cow;
			for (int i=0;i<peer_transports.size();i++)
			{
				final PEPeerTransport connection =(PEPeerTransport) peer_transports.get(i);
				final DiskManagerReadRequest dmr =disk_mgr.createReadRequest(pieceNumber, offset, dmPiece.getBlockSize(blockNumber));
				connection.sendCancel( dmr );
			}
		}
	}


//	/**
//	* This method is only called when a block is received after the initial request expired,
//	* but the data has not yet been fulfilled by any other peer, so we use the block data anyway
//	* instead of throwing it away, and cancel any outstanding requests for that block that might have
//	* been sent after initial expiry.
//	*/
//	public void writeBlockAndCancelOutstanding(int pieceNumber, int offset, DirectByteBuffer data,PEPeer sender) {
//	final int blockNumber =offset /DiskManager.BLOCK_SIZE;
//	final DiskManagerPiece dmPiece =dm_pieces[pieceNumber];
//	if (dmPiece.isWritten(blockNumber))
//	{
//	data.returnToPool();
//	return;
//	}
//	DiskManagerWriteRequest request =disk_mgr.createWriteRequest(pieceNumber, offset, data, sender);
//	disk_mgr.enqueueWriteRequest(request, this);

//	// cancel any matching outstanding download requests
//	List peer_transports =peer_transports_cow;
//	for (int i =0; i <peer_transports.size(); i++)
//	{
//	PEPeerTransport connection =(PEPeerTransport) peer_transports.get(i);
//	DiskManagerReadRequest dmr =disk_mgr.createReadRequest(pieceNumber, offset, dmPiece.getBlockSize(blockNumber));
//	connection.sendCancel(dmr);
//	}
//	}


	public boolean isWritten(int piece_number, int offset)
	{
		return dm_pieces[piece_number].isWritten(offset /DiskManager.BLOCK_SIZE);
	}

	public boolean 
	validateReadRequest(
			PEPeerTransport	originator,
			int				pieceNumber, 
			int 			offset, 
			int 			length) 
	{
		if ( disk_mgr.checkBlockConsistencyForRead(originator.getClient() + ": " + originator.getIp(), true, pieceNumber, offset, length)){

			if ( enable_seeding_piece_rechecks && isSeeding()){

				DiskManagerPiece	dm_piece = dm_pieces[pieceNumber];

				int	read_count = dm_piece.getReadCount()&0xffff;

				if ( read_count < SEED_CHECK_WAIT_MARKER - 1 ){

					read_count++;

					dm_piece.setReadCount((short)read_count );
				}
			}

			return( true );
		}else{

			return( false );
		}
	}

	public boolean 
	validateHintRequest(
			PEPeerTransport	originator,
			int				pieceNumber, 
			int 			offset, 
			int 			length) 
	{
		return( disk_mgr.checkBlockConsistencyForHint(originator.getClient() + ": " + originator.getIp(),pieceNumber, offset, length ));
	}

	public boolean 
	validatePieceReply(
		PEPeerTransport		originator, 
		int 				pieceNumber, 
		int 				offset, 
		DirectByteBuffer 	data) 
	{
		return disk_mgr.checkBlockConsistencyForWrite(originator.getClient() + ": " + originator.getIp(),pieceNumber, offset, data);
	}

	public int getAvailability(int pieceNumber)
	{
		return piecePicker.getAvailability(pieceNumber); 
	}	

	public void havePiece(int pieceNumber, int pieceLength, PEPeer pcOrigin) {
		piecePicker.addHavePiece(pcOrigin, pieceNumber);
		_stats.haveNewPiece(pieceLength);

		if(superSeedMode) {
			superSeedPieces[pieceNumber].peerHasPiece(pcOrigin);
			if(pieceNumber == pcOrigin.getUniqueAnnounce()) {
				pcOrigin.setUniqueAnnounce(-1);
				superSeedModeNumberOfAnnounces--;
			}      
		}
		int availability =piecePicker.getAvailability(pieceNumber) -1;
		if (availability < 4) {
			if (dm_pieces[pieceNumber].isDone())
				availability--;
			if (availability <= 0)
				return;
			//for all peers

			final ArrayList peer_transports = peer_transports_cow;

			for (int i = peer_transports.size() - 1; i >= 0; i--) {
				final PEPeerTransport pc = (PEPeerTransport) peer_transports.get(i);
				if (pc !=pcOrigin &&pc.getPeerState() ==PEPeer.TRANSFERING &&pc.isPieceAvailable(pieceNumber))
					((PEPeerStatsImpl)pc.getStats()).statisticalSentPiece(pieceLength / availability);
			}
		}
	}

	public int getPieceLength(int pieceNumber) {
				
		return disk_mgr.getPieceLength(pieceNumber);

	}

	public int 
	getNbPeers() 
	{
		return _peers;
	}

	public int getNbSeeds() 
	{
		return _seeds;
	}

	public int getNbRemoteTCPConnections() 
	{
		return _remotesTCPNoLan;
	}

	public int getNbRemoteUDPConnections() 
	{
		return _remotesUDPNoLan;
	}
	public int getNbRemoteUTPConnections() 
	{
		return _remotesUTPNoLan;
	}
	
	public long getLastRemoteConnectionTime()
	{
		return( last_remote_time );
	}

	public PEPeerManagerStats getStats() {
		return _stats;
	}

	public int 
	getNbPeersStalledPendingLoad()
	{
		int	res = 0;

		Iterator it = peer_transports_cow.iterator();

		while( it.hasNext()){

			PEPeerTransport transport = (PEPeerTransport)it.next();

			if ( transport.isStalledPendingLoad()){

				res ++;
			}
		}

		return( res );
	}

	/**
	 * Returns the ETA time in seconds.
	 * If the returned time is 0, the download is complete.
	 * If the returned time is negative, the download
	 * is complete and it took -xxx time to complete.
	 */
	public long 
	getETA() 
	{	
		final long	now = SystemTime.getCurrentTime();

		if ( now < last_eta_calculation || now - last_eta_calculation > 900 ){

			long dataRemaining = disk_mgr.getRemainingExcludingDND();

			if ( dataRemaining > 0 ){

				int writtenNotChecked = 0;

				for (int i = 0; i < _nbPieces; i++)
				{
					if (dm_pieces[i].isInteresting()){
						writtenNotChecked +=dm_pieces[i].getNbWritten() *DiskManager.BLOCK_SIZE;
					}
				}

				dataRemaining = dataRemaining - writtenNotChecked;

				if  (dataRemaining < 0 ){
					dataRemaining	= 0;
				}
			}

			long	result;

			if (dataRemaining == 0) {
				final long timeElapsed = (_timeFinished - _timeStarted)/1000;
				//if time was spent downloading....return the time as negative
				if(timeElapsed > 1){
					result = timeElapsed * -1;
				}else{
					result = 0;
				}
			}else{

				final long averageSpeed = _averageReceptionSpeed.getAverage();
				long lETA = (averageSpeed == 0) ? Constants.CRAPPY_INFINITE_AS_LONG : dataRemaining / averageSpeed;
				// stop the flickering of ETA from "Finished" to "x seconds" when we are 
				// just about complete, but the data rate is jumpy.
				if (lETA == 0)
					lETA = 1;
				result = lETA;
			}

			last_eta				= result;
			last_eta_calculation	= now;
		}

		return( last_eta );
	}

	public boolean 
	isRTA()
	{
		return( piecePicker.getRTAProviders().size() > 0 );
	}
	
	private void
	addToPeerTransports(
		PEPeerTransport		peer )
	{
		boolean added = false;

		List limiters;

		try{
			peer_transports_mon.enter();

			// if it is already disconnected (synchronous failure during connect
			// for example) don't add it

			if ( peer.getPeerState() == PEPeer.DISCONNECTED ){

				return;
			}

			if( peer_transports_cow.contains( peer ) ){
				Debug.out( "Transport added twice" );
				return;  //we do not want to close it
			}

			if( is_running ) {
				//copy-on-write semantics
				final ArrayList new_peer_transports = new ArrayList(peer_transports_cow.size() +1);

				new_peer_transports.addAll( peer_transports_cow );

				new_peer_transports.add( peer );

				peer_transports_cow = new_peer_transports;

				added = true;
			}

			limiters = external_rate_limiters_cow;
		}
		finally{
			peer_transports_mon.exit();
		}

		if( added ) {
			boolean incoming = peer.isIncoming();
			
			_stats.haveNewConnection( incoming );
			
			if ( incoming ){
				long	connect_time = SystemTime.getCurrentTime();

				if ( connect_time > last_remote_time ){

					last_remote_time = connect_time;
				}
			}

			if ( limiters != null ){

				for (int i=0;i<limiters.size();i++){

					Object[]	entry = (Object[])limiters.get(i);

					peer.addRateLimiter((LimitedRateGroup)entry[0],((Boolean)entry[1]).booleanValue());
				}
			}

			peerAdded( peer ); 
		}
		else {
			peer.closeConnection( "PeerTransport added when manager not running" );
		}
	}

	public void
	addRateLimiter(
		LimitedRateGroup	group,
		boolean				upload )
	{
		List<PEPeer>	transports;

		try{
			peer_transports_mon.enter();

			ArrayList<Object[]>	new_limiters = new ArrayList<Object[]>( external_rate_limiters_cow==null?1:external_rate_limiters_cow.size()+1);

			if ( external_rate_limiters_cow != null ){

				new_limiters.addAll( external_rate_limiters_cow );
			}

			new_limiters.add( new Object[]{ group, new Boolean( upload )});

			external_rate_limiters_cow = new_limiters;

			transports = peer_transports_cow;

		}finally{

			peer_transports_mon.exit();
		}	

		for (int i=0;i<transports.size();i++){

			transports.get(i).addRateLimiter( group, upload );
		}
	}

	public void
	removeRateLimiter(
		LimitedRateGroup	group,
		boolean				upload )
	{
		List	transports;

		try{
			peer_transports_mon.enter();

			if ( external_rate_limiters_cow != null ){

				ArrayList	new_limiters = new ArrayList( external_rate_limiters_cow.size()-1);

				for (int i=0;i<external_rate_limiters_cow.size();i++){

					Object[]	entry = (Object[])external_rate_limiters_cow.get(i);

					if ( entry[0] != group ){

						new_limiters.add( entry );
					}
				}

				if ( new_limiters.size() == 0 ){

					external_rate_limiters_cow = null;

				}else{

					external_rate_limiters_cow = new_limiters;
				}
			}

			transports = peer_transports_cow;

		}finally{

			peer_transports_mon.exit();
		}	

		for (int i=0;i<transports.size();i++){

			((PEPeerTransport)transports.get(i)).removeRateLimiter( group, upload );
		}	
	}

	public int 
	getUploadRateLimitBytesPerSecond()
	{
		return( adapter.getUploadRateLimitBytesPerSecond());
	}

	public int 
	getDownloadRateLimitBytesPerSecond()
	{
		return( adapter.getDownloadRateLimitBytesPerSecond());
	}

		//	the peer calls this method itself in closeConnection() to notify this manager
	
	public void 
	peerConnectionClosed( 
		PEPeerTransport 	peer, 
		boolean 			connect_failed,
		boolean				network_failed ) 
	{
		boolean	connection_found = false;
		
		boolean tcpReconnect = false;
		boolean ipv6reconnect = false;
		
		try{
			peer_transports_mon.enter();
			
			int udpPort = peer.getUDPListenPort();
			
			boolean canTryUDP = UDPNetworkManager.UDP_OUTGOING_ENABLED && peer.getUDPListenPort() > 0;
			boolean canTryIpv6 = NetworkAdmin.getSingleton().hasIPV6Potential(true) && peer.getAlternativeIPv6() != null;

			if ( is_running ){

				PeerItem peer_item = peer.getPeerItemIdentity();
				PeerItem self_item = peer_database.getSelfPeer();
				

				if ( self_item == null || !self_item.equals( peer_item )){

					String	ip = peer.getIp();
					boolean wasIPv6;
					try
					{
						wasIPv6 = InetAddress.getByName(ip) instanceof Inet6Address;
					} catch (UnknownHostException e)
					{
						wasIPv6 = false;
						// something is fishy about the old address, don't try to reconnect with v6
						canTryIpv6 = false;
					}
					
					//System.out.println("netfail="+network_failed+", connfail="+connect_failed+", can6="+canTryIpv6+", was6="+wasIPv6);
					
					String	key = ip + ":" + udpPort;

					if ( peer.isTCP()){
					
						if ( connect_failed ){
							
								// TCP connect failure, try UDP later if necessary
						
							if ( canTryUDP && udp_fallback_for_failed_connection ){
								
								pending_nat_traversals.put(key, peer);
							} else if (canTryIpv6 && !wasIPv6)
							{
								tcpReconnect = true;
								ipv6reconnect = true;
							}
						}else if ( 	canTryUDP && 
									udp_fallback_for_dropped_connection && 
									network_failed && 
									seeding_mode && 
									peer.isInterested() && 
									!peer.isSeed() && !peer.isRelativeSeed() &&
									peer.getStats().getEstimatedSecondsToCompletion() > 60 &&
									FeatureAvailability.isUDPPeerReconnectEnabled()){
						
							if (Logger.isEnabled()){
								Logger.log(new LogEvent(peer, LOGID, LogEvent.LT_WARNING, "Unexpected stream closure detected, attempting recovery"));
							}
							
								// System.out.println( "Premature close of stream: " + getDisplayName() + "/" + peer.getIp());
							
							udp_reconnects.put( key, peer );
							
						}else if (	network_failed && 
									peer.isSafeForReconnect() &&
									!(seeding_mode && (peer.isSeed() || peer.isRelativeSeed() || peer.getStats().getEstimatedSecondsToCompletion() < 60)) &&
									getMaxConnections() > 0 && 
									getMaxNewConnectionsAllowed() > getMaxConnections() / 3 &&
									FeatureAvailability.isGeneralPeerReconnectEnabled()){
					
							tcpReconnect = true;							
						}
					}else if ( connect_failed ){
						
							// UDP connect failure
					
						if ( udp_fallback_for_failed_connection ){
							
							if ( peer.getData(PEER_NAT_TRAVERSE_DONE_KEY) == null){
								
								// System.out.println( "Direct reconnect failed, attempting NAT traversal" );
								
								pending_nat_traversals.put(key, peer);
							}
						}
					}
				}
			}

			if( peer_transports_cow.contains( peer )) {
				final ArrayList new_peer_transports = new ArrayList( peer_transports_cow );
				new_peer_transports.remove(peer);
				peer_transports_cow = new_peer_transports;
				connection_found  = true;
			}
		}
		finally{
			peer_transports_mon.exit();
		}

		if ( connection_found ){
			if( peer.getPeerState() != PEPeer.DISCONNECTED ) {
				System.out.println( "peer.getPeerState() != PEPeer.DISCONNECTED: " +peer.getPeerState() );
			}

			peerRemoved( peer );  //notify listeners
		}
		
		if(tcpReconnect)
			peer.reconnect(false, ipv6reconnect);
	}




	public void 
	peerAdded(
			PEPeer pc) 
	{
		adapter.addPeer(pc);  //async downloadmanager notification

		//sync peermanager notification
		final ArrayList peer_manager_listeners = peer_manager_listeners_cow;

		for( int i=0; i < peer_manager_listeners.size(); i++ ) {
			((PEPeerManagerListener)peer_manager_listeners.get(i)).peerAdded( this, pc );
		}
	}


	public void 
	peerRemoved(
			PEPeer pc) 
	{
		if ( 	is_running && 
				!seeding_mode && 
				( prefer_udp || prefer_udp_default )){
							
			int udp = pc.getUDPListenPort();
			
			if ( udp != 0 && udp == pc.getTCPListenPort()){
				
				BloomFilter filter = prefer_udp_bloom;
				
				if ( filter == null ){
					
					filter = prefer_udp_bloom = BloomFilterFactory.createAddOnly( PREFER_UDP_BLOOM_SIZE );				
				}
				
				if ( filter.getEntryCount() < PREFER_UDP_BLOOM_SIZE / 10 ){
											
					filter.add( pc.getIp().getBytes());
				}
			}
		}

		final int piece = pc.getUniqueAnnounce();
		if(piece != -1 && superSeedMode ) {
			superSeedModeNumberOfAnnounces--;
			superSeedPieces[piece].peerLeft();
		}

		int[]	reserved_pieces = pc.getReservedPieceNumbers();

		if ( reserved_pieces != null ){

			for ( int reserved_piece: reserved_pieces ){
				
				PEPiece	pe_piece = pePieces[reserved_piece];
	
				if ( pe_piece != null ){
	
					String	reserved_by = pe_piece.getReservedBy();
	
					if ( reserved_by != null && reserved_by.equals( pc.getIp())){
	
						pe_piece.setReservedBy( null );
					}
				}
			}
		}

		adapter.removePeer(pc);  //async downloadmanager notification

		//sync peermanager notification
		final ArrayList peer_manager_listeners = peer_manager_listeners_cow;

		for( int i=0; i < peer_manager_listeners.size(); i++ ) {
			((PEPeerManagerListener)peer_manager_listeners.get(i)).peerRemoved( this, pc );
		}
	}

	/** Don't pass a null to this method. All activations of pieces must go through here.
	 * @param piece PEPiece invoked; notifications of it's invocation need to be done
	 * @param pieceNumber of the PEPiece 
	 */
	public void addPiece(final PEPiece piece, final int pieceNumber, PEPeer for_peer )
	{
		addPiece( piece, pieceNumber, false, for_peer );
	}

	protected void addPiece(final PEPiece piece, final int pieceNumber, final boolean force_add, PEPeer for_peer )
	{
		pePieces[pieceNumber] =(PEPieceImpl)piece;
		nbPiecesActive++;
		if ( is_running || force_add ){
			// deal with possible piece addition by scheduler loop after closdown started
			adapter.addPiece(piece);
		}
		
		final ArrayList peer_manager_listeners = peer_manager_listeners_cow;

		for( int i=0; i < peer_manager_listeners.size(); i++ ) {
			try{
				((PEPeerManagerListener)peer_manager_listeners.get(i)).pieceAdded(this, piece, for_peer );
						
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}

	/** Sends messages to listeners that the piece is no longer active.  All closing
	 * out (deactivation) of pieces must go through here. The piece will be null upon return.
	 * @param pePiece PEPiece to remove
	 * @param pieceNumber int
	 */
	public void removePiece(PEPiece pePiece, int pieceNumber) {
		if ( pePiece != null ){
		adapter.removePiece(pePiece);
		} else {
			pePiece = pePieces[pieceNumber];
		}
		pePieces[pieceNumber] =null;
		nbPiecesActive--;
		
		if (pePiece == null) {
			Debug.outNoStack("Trying to remove piece " + pieceNumber + " when piece is null");
			return;
		}
		
		final ArrayList peer_manager_listeners = peer_manager_listeners_cow;

		for( int i=0; i < peer_manager_listeners.size(); i++ ) {
			try{
				((PEPeerManagerListener)peer_manager_listeners.get(i)).pieceRemoved(this, pePiece );
						
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}

	public int getNbActivePieces()
	{
		return nbPiecesActive;
	}

	public String getElapsedTime() {
		return TimeFormatter.format((SystemTime.getCurrentTime() - _timeStarted) / 1000);
	}

//	Returns time started in ms
	public long getTimeStarted( boolean mono) {
		return mono?_timeStarted_mono:_timeStarted;
	}

	public long getTimeStartedSeeding( boolean mono) {
		return mono?_timeStartedSeeding_mono:_timeStartedSeeding;
	}

	private byte[] computeMd5Hash(DirectByteBuffer buffer)
	{ 			
		BrokenMd5Hasher md5 	= new BrokenMd5Hasher();

		md5.reset();
		final int position = buffer.position(DirectByteBuffer.SS_DW);
		md5.update(buffer.getBuffer(DirectByteBuffer.SS_DW));
		buffer.position(DirectByteBuffer.SS_DW, position);
		ByteBuffer md5Result	= ByteBuffer.allocate(16);
		md5Result.position(0);
		md5.finalDigest( md5Result );

		final byte[] result =new byte[16];
		md5Result.position(0);
		for (int i =0; i <result.length; i++ )
		{
			result[i] = md5Result.get();
		}   

		return result;    
	}

	private void MD5CheckPiece(PEPiece piece, boolean correct)
	{
		final String[] writers =piece.getWriters();
		int offset = 0;
		for (int i =0; i <writers.length; i++ )
		{
			final int length =piece.getBlockSize(i);
			final String peer =writers[i];
			if (peer !=null)
			{
				DirectByteBuffer buffer =disk_mgr.readBlock(piece.getPieceNumber(), offset, length);

				if (buffer !=null)
				{
					final byte[] hash =computeMd5Hash(buffer);
					buffer.returnToPool();
					buffer = null;
					piece.addWrite(i,peer,hash,correct);  
				}
			}
			offset += length;
		}        
	}

	public void checkCompleted(DiskManagerCheckRequest request, boolean passed)
	{
		if ( TEST_PERIODIC_SEEDING_SCAN_FAIL_HANDLING && ((Integer) request.getUserData()).intValue() == CHECK_REASON_SEEDING_CHECK ){
			
			passed = false;	
		}
		
		try
		{
			piece_check_result_list_mon.enter();

			piece_check_result_list.add(new Object[]{request, new Integer( passed?1:0 )});
		} finally
		{
			piece_check_result_list_mon.exit();
		}
	}

	public void checkCancelled(DiskManagerCheckRequest request)
	{
		try
		{
			piece_check_result_list_mon.enter();

			piece_check_result_list.add(new Object[]{request, new Integer( 2 )});

		} finally
		{
			piece_check_result_list_mon.exit();
		}
	}

	public void checkFailed(DiskManagerCheckRequest request, Throwable cause)
	{
		try
		{
			piece_check_result_list_mon.enter();

			piece_check_result_list.add(new Object[]{request, new Integer( 0 )});

		} finally
		{
			piece_check_result_list_mon.exit();
		}		
	}

	public boolean needsMD5CheckOnCompletion(int pieceNumber)
	{
		final PEPieceImpl piece = pePieces[pieceNumber];    
		if(piece == null)
			return false;
		return piece.getPieceWrites().size() > 0;
	}

	private void processPieceCheckResult(DiskManagerCheckRequest request, int outcome)
	{
		final int check_type =((Integer) request.getUserData()).intValue();

		try{

			final int pieceNumber =request.getPieceNumber();

			// System.out.println( "processPieceCheckResult(" + _finished + "/" + recheck_on_completion + "):" + pieceNumber +
			// "/" + piece + " - " + result );

			// passed = 1, failed = 0, cancelled = 2

			if (check_type ==CHECK_REASON_COMPLETE){
				// this is a recheck, so don't send HAVE msgs
				
				if ( outcome == 0 ){
				
					// piece failed; restart the download afresh
					Debug.out(getDisplayName() + ": Piece #" +pieceNumber +" failed final re-check. Re-downloading...");

					if (!restart_initiated){

						restart_initiated	= true;
						adapter.restartDownload(true);
					}
				}

				return;

			}else if ( check_type == CHECK_REASON_SEEDING_CHECK || check_type == CHECK_REASON_BAD_PIECE_CHECK ){

				if ( outcome == 0 ){
					
					if ( check_type == CHECK_REASON_SEEDING_CHECK ){

					Debug.out(getDisplayName() + "Piece #" +pieceNumber +" failed recheck while seeding. Re-downloading...");
	
					}else{
						
						Debug.out(getDisplayName() + "Piece #" +pieceNumber +" failed recheck after being reported as bad. Re-downloading...");
					}

					Logger.log(new LogAlert(this, LogAlert.REPEATABLE, LogAlert.AT_ERROR,
							"Download '" + getDisplayName() + "': piece " + pieceNumber
									+ " has been corrupted, re-downloading"));

					if ( !restart_initiated ){

						restart_initiated = true;

						adapter.restartDownload(true);
					}
				}

				return;
			}

			// piece can be null when running a recheck on completion
				// actually, give the above code I don't think this is true anymore...
			
			final PEPieceImpl pePiece =pePieces[pieceNumber];

			if ( outcome == 1 || is_metadata_download ){
			
			//  the piece has been written correctly
				 
				try{
					if (pePiece !=null){
					
						if (needsMD5CheckOnCompletion(pieceNumber)){
						MD5CheckPiece(pePiece, true);
						}

						final List list =pePiece.getPieceWrites();
						
						if (list.size() >0 ){
						
							//For each Block
							for (int i =0; i <pePiece.getNbBlocks(); i++ ){

								//System.out.println("Processing block " + i);
								//Find out the correct hash
								final List listPerBlock =pePiece.getPieceWrites(i);
								byte[] correctHash = null;
								//PEPeer correctSender = null;
								Iterator iterPerBlock = listPerBlock.iterator();
								while (iterPerBlock.hasNext())
								{
									final PEPieceWriteImpl write =(PEPieceWriteImpl) iterPerBlock.next();
									if (write.isCorrect())
									{
										correctHash = write.getHash();
										//correctSender = write.getSender();
									}
								}
								//System.out.println("Correct Hash " + correctHash);
								//If it's found                       
								if (correctHash !=null)
								{
									iterPerBlock = listPerBlock.iterator();
									while (iterPerBlock.hasNext())
									{
										final PEPieceWriteImpl write =(PEPieceWriteImpl) iterPerBlock.next();
										if (!Arrays.equals(write.getHash(), correctHash))
										{
											//Bad peer found here
											badPeerDetected(write.getSender(),pieceNumber);
										}
									}
								}              
							}            
						}
					}
				}finally{
						// regardless of any possible failure above, tidy up correctly
					
				removePiece(pePiece, pieceNumber);

				// send all clients a have message
				sendHave(pieceNumber);  //XXX: if Done isn't set yet, might refuse to send this piece
				}
			}else if ( outcome == 0 ){
		
				//    the piece is corrupt
					
				if ( pePiece != null ){

					try{
					MD5CheckPiece(pePiece, false);

					final String[] writers =pePiece.getWriters();
					final List uniqueWriters =new ArrayList();
					final int[] writesPerWriter =new int[writers.length];
					for (int i =0; i <writers.length; i++ )
					{
						final String writer =writers[i];
						if (writer !=null)
						{
							int writerId = uniqueWriters.indexOf(writer);
							if (writerId ==-1)
							{
								uniqueWriters.add(writer);
								writerId = uniqueWriters.size() - 1;
							}
							writesPerWriter[writerId]++;
						}
					}
					final int nbWriters =uniqueWriters.size();
					if (nbWriters ==1)
					{
						//Very simple case, only 1 peer contributed for that piece,
						//so, let's mark it as a bad peer
							
							String	bad_ip = (String)uniqueWriters.get(0);
							
							PEPeerTransport bad_peer = getTransportFromAddress( bad_ip );
						
							if ( bad_peer != null ){
								
								bad_peer.sendBadPiece( pieceNumber );
							}
							
							badPeerDetected( bad_ip, pieceNumber);

						//and let's reset the whole piece
						pePiece.reset();
							
						}else if ( nbWriters > 1 ){
						
						int maxWrites = 0;
						String bestWriter =null;
							
							PEPeerTransport	bestWriter_transport = null;
	
							for (int i =0; i <uniqueWriters.size(); i++ ){
							
							final int writes =writesPerWriter[i];
								
								if (writes > maxWrites ){
								
								final String writer =(String) uniqueWriters.get(i);
									
									PEPeerTransport pt =getTransportFromAddress(writer);
									
									if (	pt !=null &&
											pt.getReservedPieceNumbers() == null &&
											!ip_filter.isInRange(writer, getDisplayName(),getTorrentHash())){
									
									bestWriter = writer;
									maxWrites = writes;
										
										bestWriter_transport	= pt;
								}
							}
						}
							
							if ( bestWriter !=null ){
	
								pePiece.setReservedBy(bestWriter);
								
								bestWriter_transport.addReservedPieceNumber(pePiece.getPieceNumber());
								
								pePiece.setRequestable();
								
								for (int i =0; i <pePiece.getNbBlocks(); i++ ){
								
								// If the block was contributed by someone else
									
									if (writers[i] == null ||!writers[i].equals( bestWriter )){
																			
									pePiece.reDownloadBlock(i);
								}
							}
							}else{
							
							//In all cases, reset the piece
							pePiece.reset();
						}            
						}else{
						
						//In all cases, reset the piece
						pePiece.reset();
					} 


					//if we are in end-game mode, we need to re-add all the piece chunks
					//to the list of chunks needing to be downloaded
					piecePicker.addEndGameChunks(pePiece);
					_stats.hashFailed(pePiece.getLength());
					
					}catch( Throwable e ){
						
						Debug.printStackTrace(e);
						
							// anything craps out in the above code, ensure we tidy up
						
						pePiece.reset();
				}
				}else{
					
						// no active piece for some reason, clear down DM piece anyway
					
					Debug.out(getDisplayName() + "Piece #" +pieceNumber +" failed check and no active piece, resetting..." );

					dm_pieces[pieceNumber].reset();
				}
			}else{
				
				// cancelled, download stopped
			}
		}finally{
			
			if (check_type ==CHECK_REASON_SCAN ){
				rescan_piece_time	= SystemTime.getCurrentTime();
			}

			if (!seeding_mode ){
				checkFinished( false );	 
		}
	}
	}

	
	private void badPeerDetected(String ip,  int piece_number )
	{
		boolean hash_fail = piece_number >= 0;
		
			// note that peer can be NULL but things still work in the main
		
		PEPeerTransport peer = getTransportFromAddress(ip);
		
		if ( hash_fail && peer != null ){
			
			Iterator<PEPeerManagerListener> it = peer_manager_listeners_cow.iterator();

			while( it.hasNext()){
				
				try{
					it.next().peerSentBadData( this, peer, piece_number );
							
				}catch( Throwable e ){
					
					Debug.printStackTrace(e);
				}
			}
		}
		// Debug.out("Bad Peer Detected: " + peerIP + " [" + peer.getClient() + "]");

		IpFilterManager filter_manager =IpFilterManagerFactory.getSingleton();

		//Ban fist to avoid a fast reco of the bad peer
		
		int nbWarnings = filter_manager.getBadIps().addWarningForIp(ip);

		boolean	disconnect_peer = false;

		// no need to reset the bad chunk count as the peer is going to be disconnected and
		// if it comes back it'll start afresh
		
		// warning limit only applies to hash-fails, discards cause immediate action
		
		if ( nbWarnings > WARNINGS_LIMIT ){
		
			if (COConfigurationManager.getBooleanParameter("Ip Filter Enable Banning")){
			
				// if a block-ban occurred, check other connections
				
				if (ip_filter.ban(ip, getDisplayName(), false )){
				
					checkForBannedConnections();
				}

				// Trace the ban
				if (Logger.isEnabled()){
					Logger.log(new LogEvent(peer, LOGID, LogEvent.LT_ERROR, ip +" : has been banned and won't be able "
						+"to connect until you restart azureus"));
				}
				
				disconnect_peer = true;
			}
		}else if ( !hash_fail ){
			
				// for failures due to excessive discard we boot the peer anyway
			
			disconnect_peer	= true;
			
		}
		
		if ( disconnect_peer ){
			
			if ( peer != null ){

					final int ps =peer.getPeerState();

					// might have been through here very recently and already started closing
					// the peer (due to multiple bad blocks being found from same peer when checking piece)
					if (!(ps ==PEPeer.CLOSING ||ps ==PEPeer.DISCONNECTED))
					{
						//	Close connection
					closeAndRemovePeer(peer, "has sent too many " + (hash_fail?"bad pieces":"discarded blocks") + ", " +WARNINGS_LIMIT +" max.", true);      	
				}
			}    
		}
	}

	public PEPiece[] getPieces()
	{
		return pePieces;
	}

	public PEPiece getPiece(int pieceNumber)
	{
		return pePieces[pieceNumber];
	}

	public PEPeerStats
	createPeerStats(
			PEPeer	owner )
	{
		return( new PEPeerStatsImpl( owner ));
	}


	public DiskManagerReadRequest
	createDiskManagerRequest(
			int pieceNumber,
			int offset,
			int length )
	{
		return( disk_mgr.createReadRequest( pieceNumber, offset, length ));
	}

	public boolean
	requestExists(
			String			peer_ip,
			int				piece_number,
			int				offset,
			int				length )
	{
		ArrayList peer_transports = peer_transports_cow;

		DiskManagerReadRequest	request = null;

		for (int i=0; i < peer_transports.size(); i++) {

			PEPeerTransport conn = (PEPeerTransport)peer_transports.get( i );

			if ( conn.getIp().equals( peer_ip )){

				if ( request == null ){

					request = createDiskManagerRequest( piece_number, offset, length );					
				}

				if ( conn.getRequestIndex( request ) != -1 ){

					return( true );
				}
			}
		}

		return( false );
	}

	public boolean
	seedPieceRecheck()
	{
		if ( !( enable_seeding_piece_rechecks || isSeeding())){

			return( false );
		}

		int	max_reads 		= 0;
		int	max_reads_index = 0;

		if ( TEST_PERIODIC_SEEDING_SCAN_FAIL_HANDLING ){
			
			max_reads_index = (int)(Math.random()*dm_pieces.length);;
			max_reads = dm_pieces[ max_reads_index ].getNbBlocks()*3;

		}else{

			for (int i=0;i<dm_pieces.length;i++){
	
					// skip dnd pieces
				
				DiskManagerPiece	dm_piece = dm_pieces[i];
				
				if ( !dm_piece.isDone()){
					
					continue;
				}
				
				int	num = dm_piece.getReadCount()&0xffff;
	
				if ( num > SEED_CHECK_WAIT_MARKER ){
	
					// recently been checked, skip for a while
	
						num--;
	
					if ( num == SEED_CHECK_WAIT_MARKER ){
	
						num = 0;
					}
	
					dm_piece.setReadCount((short)num);
	
				}else{
	
					if ( num > max_reads ){
	
						max_reads 		= num;
						max_reads_index = i;
					}
				}
			}
		}
		
		if ( max_reads > 0 ){

			DiskManagerPiece	dm_piece = dm_pieces[ max_reads_index ];

			// if the piece has been read 3 times (well, assuming each block is read once,
			// which is obviously wrong, but...)

			if ( max_reads >= dm_piece.getNbBlocks() * 3 ){

				DiskManagerCheckRequest	req = disk_mgr.createCheckRequest( max_reads_index, new Integer( CHECK_REASON_SEEDING_CHECK ) );

				req.setAdHoc( true );

				req.setLowPriority( true );

				if (Logger.isEnabled())
					Logger.log(new LogEvent(disk_mgr.getTorrent(), LOGID,
							"Rechecking piece " + max_reads_index + " while seeding as most active"));

				disk_mgr.enqueueCheckRequest( req, this );

				dm_piece.setReadCount((short)65535);

				// clear out existing, non delayed pieces so we start counting piece activity
				// again

				for (int i=0;i<dm_pieces.length;i++){

					if ( i != max_reads_index ){

						int	num = dm_pieces[i].getReadCount()&0xffff;

						if ( num < SEED_CHECK_WAIT_MARKER ){

							dm_pieces[i].setReadCount((short)0 );
						}
					}
				}

				return( true );
			}
		}

		return( false );
	}

	public void
	addListener(
			PEPeerManagerListener	l )
	{
		try{
			this_mon.enter();

			//copy on write
			final ArrayList peer_manager_listeners = new ArrayList( peer_manager_listeners_cow.size() + 1 );      
			peer_manager_listeners.addAll( peer_manager_listeners_cow );
			peer_manager_listeners.add( l );
			peer_manager_listeners_cow = peer_manager_listeners;

		}finally{

			this_mon.exit();
		}
	}

	public void
	removeListener(
			PEPeerManagerListener	l )
	{
		try{
			this_mon.enter();

			//copy on write
			final ArrayList peer_manager_listeners = new ArrayList( peer_manager_listeners_cow );
			peer_manager_listeners.remove( l );
			peer_manager_listeners_cow = peer_manager_listeners;

		}finally{

			this_mon.exit();
		}
	}


	public void 
	parameterChanged(
			String parameterName)
	{   
		if ( parameterName.equals("Ip Filter Enabled")){

			checkForBannedConnections();
		}
	}

	private void
	checkForBannedConnections()
	{
		if ( ip_filter.isEnabled()){  //if ipfiltering is enabled, remove any existing filtered connections    	
			ArrayList to_close = null;

			final ArrayList	peer_transports = peer_transports_cow;
		
		String name 	= getDisplayName();
		byte[]	hash	= getTorrentHash();
		
			for (int i=0; i < peer_transports.size(); i++) {
				final PEPeerTransport conn = (PEPeerTransport)peer_transports.get( i );

  			if ( ip_filter.isInRange( conn.getIp(), name, hash )) {        	
					if( to_close == null )  to_close = new ArrayList();
					to_close.add( conn );
				}
			}

			if( to_close != null ) {		
				for( int i=0; i < to_close.size(); i++ ) {  			
					closeAndRemovePeer( (PEPeerTransport)to_close.get(i), "IPFilter banned IP address", true );
				}
			}
		}
	}

	public boolean
	isSeeding()
	{
		return( seeding_mode );
	}

	public boolean
	isMetadataDownload()
	{
		return( is_metadata_download );
	}
	
	public int
	getTorrentInfoDictSize()
	{
		return( metadata_infodict_size );
	}
	  
	public void
	setTorrentInfoDictSize(
		int		size )
	{
		metadata_infodict_size	= size;
	}
	  
	public boolean isInEndGameMode() {
		return piecePicker.isInEndGameMode();
	}

	public boolean isSuperSeedMode() {
		return superSeedMode;
	}

	public boolean
	canToggleSuperSeedMode()
	{
		if ( superSeedMode ){

			return( true );
		}

		return( superSeedPieces == null && this.getRemaining() ==0  );
	}

	public void 
	setSuperSeedMode(
			boolean _superSeedMode) 
	{
		if ( _superSeedMode == superSeedMode ){

			return;
		}

		boolean	kick_peers = false;

		if ( _superSeedMode ){

			if ( superSeedPieces == null && this.getRemaining() ==0  ){

				superSeedMode = true;

				initialiseSuperSeedMode();

				kick_peers	= true;
			}
		}else{

			superSeedMode = false;

			kick_peers	= true;
		}

		if ( kick_peers ){

			// turning on/off super-seeding, gotta kick all connected peers so they get the
			// "right" bitfield

			ArrayList peer_transports = peer_transports_cow;

			for (int i=0; i < peer_transports.size(); i++) {

				PEPeerTransport conn = (PEPeerTransport)peer_transports.get( i );

				closeAndRemovePeer( conn, "Turning on super-seeding", false );
			}
		}
	}    

	private void
	initialiseSuperSeedMode()
	{
		superSeedPieces = new SuperSeedPiece[_nbPieces];
		for(int i = 0 ; i < _nbPieces ; i++) {
			superSeedPieces[i] = new SuperSeedPiece(this,i);
		}
	}

	private void updatePeersInSuperSeedMode() {
		if(!superSeedMode) {
			return;
		}  

		//Refresh the update time in case this is needed
		for(int i = 0 ; i < superSeedPieces.length ; i++) {
			superSeedPieces[i].updateTime();
		}

		//Use the same number of announces than unchoke
		int nbUnchoke = adapter.getMaxUploads();
		if(superSeedModeNumberOfAnnounces >= 2 * nbUnchoke)
			return;


		//Find an available Peer
		PEPeer selectedPeer = null;
		List sortedPeers = null;

		final ArrayList	peer_transports = peer_transports_cow;

		sortedPeers = new ArrayList(peer_transports.size());
		Iterator iter = peer_transports.iterator();
		while(iter.hasNext()) {
			sortedPeers.add(new SuperSeedPeer((PEPeer)iter.next()));
		}      

		Collections.sort(sortedPeers);
		iter = sortedPeers.iterator();
		while(iter.hasNext()) {
			final PEPeer peer = ((SuperSeedPeer)iter.next()).peer;
			if((peer.getUniqueAnnounce() == -1) && (peer.getPeerState() == PEPeer.TRANSFERING)) {
				selectedPeer = peer;
				break;
			}
		}      

		if(selectedPeer == null ||selectedPeer.getPeerState() >=PEPeer.CLOSING)
			return;

		if(selectedPeer.getUploadHint() == 0) {
			//Set to infinite
			selectedPeer.setUploadHint(Constants.CRAPPY_INFINITY_AS_INT);
		}

		//Find a piece
		boolean found = false;
		SuperSeedPiece piece = null;
		boolean loopdone = false;  // add loop status
		
		while(!found) {
			piece = superSeedPieces[superSeedModeCurrentPiece];
			if(piece.getLevel() > 0) {
				piece = null;
				superSeedModeCurrentPiece++;
				if(superSeedModeCurrentPiece >= _nbPieces) {
					superSeedModeCurrentPiece = 0;
					
					if (loopdone) {  // if already been here, has been full loop through pieces, quit
						//quit superseed mode
						superSeedMode = false;
						closeAndRemoveAllPeers( "quiting SuperSeed mode", true );
						return;
					}
					else {                                                                                  
						// loopdone==false --> first time here --> go through the pieces                    
						// for a second time to check if reserved pieces have got freed due to peers leaving
						loopdone = true;                                                                    
					}				
				}
			} else {
				found = true;
			}			  
		}

		if(piece == null) {
			return;
		}

		//If this peer already has this piece, return (shouldn't happen)
		if(selectedPeer.isPieceAvailable(piece.getPieceNumber())) {
			return;
		}

		selectedPeer.setUniqueAnnounce(piece.getPieceNumber());
		superSeedModeNumberOfAnnounces++;
		piece.pieceRevealedToPeer();
		((PEPeerTransport)selectedPeer).sendHave(piece.getPieceNumber());		
	}

	public void updateSuperSeedPiece(PEPeer peer,int pieceNumber) {
		// currently this gets only called from bitfield scan function in PEPeerTransportProtocol
		if (!superSeedMode)
			return;
		superSeedPieces[pieceNumber].peerHasPiece(null);
		if(peer.getUniqueAnnounce() == pieceNumber)
		{
			peer.setUniqueAnnounce(-1);
			superSeedModeNumberOfAnnounces--;        
		}
	}

	public boolean
	isExtendedMessagingEnabled()
	{
		return( adapter.isExtendedMessagingEnabled());
	}

	public boolean
	isPeerExchangeEnabled()
	{
		return( adapter.isPeerExchangeEnabled());
	}

	public LimitedRateGroup getUploadLimitedRateGroup() {  return upload_limited_rate_group;  }

	public LimitedRateGroup getDownloadLimitedRateGroup() {  return download_limited_rate_group;  }


	/** To retreive arbitrary objects against this object. */
	public Object 
	getData(
			String key) 
	{
		try{
			this_mon.enter();

			if (user_data == null) return null;

			return user_data.get(key);

		}finally{

			this_mon.exit();
		}
	}

	/** To store arbitrary objects against a control. */

	public void 
	setData(
			String key, 
			Object value) 
	{
		try{
			this_mon.enter();

			if (user_data == null) {
				user_data = new HashMap();
			}
			if (value == null) {
				if (user_data.containsKey(key))
					user_data.remove(key);
			} else {
				user_data.put(key, value);
			}
		}finally{
			this_mon.exit();
		}
	}

	public int
	getConnectTimeout(
		int		ct_def )
	{
		if ( ct_def <= 0 ){
			
			return( ct_def );
		}
		
		if ( seeding_mode ){
			
				// seeding mode connections are already de-prioritised so nothing to do
			
			return( ct_def );
		}
		
		int max_sim_con = TCPConnectionManager.MAX_SIMULTANIOUS_CONNECT_ATTEMPTS;
		
			// high, let's not mess with things
		
		if ( max_sim_con >= 50 ){
			
			return( ct_def );
		}
		
			// we have somewhat limited outbound connection limits, see if it makes sense to
			// reduce the connect timeout to prevent connection stall due to a bunch getting
			// stuck 'connecting' for a long time and stalling us
		
		int	connected			= _seeds + _peers;
		int	connecting			= _tcpConnectingConnections;
		int queued				= _tcpPendingConnections;
		
		int	not_yet_connected 	= peer_database.getDiscoveredPeerCount();
		
		int	max = getMaxConnections();
		
		int	potential = connecting + queued + not_yet_connected;
		
		/*
		System.out.println( 
				"connected=" + connected + 
				", queued=" + queued +
				", connecting=" + connecting +
				", queued=" + queued +
				", not_yet=" + not_yet_connected +
				", max=" + max );
		*/
		
			// not many peers -> don't amend
		
		int	lower_limit = max/4;
		
		if ( potential <= lower_limit || max == lower_limit ){
		
			return( ct_def );
		}
		
			// if we got lots of potential, use minimum delay
		
		final int MIN_CT = 7500;
		
		if ( potential >= max ){
			
			return( MIN_CT );
		}
	
			// scale between MIN and ct_def
		
		int pos 	= potential - lower_limit;
		int	scale 	= max - lower_limit;
			
		int res =  MIN_CT + ( ct_def - MIN_CT )*(scale - pos )/scale;
		
		// System.out.println( "scaled->" + res );
		
		return( res );
	}
	
	private void doConnectionChecks() 
	{
		//every 1 second
		if ( mainloop_loop_count % MAINLOOP_ONE_SECOND_INTERVAL == 0 ){
			final ArrayList peer_transports = peer_transports_cow;

			int num_waiting_establishments = 0;

			int udp_connections = 0;

			for( int i=0; i < peer_transports.size(); i++ ) {
				final PEPeerTransport transport = (PEPeerTransport)peer_transports.get( i );

				//update waiting count
				final int state = transport.getConnectionState();
				if( state == PEPeerTransport.CONNECTION_PENDING || state == PEPeerTransport.CONNECTION_CONNECTING ) {
					num_waiting_establishments++;
				}

				if ( !transport.isTCP()){

					udp_connections++;
				}
			}

			int	allowed_seeds = getMaxSeedConnections();

			if ( allowed_seeds > 0 ){

				int	to_disconnect = _seeds - allowed_seeds;

				if ( to_disconnect > 0 ){

					// seeds are limited by people trying to get a reasonable upload by connecting
					// to leechers where possible. disconnect seeds from end of list to prevent
					// cycling of seeds

					for( int i=peer_transports.size()-1; i >= 0 && to_disconnect > 0; i-- ){

						final PEPeerTransport transport = (PEPeerTransport)peer_transports.get( i );

						if ( transport.isSeed()){

							closeAndRemovePeer( transport, "Too many seeds", false );

							to_disconnect--;
						}
					}
				}
			}

			//pass from storage to connector
			int allowed = getMaxNewConnectionsAllowed();

			if( allowed < 0 || allowed > 1000 )  allowed = 1000;  //ensure a very upper limit so it doesnt get out of control when using PEX

			if( adapter.isNATHealthy()) {  //if unfirewalled, leave slots avail for remote connections
				final int free = getMaxConnections() / 20;  //leave 5%
				allowed = allowed - free;
			}

			if( allowed > 0 ) {
				//try and connect only as many as necessary
				
				final int wanted = TCPConnectionManager.MAX_SIMULTANIOUS_CONNECT_ATTEMPTS - num_waiting_establishments;
				
				if( wanted > allowed ) {
					num_waiting_establishments += wanted - allowed;
				}

				int	remaining = allowed;
				
				int	tcp_remaining = TCPNetworkManager.getSingleton().getConnectDisconnectManager().getMaxOutboundPermitted();
				
				int	udp_remaining = UDPNetworkManager.getSingleton().getConnectionManager().getMaxOutboundPermitted();

				//load stored peer-infos to be established
				while( num_waiting_establishments < TCPConnectionManager.MAX_SIMULTANIOUS_CONNECT_ATTEMPTS && ( tcp_remaining > 0 || udp_remaining > 0 )){        	
					if( !is_running )  break;        	

					final PeerItem item = peer_database.getNextOptimisticConnectPeer();

					if( item == null || !is_running )  break;

					final PeerItem self = peer_database.getSelfPeer();
					if( self != null && self.equals( item ) ) {
						continue;
					}

					if( !isAlreadyConnected( item ) ) {
						final String source = PeerItem.convertSourceString( item.getSource() );

						final boolean use_crypto = item.getHandshakeType() == PeerItemFactory.HANDSHAKE_TYPE_CRYPTO;

						int	tcp_port = item.getTCPPort();
						int	udp_port = item.getUDPPort();
						
						if ( udp_port == 0 && udp_probe_enabled ){
							
								// for probing we assume udp port same as tcp
							
							udp_port = tcp_port;
						}
						
						boolean	prefer_udp_overall = prefer_udp || prefer_udp_default;
						
						if ( prefer_udp_overall && udp_port == 0 ){
						
								// see if we have previous record of this address as udp connectable
							
							byte[]	address = item.getIP().getBytes();
							
							BloomFilter	bloom = prefer_udp_bloom;
							
							if ( bloom != null && bloom.contains( address )){
																
								udp_port = tcp_port;
							}
						}
						
						boolean	tcp_ok = TCPNetworkManager.TCP_OUTGOING_ENABLED && tcp_port > 0 && tcp_remaining > 0;
						boolean udp_ok = UDPNetworkManager.UDP_OUTGOING_ENABLED && udp_port > 0 && udp_remaining > 0;

						if ( tcp_ok && !( prefer_udp_overall && udp_ok )){

							if ( makeNewOutgoingConnection( source, item.getAddressString(), tcp_port, udp_port, true, use_crypto, item.getCryptoLevel(), null) == null) {
								
								tcp_remaining--;

								num_waiting_establishments++;
								remaining--;
							}
						}else if ( udp_ok ){

							if ( makeNewOutgoingConnection( source, item.getAddressString(), tcp_port, udp_port, false, use_crypto, item.getCryptoLevel(), null) == null) {
									
								udp_remaining--;

								num_waiting_establishments++;

								remaining--;
							}
						}
					}          
				}

				if ( 	UDPNetworkManager.UDP_OUTGOING_ENABLED &&
						remaining > 0 &&
						udp_remaining > 0 && 
						udp_connections < MAX_UDP_CONNECTIONS ){
					
					doUDPConnectionChecks( remaining );
				}
			}
		}

		//every 5 seconds
		if ( mainloop_loop_count % MAINLOOP_FIVE_SECOND_INTERVAL == 0 ) {
			final ArrayList peer_transports = peer_transports_cow;

			for( int i=0; i < peer_transports.size(); i++ ) {
				final PEPeerTransport transport = (PEPeerTransport)peer_transports.get( i );

				//check for timeouts
				if( transport.doTimeoutChecks() )  continue;

				//keep-alive check
				transport.doKeepAliveCheck();

				//speed tuning check
				transport.doPerformanceTuningCheck();
			}
		}

		// every 10 seconds check for connected + banned peers
		if ( mainloop_loop_count % MAINLOOP_TEN_SECOND_INTERVAL == 0 )
		{
			final long	last_update = ip_filter.getLastUpdateTime();
			if ( last_update != ip_filter_last_update_time )
			{
				ip_filter_last_update_time	= last_update;
				checkForBannedConnections();
			}
		}

		//every 30 seconds
		if ( mainloop_loop_count % MAINLOOP_THIRTY_SECOND_INTERVAL == 0 ) {
			//if we're at our connection limit, time out the least-useful
			//one so we can establish a possibly-better new connection
			optimisticDisconnectCount = 0;
			if( getMaxNewConnectionsAllowed() == 0 ) {  //we've reached limit        
				doOptimisticDisconnect( false, false );
			}
		}


		//sweep over all peers in a 60 second timespan
		float percentage = ((mainloop_loop_count % MAINLOOP_SIXTY_SECOND_INTERVAL) + 1F) / (1F *MAINLOOP_SIXTY_SECOND_INTERVAL);
		int goal;
		if(mainloop_loop_count % MAINLOOP_SIXTY_SECOND_INTERVAL == 0)
		{
			goal = 0;
			sweepList = peer_transports_cow;
		} else
			goal = (int)Math.floor(percentage * sweepList.size());
		
		for( int i=nextPEXSweepIndex; i < goal && i < sweepList.size(); i++) {
			//System.out.println(mainloop_loop_count+" %:"+percentage+" start:"+nextPEXSweepIndex+" current:"+i+" <"+goal+"/"+sweepList.size());
			final PEPeerTransport peer = (PEPeerTransport)sweepList.get( i );
			peer.updatePeerExchange();
		}

		nextPEXSweepIndex = goal;
	}
	
	private void
	doUDPConnectionChecks(
		int		number )
	{
		List	new_connections = null;
		
		try{
			peer_transports_mon.enter();

			long	now = SystemTime.getCurrentTime();
			
			if ( udp_reconnects.size() > 0 && now - last_udp_reconnect >= UDP_RECONNECT_MIN_MILLIS ){
				
				last_udp_reconnect = now;
				
				Iterator it = udp_reconnects.values().iterator();
				
				PEPeerTransport	peer = (PEPeerTransport)it.next();

				it.remove();

				if (Logger.isEnabled()){
					Logger.log(new LogEvent(this, LOGID, LogEvent.LT_INFORMATION, "Reconnecting to previous failed peer " + peer.getPeerItemIdentity().getAddressString()));
				}

				if ( new_connections == null ){
					
					new_connections = new ArrayList();
				}
				
				new_connections.add( peer );

				number--;
				
				if ( number <= 0 ){
					
					return;
				}
			}
			
			if ( pending_nat_traversals.size() == 0 ){

				return;
			}

			int max = MAX_UDP_TRAVERSAL_COUNT;

			// bigger the swarm, less chance of doing it

			if ( seeding_mode ){

				if ( _peers > 8 ){

					max = 0;

				}else{

					max = 1;
				}
			}else if ( _seeds > 8 ){

				max = 0;

			}else if ( _seeds > 4 ){

				max = 1;
			}

			int	avail = max - udp_traversal_count;

			int	to_do = Math.min( number, avail );

			Iterator	it = pending_nat_traversals.values().iterator();

			while( to_do > 0 && it.hasNext()){

				to_do--;

				final PEPeerTransport	peer = (PEPeerTransport)it.next();

				it.remove();

				PeerNATTraverser.getSingleton().create(
						this,
						new InetSocketAddress( peer.getPeerItemIdentity().getAddressString(), peer.getPeerItemIdentity().getUDPPort() ),
						new PeerNATTraversalAdapter()
						{
							private boolean	done;

							public void
							success(
									InetSocketAddress	target )
							{
								complete();
								
								PEPeerTransport newTransport = peer.reconnect(true, false);
								
								if( newTransport != null ){
									
									newTransport.setData(PEER_NAT_TRAVERSE_DONE_KEY, "");
								}
							}

							public void
							failed()
							{
								complete();
							}

							protected void
							complete()
							{
								try{
									peer_transports_mon.enter();

									if ( !done ){

										done = true;

										udp_traversal_count--;
									}
								}finally{

									peer_transports_mon.exit();
								}
							}
						});

				udp_traversal_count++;
			}
		}finally{

			peer_transports_mon.exit();
			
			if ( new_connections != null ){
				
				for (int i=0;i<new_connections.size();i++){
			
					PEPeerTransport	peer_item = (PEPeerTransport)new_connections.get(i);

						// don't call when holding monitor - deadlock potential
					peer_item.reconnect(true, false);

				}
			}
		}
	}
	
	// counter is reset every 30s by doConnectionChecks()
	private int optimisticDisconnectCount = 0;

	public boolean doOptimisticDisconnect( boolean	pending_lan_local_peer, boolean force )
	{
		
		final ArrayList peer_transports = peer_transports_cow;
		PEPeerTransport max_transport = null;
		PEPeerTransport max_seed_transport		= null;
		PEPeerTransport max_non_lan_transport 	= null;

		long max_time = 0;
		long max_seed_time 		= 0;
		long max_non_lan_time	= 0;
		

		List<Long> activeConnectionTimes = new ArrayList<Long>(peer_transports.size());

		int	lan_peer_count	= 0;

		for( int i=0; i < peer_transports.size(); i++ ) {
			final PEPeerTransport peer = (PEPeerTransport)peer_transports.get( i );

			if( peer.getConnectionState() == PEPeerTransport.CONNECTION_FULLY_ESTABLISHED ) {

				final long timeSinceConnection =peer.getTimeSinceConnectionEstablished();
				final long timeSinceSentData =peer.getTimeSinceLastDataMessageSent();				
				
				activeConnectionTimes.add(timeSinceConnection);
				
				long peerTestTime = 0;
				if( seeding_mode){
					if( timeSinceSentData != -1 )
						peerTestTime = timeSinceSentData;  //ensure we've sent them at least one data message to qualify for drop
				}else{
					final long timeSinceGoodData =peer.getTimeSinceGoodDataReceived();
					

					if( timeSinceGoodData == -1 ) 
						peerTestTime +=timeSinceConnection;   //never received
					else
						peerTestTime +=timeSinceGoodData;

					// try to drop unInteresting in favor of Interesting connections
					if (!peer.isInteresting())
					{
						if (!peer.isInterested())   // if mutually unInterested, really try to drop the connection
							peerTestTime +=timeSinceConnection +timeSinceSentData;   // if we never sent, it will subtract 1, which is good
						else
							peerTestTime +=(timeSinceConnection -timeSinceSentData); // try to give interested peers a chance to get data

						peerTestTime *=2;
					}

					peerTestTime +=peer.getSnubbedTime();
				}

				if( !peer.isIncoming() ){
					peerTestTime = peerTestTime * 2;   //prefer to drop a local connection, to make room for more remotes
				}

				if ( peer.isLANLocal()){

					lan_peer_count++;

				}else{

					if( peerTestTime > max_non_lan_time ) {
						max_non_lan_time = peerTestTime;
						max_non_lan_transport = peer;
					}
				}



				// anti-leech checks
				if( !seeding_mode ) {

					// remove long-term snubbed peers with higher probability
					peerTestTime += peer.getSnubbedTime();
					if(peer.getSnubbedTime() > 2*60) {peerTestTime*=1.5;}

					PEPeerStats pestats = peer.getStats();
					// everybody has deserverd a chance of half an MB transferred data
					if(pestats.getTotalDataBytesReceived()+pestats.getTotalDataBytesSent() > 1024*512 ) {
						boolean goodPeer = true;
						
						// we don't like snubbed peers with a negative gain
						if( peer.isSnubbed() && pestats.getTotalDataBytesReceived() < pestats.getTotalDataBytesSent() ) {
							peerTestTime *= 1.5;
							goodPeer = false;
						}
						// we don't like peers with a very bad ratio (10:1)
						if( pestats.getTotalDataBytesSent() > pestats.getTotalDataBytesReceived() * 10 ) {
							peerTestTime *= 2;
							goodPeer = false;
						}
						// modify based on discarded : overall downloaded ratio
						if( pestats.getTotalDataBytesReceived() > 0 && pestats.getTotalBytesDiscarded() > 0 ) {
							peerTestTime = (long)(peerTestTime *( 1.0+((double)pestats.getTotalBytesDiscarded()/(double)pestats.getTotalDataBytesReceived())));
						}
						
						// prefer peers that do some work, let the churn happen with peers that did nothing
						if(goodPeer)
							peerTestTime *= 0.7;							
					}
				}


				if( peerTestTime > max_time ) {
					max_time = peerTestTime;
					max_transport = peer;
				}

				if ( peer.isSeed() || peer.isRelativeSeed()){

					if( peerTestTime > max_seed_time ) {
						max_seed_time = peerTestTime;
						max_seed_transport = peer;
					}
				}
			}
		}
		
		long medianConnectionTime;
		
		if ( activeConnectionTimes.size() > 0 ){
			Collections.sort(activeConnectionTimes);
			medianConnectionTime = activeConnectionTimes.get(activeConnectionTimes.size()/2);
		}else{
			medianConnectionTime = 0;
		}
		
		// allow 1 disconnect every 30s per 30 peers; 2 at least every 30s
		int maxOptimistics = Math.max(getMaxConnections()/30,2);
		
		// avoid unnecessary churn, e.g. 
		if(!pending_lan_local_peer && !force && optimisticDisconnectCount >= maxOptimistics && medianConnectionTime < 5*60*1000)
			return false;
		
		
		// don't boot lan peers if we can help it (unless we have a few of them)

		if ( max_transport != null ){

			final int LAN_PEER_MAX	= 4;

			if ( max_transport.isLANLocal() && lan_peer_count < LAN_PEER_MAX && max_non_lan_transport != null ){

				// override lan local max with non-lan local max

				max_transport	= max_non_lan_transport;
				max_time		= max_non_lan_time;
			}

			// if we have a seed limit, kick seeds in preference to non-seeds

			if( getMaxSeedConnections() > 0 && max_seed_transport != null && max_time > 5*60*1000 ) {
				closeAndRemovePeer( max_seed_transport, "timed out by doOptimisticDisconnect()", true );
				optimisticDisconnectCount++;
				return true;
			}

			if( max_transport != null && max_time > 5 *60*1000 ) {  //ensure a 5 min minimum test time
				closeAndRemovePeer( max_transport, "timed out by doOptimisticDisconnect()", true );
				optimisticDisconnectCount++;
				return true;
			}

			// kick worst peers to accomodate lan peer

			if ( pending_lan_local_peer && lan_peer_count < LAN_PEER_MAX ){
				closeAndRemovePeer( max_transport, "making space for LAN peer in doOptimisticDisconnect()", true );
				optimisticDisconnectCount++;
				return true;
			}
			
			if ( force ){
								
				closeAndRemovePeer( max_transport, "force removal of worst peer in doOptimisticDisconnect()", true );
				
				return true;
			}
		}else if ( force ){
			
			if ( peer_transports.size() > 0 ){
				
				PEPeerTransport pt = (PEPeerTransport)peer_transports.get( new Random().nextInt( peer_transports.size()));
				
				closeAndRemovePeer( pt, "force removal of random peer in doOptimisticDisconnect()", true );
				
				return true;
			}
		}

		return false;
	}


	public PeerExchangerItem createPeerExchangeConnection( final PEPeerTransport base_peer ) {
		if( base_peer.getTCPListenPort() > 0 ) {  //only accept peers whose remote port is known
			final PeerItem peer = 
				PeerItemFactory.createPeerItem( base_peer.getIp(),
						base_peer.getTCPListenPort(),
						PeerItemFactory.PEER_SOURCE_PEER_EXCHANGE,
						base_peer.getPeerItemIdentity().getHandshakeType(),
						base_peer.getUDPListenPort(),
						PeerItemFactory.CRYPTO_LEVEL_1,
						0 );

			return peer_database.registerPeerConnection( peer, new PeerExchangerItem.Helper(){
				public boolean 
				isSeed()
				{
					return base_peer.isSeed();  
				}
			});
		}

		return null;
	}



	private boolean isAlreadyConnected( PeerItem peer_id ) {
		final ArrayList peer_transports = peer_transports_cow;
		for( int i=0; i < peer_transports.size(); i++ ) {
			final PEPeerTransport peer = (PEPeerTransport)peer_transports.get( i );
			if( peer.getPeerItemIdentity().equals( peer_id ) )  return true;
		}
		return false;
	}


	public void peerVerifiedAsSelf( PEPeerTransport self ) {
		if( self.getTCPListenPort() > 0 ) {  //only accept self if remote port is known
			final PeerItem peer = PeerItemFactory.createPeerItem( self.getIp(), self.getTCPListenPort(),
					PeerItem.convertSourceID( self.getPeerSource() ), self.getPeerItemIdentity().getHandshakeType(), self.getUDPListenPort(),PeerItemFactory.CRYPTO_LEVEL_CURRENT, 0 );
			peer_database.setSelfPeer( peer );
		}
	}

	public boolean 
	canIPBeBanned(
			String ip ) 
	{
		return true;
	}
	
	public boolean
	canIPBeBlocked(
		String	ip,
		byte[]	torrent_hash )
	{
		return true;
	}
	
	public long 
	getHiddenBytes() 
	{
		if ( hidden_piece < 0 ){
			
			return( 0 );
		}
		
		return( dm_pieces[hidden_piece].getLength());
	}
	
	public int
	getHiddenPiece()
	{
		return( hidden_piece );
	}
	
	public int
	getUploadPriority()
	{
		return( adapter.getUploadPriority());
	}
	
	public void IPBlockedListChanged(IpFilter filter) {
		Iterator	it = peer_transports_cow.iterator();
		
		String	name 	= getDisplayName();
		byte[]	hash	= getTorrentHash();
		
		while( it.hasNext()){
			try {
  			PEPeerTransport	peer = (PEPeerTransport)it.next();
  			
  			if (filter.isInRange(peer.getIp(), name, hash )) {
  				peer.closeConnection( "IP address blocked by filters" );
  			}
			} catch (Exception e) {
			}
		}
	}

	public void IPBanned(BannedIp ip)
	{
		for (int i =0; i <_nbPieces; i++ )
		{
			if (pePieces[i] !=null)
				pePieces[i].reDownloadBlocks(ip.getIp());
		}
	}


	public int getAverageCompletionInThousandNotation()
	{
		final ArrayList peer_transports = peer_transports_cow;

		if (peer_transports !=null)
		{
			final long total =disk_mgr.getTotalLength();

			final int my_completion = total == 0
			? 1000
					: (int) ((1000 * (total - disk_mgr.getRemainingExcludingDND())) / total);

			int sum = my_completion == 1000 ? 0 : my_completion;  //add in our own percentage if not seeding
			int num = my_completion == 1000 ? 0 : 1;

			for (int i =0; i <peer_transports.size(); i++ )
			{
				final PEPeer peer =(PEPeer) peer_transports.get(i);

				if (peer.getPeerState() ==PEPeer.TRANSFERING &&!peer.isSeed())
				{
					num++;
					sum += peer.getPercentDoneInThousandNotation();
				}
			}

			return num > 0 ? sum / num : 0;
		}

		return -1;
	}

	public int
	getMaxConnections()
	{
		return( adapter.getMaxConnections());
	}

	public int
	getMaxSeedConnections()
	{
		return( adapter.getMaxSeedConnections());
	}

	public int
	getMaxNewConnectionsAllowed()
	{
		final int	dl_max = getMaxConnections();

		final int	allowed_peers = PeerUtils.numNewConnectionsAllowed(getPeerIdentityDataID(), dl_max );

		return( allowed_peers );
	}
	
	public int getSchedulePriority() {
		return isSeeding() ? Integer.MAX_VALUE : adapter.getPosition();
	}

	public boolean
	hasPotentialConnections()
	{
		return( pending_nat_traversals.size() + peer_database.getDiscoveredPeerCount() > 0 );
	}
	
	public String 
	getRelationText() 
	{
		return( adapter.getLogRelation().getRelationText());
	}

	public Object[] 
	              getQueryableInterfaces() 
	{
		return( adapter.getLogRelation().getQueryableInterfaces());
	}



	public PEPeerTransport getTransportFromIdentity( byte[] peer_id ) {
		final ArrayList	peer_transports = peer_transports_cow;      	
		for( int i=0; i < peer_transports.size(); i++ ) {
			final PEPeerTransport conn = (PEPeerTransport)peer_transports.get( i );			
			if( Arrays.equals( peer_id, conn.getId() ) )   return conn;
		}
		return null;
	}


	/* peer item is not reliable for general use
	public PEPeerTransport getTransportFromPeerItem(PeerItem peerItem)
	{
		ArrayList peer_transports =peer_transports_cow;
		for (int i =0; i <peer_transports.size(); i++)
		{
			PEPeerTransport pt =(PEPeerTransport) peer_transports.get(i);
			if (pt.getPeerItemIdentity().equals(peerItem))
				return pt;
		}
		return null;
	}
	 */

	public PEPeerTransport getTransportFromAddress(String peer)
	{
		final ArrayList peer_transports =peer_transports_cow;
		for (int i =0; i <peer_transports.size(); i++)
		{
			final PEPeerTransport pt =(PEPeerTransport) peer_transports.get(i);
			if (peer.equals(pt.getIp()))
				return pt;
		}
		return null;
	}

	// Snubbed peers accounting
	public void incNbPeersSnubbed()
	{
		nbPeersSnubbed++;
	}

	public void decNbPeersSnubbed()
	{
		nbPeersSnubbed--;
	}

	public void setNbPeersSnubbed(int n)
	{
		nbPeersSnubbed =n;
	}

	public int getNbPeersSnubbed()
	{
		return nbPeersSnubbed;
	}
	
	public boolean
	getPreferUDP()
	{
		return( prefer_udp );
	}
	
	public void
	setPreferUDP(
		boolean	prefer )
	{
		 prefer_udp = prefer;
	}

	public boolean 
	isPeerSourceEnabled(
		String peer_source ) 
	{
		return( adapter.isPeerSourceEnabled( peer_source ));
	}
	
	public void
	peerDiscovered(
		PEPeerTransport		finder,
		PeerItem			pi )
	{
		final ArrayList peer_manager_listeners = peer_manager_listeners_cow;

		for( int i=0; i < peer_manager_listeners.size(); i++ ) {
			try{
				((PEPeerManagerListener)peer_manager_listeners.get(i)).peerDiscovered( this, pi, finder );
						
			}catch( Throwable e ){
				
				Debug.printStackTrace(e);
			}
		}
	}
	
	public TrackerPeerSource
	getTrackerPeerSource()
	{
		return(
			new TrackerPeerSourceAdapter()
			{
				public int
				getType()
				{
					return( TP_PEX );
				}
				
				public int
				getStatus()
				{
					return( isPeerExchangeEnabled()?ST_ONLINE:ST_DISABLED );
				}
				
				public String
				getName()
				{
					return( 
						MessageText.getString( "tps.pex.details", 
							new String[]{ 
								String.valueOf( peer_transports_cow.size()), 
								String.valueOf( peer_database.getExchangedPeerCount()),
								String.valueOf( peer_database.getDiscoveredPeerCount())}));
				}
				
				public int
				getPeers()
				{
					return( peer_database.getExchangedPeersUsed());
				}
			});
	}
	
	public void
	generateEvidence(
			IndentWriter		writer )
	{
		writer.println( "PeerManager: seeding=" + seeding_mode );

		writer.println( 
				"    udp_fb=" + pending_nat_traversals.size() +
				",udp_tc=" + udp_traversal_count +
				",pd=[" + peer_database.getString() + "]");
		
		String	pending_udp = "";
		
		try{
			peer_transports_mon.enter();

			Iterator	it = pending_nat_traversals.values().iterator();
			
			while( it.hasNext()){
			
				PEPeerTransport peer = (PEPeerTransport)it.next();
			
				pending_udp += (pending_udp.length()==0?"":",") + peer.getPeerItemIdentity().getAddressString() + ":" + peer.getPeerItemIdentity().getUDPPort();
			}
		}finally{
			
			peer_transports_mon.exit();
		}
		
		if ( pending_udp.length() > 0 ){
			
			writer.println( "    pending_udp=" + pending_udp );
		}
		
		List	traversals = PeerNATTraverser.getSingleton().getTraversals( this );
		
		String	active_udp = "";
		
		Iterator it = traversals.iterator();
		
		while( it.hasNext()){
			
			InetSocketAddress ad = (InetSocketAddress)it.next();
			
			active_udp += (active_udp.length()==0?"":",") + ad.getAddress().getHostAddress() + ":" + ad.getPort();
		}
		
		if ( active_udp.length() > 0 ){
			
			writer.println( "    active_udp=" + active_udp );
		}
		
		if ( !seeding_mode ){

			writer.println( "  Active Pieces" );

			int	num_active = 0;

			try{
				writer.indent();

				String	str	= "";
				int		num	= 0;

				for (int i=0;i<pePieces.length;i++){

					PEPiece	piece = pePieces[i];

					if ( piece != null ){

						num_active++;

						str += (str.length()==0?"":",") + "#" + i + " " + dm_pieces[i].getString() + ": " + piece.getString();

						num++;

						if ( num == 20 ){

							writer.println( str );
							str = "";
							num	= 0;
						}
					}
				}

				if ( num > 0 ){
					writer.println(str);
				}

			}finally{

				writer.exdent();
			}

			if ( num_active == 0 ){

				writer.println( "  Inactive Pieces (excluding done/skipped)" );

				try{
					writer.indent();

					String	str	= "";
					int		num	= 0;

					for (int i=0;i<dm_pieces.length;i++){

						DiskManagerPiece	dm_piece = dm_pieces[i];

						if ( dm_piece.isInteresting()){

							str += (str.length()==0?"":",") + "#" + i + " " + dm_pieces[i].getString();

							num++;

							if ( num == 20 ){

								writer.println( str );
								str = "";
								num	= 0;
							}
						}
					}

					if ( num > 0 ){

						writer.println(str);
					}

				}finally{

					writer.exdent();
				}
			}

			piecePicker.generateEvidence( writer );
		}

		try{
			peer_transports_mon.enter();

			writer.println( "Peers: total = " + peer_transports_cow.size()); 

			writer.indent();

			try{
				writer.indent();

				it = peer_transports_cow.iterator();

				while( it.hasNext()){

					PEPeerTransport	peer = (PEPeerTransport)it.next();

					peer.generateEvidence( writer );
				}
			}finally{

				writer.exdent();
			}
		}finally{

			peer_transports_mon.exit();

			writer.exdent();
		}
		
		disk_mgr.generateEvidence( writer );
	}
}
