/*
 * Created on 22 Jun 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 46,603.30 euros
 * 8 Allee Lenotre, La Grille Royale, 78600 Le Mesnil le Roi, France.
 *
 */

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

import java.io.IOException;
import java.nio.ByteBuffer;

import org.gudy.azureus2.core3.util.AEDiagnostics;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.core3.util.TimeFormatter;

import com.aelitis.azureus.core.networkmanager.EventWaiter;
import com.aelitis.azureus.core.networkmanager.NetworkConnection;
import com.aelitis.azureus.core.networkmanager.Transport;

public abstract class 
TransportImpl 
	implements Transport
{
	private TransportHelperFilter filter;

	private static final TransportStats stats = AEDiagnostics.TRACE_TCP_TRANSPORT_STATS ? new TransportStats() : null;  

	private ByteBuffer data_already_read = null;

	private volatile EventWaiter read_waiter;
	private volatile EventWaiter write_waiter;
	private volatile boolean is_ready_for_write = false;
	private volatile boolean is_ready_for_read = false;
	private Throwable write_select_failure = null;
	private Throwable read_select_failure = null;

	private long	last_ready_for_read = SystemTime.getSteppedMonotonousTime();
	
	private boolean	trace;
	
	protected
	TransportImpl()
	{
	}
	
	public void
	setFilter(
		TransportHelperFilter	_filter )
	{
		filter	= _filter;
		
		if ( trace && _filter != null ){
			
			_filter.setTrace( true );
		}
	}
	
	public TransportHelperFilter
	getFilter()
	{
		return( filter );
	}
	
	public void 
	setAlreadyRead( 
		ByteBuffer bytes_already_read )
	{
		if ( bytes_already_read != null && bytes_already_read.hasRemaining()){

			if ( data_already_read != null ) {
			    ByteBuffer new_bb = ByteBuffer.allocate(data_already_read.remaining() + bytes_already_read.remaining());
			    new_bb.put(bytes_already_read);
			    new_bb.put(data_already_read);
			    new_bb.position(0);
			    data_already_read = new_bb;
			}
			else {
				data_already_read	= bytes_already_read;
			}
			is_ready_for_read = true;
		}
	}

	  
	public String 
	getEncryption(boolean verbose)
	{ 
		return( filter==null?"":filter.getName(verbose)); 
	}
	
	public String
	getProtocol()
	{
			// default impl = extract from encryption
		
		String s = getEncryption( false );
		
		int pos = s.indexOf( '(' );
		
		if ( pos != -1 ){
			
			s = s.substring( pos+1 );
			
			pos = s.indexOf( ')' );
			
			if ( pos > 0 ){
				
				return( s.substring( 0, pos ));
			}
		}
		
		return( "" );
	}
	
	public boolean 
	isEncrypted()
	{ 
		return( filter==null?false:filter.isEncrypted());
	}
	 
	  /**
	   * Is the transport ready to write,
	   * i.e. will a write request result in >0 bytes written.
	   * @return true if the transport is write ready, false if not yet ready
	   */
	
	public boolean 
	isReadyForWrite( 
		EventWaiter waiter ) 
	{
		if ( waiter != null ){
			
			write_waiter = waiter;
		}
		
		return is_ready_for_write;  
	}
	  
	protected boolean
	readyForWrite(
		boolean	ready )
	{
		if ( trace ){
			TimeFormatter.milliTrace( "trans: readyForWrite -> " + ready );
		}
		if ( ready ){
		  	boolean	progress = !is_ready_for_write;
	        is_ready_for_write = true;
	        EventWaiter ww = write_waiter;
	        if ( ww != null ){
	        	ww.eventOccurred();
	        }
	        return progress;
		}else{
			is_ready_for_write = false;
			return( false );
		}
	}
	  
	protected void
	writeFailed(
		Throwable	msg )
	{
		msg.fillInStackTrace();
		
		write_select_failure = msg;
	
		is_ready_for_write = true;  //set to true so that the next write attempt will throw an exception
	}
	
	  /**
	   * Is the transport ready to read,
	   * i.e. will a read request result in >0 bytes read.
	   * @return 0 if the transport is read ready, millis since last ready or -1 if never ready
	   */
	
	public long 
	isReadyForRead( 
		EventWaiter waiter ) 
	{
		if ( waiter != null ){
			read_waiter = waiter;
		}
	
		boolean ready = is_ready_for_read || 
						data_already_read != null || 
						( filter != null && filter.hasBufferedRead());
		
		long	now = SystemTime.getSteppedMonotonousTime();
		
		if ( ready ){
			
			last_ready_for_read = now;
			
			return( 0 );
		}
				
		long	diff = now - last_ready_for_read + 1;	// make sure > 0
					
		return( diff );
	}
	    
	protected boolean
	readyForRead(
		boolean	ready )
	{
		if ( ready ){
		   	boolean	progress = !is_ready_for_read;
	        is_ready_for_read = true;
	        EventWaiter rw = read_waiter;
	        if ( rw != null ){
	        	rw.eventOccurred();
	        }
	        return progress;
		}else{
			is_ready_for_read = false;
			return( false );
		}
	}
	  
	public void
	setReadyForRead()
	{
		readyForRead( true );
	}
	
	protected void
	readFailed(
		Throwable	msg )
	{
		msg.fillInStackTrace();	// msg picked up on another thread - make sure trace is available

		read_select_failure = msg;
	
		is_ready_for_read = true;  //set to true so that the next read attempt will throw an exception
	}
	
	  /**
	   * Write data to the transport from the given buffers.
	   * NOTE: Works like GatheringByteChannel.
	   * @param buffers from which bytes are to be retrieved
	   * @param array_offset offset within the buffer array of the first buffer from which bytes are to be retrieved
	   * @param length maximum number of buffers to be accessed
	   * @return number of bytes written
	   * @throws IOException on write error
	   */
	
	public long 
	write( 
		ByteBuffer[] buffers, 
		int array_offset, 
		int length ) 
	
		throws IOException 
	{
	  	if ( write_select_failure != null ){
	  			  		
	  		throw new IOException( "write_select_failure: " + write_select_failure.getMessage() );
	  	}
	    
	  	if ( filter == null )  return 0;
	  	
	  	long written = filter.write( buffers, array_offset, length );

	  	if ( stats != null )  stats.bytesWritten( (int)written );  //TODO
	       
	  	if ( written < 1 )  requestWriteSelect();
	      
	  	return written;
	}
	  
	  /**
	   * Read data from the transport into the given buffers.
	   * NOTE: Works like ScatteringByteChannel.
	   * @param buffers into which bytes are to be placed
	   * @param array_offset offset within the buffer array of the first buffer into which bytes are to be placed
	   * @param length maximum number of buffers to be accessed
	   * @return number of bytes read
	   * @throws IOException on read error
	   */
	
	public long 
	read( 
		ByteBuffer[] buffers, 
		int array_offset, 
		int length ) 
	
		throws IOException 
	{
		if( read_select_failure != null ) {
	    			
			throw new IOException( "read_select_failure: " + read_select_failure.getMessage() );
	    }  
	    
	    	//insert already-read data into the front of the stream
		
		if ( data_already_read != null ) {
	    
			int inserted = 0;
	      
			for( int i = array_offset; i < (array_offset + length); i++ ) {
				
				ByteBuffer bb = buffers[ i ];
	        
				int orig_limit = data_already_read.limit();
	        
				if( data_already_read.remaining() > bb.remaining() ) {
					
					data_already_read.limit( data_already_read.position() + bb.remaining() ); 
				}
	        
				inserted += data_already_read.remaining();
	        
				bb.put( data_already_read );
	        
				data_already_read.limit( orig_limit );
	        
				if( !data_already_read.hasRemaining() ) {
					
					data_already_read = null;
					
					break;
				}
			}
	      
			if( !buffers[ array_offset + length - 1 ].hasRemaining() ) {  //the last buffer has nothing left to read into normally
	        
				return inserted;  //so return right away, skipping socket read
			}      
	    }
	 
		if ( filter == null ){
			
			throw( new IOException( "Transport not ready" ));
		}

	    long bytes_read = filter.read( buffers, array_offset, length );

	    if( stats != null )  stats.bytesRead( (int)bytes_read );  //TODO
	    
	    if( bytes_read == 0 ){
	    	
	    	requestReadSelect();
	    }
	    
	    return bytes_read;
	}
	
	private void 
	requestWriteSelect() 
	{
		is_ready_for_write = false;
		
		if( filter != null ){
			
		   	filter.getHelper().resumeWriteSelects();
		}
	}
		  
		  
	private void 
	requestReadSelect() 
	{
		is_ready_for_read = false;
		
		if ( filter != null ){
			
		    filter.getHelper().resumeReadSelects();
		}
	} 
	
	public void
	connectedInbound()
	{	   
		registerSelectHandling();
	}
	
	public void
	connectedOutbound()
	{
		registerSelectHandling();
	}
	
	public void
	bindConnection(
		NetworkConnection	connection )
	{
	}

	public void
	unbindConnection(
		NetworkConnection	connection )
	{
	}
	  
	private void 
	registerSelectHandling() 
	{
		TransportHelperFilter	filter = getFilter();
		
		if( filter == null ) {
			Debug.out( "ERROR: registerSelectHandling():: filter == null" );
			return;
		}
	
		TransportHelper	helper = filter.getHelper();
		
		//read selection
		
		helper.registerForReadSelects(
			new TransportHelper.selectListener()
			{
			   	public boolean 
		    	selectSuccess(
		    		TransportHelper	helper, 
		    		Object 			attachment )
			   	{
			   		return( readyForRead( true ));
			   	}

		        public void 
		        selectFailure(
		        	TransportHelper	helper,
		        	Object 			attachment, 
		        	Throwable 		msg)
		        {
		        	readFailed( msg );
		        }
			},
			null );

		helper.registerForWriteSelects(
				new TransportHelper.selectListener()
				{
				   	public boolean 
			    	selectSuccess(
			    		TransportHelper	helper, 
			    		Object 			attachment )
				   	{
				   		return( readyForWrite( true ));
				   	}

			        public void 
			        selectFailure(
			        	TransportHelper	helper,
			        	Object 			attachment, 
			        	Throwable 		msg)
			        {
			        	writeFailed( msg );
			        }
				},
				null );
	}
	
	 
	public void
	setTrace(
		boolean	on )
	{
		trace	= on;
		
		TransportHelperFilter	filter = getFilter();

		if ( filter != null ){

			filter.setTrace( on );
		}
	}
}
