/*
 * Created on 3 Oct 2006
 * Created by Paul Gardner
 * Copyright (C) 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 63.529,40 euros
 * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
 *
 */

package com.aelitis.azureus.core.networkmanager.impl.http;

import java.io.IOException;
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.DiskManager;
import org.gudy.azureus2.core3.logging.LogEvent;
import org.gudy.azureus2.core3.logging.LogIDs;
import org.gudy.azureus2.core3.logging.Logger;
import org.gudy.azureus2.core3.peer.impl.PEPeerControl;
import org.gudy.azureus2.core3.peer.impl.PEPeerTransport;
import org.gudy.azureus2.core3.peer.util.PeerUtils;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.DirectByteBuffer;
import org.gudy.azureus2.core3.util.SimpleTimer;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimeFormatter;
import org.gudy.azureus2.core3.util.TimerEvent;
import org.gudy.azureus2.core3.util.TimerEventPerformer;

import com.aelitis.azureus.core.networkmanager.NetworkConnection;
import com.aelitis.azureus.core.networkmanager.OutgoingMessageQueue;
import com.aelitis.azureus.core.networkmanager.RawMessage;
import com.aelitis.azureus.core.networkmanager.impl.RawMessageImpl;
import com.aelitis.azureus.core.peermanager.messaging.Message;
import com.aelitis.azureus.core.peermanager.messaging.bittorrent.BTBitfield;
import com.aelitis.azureus.core.peermanager.messaging.bittorrent.BTHandshake;
import com.aelitis.azureus.core.peermanager.messaging.bittorrent.BTHave;
import com.aelitis.azureus.core.peermanager.messaging.bittorrent.BTInterested;
import com.aelitis.azureus.core.peermanager.messaging.bittorrent.BTPiece;
import com.aelitis.azureus.core.peermanager.messaging.bittorrent.BTRequest;
import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.core.util.HTTPUtils;

