/*
 * Created on 03-Mar-2005
 * Created by Paul Gardner
 * 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 com.aelitis.net.magneturi.impl;

import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;

import org.bouncycastle.util.encoders.Base64;
import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.logging.*;
import org.gudy.azureus2.core3.util.*;

import com.aelitis.azureus.core.util.CaseSensitiveFileMap;
import com.aelitis.azureus.core.util.png.PNG;
import com.aelitis.net.magneturi.MagnetURIHandler;
import com.aelitis.net.magneturi.MagnetURIHandlerListener;
import com.aelitis.net.magneturi.MagnetURIHandlerProgressListener;

/**
 * @author parg
 *
 */

public class 
MagnetURIHandlerImpl 
	extends MagnetURIHandler
{
  private static final LogIDs LOGID = LogIDs.NET;
		// see http://magnet-uri.sourceforge.net/magnet-draft-overview.txt
	
	private static MagnetURIHandlerImpl		singleton;
	
	private static AEMonitor				class_mon = new AEMonitor( "MagnetURLHandler:class" );
	
	private static final int				DOWNLOAD_TIMEOUT	= 3*60*1000;
	
	protected static final String	NL			= "\015\012";
	
	private final static boolean DEBUG = false;

	public static MagnetURIHandler
	getSingleton()
	{
		try{
			class_mon.enter();
		
			if ( singleton == null ){
			
				singleton	= new MagnetURIHandlerImpl();
			}
		
			return( singleton );
			
		}finally{
			
			class_mon.exit();
		}
	}
	
	private int		port;
	
	private List	listeners	= new ArrayList();
	
	private Map		info_map 	= new HashMap();
	
	protected
	MagnetURIHandlerImpl()
	{
		ServerSocket	socket	= null;
		
		for (int i=45100;i<=45199;i++){
			
			try{
				
			   socket = new ServerSocket(i, 50, InetAddress.getByName("127.0.0.1"));

			   port	= i;
			   
			   break;
			   
			}catch( Throwable e ){
				
			}
		}
		
		if ( socket == null ){
			
			// no free sockets, not much we can do
			if (Logger.isEnabled())
				Logger.log(new LogEvent(LOGID, LogEvent.LT_ERROR,
						"MagnetURI: no free sockets, giving up"));
			
		}else{
			if (Logger.isEnabled())
				Logger.log(new LogEvent(LOGID, "MagnetURI: bound on "
						+ socket.getLocalPort()));
			
			final ServerSocket	f_socket = socket;
			
			Thread t = 
				new AEThread("MagnetURIHandler")
				{
					public void
					runSupport()
					{
						int	errors 	= 0;
						int	ok		= 0;
						
						while(true){
							
							try{
						
								final Socket sck = f_socket.accept();
								
								ok++;
								
								errors	= 0;
								
								Thread t = 
									new AEThread( "MagnetURIHandler:processor" )
									{
										public void
										runSupport()
										{
											boolean	close_socket	= true;
											
											try{
											        String address = sck.getInetAddress().getHostAddress();
										        
										        if ( address.equals("localhost") || address.equals("127.0.0.1")) {
										        	
										        	BufferedReader br = new BufferedReader(new InputStreamReader(sck.getInputStream(),Constants.DEFAULT_ENCODING));
										        	
										        	String line = br.readLine();

										        	if (DEBUG) {
										        		System.out.println("=====");
										        		System.out.println("Traffic Class: "
										        				+ sck.getTrafficClass());
										        		System.out.println("OS: " + sck.getOutputStream());
										        		System.out.println("isBound? " + sck.isBound()
										        				+ "; isClosed=" + sck.isClosed() + "; isConn="
										        				+ sck.isConnected() + ";isIShutD "
										        				+ sck.isInputShutdown() + ";isOShutD "
										        				+ sck.isOutputShutdown());
										        		System.out.println("- - - -");
										        		System.out.println(line);

										        		while (br.ready()) {
										        			String extraline = br.readLine();
										        			System.out.println(extraline);
										        		}
										        		System.out.println("=====");
										        	}


										        	if ( line != null ){
										        		
											        	if ( line.toUpperCase().startsWith( "GET " )){
											        	
											        		Logger.log(new LogEvent(LOGID,
											        					"MagnetURIHandler: processing '" + line + "'"));
			
											        		line = line.substring(4);
											        		
											        		int	pos = line.lastIndexOf(' ');
											        		
											        		line = line.substring( 0, pos );
											        		
											        		close_socket = process( line, br, sck.getOutputStream() );
											        		
											        	}else{
											        		
															Logger.log(new LogEvent(LOGID, LogEvent.LT_WARNING,
																		"MagnetURIHandler: invalid command - '" + line
																			+ "'"));
											        	}
										        	}else{
										        		
											       		Logger.log(new LogEvent(LOGID, LogEvent.LT_WARNING,
											       				"MagnetURIHandler: connect from "
											       				+ "'" + address + "': no data read"));
									        		
										        	}
										        	
											   }else{
												   
											      	Logger.log(new LogEvent(LOGID, LogEvent.LT_WARNING,
											      				"MagnetURIHandler: connect from "
											       				+ "invalid address '" + address + "'"));
										        }
											}catch( Throwable e ){
												
												if ( !(e instanceof IOException || e instanceof SocketException )){
													
													Debug.printStackTrace(e);
												}
											}finally{
												
												try{
														// leave client to close socket if not requested
													
													if ( close_socket ){
														
														sck.close();
													}
													
												}catch( Throwable e ){
												}
											}
										}
									};
								
								t.setDaemon( true );
								
								t.start();
								
							}catch( Throwable e ){
								
								Debug.printStackTrace(e);
								
								errors++;
								
								if ( errors > 100 ){
									if (Logger.isEnabled())
										Logger.log(new LogEvent(LOGID,
										"MagnetURIHandler: bailing out, too many socket errors"));
								}
							}
						}
					}
				};
				
			t.setDaemon( true );
			
			t.start();
		}
	}
	
	protected boolean
	process(
		String			get,
		BufferedReader	is,
		OutputStream	os )
	
		throws IOException
	{
		//System.out.println( "get = " + get );
		
			// magnet:?xt=urn:sha1:YNCKHTQCWBTRNJIV4WNAE52SJUQCZO5C
		
		Map		original_params 			= new HashMap();
		Map		lc_params					= new HashMap();
		
		List 	source_params	= new ArrayList();
		
		int	pos	= get.indexOf( '?' );
		
		if ( pos != -1 ){
					
			StringTokenizer	tok = new StringTokenizer( get.substring( pos+1 ), "&" );
			if (DEBUG) {
				System.out.println("params:" + get.substring( pos+1 ));
			}
			
			while( tok.hasMoreTokens()){
				
				String	arg = tok.nextToken();
				
				pos	= arg.indexOf( '=' );
				
				if ( pos == -1 ){
					
					String lhs = arg.trim();
					
					original_params.put( lhs, "" );
					
					lc_params.put( lhs.toLowerCase(), "" );
					
				}else{
										
					try{
						String	lhs 	= arg.substring( 0, pos ).trim();
						String	lc_lhs 	= lhs.toLowerCase();
									
						String	rhs = URLDecoder.decode( arg.substring( pos+1 ).trim(), Constants.DEFAULT_ENCODING);

						original_params.put( lhs, rhs );
						
						lc_params.put( lc_lhs, rhs );
						
						if ( lc_lhs.equals( "xsource" )){
							
							source_params.add( rhs );
						}
					}catch( UnsupportedEncodingException e ){
						
						Debug.printStackTrace( e );
					}
				}
			}
		}
		

		if ( get.startsWith( "/magnet10/badge.img" )){
			
			for (int i=0;i<listeners.size();i++){
				
				byte[]	data = ((MagnetURIHandlerListener)listeners.get(i)).badge();
					
				if ( data != null ){
					
					writeReply( os, "image/gif", data );
					
					return( true );
				}
			}
			
			writeNotFound( os );
			
			return( true );
			
		}else if ( get.startsWith( "/magnet10/canHandle.img?" )){

			String urn = (String)lc_params.get( "xt" );

			if ( urn != null && urn.toLowerCase().startsWith( "urn:btih:")){
			
				for (int i=0;i<listeners.size();i++){
					
					byte[]	data = ((MagnetURIHandlerListener)listeners.get(i)).badge();
						
					if ( data != null ){
						
						writeReply( os, "image/gif", data );
						
						return( true );
					}
				}
			}
			
			writeNotFound( os );
			
			return( true );
			
		}else if ( get.startsWith( "/azversion" )){

			writeReply( os, "text/plain", Constants.AZUREUS_VERSION );
			
			return( true );
			
		}else if ( 	get.startsWith( "/magnet10/options.js?" ) ||
					get.startsWith( "/magnet10/default.js?" )){
		
			String	resp = "";
			
			resp += getJS( "magnetOptionsPreamble" );
			
			resp += getJSS( "<a href=\\\"http://127.0.0.1:\"+(45100+magnetCurrentSlot)+\"/select/?\"+magnetQueryString+\"\\\" target=\\\"_blank\\\">" );
			resp += getJSS( "<img src=\\\"http://127.0.0.1:\"+(45100+magnetCurrentSlot)+\"/magnet10/badge.img\\\">" );
			resp += getJSS( "Download with Azureus" );
			resp += getJSS( "</a>" );

			resp += getJS( "magnetOptionsPostamble" );
			
			resp += "magnetOptionsPollSuccesses++";
			
			writeReply( os, "application/x-javascript", resp );
		
			return( true );
			
		}else if ( get.startsWith( "/magnet10/pause" )){
			
			try{
				Thread.sleep( 250 );
				
			}catch( Throwable e ){
				
			}
			writeNotFound( os );
			
			return( true );
			
		}else if ( get.startsWith( "/select/" )){

			String	fail_reason = "";
			
			boolean	ok = false;
			
			String urn = (String)lc_params.get( "xt" );

			if ( urn == null ){
				
				fail_reason	= "xt missing";
				
			}else{
					
				String	lc_urn = urn.toLowerCase();
				
				try{
				
					URL	url;
					
					if ( lc_urn.startsWith( "http:") || lc_urn.startsWith( "https:" )){
						
						url = new URL( urn );
						
					}else{
						
						url = new URL( "magnet:?xt=" + urn );
					}
					
					for (int i=0;i<listeners.size();i++){
						
						if (((MagnetURIHandlerListener)listeners.get(i)).download( url )){
							
							ok = true;
							
							break;
						}
					}
					
					if ( !ok ){
						
						fail_reason = "No listeners accepted the operation";
					}
				}catch( Throwable e ){
					
					Debug.printStackTrace(e);
					
					fail_reason	= Debug.getNestedExceptionMessage(e);
				}
			}
			
			if ( ok ){
				
				if ( "image".equalsIgnoreCase((String)lc_params.get( "result" ))){
					
					for (int i=0;i<listeners.size();i++){

						byte[]	data = ((MagnetURIHandlerListener)listeners.get(i)).badge();
					
						if ( data != null ){
							
							writeReply( os, "image/gif", data );
							
							return( true );
						}
					}
				}
					
				writeReply( os, "text/plain", "Download initiated" );
				
			}else{
				
				writeReply( os, "text/plain", "Download initiation failed: " + fail_reason );
			}
			
		}else if ( get.startsWith( "/download/" )){
			
			String urn = (String)lc_params.get( "xt" );
			
			if ( urn == null || !( urn.toLowerCase().startsWith( "urn:sha1:") || urn.toLowerCase().startsWith( "urn:btih:"))){
				if (Logger.isEnabled())
					Logger.log(new LogEvent(LOGID, LogEvent.LT_WARNING,
							"MagnetURIHandler: " + "invalid command - '" + get + "'"));
				
				return( true );
			}
			
			final PrintWriter	pw = new PrintWriter( new OutputStreamWriter( os, "UTF-8" ));

			try{			
				pw.print( "HTTP/1.0 200 OK" + NL ); 

				pw.flush();
								
				String	base_32 = urn.substring(9);
				
				List	sources = new ArrayList();
				
				for (int i=0;i<source_params.size();i++){
					
					String	source = (String)source_params.get(i);
					
					int	p = source.indexOf(':');
					
					if ( p != -1 ){
						
						try{
							InetSocketAddress	sa = new InetSocketAddress( source.substring(0,p), Integer.parseInt( source.substring(p+1)));
							
							sources.add( sa );
							
						}catch( Throwable e ){
							
							Debug.printStackTrace(e);
						}
					}
				}
					
				InetSocketAddress[]	s = new InetSocketAddress[ sources.size()];
				
				sources.toArray( s );
				
				if (Logger.isEnabled())
					Logger.log(new LogEvent(LOGID, "MagnetURIHandler: download of '"
							+ base_32 + "' starts (initial sources=" + s.length + ")"));

				byte[] sha1 = Base32.decode( base_32 );
				
				byte[]	data = null;
				
				
				for (int i=0;i<listeners.size();i++){
				
					data = ((MagnetURIHandlerListener)listeners.get(i)).download(
							new MagnetURIHandlerProgressListener()
							{
								public void
								reportSize(
									long	size )
								{
									pw.print( "X-Report: " + getMessageText( "torrent_size", String.valueOf( size )) + NL );
									
									pw.flush();
								}
								
								public void
								reportActivity(
									String	str )
								{
									pw.print( "X-Report: " + str + NL );
																			
									pw.flush();
								}
								
								public void
								reportCompleteness(
									int		percent )
								{
									pw.print( "X-Report: " + getMessageText( "percent", String.valueOf(percent)) + NL );
									
									pw.flush();
								}
							},
							sha1, 
							s,
							DOWNLOAD_TIMEOUT );
					
					if ( data != null ){
						
						break;
					}
				}
				
				if (Logger.isEnabled())
					Logger.log(new LogEvent(LOGID, "MagnetURIHandler: download of '"
							+ base_32
							+ "' completes, data "
							+ (data == null ? "not found"
									: ("found, length = " + data.length))));

				if ( data != null ){
					
					pw.print( "Content-Length: " + data.length + NL + NL );
					
					pw.flush();
					
					os.write( data );
					
					os.flush();
					
				}else{
					
						// HACK: don't change the "error:" message below, it is used by TorrentDownloader to detect this
						// condition
					
					pw.print( "X-Report: error: " + getMessageText( "no_sources" ) + NL );
					
					pw.flush();
					
						// pause on error
					
					return( !lc_params.containsKey( "pause_on_error" ));
				}
			}catch( Throwable e ){
				
					// don't remove the "error:" (see above)
				
				pw.print( "X-Report: error: " + getMessageText( "error", Debug.getNestedExceptionMessage(e)) + NL );
				
				pw.flush();
				
				// Debug.printStackTrace(e);
				
					// pause on error
				
				return( !lc_params.containsKey( "pause_on_error" ));
			}
		}else if ( get.startsWith( "/getinfo?" )){

			String name = (String)lc_params.get( "name" );

			if ( name != null ){
				
				Integer	info = (Integer)info_map.get( name );
				
				int	value = Integer.MIN_VALUE;
				
				if ( info != null ){					
					
					value = info.intValue();
					
				}else{
					
					for (int i=0;i<listeners.size() && value == Integer.MIN_VALUE;i++){
						
							// no idea why we copy, but let's keep doing so
						
						HashMap paramsCopy = new HashMap();
						
						paramsCopy.putAll( original_params);

						value = ((MagnetURIHandlerListener)listeners.get(i)).get( name, paramsCopy );
					}
				}
								
				if ( value == Integer.MIN_VALUE ){
					
						// no value, see if we have a default
					
					String	def_str = (String)lc_params.get( "default" );

					if ( def_str != null ){
						
						try{
							value = Integer.parseInt( def_str );
							
						}catch( Throwable e ){
							
							Debug.printStackTrace( e );
						}
					}
				}else{
					
						// have a value, see if we have a max
					
					String	max_str = (String)lc_params.get( "max" );

					if ( max_str != null ){
						
						try{
							int	max = Integer.parseInt( max_str );
							
							if ( value > max ){
								
								value = max;
							}
						}catch( Throwable e ){
							
							Debug.printStackTrace(e);
						}
					}
				}
				
				if ( value != Integer.MIN_VALUE ){
	
					if ( value < 0 ){
						
						value = 0;
					}
					
						// need to trim if too large
					
					if ( value > 1024 * 1024 ){
						
						value = 1024 * 1024;	
					}
										
						// see if we need to div/mod for clients that don't support huge images
						// e.g. http://localhost:45100/getinfo?name=Plugin.azupnpav.content_port&mod=8
					
					int	width 	= value;
					int	height	= 1;

						// divmod -> encode div+1 as width, mod+1 as height
					
					String	div_mod = (String)lc_params.get( "divmod" );
					
					if ( div_mod != null ){
						
						int	n = Integer.parseInt( div_mod );
						
						width 	= ( value / n ) + 1;
						height	= ( value % n ) + 1;
						
					}else{
						
						String	div = (String)lc_params.get( "div" );
						
						if ( div != null ){
							
							width = value / Integer.parseInt( div );
							
						}else{
							
							String	mod = (String)lc_params.get( "mod" );
							
							if ( mod != null ){
								
								width = value % Integer.parseInt( mod );
							}
						}
					}

					String	img_type = (String)lc_params.get( "img_type" );
					
					if ( img_type != null && img_type.equals( "png" )){
						
						byte[]	data = PNG.getPNGBytesForSize( width, height );
												
						writeReply( os, "image/png", data );
						
					}else{
						
						ByteArrayOutputStream	baos = new ByteArrayOutputStream();				

						writeImage( baos, width, height );
						
						byte[]	data = baos.toByteArray();
												
						writeReply( os, "image/bmp", data );
					}
					
					return( true );
				}
			}
			
			writeNotFound( os );
			
			return( true );
			
		}else if ( get.startsWith( "/setinfo?" )){

			String name 	= (String)lc_params.get( "name" );
			
			if ( name != null ){

				boolean	result = false;
				
				for (int i=0;i<listeners.size() && !result;i++){
					
						// no idea why we copy, but let's keep on doing so
					
					HashMap paramsCopy = new HashMap();
					
					paramsCopy.putAll( original_params );
					
					result = ((MagnetURIHandlerListener)listeners.get(i)).set( name, paramsCopy );
				}
				
				int	width 	= result?20:10;
				int height 	= result?20:10;
				
				String	img_type = (String)lc_params.get( "img_type" );

				if ( img_type != null && img_type.equals( "png" )){
					
					byte[]	data = PNG.getPNGBytesForSize( width, height );
					
					writeReply( os, "image/png", data );

				}else{
					
					ByteArrayOutputStream	baos = new ByteArrayOutputStream();
	
					writeImage( baos, width, height);
					
					byte[]	data = baos.toByteArray();
											
					writeReply( os, "image/bmp", data );
				}
				
				return( true );
			}
		}else if ( get.equals( "/browserheaders.js" )){

			String	headers_str = "";
			
			while( true ){
				
				String	header = is.readLine();
				
				if ( header == null ){
					
					break;
				}
				
				header = header.trim();
				
				if ( header.length() == 0 ){
					
					break;
				}
				
				headers_str += (headers_str.length()==0?"":"\n") + header;
			}
			
			String script = "var headers = \"" + new String( Base64.encode( headers_str.getBytes( "UTF-8" ))) + "\";";

			
			writeReply( os, "application/x-javascript", script );
		}
		
		return( true );
	}
	
	/**
	 * @param os
	 * @param width
	 * @param height
	 *
	 * @since 3.0.2.1
	 */
	private void writeImage(OutputStream os, int width, int height) {
		
			// DON'T CHANGE ANY OF THIS WITHOUT (AT LEAST) CHANGING THE JWS LAUNCHER CODE
			// AS IT MANUALLY DECODES THE BMP TO DETERMINE ITS SIZE!!!!
		
		int rowWidth = width / 8;
		if ((rowWidth % 4) != 0) {
			rowWidth = ((rowWidth / 4) + 1) * 4;
		}
		int imageSize = rowWidth * height;
		int fileSize = 54 + imageSize;
		try {
			os.write(new byte[] {
				'B',
				'M'
			});
			write4Bytes(os, fileSize);
			write4Bytes(os, 0);
			write4Bytes(os, 54); // data pos

			write4Bytes(os, 40); // header size
			write4Bytes(os, width);
			write4Bytes(os, height);
			write4Bytes(os, (1 << 16) + 1); // 1 plane and 1 bpp color
			write4Bytes(os, 0);
			write4Bytes(os, imageSize);
			write4Bytes(os, 0);
			write4Bytes(os, 0);
			write4Bytes(os, 0);
			write4Bytes(os, 0);

			byte[] data = new byte[imageSize];
			os.write(data);

		} catch (IOException e) {
			Debug.out(e);
		}
	}
	
	private void write4Bytes(OutputStream os, long l) {
		try {
			os.write((int) (l & 0xFF));
			os.write((int) ((l >> 8) & 0xFF));
			os.write((int) ((l >> 16) & 0xFF));
			os.write((int) ((l >> 24) & 0xFF));
		} catch (IOException e) {
			Debug.out(e);
		}
	}

	protected String
	getMessageText(
		String	resource )
	{
		return( MessageText.getString( "MagnetURLHandler.report." + resource ));
	}
	
	protected String
	getMessageText(
		String	resource,
		String	param )
	{
		return( MessageText.getString( "MagnetURLHandler.report." + resource, new String[]{ param } ));
	}
	
	protected String
	getJS(
		String	s )
	{
		return( "document.write(" + s + ");" + NL );
	}
	
	protected String
	getJSS(
		String	s )
	{
		return( "document.write(\"" + s + "\");" + NL );
	}
	
	protected void
	writeReply(
		OutputStream		os,
		String				content_type,
		String				content )
	
		throws IOException
	{
		writeReply( os, content_type, content.getBytes());
	}
	
	protected void
	writeReply(
		OutputStream		os,
		String				content_type,
		byte[]				content )
	
		throws IOException
	{
		PrintWriter	pw = new PrintWriter( new OutputStreamWriter( os ));

		pw.print( "HTTP/1.1 200 OK" + NL );
		pw.print( "Cache-Control: no-cache" + NL );
		pw.print( "Pragma: no-cache" + NL );
		pw.print( "Content-Type: " + content_type + NL );
		pw.print( "Content-Length: " + content.length + NL + NL );
		
		pw.flush();

		os.write( content );

	}
	
	protected void
	writeNotFound(
		OutputStream		os )
	
		throws IOException
	{
		PrintWriter	pw = new PrintWriter( new OutputStreamWriter( os ));

		pw.print( "HTTP/1.0 404 Not Found" + NL + NL );

		pw.flush();
	}
	
	public int
	getPort()
	{
		return( port );
	}
	
	public void
	addInfo(
		String		name,
		int			info )
	{
		info_map.put( name, new Integer(info));
		
		Logger.log(new LogEvent(LOGID, LogEvent.LT_INFORMATION,"MagnetURIHandler: global info registered: " + name + " -> " + info ));
	}
	
	public void
	addListener(
		MagnetURIHandlerListener	l )
	{
		listeners.add( l );
	}
	
	public void
	removeListener(
		MagnetURIHandlerListener	l )
	{
		listeners.remove( l );
	}
	
	public static void
	main(
		String[]	args )
	{
		new MagnetURIHandlerImpl();
		
		try{
			Thread.sleep(1000000);
		}catch( Throwable e ){
			
		}
	}
}
