/*
 * Created on 16-Dec-2005
 * Created by Paul Gardner
 * Copyright (C) Azureus Software, Inc, 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.
 *
 */

package com.aelitis.azureus.plugins.extseed.util;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.Proxy;
import java.net.Socket;
import java.net.URL;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLSession;

import org.gudy.azureus2.core3.security.SEPasswordListener;
import org.gudy.azureus2.core3.security.SESecurityManager;
import org.gudy.azureus2.core3.util.AENetworkClassifier;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.plugins.utils.resourcedownloader.ResourceDownloaderException;

import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin;
import com.aelitis.azureus.core.proxy.AEProxyFactory;
import com.aelitis.azureus.core.proxy.AEProxyFactory.PluginProxy;
import com.aelitis.azureus.plugins.extseed.ExternalSeedException;

public class 
ExternalSeedHTTPDownloaderRange 
	implements ExternalSeedHTTPDownloader, SEPasswordListener
{
	public static final String	NL = "\r\n";
	

	private final URL		very_original_url;
	private String			user_agent;
	
	private URL			redirected_url;
	private int			consec_redirect_fails;
	
	private int			last_response;
	private int			last_response_retry_after_secs;
    	
	public
	ExternalSeedHTTPDownloaderRange(
		URL		_url,
		String	_user_agent )
	{
		very_original_url	= _url;
		user_agent			= _user_agent;
	}
	
	public URL
	getURL()
	{
		return( very_original_url );
	}
	
	public void
	download(
		int									length,
		ExternalSeedHTTPDownloaderListener	listener,
		boolean								con_fail_is_perm_fail )
	
		throws ExternalSeedException
	{
		download( new String[0], new String[0], length, listener, con_fail_is_perm_fail );
	}
	
	public void
	downloadRange(
		long								offset,
		int									length,
		ExternalSeedHTTPDownloaderListener	listener,
		boolean								con_fail_is_perm_fail )
	
		throws ExternalSeedException
	{
		download( 	new String[]{ "Range" }, new String[]{ "bytes=" + offset + "-" + (offset+length-1)},
					length,
					listener,
					con_fail_is_perm_fail );
	}
	
	public void
	download(
		String[]							prop_names,
		String[]							prop_values,
		int									length,
		ExternalSeedHTTPDownloaderListener	listener,
		boolean								con_fail_is_perm_fail )
	
		throws ExternalSeedException
	{
		boolean	connected = false;
		
		InputStream	is	= null;
				
		String	outcome = "";
		
		PluginProxy		plugin_proxy	= null;
		
		boolean proxy_ok = false;
		
		try{
			SESecurityManager.setThreadPasswordHandler( this );
			
			if ( NetworkAdmin.getSingleton().hasMissingForcedBind()){
				
				throw( new ExternalSeedException( "Forced bind address is missing" ));
			}
			
			// System.out.println( "Connecting to " + url + ": " + Thread.currentThread().getId());

			HttpURLConnection	connection;
			int					response;
			
			Set<String>	redirect_urls = new HashSet<String>();
							
redirect_loop:
			while( true ){

				URL	original_url 	= redirected_url==null?very_original_url:redirected_url;
				URL current_url		= original_url;
				
				if ( plugin_proxy != null ){
					
					plugin_proxy.setOK( true );
					
					plugin_proxy = null;
				}
				
				Proxy	current_proxy = null;
				
				if ( AENetworkClassifier.categoriseAddress( original_url.getHost()) != AENetworkClassifier.AT_PUBLIC ){
					
					plugin_proxy = AEProxyFactory.getPluginProxy( "webseed", original_url );
					
					if ( plugin_proxy != null ){
						
						current_url		= plugin_proxy.getURL();
				
						current_proxy	= plugin_proxy.getProxy();
					}
				}
				
				for ( int ssl_loop=0; ssl_loop<2; ssl_loop++ ){
					
					try{
						if ( current_proxy == null ){
							
							connection = (HttpURLConnection)current_url.openConnection();
							
						}else{
							
							connection = (HttpURLConnection)current_url.openConnection( current_proxy );
						}
						
						if ( connection instanceof HttpsURLConnection ){
							
							HttpsURLConnection ssl_con = (HttpsURLConnection)connection;
							
								// allow for certs that contain IP addresses rather than dns names
		  	
							ssl_con.setHostnameVerifier(
								new HostnameVerifier()
								{
									public boolean
									verify(
										String		host,
										SSLSession	session )
									{
										return( true );
									}
								});
						}
						
						connection.setRequestProperty( "Connection", "Keep-Alive" );
						connection.setRequestProperty( "User-Agent", user_agent );
						
						for (int i=0;i<prop_names.length;i++){
							
							connection.setRequestProperty( prop_names[i], prop_values[i] );
						}
						
						if ( plugin_proxy != null ){
							
							connection.setRequestProperty( "HOST", plugin_proxy.getURLHostRewrite() + (original_url.getPort()==-1?"":(":" + original_url.getPort())));
						}

						int	time_remaining	= listener.getPermittedTime();
						
						if ( time_remaining > 0 ){
							
							connection.setConnectTimeout( time_remaining );
						}
									
						connection.connect();
					
						time_remaining	= listener.getPermittedTime();
											
						if ( time_remaining < 0 ){
							
							throw( new IOException( "Timeout during connect" ));
						}
						
						connection.setReadTimeout( time_remaining );
								
						connected	= true;
						
						response = connection.getResponseCode();
		
						if (	response == HttpURLConnection.HTTP_ACCEPTED || 
								response == HttpURLConnection.HTTP_OK ||
								response == HttpURLConnection.HTTP_PARTIAL ){
							
							if ( redirected_url != null ){
								
								consec_redirect_fails = 0;
							}
							
							break redirect_loop;
							
						}else if ( 	response == HttpURLConnection.HTTP_MOVED_TEMP ||
									response == HttpURLConnection.HTTP_MOVED_PERM ){
							
								// auto redirect doesn't work from http to https or vice-versa
							
							String	move_to = connection.getHeaderField( "location" );
							
							if ( move_to != null ){
								
								if ( redirect_urls.contains( move_to ) || redirect_urls.size() > 32 ){
									
									throw( new ExternalSeedException( "redirect loop" )); 
								}
								
								redirect_urls.add( move_to );
								
								redirected_url = new URL( move_to );
								
								continue redirect_loop;
							}
						}
						
						if ( redirected_url == null ){
							
							break redirect_loop;
						}
						
							// try again with original URL
						
						consec_redirect_fails++;
						
						redirected_url = null;
					
					}catch( SSLException e ){
						
						if ( ssl_loop == 0 ){
							
							if ( SESecurityManager.installServerCertificates( current_url ) != null ){
								
									// certificate has been installed
								
								continue;	// retry with new certificate
							}
						}
	
						throw( e );
					}
					
						// don't need another SSL loop
					
					break;
				}
			}
			
			if ( plugin_proxy == null ){
				
				URL final_url = connection.getURL();
				
				if ( consec_redirect_fails < 10 && !very_original_url.toExternalForm().equals( final_url.toExternalForm())){
					
					redirected_url = final_url;
				}
			}
			
			last_response	= response;
			
			last_response_retry_after_secs	= -1;
			
            if ( response == 503 ){
                           
                	// webseed support for temp unavail - read the retry_after
            	
            	long retry_after_date = connection.getHeaderFieldDate("Retry-After", -1L);
            	
                if ( retry_after_date <= -1 ){
                	
                	last_response_retry_after_secs = connection.getHeaderFieldInt("Retry-After", -1);
                    
                }else{
                	
                	last_response_retry_after_secs = (int)((retry_after_date - System.currentTimeMillis())/1000);
                	
                	if ( last_response_retry_after_secs < 0 ){
                		
                		last_response_retry_after_secs = -1;
                	}
                }
            }
            
			is = connection.getInputStream();
			
			proxy_ok = true;

			if ( 	response == HttpURLConnection.HTTP_ACCEPTED || 
					response == HttpURLConnection.HTTP_OK ||
					response == HttpURLConnection.HTTP_PARTIAL ){
								
				int	pos = 0;
				
				byte[]	buffer 		= null;
				int		buffer_pos	= 0;
				int		buffer_len	= 0;

				while( pos < length ){
					
					if ( buffer == null ){
						
						buffer 		= listener.getBuffer();						
						buffer_pos	= listener.getBufferPosition();
						buffer_len	= listener.getBufferLength();
					}

					listener.setBufferPosition( buffer_pos );
					
					int	to_read = buffer_len - buffer_pos;
					
					int	permitted = listener.getPermittedBytes();
					
					if ( permitted < to_read ){
						
						to_read	= permitted;
					}
					
					int	len = is.read( buffer, buffer_pos, to_read );
					
					if ( len < 0 ){
						
						break;
					}
					
					listener.reportBytesRead( len );
					
					pos	+= len;
					
					buffer_pos	+= len;
					
					if ( buffer_pos == buffer_len ){
						
						listener.done();
						
						buffer		= null;
						buffer_pos	= 0;
					}
				}
				
				if ( pos != length ){
					
					String	log_str;
					
					if ( buffer == null ){
						
						log_str = "No buffer assigned";
						
					}else{
						
						log_str =  new String( buffer, 0, length );
						
						if ( log_str.length() > 64 ){
							
							log_str = log_str.substring( 0, 64 );
						}
					}
					
					outcome = "Connection failed: data too short - " + length + "/" + pos + " [" + log_str + "]";
					
					throw( new ExternalSeedException( outcome ));
				}
				
				outcome = "read " + pos + " bytes";
				
				// System.out.println( "download length: " + pos );
				
			}else{
				
				outcome = "Connection failed: " + connection.getResponseMessage();
				
				ExternalSeedException	error = new ExternalSeedException( outcome );
				
				error.setPermanentFailure( true );
				
				throw( error );
			}
		}catch( IOException e ){
			
			if ( con_fail_is_perm_fail && !connected ){
				
				outcome = "Connection failed: " + e.getMessage();
				
				ExternalSeedException	error = new ExternalSeedException( outcome );
				
				error.setPermanentFailure( true );
				
				throw( error );

			}else{
				
				outcome =  "Connection failed: " + Debug.getNestedExceptionMessage( e );
                
                if ( last_response_retry_after_secs >= 0){
                	
                    outcome += ", Retry-After: " + last_response_retry_after_secs + " seconds";
                }
				                
				ExternalSeedException excep = new ExternalSeedException( outcome, e );
				
				if ( e instanceof FileNotFoundException ){
					
					excep.setPermanentFailure( true );
				}
				
				throw( excep );
			}
		}catch( Throwable e ){
			
			if ( e instanceof ExternalSeedException ){
				
				throw((ExternalSeedException)e);
			}
			
			outcome = "Connection failed: " + Debug.getNestedExceptionMessage( e );
			
			throw( new ExternalSeedException("Connection failed", e ));
			
		}finally{
			
			SESecurityManager.unsetThreadPasswordHandler();

			// System.out.println( "Done to " + url + ": " + Thread.currentThread().getId() + ", outcome=" + outcome );

			if ( is != null ){
				
				try{
					is.close();
					
				}catch( Throwable e ){
					
				}
			}
						
			if ( plugin_proxy != null ){
				
				plugin_proxy.setOK( proxy_ok );
			}
		}
	}
	
	public void
	downloadSocket(
		int									length,
		ExternalSeedHTTPDownloaderListener	listener,
		boolean								con_fail_is_perm_fail )
	        	
	    throws ExternalSeedException
	{
		downloadSocket( new String[0], new String[0], length, listener, con_fail_is_perm_fail );
	}
	
	public void
	downloadSocket(
		String[]							prop_names,
		String[]							prop_values,
		int									length,
		ExternalSeedHTTPDownloaderListener	listener,
		boolean								con_fail_is_perm_fail )
	
		throws ExternalSeedException
	{
		Socket	socket	= null;
		
		boolean	connected = false;
		
		PluginProxy		plugin_proxy 	= null;
		boolean			proxy_ok		= false;
		
		try{			
			String	output_header = 
				"GET " + very_original_url.getPath() + "?" + very_original_url.getQuery() + " HTTP/1.1" + NL +
				"Host: " + very_original_url.getHost() + (very_original_url.getPort()==-1?"":( ":" + very_original_url.getPort())) + NL +
				"Accept: */*" + NL +
				"Connection: Close" + NL +	// if we want to support keep-alive we'll need to implement a socket cache etc.
				"User-Agent: " + user_agent + NL;
		
			for (int i=0;i<prop_names.length;i++){
				
				output_header += prop_names[i] + ":" + prop_values[i] + NL;
			}
			
			output_header += NL;
			
			int	time_remaining	= listener.getPermittedTime();
			
			URL	original_url 	= very_original_url;
			URL current_url		= original_url;	
	
			Proxy	current_proxy = null;
			
			if ( AENetworkClassifier.categoriseAddress( very_original_url.getHost()) != AENetworkClassifier.AT_PUBLIC ){
				
				plugin_proxy = AEProxyFactory.getPluginProxy( "webseed", original_url );
				
				if ( plugin_proxy != null ){
					
					current_url		= plugin_proxy.getURL();
			
					current_proxy	= plugin_proxy.getProxy();
				}
			}
		
			if ( time_remaining > 0 ){
				
				if ( current_proxy == null ){
				
					socket = new Socket();
					
				}else{
					
					socket = new Socket( current_proxy );
				}
				
				socket.connect( new InetSocketAddress( current_url.getHost(), current_url.getPort()==-1?current_url.getDefaultPort():current_url.getPort()), time_remaining );
				
			}else{
		
				if ( current_proxy == null ){
				
					socket = new Socket(  current_url.getHost(), current_url.getPort()==-1?current_url.getDefaultPort():current_url.getPort());
					
				}else{
					
					socket = new Socket( current_proxy );

					socket.connect( new InetSocketAddress( current_url.getHost(), current_url.getPort()==-1?current_url.getDefaultPort():current_url.getPort()));
				}
			}
			
			connected	= true;
			
			proxy_ok = true;
			
			time_remaining	= listener.getPermittedTime();

			if ( time_remaining < 0 ){
					
				throw( new IOException( "Timeout during connect" ));
				
			}else if ( time_remaining > 0 ){
				
				socket.setSoTimeout( time_remaining );
			}
			
			OutputStream	os = socket.getOutputStream();
			
			os.write( output_header.getBytes( "ISO-8859-1" ));
			
			os.flush();
			
			InputStream is = socket.getInputStream();
			
			try{
				String	input_header = "";
				
				while( true ){
					
					byte[]	buffer = new byte[1];
					
					int	len = is.read( buffer );
					
					if ( len < 0 ){
						
						throw( new IOException( "input too short reading header" ));
					}
					
					input_header	+= (char)buffer[0];
					
					if ( input_header.endsWith(NL+NL)){
					
						break;
					}
				}
								
				// HTTP/1.1 403 Forbidden
				
				int	line_end = input_header.indexOf(NL);
				
				if ( line_end == -1 ){
					
					throw( new IOException( "header too short" ));
				}
				
				String	first_line = input_header.substring(0,line_end);
				
				StringTokenizer	tok = new StringTokenizer(first_line, " " );
				
				tok.nextToken();
				
				int	response = Integer.parseInt( tok.nextToken());
				
				last_response	= response;
				
				last_response_retry_after_secs	= -1;
				
				String	response_str	= tok.nextToken();				
				
				if ( 	response == HttpURLConnection.HTTP_ACCEPTED || 
						response == HttpURLConnection.HTTP_OK ||
						response == HttpURLConnection.HTTP_PARTIAL ){
					
					byte[]	buffer 		= null;
					int		buffer_pos	= 0;
					int		buffer_len	= 0;
					
					int	pos = 0;
					
					while( pos < length ){
						
						if ( buffer == null ){
							
							buffer 		= listener.getBuffer();							
							buffer_pos	= listener.getBufferPosition();
							buffer_len	= listener.getBufferLength();
						}
						
						int	to_read = buffer_len - buffer_pos;
						
						int	permitted = listener.getPermittedBytes();
						
						if ( permitted < to_read ){
							
							to_read	= permitted;
						}
						
						int	len = is.read( buffer, buffer_pos, to_read );
						
						if ( len < 0 ){
							
							break;
						}
						
						listener.reportBytesRead( len );
						
						pos	+= len;
						
						buffer_pos	+= len;
						
						if ( buffer_pos == buffer_len ){
							
							listener.done();
							
							buffer		= null;
							buffer_pos	= 0;
						}
					}
					
					if ( pos != length ){
						
						String	log_str;
						
						if ( buffer == null ){
							
							log_str = "No buffer assigned";
							
						}else{
							
							log_str =  new String( buffer, 0, buffer_pos>64?64:buffer_pos );
						}
						
						throw( new ExternalSeedException("Connection failed: data too short - " + length + "/" + pos + " [last=" + log_str + "]" ));
					}
					
					// System.out.println( "download length: " + pos );
										
				}else if ( 	response == 503 ){
					
						// webseed support for temp unavail - read the data
					
					String	data_str = "";
					
					while( true ){
						
						byte[]	buffer = new byte[1];
						
						int	len = is.read( buffer );
						
						if ( len < 0 ){
							
							break;
						}
						
						data_str += (char)buffer[0];
					}
					
					last_response_retry_after_secs = Integer.parseInt( data_str );
				
						// this gets trapped below and turned into an appropriate ExternalSeedException
					
					throw( new IOException( "Server overloaded" ));
					
				}else{
					
					ExternalSeedException	error = new ExternalSeedException("Connection failed: " + response_str );
					
					error.setPermanentFailure( true );
					
					throw( error );
				}
			}finally{
				
				is.close();
			}
			
		}catch( IOException e ){
			
			if ( con_fail_is_perm_fail && !connected ){
				
				ExternalSeedException	error = new ExternalSeedException("Connection failed: " + e.getMessage());
				
				error.setPermanentFailure( true );
				
				throw( error );

			}else{
				
				String outcome =  "Connection failed: " + Debug.getNestedExceptionMessage( e );

				if ( last_response_retry_after_secs >= 0 ){
	                	
					outcome += ", Retry-After: " + last_response_retry_after_secs + " seconds";
	            }

				throw( new ExternalSeedException( outcome, e ));
			}
		}catch( Throwable e ){
						
			if ( e instanceof ExternalSeedException ){
				
				throw((ExternalSeedException)e);
			}
			
			throw( new ExternalSeedException("Connection failed", e ));
			
		}finally{
			
			if ( socket != null ){
				
				try{
					socket.close();
					
				}catch( Throwable e ){
				}
			}
			
			if ( plugin_proxy != null ){
				
				plugin_proxy.setOK( proxy_ok );
			}
		}
	}
	
	public void
	deactivate()
	{	
	}
	
	public PasswordAuthentication
	getAuthentication(
		String		realm,
		URL			tracker )
	{
		return( null );
	}
	
	public void
	setAuthenticationOutcome(
		String		realm,
		URL			tracker,
		boolean		success )
	{
	}
	
	public void
	clearPasswords()
	{
	}
	
	public int
	getLastResponse()
	{
		return( last_response );
	}
	
	public int
	getLast503RetrySecs()
	{
		return( last_response_retry_after_secs );
	}
	
	public static void
	main(
		String[]		args )
	{
		try{
			String	url_str = "";
			
			ExternalSeedHTTPDownloader downloader = 
		
				new ExternalSeedHTTPDownloaderRange(
						new URL( url_str ),
						"Azureus" );
				
			downloader.downloadRange( 
				0, 1,
				new ExternalSeedHTTPDownloaderListener()
				{
					private int	position;
					
					public byte[]
		        	getBuffer()
		        	
		        		throws ExternalSeedException
		        	{
						return( new byte[1024] );
		        	}
		        	
		        	public void
		        	setBufferPosition(
		        		int	_position )
		        	{
		        		position = _position;
		        	}
		        	
		        	public int
		        	getBufferPosition()
		        	{
		        		return( position );
		        	}
		        	
		        	public int
		        	getBufferLength()
		        	{
		        		return( 1024 );
		        	}
		        	
		        	public int
		        	getPermittedBytes()
		        	
		        		throws ExternalSeedException
		        	{
		        		return( 1024 );
		        	}
		        	
		        	public int
		           	getPermittedTime()
		        	{
		        		return( Integer.MAX_VALUE );
		        	}
		        	
		        	public void
		        	reportBytesRead(
		        		int		num )
		        	{
		        		System.out.println( "read " + num );
		        	}
		        	
		        	public boolean 
		        	isCancelled() 
		        	{
		        		return false;
		        	}
		        	
		        	public void
		        	done()
		        	{
		        		System.out.println( "done" );
		        	}
				},
				true );
			
		}catch( Throwable e ){
			
			e.printStackTrace();
		}
	}
}