public abstract class 
HTTPNetworkConnection 
{
	protected static final LogIDs LOGID = LogIDs.NWMAN;

	private static final int	MAX_OUTSTANDING_BT_REQUESTS	= 16;
	
	protected static final String	NL			= "\r\n";

	private static final String	HDR_SERVER 				= "Server: " + Constants.AZUREUS_NAME + " " + Constants.AZUREUS_VERSION + NL;
	private static final String HDR_KEEP_ALIVE_TIMEOUT 	= "Keep-Alive: timeout=30" + NL;
	private static final String HDR_CACHE_CONTROL		= "Cache-Control: public, max-age=86400" + NL;

	private static final String	DEFAULT_CONTENT_TYPE	= HTTPUtils.guessContentTypeFromFileType(null);
	
	private static int        max_read_block_size;

	static{
	
	    ParameterListener param_listener = new ParameterListener() {
	            public void
	            parameterChanged(
	                String  str )
	            {
	                max_read_block_size = COConfigurationManager.getIntParameter( "BT Request Max Block Size" );
	            }
	    };
	
	    COConfigurationManager.addAndFireParameterListener( "BT Request Max Block Size", param_listener);
	}
	
	private static final int	TIMEOUT_CHECK_PERIOD			= 15*1000;
	private static final int	DEAD_CONNECTION_TIMEOUT_PERIOD	= 30*1000;
	private static final int	MAX_CON_PER_ENDPOINT			= 5*1000;

	private static Map<networkConnectionKey,List<HTTPNetworkConnection>>	http_connection_map = new HashMap<networkConnectionKey,List<HTTPNetworkConnection>>();
	
	static{
		SimpleTimer.addPeriodicEvent(
			"HTTPNetworkConnection:timer",
			TIMEOUT_CHECK_PERIOD,
			new TimerEventPerformer()
			{
				public void 
				perform(
					TimerEvent event ) 
				{
					synchronized( http_connection_map ){
						
						boolean	 check = true;
						
						while( check ){
							
							check = false;
	
							Iterator<Map.Entry<networkConnectionKey,List<HTTPNetworkConnection>>>	it = http_connection_map.entrySet().iterator();
							
							while( it.hasNext()){
								
								Map.Entry<networkConnectionKey,List<HTTPNetworkConnection>>	entry = it.next();
								
								networkConnectionKey	key = (networkConnectionKey)entry.getKey();
								
								List<HTTPNetworkConnection>	connections = entry.getValue();
								
								/*
								String	times = "";
								
								for (int i=0;i<connections.size();i++){
									
									HTTPNetworkConnection	connection = (HTTPNetworkConnection)connections.get(i);
								
									times += (i==0?"":",") + connection.getTimeSinceLastActivity();
								}
								
								System.out.println( "HTTPNC: " + key.getName() + " -> " + connections.size() + " - " + times );
								*/
								
								if ( checkConnections( connections )){
									
										// might have a concurrent mod to the iterator
									
									if ( !http_connection_map.containsKey( key )){
										
										check	= true;
										
										break;
									}
								}
							}
						}
					}
				}
			});
	}
	
	protected static boolean
	checkConnections(
		List<HTTPNetworkConnection>	connections )
	{
		boolean	some_closed = false;
				
		HTTPNetworkConnection	oldest 			= null;
		long					oldest_time		= -1;
		
		Iterator<HTTPNetworkConnection>	it = connections.iterator();
			
		List<HTTPNetworkConnection>	timed_out = new ArrayList<HTTPNetworkConnection>();
		
		while( it.hasNext()){
			
			HTTPNetworkConnection	connection = (HTTPNetworkConnection)it.next();
		
			long	time = connection.getTimeSinceLastActivity();
		
			if ( time > DEAD_CONNECTION_TIMEOUT_PERIOD ){
				
				if ( connection.getRequestCount() == 0 ){
																
					timed_out.add( connection );
					
					continue;
				}
			}
						
			if ( time > oldest_time && !connection.isClosing()){
					
				oldest_time		= time;
					
				oldest	= connection;
			}
		}
		
		for (int i=0;i<timed_out.size();i++){
			
			((HTTPNetworkConnection)timed_out.get(i)).close( "Timeout" );
			
			some_closed	= true;
		}
		
		if ( connections.size() - timed_out.size() > MAX_CON_PER_ENDPOINT ){
			
			oldest.close( "Too many connections from initiator");
				
			some_closed	= true;
		}
		
		return( some_closed );
	}
	
	private HTTPNetworkManager	manager;
	private NetworkConnection	connection;
	private PEPeerTransport		peer;
	
	private HTTPMessageDecoder	decoder;
	private HTTPMessageEncoder	encoder;
	
	private boolean			sent_handshake	= false;
	
	private byte[]	peer_id	= PeerUtils.createWebSeedPeerID();

	private boolean	choked	= true;
	
	private List<httpRequest>		http_requests			= new ArrayList<httpRequest>();
	private List<BTRequest>			choked_requests 		= new ArrayList<BTRequest>();
	private List<pendingRequest>	outstanding_requests 	= new ArrayList<pendingRequest>();
	
	private BitSet	piece_map	= new BitSet();
	
	private long	last_http_activity_time;
	
	private networkConnectionKey	network_connection_key;
	
	private boolean	closing;
	private boolean	destroyed;
		
	private String	last_modified_date;
	private String	content_type	= DEFAULT_CONTENT_TYPE;
	
	private CopyOnWriteList<requestListener>	request_listeners = null;
	
	protected
	HTTPNetworkConnection(
		HTTPNetworkManager		_manager,
		NetworkConnection		_connection,
		PEPeerTransport			_peer )
	{
		manager			= _manager;
		connection		= _connection;
		peer			= _peer;
		
		DiskManager dm = peer.getManager().getDiskManager();
		
		long	last_modified = 0;
		
		try{
			last_modified = dm.getFiles()[0].getFile(true).lastModified();
						
		}catch( Throwable e ){
		}
		
		last_modified_date = TimeFormatter.getHTTPDate( last_modified );
		
		network_connection_key = new networkConnectionKey();
			
		last_http_activity_time	= SystemTime.getCurrentTime();
		
		decoder	= (HTTPMessageDecoder)connection.getIncomingMessageQueue().getDecoder();
		encoder = (HTTPMessageEncoder)connection.getOutgoingMessageQueue().getEncoder();

		synchronized( http_connection_map ){
						
			List<HTTPNetworkConnection>	connections = http_connection_map.get( network_connection_key );
			
			if ( connections == null ){
				
				connections = new ArrayList<HTTPNetworkConnection>();
				
				http_connection_map.put( network_connection_key, connections );
			}
			
			connections.add( this );
			
			if ( connections.size() > MAX_CON_PER_ENDPOINT ){
				
				checkConnections( connections );
			}
		}
		
			// note that the decoder can synchronously call-back if is preloaded with a header
			// here...
		
		encoder.setConnection( this );
		decoder.setConnection( this );
	}
	
	protected boolean
	isSeed()
	{
		if ( ( !peer.getControl().isSeeding()) || peer.getControl().getHiddenBytes() > 0 ){
			
			if (Logger.isEnabled()){
				Logger.log(new LogEvent(peer,LOGID, "Download is not seeding" ));
			}   	
			
			sendAndClose( manager.getNotFound());
			
			return( false );
		}
		
		return( true );
	}
	
	protected void
	setContentType(
		String	ct )
	{
		content_type	= ct;
	}
	
	protected HTTPNetworkManager
	getManager()
	{
		return( manager );
	}
	
	protected NetworkConnection
	getConnection()
	{
		return( connection );
	}
	
	protected PEPeerTransport
	getPeer()
	{
		return( peer );
	}
	protected PEPeerControl
	getPeerControl()
	{
		return( peer.getControl());
	}
	
	protected RawMessage
	encodeChoke()
	{
		synchronized( outstanding_requests ){
			
			choked	= true;
		}
		
		return( null );
	}
	
	protected RawMessage
	encodeUnchoke()
	{		
		synchronized( outstanding_requests ){
			
			choked	= false;
			
			for (int i=0;i<choked_requests.size();i++){
								
				decoder.addMessage((BTRequest)choked_requests.get(i));
			}
			
			choked_requests.clear();
		}
		
		return( null );
	}
	
	protected RawMessage
	encodeBitField()
	{
		decoder.addMessage( new BTInterested((byte)1));
		
		return( null );
	}
	
	protected void
	readWakeup()
	{
		connection.getTransport().setReadyForRead();
	}

	protected RawMessage
	encodeHandShake(
		Message	message )
	{
		return( null );
	}
	
	protected abstract void
	decodeHeader(
		HTTPMessageDecoder		decoder,
		String					header )
	
		throws IOException;

	protected String
	encodeHeader(
		httpRequest	request )
	{
		String	current_date = TimeFormatter.getHTTPDate( SystemTime.getCurrentTime());
		
		StringBuffer	res = new StringBuffer(256);
		
		boolean	partial = request.isPartialContent();
		
		res.append( "HTTP/1.1 " );
		res.append( partial?"206 Partial Content":"200 OK" );
			res.append( NL );

		res.append( "Content-Type: " );
		res.append( content_type );
	 		res.append( NL );		
	 		
		res.append( "Date: " );
		res.append( current_date );
		 	res.append( NL );
		 	
		res.append( "Last-Modified: " );
		res.append( last_modified_date );
			res.append( NL );

		res.append( HDR_CACHE_CONTROL );
		
			// not sure about ETag. I was going to use the torrent hash but I don't understand the link
			// between URL, range requests and ETags. Do we need to generate different ETags for each
			// webseed piece request URL or can we use the torrent hash and rely on the fact that the
			// URL changes? Are range-requests irrelevant as far as ETags go - I'd like to think so...
		
		res.append( HDR_SERVER );
		
		if ( partial ){
			
			long[] offsets = request.getOriginalOffsets();
			long[] lengths = request.getOriginalLengths();
			
			long content_length = request.getContentLength();
			
			if ( offsets.length == 1 && content_length > 0 ){
				
				res.append( "Content-Range: bytes " + offsets[0] + "-" + (offsets[0]+lengths[0]-1) + "/" + content_length );
				res.append( NL );
			}
		}
		res.append( "Connection: " );
		res.append( request.keepAlive()?"Keep-Alive":"Close" );
			res.append( NL );
		
		if ( request.keepAlive()){
				
			res.append( HDR_KEEP_ALIVE_TIMEOUT );
		}
		
		res.append( "Content-Length: " );
		res.append( request.getTotalLength());
		res.append( NL );
		
		res.append( NL );
						
		return( res.toString());
	}
	
	protected void
	addRequest(
		httpRequest		request )
	
		throws IOException
	{
		last_http_activity_time	= SystemTime.getCurrentTime();
		
		PEPeerControl	control = getPeerControl();
		
		if ( !sent_handshake ){
			
			sent_handshake	= true;
			
			decoder.addMessage( new BTHandshake( control.getHash(), peer_id, false, (byte)1 ));
			
			byte[]	bits = new byte[(control.getPieces().length +7) /8];
			
			DirectByteBuffer buffer = new DirectByteBuffer( ByteBuffer.wrap( bits ));
			
			decoder.addMessage( new BTBitfield( buffer, (byte)1 ));
		}
		
		synchronized( outstanding_requests ){

			http_requests.add( request );
		}
		
		submitBTRequests();
	}
	
	protected void
	submitBTRequests()
	
		throws IOException
	{
		PEPeerControl	control = getPeerControl();

		long	piece_size = control.getPieceLength(0);
	
		synchronized( outstanding_requests ){

			while( outstanding_requests.size() < MAX_OUTSTANDING_BT_REQUESTS && http_requests.size() > 0 ){
				
				httpRequest	http_request = (httpRequest)http_requests.get(0);
				
				long[]	offsets	= http_request.getModifiableOffsets();
				long[]	lengths	= http_request.getModifiableLengths();
				
				int	index	= http_request.getIndex();
				
				long	offset 	= offsets[index];
				long	length	= lengths[index];
				
				int		this_piece_number 	= (int)(offset / piece_size);
				int		this_piece_size		= control.getPieceLength( this_piece_number );
				
				int		offset_in_piece 	= (int)( offset - ( this_piece_number * piece_size ));
				
				int		space_this_piece 	= this_piece_size - offset_in_piece;
				
				int		request_size = (int)Math.min( length, space_this_piece );
				
				request_size = Math.min( request_size, max_read_block_size );
				
				addBTRequest( 
					new BTRequest( 
							this_piece_number, 
							offset_in_piece, 
							request_size,
							(byte)1),
					http_request );
					
				if ( request_size == length ){
					
					if ( index == offsets.length - 1 ){
						
						http_requests.remove(0);
						
					}else{
						
						http_request.setIndex( index+1 );
					}
				}else{
					offsets[index] += request_size;
					lengths[index] -= request_size;
				}
			}
		}
	}
	
	protected void
	addBTRequest(
		BTRequest		request,
		httpRequest		http_request )
	
		throws IOException
	{
		synchronized( outstanding_requests ){
				
			if ( destroyed ){
				
				throw( new IOException( "HTTP connection destroyed" ));
			}
			
			outstanding_requests.add( new pendingRequest( request, http_request ));
			
			if ( choked ){
					
				if ( choked_requests.size() > 1024 ){
					
					Debug.out( "pending request limit exceeded" );
					
				}else{
				
					choked_requests.add( request );
				}
			}else{
				
				decoder.addMessage( request );
			}
		}
	}
	
	protected RawMessage[]
	encodePiece(
		Message		message )
	{
		last_http_activity_time	= SystemTime.getCurrentTime();
		
		BTPiece	piece = (BTPiece)message;
		
		List<pendingRequest>	ready_requests = new ArrayList<pendingRequest>();
		
		boolean	found = false;
		
		synchronized( outstanding_requests ){

			if ( destroyed ){
				
				return( new RawMessage[]{ getEmptyRawMessage( message )});
			}
		
			for (int i=0;i<outstanding_requests.size();i++){
				
				pendingRequest	req = outstanding_requests.get(i);
				
				if ( 	req.getPieceNumber() == piece.getPieceNumber() &&
						req.getStart() 	== piece.getPieceOffset() &&
						req.getLength() == piece.getPieceData().remaining( DirectByteBuffer.SS_NET )){
		
					if ( req.getBTPiece() == null ){
					
						req.setBTPiece( piece );
						
						found	= true;
						
						if ( i == 0 ){
							
							Iterator<pendingRequest>	it = outstanding_requests.iterator();
							
							while( it.hasNext()){
								
								pendingRequest r = it.next();
								
								BTPiece	btp = r.getBTPiece();
								
								if ( btp == null ){
									
									break;
								}
								
								it.remove();
								
								ready_requests.add( r );
							}
						}
					
						break;
					}
				}
			}
		}
		
		if ( !found ){
			
			Debug.out( "request not matched" );
			
			return( new RawMessage[]{ getEmptyRawMessage( message )});
		}
		
		if ( ready_requests.size() == 0 ){
			
			return( new RawMessage[]{ getEmptyRawMessage( message )});
		}
		
		try{
			submitBTRequests();
			
		}catch( IOException e ){
			
		}
		
		pendingRequest req	= (pendingRequest)ready_requests.get(0);
		
		DirectByteBuffer[]	buffers;
		
		httpRequest	http_request = req.getHTTPRequest();
		
		RawMessage[]	raw_messages = new RawMessage[ ready_requests.size()];
		
		for (int i=0;i<raw_messages.length;i++){
			
			buffers = new DirectByteBuffer[ 2 ];

			if ( !http_request.hasSentFirstReply()){
		
				http_request.setSentFirstReply();
						
				String	header = encodeHeader( http_request );
			
				buffers[0] = new DirectByteBuffer( ByteBuffer.wrap( header.getBytes()));
			
			}else{
			
					// we have to do this as core code assumes buffer entry 0 is protocol
			
				buffers[0] = new DirectByteBuffer( ByteBuffer.allocate(0));
			}
					
			req	= (pendingRequest)ready_requests.get(i);

			BTPiece	this_piece = req.getBTPiece();
			
			int	piece_number = this_piece.getPieceNumber();
			
			if ( !piece_map.get( piece_number )){
				
					// kinda crappy as it triggers on first block of piece, however better
					// than nothing
				
				piece_map.set( piece_number );
				
				decoder.addMessage( new BTHave( piece_number, (byte)1 ));
			}
			
			buffers[1] = this_piece.getPieceData();
			
			req.logQueued();
			
			if ( request_listeners != null ){
				
				Iterator<requestListener> it = request_listeners.iterator();
			
				while( it.hasNext()){
					
					((requestListener)it.next()).requestComplete( req );
				}
			}
			
			raw_messages[i] = 
				new RawMessageImpl( 
						this_piece, 
						buffers,
						RawMessage.PRIORITY_HIGH, 
						true, 
						new Message[0] );
		}
		
		return( raw_messages );
	}
	
	protected int
	getRequestCount()
	{
		synchronized( outstanding_requests ){

			return( http_requests.size());
		}
	}
	
	protected boolean
	isClosing()
	{
		return( closing );
	}
	
	protected void
	close(
		String	reason )
	{
		closing	= true;
		
		peer.getControl().removePeer( peer );
	}
	
	protected void
	destroy()
	{
		synchronized( http_connection_map ){

			List<HTTPNetworkConnection>	connections = http_connection_map.get( network_connection_key );
			
			if ( connections != null ){
				
				connections.remove( this );
				
				if ( connections.size() == 0 ){
					
					http_connection_map.remove( network_connection_key );
				}
			}
		}
		
		synchronized( outstanding_requests ){

			destroyed	= true;
			
			for (int i=0;i<outstanding_requests.size();i++){
				
				pendingRequest	req = (pendingRequest)outstanding_requests.get(i);
				
				BTPiece	piece = req.getBTPiece();
				
				if ( piece != null ){
					
					piece.destroy();
				}
			}
			
			outstanding_requests.clear();
			
			for (int i=0;i<choked_requests.size();i++){
				
				BTRequest	req = (	BTRequest)choked_requests.get(i);
								
				req.destroy();
			}
			
			choked_requests.clear();
		}
	}
	
	protected long
	getTimeSinceLastActivity()
	{
		long	now = SystemTime.getCurrentTime();
		
		if ( now < last_http_activity_time ){
			
			last_http_activity_time = now;
		}
		
		return( now - last_http_activity_time );
	}
	
	protected void
	log(
		String	str )
	{
		if (Logger.isEnabled()){
			Logger.log(new LogEvent( getPeer(),LOGID, str));
		}   
	}
	
	protected RawMessage
	getEmptyRawMessage(
		Message	message )
	{		
		return( 
			new RawMessageImpl( 
					message, 
					new DirectByteBuffer[]{ new DirectByteBuffer( ByteBuffer.allocate(0))},
					RawMessage.PRIORITY_HIGH, 
					true, 
					new Message[0] ));
	}
	
	protected void
	sendAndClose(
		String		data )
	{
		final Message	http_message = new HTTPMessage( data );
		
		getConnection().getOutgoingMessageQueue().registerQueueListener(
			new OutgoingMessageQueue.MessageQueueListener()
			{
				public boolean 
				messageAdded( 
					Message message )
				{	
					return( true );
				}
				   
				public void 
				messageQueued( 
					Message message )
				{			
				}
				    
				public void 
				messageRemoved( 
					Message message )
				{
				}
				    
				public void 
				messageSent( 
					Message message )
				{
					if ( message == http_message ){
						
						close( "Close after message send complete" );
					}
				}
				    
			    public void 
			    protocolBytesSent( 
			    	int byte_count )
			    {	
			    }
				 
			    public void 
			    dataBytesSent( 
			    	int byte_count )
			    {	
			    }
			    
			    public void flush(){}
			});
		
		getConnection().getOutgoingMessageQueue().addMessage( http_message, false );
	}
	
	protected void
	flushRequests(
		final flushListener		l )
	{
		boolean	sync_fire = false;
		
		synchronized( outstanding_requests ){

			final int request_count = outstanding_requests.size();
						
			if ( request_count == 0 ){
				
				sync_fire = true;
				
			}else{
				
				if ( request_listeners == null ){
					
					request_listeners = new CopyOnWriteList<requestListener>();
				}
				
				request_listeners.add(
					new requestListener()
					{
						int	num_to_go = request_count;
						
						public void
						requestComplete(
							pendingRequest r )
						{
							num_to_go--;
							
							if ( num_to_go == 0 ){
								
								request_listeners.remove( this );
								
								flushRequestsSupport( l );
							}
						}
					});
			}
		}
		
		if ( sync_fire ){
			
			flushRequestsSupport( l );
		}
	}
	
	protected void
	flushRequestsSupport(
		final flushListener		l )
	{
		OutgoingMessageQueue omq = getConnection().getOutgoingMessageQueue();
		
		final Message	http_message = new HTTPMessage( new byte[0] );
		
		omq.registerQueueListener(
			new OutgoingMessageQueue.MessageQueueListener()
			{
				public boolean 
				messageAdded( 
					Message message )
				{	
					return( true );
				}
				   
				public void 
				messageQueued( 
					Message message )
				{			
				}
				    
				public void 
				messageRemoved( 
					Message message )
				{
				}
				    
				public void 
				messageSent( 
					Message message )
				{
					if ( message == http_message ){
						
						l.flushed();
					}
				}
				    
			    public void 
			    protocolBytesSent( 
			    	int byte_count )
			    {	
			    }
				 
			    public void 
			    dataBytesSent( 
			    	int byte_count )
			    {	
			    }
			    
			    public void flush(){}
			});
		
		omq.addMessage( http_message, false );
		
			// if after adding the message there's no bytes on the queue then we need to trigger an
			// immediate flushed event as the queue won't get processed (0 bytes on it...)
		
		if ( omq.getTotalSize() == 0 ){
			
			l.flushed();
		}
	}
	
	protected class
	httpRequest
	{
		private final long[]	orig_offsets;
		private final long[]	orig_lengths;		
		private final long		content_length;
		private final boolean	partial_content;
		private final boolean	keep_alive;

		private final long[]	mod_offsets;
		private final long[]	mod_lengths;

		private int		index;
		private long	total_length;
		private boolean	sent_first_reply;
				
		protected
		httpRequest(
			long[]		_offsets,
			long[]		_lengths,
			long		_content_length,
			boolean		_partial_content,
			boolean		_keep_alive )
		{
			orig_offsets	= _offsets;
			orig_lengths	= _lengths;
			content_length	= _content_length;
			partial_content	= _partial_content;
			keep_alive		= _keep_alive;
			
			/*
			String	str ="";
			for (int i=0;i<lengths.length;i++){	
				str += (i==0?"":",") +"[" + offsets[i] + "/" + lengths[i] + "]";
			}
			System.out.println( network_connection_key.getName() + ": requested " + str + ",part=" + partial_content +",ka=" + keep_alive );
			*/
			
			mod_offsets = orig_offsets.clone();
			mod_lengths = orig_lengths.clone();
			
			for (int i=0;i<orig_lengths.length;i++){
				
				total_length += orig_lengths[i];
			}
		}
				
		protected boolean
		isPartialContent()
		{
			return( partial_content );
		}
		
		protected long
		getContentLength()
		{
			return( content_length );
		}
		
		protected boolean
		hasSentFirstReply()
		{
			return( sent_first_reply );
		}
		
		protected void
		setSentFirstReply()
		{
			sent_first_reply	= true;
		}
		
		protected long[]
		getOriginalOffsets()
		{
			return( orig_offsets );
		}
		
		protected long[]
   		getOriginalLengths()
   		{
   			return( orig_lengths );
   		}
		
		protected long[]
   		getModifiableOffsets()
   		{
   			return( mod_offsets );
   		}
   		
   		protected long[]
  		getModifiableLengths()
  		{
  			return( mod_lengths );
  		}
		       		
		protected int
		getIndex()
		{
			return( index );
		}
		
		protected void
		setIndex(
			int	_index )
		{
			index = _index;
		}
		
		protected long
		getTotalLength()
		{
			return( total_length );
		}
		
		protected boolean
		keepAlive()
		{
			return( keep_alive );
		}
	}
	
	protected interface
	flushListener
	{
		public void
		flushed();
	}
	
	protected interface
	requestListener
	{
		public void
		requestComplete(
			pendingRequest	request );
	}
	
	protected class
	pendingRequest
	{
		private int	piece;
		private int	start;
		private int	length;
				
		private httpRequest	http_request;
		
		private BTPiece	bt_piece;
				
		protected
		pendingRequest(
			BTRequest		_request,
			httpRequest		_http_request )
		{
			piece	= _request.getPieceNumber();
			start	= _request.getPieceOffset();
			length	= _request.getLength();
			
			http_request	= _http_request;
			
			/*
			if ( peer.getIp().equals( "64.71.5.2")){
				
				TimeFormatter.milliTrace( 
						"http_req_create: " +
							piece + "/" + start + 
							" [hr=" + http_requests.size() + 
							",cr=" + choked_requests.size() + 
							",or=" + outstanding_requests.size() + 
							",d=" + decoder.getQueueSize() + "]" );
			}
			*/
		}
		
		protected int
		getPieceNumber()
		{
			return( piece );
		}
		
		protected int
		getStart()
		{
			return( start );
		}
		
		protected int
		getLength()
		{
			return( length );
		}
		
		protected httpRequest
		getHTTPRequest()
		{
			return( http_request );
		}
		
		protected BTPiece
		getBTPiece()
		{
			return( bt_piece );
		}
		
		protected void
		setBTPiece(
			BTPiece	_bt_piece )
		{
			bt_piece	= _bt_piece;
			
			/*
			if ( peer.getIp().equals( "64.71.5.2")){

				TimeFormatter.milliTrace( 
					"http_req_data: " +
						piece + "/" + start + 
						" [hr=" + http_requests.size() + 
						",cr=" + choked_requests.size() + 
						",or=" + outstanding_requests.size() + 
						",d=" + decoder.getQueueSize() + "]" );
			}
			*/
		}
		
		protected void
		logQueued()
		{
			/*
			if ( peer.getIp().equals( "64.71.5.2")){

				TimeFormatter.milliTrace( 
					"http_req_out: " +
						piece + "/" + start + 
						" [hr=" + http_requests.size() + 
						",cr=" + choked_requests.size() + 
						",or=" + outstanding_requests.size() + 
						",d=" + decoder.getQueueSize() + "]" );
			}
			*/
		}
	}
	
	protected class
	networkConnectionKey
	{
		public boolean 
		equals(Object obj) 
		{
			networkConnectionKey	other = (networkConnectionKey)obj;
			
			return( Arrays.equals( getAddress(), other.getAddress()) &&
					Arrays.equals(getHash(),other.getHash()));	
		}
			
		protected String
		getName()
		{
			return( peer.getControl().getDisplayName() + ": " + connection.getEndpoint().getNotionalAddress().getAddress().getHostAddress());
		}
		
		protected byte[]
		getAddress()
		{
			return( connection.getEndpoint().getNotionalAddress().getAddress().getAddress());
		}
		
		protected byte[]
		getHash()
		{
			return( peer.getControl().getHash());
		}
		
		public int 
		hashCode() 
		{
			return( peer.getControl().hashCode());

		}
	}
}
