/*
 * Created on 2 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.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;

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.PEPeerTransport;
import org.gudy.azureus2.core3.torrent.TOTorrentFile;
import org.gudy.azureus2.core3.util.BEncoder;

import com.aelitis.azureus.core.networkmanager.NetworkConnection;
import com.aelitis.azureus.core.networkmanager.NetworkManager;
import com.aelitis.azureus.core.networkmanager.Transport;
import com.aelitis.azureus.core.networkmanager.impl.TransportHelper;
import com.aelitis.azureus.core.networkmanager.impl.tcp.IncomingSocketChannelManager;
import com.aelitis.azureus.core.peermanager.PeerManager;
import com.aelitis.azureus.core.peermanager.PeerManagerRegistration;
import com.aelitis.azureus.core.peermanager.PeerManagerRoutingListener;
import com.aelitis.azureus.core.peermanager.messaging.MessageStreamDecoder;
import com.aelitis.azureus.core.peermanager.messaging.MessageStreamEncoder;
import com.aelitis.azureus.core.peermanager.messaging.MessageStreamFactory;
import com.aelitis.azureus.core.stats.AzureusCoreStats;
import com.aelitis.azureus.core.stats.AzureusCoreStatsProvider;
import com.aelitis.azureus.core.util.CopyOnWriteList;

public class 
HTTPNetworkManager 
{
	private static final String	NL			= "\r\n";

	private static final LogIDs LOGID = LogIDs.NWMAN;

	private static final HTTPNetworkManager instance = new HTTPNetworkManager();

	public static HTTPNetworkManager getSingleton(){ return( instance ); }

	
	private final IncomingSocketChannelManager http_incoming_manager;

	private long	total_requests;
	private long	total_webseed_requests;
	private long	total_getright_requests;
	private long	total_invalid_requests;
	private long	total_ok_requests;
	
	private CopyOnWriteList<URLHandler>	url_handlers = new CopyOnWriteList<URLHandler>();
	
	private 
	HTTPNetworkManager()
	{	
		  Set	types = new HashSet();
		  
		  types.add( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_COUNT );
		  types.add( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_OK_COUNT );
		  types.add( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_INVALID_COUNT );
		  types.add( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_WEBSEED_COUNT );
		  types.add( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_GETRIGHT_COUNT );
			
		  AzureusCoreStats.registerProvider( 
				  types,
				  new AzureusCoreStatsProvider()
				  {
						public void
						updateStats(
							Set		types,
							Map		values )
						{
							if ( types.contains( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_COUNT )){
								
								values.put( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_COUNT, new Long( total_requests ));
							}	
							
							if ( types.contains( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_OK_COUNT )){
								
								values.put( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_OK_COUNT, new Long( total_ok_requests ));
							}					

							if ( types.contains( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_INVALID_COUNT )){
								
								values.put( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_INVALID_COUNT, new Long( total_invalid_requests ));
							}					

							if ( types.contains( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_WEBSEED_COUNT )){
								
								values.put( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_WEBSEED_COUNT, new Long( total_webseed_requests ));
							}	
							
							if ( types.contains( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_GETRIGHT_COUNT )){
								
								values.put( AzureusCoreStats.ST_NET_HTTP_IN_REQUEST_GETRIGHT_COUNT, new Long( total_getright_requests));							}					

						}
				  });
		/*
		try{
			System.out.println( "/webseed?info_hash=" + URLEncoder.encode( new String( ByteFormatter.decodeString("C9C04D96F11FB5C5ECC99D418D3575FBFC2208B0"), "ISO-8859-1"), "ISO-8859-1" ));
			
		}catch( Throwable e ){
			
			e.printStackTrace();
		}
		*/
		
		http_incoming_manager = new IncomingSocketChannelManager( "HTTP.Data.Listen.Port", "HTTP.Data.Listen.Port.Enable" );
		
		NetworkManager.ByteMatcher matcher =
		   	new NetworkManager.ByteMatcher() 
		    {
				public int matchThisSizeOrBigger(){	return( 4 + 1 + 11 ); } // GET ' ' <url of 1> ' HTTP/1.1<cr><nl>'
				
		    	public int maxSize() { return 256; }	// max GET <url> size - boiler plate plus small url plus hash
		    	public int minSize() { return 3; }		// enough to match GET

		    	public Object
		    	matches( 
		    		TransportHelper		transport,
		    		ByteBuffer 			to_compare, 
		    		int 				port ) 
		    	{
		    		total_requests++;
		    		
		    		InetSocketAddress	address = transport.getAddress();
		    		
		    		int old_limit 		= to_compare.limit();
		    		int old_position 	= to_compare.position();

		    		boolean 	ok = false;
		    		
		    		try{
			    		byte[]	head = new byte[3];
			    		
			    		to_compare.get( head );
			    		
			    			// note duplication of this in min-matches below
			    		
			    		if ( head[0] != 'G' || head[1] != 'E' || head[2] != 'T' ){
			    			
			    			return( null );
			    		}
			    		
			    		byte[]	line_bytes = new byte[to_compare.remaining()];
			    		
			    		to_compare.get( line_bytes );
			    		
			    		try{
			    				// format is GET url HTTP/1.1<NL>
			    			
				    		String	url = new String( line_bytes, "ISO-8859-1" );
				    		
				    		int	space = url.indexOf(' ');
				    		
				    		if ( space == -1 ){
				    			
				    			return( null );
				    		}
				    		
				    			// note that we don't insist on a full URL here, just the start of one
				    		
				    		url = url.substring( space + 1 ).trim();
				    				
				    		if ( url.indexOf( "/index.html") != -1 ){
				    			
				    			ok	= true;
				    			
					    		return( new Object[]{ transport, getIndexPage() });
					    		
				    		}else if ( url.indexOf( "/ping.html") != -1 ){
					    			
				    				// ping is used for inbound HTTP port checking
				    			
				    			ok	= true;
				    			
						    	return( new Object[]{ transport, getPingPage( url ) });

				    		}else if ( url.indexOf( "/test503.html" ) != -1 ){
				    			
				    			ok	= true;
				    			
					    		return( new Object[]{ transport, getTest503()});
				    		}

				    		String	hash_str = null;
				    		
				    		int	hash_pos = url.indexOf( "?info_hash=" );
				    						    		
				    		if ( hash_pos != -1 ){
				    							    			
				    			int	hash_start = hash_pos + 11;
				    			
				    			int	hash_end = url.indexOf( '&', hash_pos );
				    							    			
				    			if ( hash_end == -1 ){
				    				
				    					// not read the end yet
				    				
				    				return( null );
				    				
				    			}else{
				    								    				
				    				hash_str = url.substring( hash_start, hash_end );
				    			}
				    		}else{
			    		
				    			hash_pos = url.indexOf( "/files/" );
					    		
					    		if ( hash_pos != -1 ){
					    							    			
					    			int	hash_start = hash_pos + 7;
	
					    			int	hash_end = url.indexOf('/', hash_start );
					    			
					    			if ( hash_end == -1 ){
					    				
					    					// not read the end of the hash yet
					    				
					    				return( null );
					    				
					    			}else{
					    									    				
					    				hash_str = url.substring( hash_start, hash_end );
					    			}
					    		}
				    		}
				    		
				    		if ( hash_str != null ){
			    				
			    				byte[]	hash = URLDecoder.decode( hash_str, "ISO-8859-1" ).getBytes( "ISO-8859-1" );
			    								    				
			    				PeerManagerRegistration reg_data = PeerManager.getSingleton().manualMatchHash( address, hash );
			    				
			    				if ( reg_data != null ){
			    					
			    						// trim back URL as it currently has header in it too
			    					
			    					int	pos = url.indexOf( ' ' );
			    					
			    					String	trimmed = pos==-1?url:url.substring(0,pos);
			    					
			    					ok	= true;
			    					
			    					return( new Object[]{ trimmed, reg_data });
			    				}
				    		}else{
				    			
				    			int	link_pos = url.indexOf( "/links/" );
				    			
				    			if ( link_pos != -1 ){
				    			
			    					int	pos = url.indexOf( ' ', link_pos );

			    					if ( pos == -1 ){
			    						
			    						return( null );
			    					}
			    					
			    					String link = url.substring(0,pos).substring( link_pos+7 );
			    					
				    				link = URLDecoder.decode( link, "UTF-8" );
			    					
				    				PeerManagerRegistration reg_data = PeerManager.getSingleton().manualMatchLink( address, link );
				    				
				    				if ( reg_data != null ){

				    					TOTorrentFile	file = reg_data.getLink( link );
				    					
				    					if ( file != null ){
				    						
					    					StringBuffer	target_url = new StringBuffer( 512 );
					    					
					    					target_url.append( "/files/" );
					    					
					    					target_url.append( URLEncoder.encode(  new String( file.getTorrent().getHash(), "ISO-8859-1" ), "ISO-8859-1" ));
					    					
					    					byte[][]	bits = file.getPathComponents();
					    					
					    					for (int i=0;i<bits.length;i++){
					    						
					    						target_url.append( "/" );
					    						
					    						target_url.append( URLEncoder.encode( new String( bits[i], "ISO-8859-1" ), "ISO-8859-1" ));
					    					}
					    					
					    					ok	= true;
					    					
					    					return( new Object[]{ target_url.toString(), reg_data });
				    					}
				    				}
				    			}
				    		}
				    		
				    		String trimmed = url;
				    		
				    		int	pos = trimmed.indexOf( ' ' );
				    		
				    		if ( pos != -1 ){
				    			
				    			trimmed = trimmed.substring( 0, pos );
				    		}
				    		
				    		for ( URLHandler handler: url_handlers ){
				    			
				    			if ( handler.matches( trimmed )){
				    							    					
			    					ok	= true;
			    					
			    					return( new Object[]{ handler, transport, "GET " + url });
				    			}
				    		}
		   					if (Logger.isEnabled()){
	    						Logger.log(new LogEvent(LOGID, "HTTP decode from " + address + " failed: no match for " + url ));
	    					}
		   					
				    		return( new Object[]{ transport, getNotFound() });
				    		
			    		}catch( Throwable e ){
			    			
		   					if (Logger.isEnabled()){
	    						Logger.log(new LogEvent(LOGID, "HTTP decode from " + address + " failed, " + e.getMessage()));
	    					}
	
		   					return( null );
			    		}
		    		}finally{
		    		
		    			if ( ok ){
		    				
		    				total_ok_requests++;
		    				
		    			}else{
		    				
		    				total_invalid_requests++;
		    			}
			    			//	restore buffer structure
		    			
			    		to_compare.limit( old_limit );
			    		to_compare.position( old_position );
		    		}
		    	}
		    	
		    	public Object 
		    	minMatches( 
		    		TransportHelper		transport,
		    		ByteBuffer 			to_compare, 
		    		int 				port ) 
		    	{ 
		    		byte[]	head = new byte[3];
		    		
		    		to_compare.get( head );
		    		
		    		if (head[0] != 'G' || head[1] != 'E' || head[2] != 'T' ){
		    			
		    			return( null );
		    		}

		    		return( "" );
		    	}

		    	public byte[][] 
		    	getSharedSecrets()
		    	{
		    		return( null );	
		    	}
		    	
		    	 public int 
		    	 getSpecificPort()
		    	 {
		    		 return( http_incoming_manager.getTCPListeningPortNumber());
		    	 }
		    };
		    
	    // register for incoming connection routing
	    NetworkManager.getSingleton().requestIncomingConnectionRouting(
	        matcher,
	        new NetworkManager.RoutingListener() 
	        {
	        	public void 
	        	connectionRouted( 
	        		final NetworkConnection 	connection, 
	        		Object 						_routing_data ) 
	        	{
	        		Object[]	x = (Object[])_routing_data;
	        		
	        		Object	entry1 = x[0];
	        		
	        		if ( entry1 instanceof TransportHelper ){
	        			
	        				// routed on failure
	        			
	        			writeReply(connection, (TransportHelper)x[0], (String)x[1]);
	        			
	        			return;
	        			
	        		}else if ( entry1 instanceof URLHandler ){
	        			
	        			((URLHandler)entry1).handle(
	        					(TransportHelper)x[1],
	        					(String)x[2] );
	        		
	        			return;
	        		}
	        		
	        		final String					url 			= (String)entry1;
	        		final PeerManagerRegistration	routing_data 	= (PeerManagerRegistration)x[1];
	        		
   					if (Logger.isEnabled()){
						Logger.log(new LogEvent(LOGID, "HTTP connection from " + connection.getEndpoint().getNotionalAddress() + " routed successfully on '" + url + "'" ));
   					}   					
   					   					
	        		PeerManager.getSingleton().manualRoute(
	        				routing_data, 
	        				connection,
	        				new PeerManagerRoutingListener()
	        				{
	        					public boolean
	        					routed(
	        						PEPeerTransport		peer )
	        					{
	        						if ( url.indexOf( "/webseed" ) != -1 ){
	        							
	        							total_webseed_requests++;
	        							
	        							new HTTPNetworkConnectionWebSeed( HTTPNetworkManager.this, connection, peer );
	        							
	        							return( true );
	        							
	        						}else if ( url.indexOf( "/files/" ) != -1 ){

	        							total_getright_requests++;
	        							
	        							new HTTPNetworkConnectionFile( HTTPNetworkManager.this, connection, peer );
	        							
	        							return( true );
	        						}
	        						
	        						return( false );
	        					}
	        				});
	        	}
	        	
	        	public boolean
	      	  	autoCryptoFallback()
	        	{
	        		return( false );
	        	}
	        	},
	        new MessageStreamFactory() {
	          public MessageStreamEncoder createEncoder() {  return new HTTPMessageEncoder();  }
	          public MessageStreamDecoder createDecoder() {  return new HTTPMessageDecoder();  }
	        });
	}
	
	
	protected void
	reRoute(
		final HTTPNetworkConnection		old_http_connection,
		final byte[]					old_hash,
		final byte[]					new_hash,
		final String					header )
	{
		final NetworkConnection	old_connection = old_http_connection.getConnection();
		
		PeerManagerRegistration reg_data = 
			PeerManager.getSingleton().manualMatchHash( 
					old_connection.getEndpoint().getNotionalAddress(), 
					new_hash );

		if ( reg_data == null ){
			
			old_http_connection.close( "Re-routing failed - registration not found" );
			
			return;
		}
	
		final Transport transport = old_connection.detachTransport();
		
		old_http_connection.close( "Switching torrents" );
		
		final NetworkConnection new_connection = 
			NetworkManager.getSingleton().bindTransport(
					transport,
					new HTTPMessageEncoder(),
					new HTTPMessageDecoder( header ));
		
		PeerManager.getSingleton().manualRoute(
				reg_data, 
				new_connection,
				new PeerManagerRoutingListener()
				{
					public boolean
					routed(
						PEPeerTransport		peer )
					{
						HTTPNetworkConnection new_http_connection;
						
						if ( header.indexOf( "/webseed" ) != -1 ){
							
							new_http_connection = new HTTPNetworkConnectionWebSeed( HTTPNetworkManager.this, new_connection, peer );
							
						}else if ( header.indexOf( "/files/" ) != -1 ){

							new_http_connection = new HTTPNetworkConnectionFile( HTTPNetworkManager.this, new_connection, peer );
						
						}else{
							
							return( false );
						}
						
							// fake a wakeup so pre-read header is processed
						
						new_http_connection.readWakeup();
						
						/*
						System.out.println( 
								"Re-routed " + new_connection.getEndpoint().getNotionalAddress() +
								" from " + ByteFormatter.encodeString( old_hash ) + " to " +
								 ByteFormatter.encodeString( new_hash ) );
						*/
						
						return( true );
					}
				});
	}
	
	public boolean
	isHTTPListenerEnabled()
	{
		return( http_incoming_manager.isEnabled());
	}
	
	public int
	getHTTPListeningPortNumber()
	{
		return( http_incoming_manager.getTCPListeningPortNumber());
	}
	
	public void
	setExplicitBindAddress(
		InetAddress	address )
	{
		http_incoming_manager.setExplicitBindAddress( address );
	}
	
	public void
	clearExplicitBindAddress()
	{
		http_incoming_manager.clearExplicitBindAddress();
	}
	
	public boolean
	isEffectiveBindAddress(
		InetAddress		address )
	{
		return( http_incoming_manager.isEffectiveBindAddress( address ));
	}
	
	protected String
	getIndexPage()
	{
		return( "HTTP/1.1 200 OK" + NL + 
				"Connection: Close" + NL +
				"Content-Length: 0" + NL +
				NL );
	}
	
	protected String
	getPingPage(
		String	url )
	{
		int	pos = url.indexOf( ' ' );

		if ( pos != -1 ){
			
			url = url.substring( 0, pos );
		}
		
		pos = url.indexOf( '?' );
		
		Map	response = new HashMap();
		
		boolean	ok = false;
		
		if ( pos != -1 ){
			
			StringTokenizer tok = new StringTokenizer(url.substring(pos+1), "&");
			
			while( tok.hasMoreTokens()){
				
				String	token = tok.nextToken();
				
				pos	= token.indexOf('=');
				
				if ( pos != -1 ){
					
					String	lhs = token.substring(0,pos);
					String	rhs = token.substring(pos+1);
					
					if ( lhs.equals( "check" )){
						
						response.put( "check", rhs );
						
						ok = true;
					}
				}
			}
		}
		
		if ( ok ){
			
			try{
				byte[]	bytes = BEncoder.encode( response );
			
				byte[]	length = new byte[4];
				
				ByteBuffer.wrap( length ).putInt( bytes.length );
								
				return( "HTTP/1.1 200 OK" + NL + 
						"Connection: Close" + NL +
						"Content-Length: " + ( bytes.length + 4 )+ NL +
						NL + 
						new String( length, "ISO-8859-1" ) + new String( bytes, "ISO-8859-1" ) );
				
			}catch( Throwable e ){
			}
		}
		
		return( getNotFound());

	}

	protected String
	getTest503()
	{
		return( "HTTP/1.1 503 Service Unavailable" + NL + 
				"Connection: Close" + NL +
				"Content-Length: 4" + NL +
				NL + 
				"1234" );
	}
	
	protected String
	getNotFound()
	{
		return( "HTTP/1.1 404 Not Found" + NL +
				"Connection: Close" + NL +
				"Content-Length: 0" + NL +
				NL );
	}
	
	protected String
	getRangeNotSatisfiable()
	{
		return( "HTTP/1.1 416 Not Satisfiable" + NL +
				"Connection: Close" + NL +
				"Content-Length: 0" + NL +
				NL );
	}
	
	
	protected void
	writeReply(
		final NetworkConnection		connection,
		final TransportHelper		transport,
		final String				data )
	{
		byte[]	bytes;
		
		try{
			bytes = data.getBytes( "ISO-8859-1" );
			
		}catch( UnsupportedEncodingException e ){
			
			bytes = data.getBytes();
		}
				
		final ByteBuffer bb = ByteBuffer.wrap( bytes );
		
		try{
			transport.write( bb, false );
			
			if ( bb.remaining() > 0 ){
				
				transport.registerForWriteSelects(
					new TransportHelper.selectListener()
					{
					  	public boolean 
				    	selectSuccess(
				    		TransportHelper	helper, 
				    		Object 			attachment )
					  	{
					  		try{
					  			int written = helper.write( bb, false );
					  			
					  			if ( bb.remaining() > 0 ){
					  			
					  				helper.registerForWriteSelects( this, null );
					  				
					  			}else{
					  				
				  					if (Logger.isEnabled()){
										Logger.log(new LogEvent(LOGID, "HTTP connection from " + connection.getEndpoint().getNotionalAddress() + " closed" ));
				   					}   					

									connection.close();
					  			}
					  			
					  			return( written > 0 );
					  			
					  		}catch( Throwable e ){
					  			
					  			helper.cancelWriteSelects();
					  			
			  					if (Logger.isEnabled()){
									Logger.log(new LogEvent(LOGID, "HTTP connection from " + connection.getEndpoint().getNotionalAddress() + " failed to write error '" + data + "'" ));
			   					}   					

					  			connection.close();
					  			
					  			return( false );
					  		}
					  	}

				        public void 
				        selectFailure(
				        	TransportHelper	helper,
				        	Object 			attachment, 
				        	Throwable 		msg)
				        {
				        	helper.cancelWriteSelects();
				        	
		  					if (Logger.isEnabled()){
								Logger.log(new LogEvent(LOGID, "HTTP connection from " + connection.getEndpoint().getNotionalAddress() + " failed to write error '" + data + "'" ));
		   					}   					

				        	connection.close();
				        }
					},
					null );
			}else{

				if (Logger.isEnabled()){
					Logger.log(new LogEvent(LOGID, "HTTP connection from " + connection.getEndpoint().getNotionalAddress() + " closed" ));
   				}   					

				connection.close();
			}
		}catch( Throwable e ){
			
			if (Logger.isEnabled()){
				Logger.log(new LogEvent(LOGID, "HTTP connection from " + connection.getEndpoint().getNotionalAddress() + " failed to write error '" + data + "'" ));
			}   					

			connection.close();
		}
	}
	
	public void
	addURLHandler(
		URLHandler	handler )
	{
		url_handlers.add( handler );
	}
	
	public void
	removeURLHandler(
		URLHandler	handler )
	{
		url_handlers.remove( handler );
	}
	
	public interface
	URLHandler
	{
		public boolean
		matches(
			String		url );
		
		public void
		handle(
			TransportHelper		transport,
			String				header_so_far );
		
	}
}
