/*
 * Created on Feb 11, 2009
 * 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.core.download;

import java.io.*;
import java.net.HttpURLConnection;
import java.net.PasswordAuthentication;
import java.net.URL;
import java.util.*;

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.AETemporaryFileHandler;
import org.gudy.azureus2.core3.util.Base32;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.FileUtil;
import org.gudy.azureus2.core3.util.SHA1Simple;
import org.gudy.azureus2.core3.util.UrlUtils;
import org.gudy.azureus2.plugins.disk.DiskManagerChannel;
import org.gudy.azureus2.plugins.disk.DiskManagerEvent;
import org.gudy.azureus2.plugins.disk.DiskManagerFileInfo;
import org.gudy.azureus2.plugins.disk.DiskManagerListener;
import org.gudy.azureus2.plugins.disk.DiskManagerRandomReadRequest;
import org.gudy.azureus2.plugins.disk.DiskManagerRequest;
import org.gudy.azureus2.plugins.download.Download;
import org.gudy.azureus2.plugins.download.DownloadException;
import org.gudy.azureus2.plugins.utils.PooledByteBuffer;
import org.gudy.azureus2.pluginsimpl.local.utils.PooledByteBufferImpl;

import com.aelitis.azureus.core.util.CopyOnWriteList;
import com.aelitis.azureus.plugins.extseed.ExternalSeedException;

public class 
DiskManagerFileInfoURL
	implements DiskManagerFileInfo, SEPasswordListener
{
	private URL					url;
	private byte[]				hash;
	
	private File				file;
	
	private Object	lock = new Object();
	
	private URL			redirected_url;
	private int			consec_redirect_fails;

	private volatile boolean		file_cached;
	
	public
	DiskManagerFileInfoURL(
		URL					_url )
	{
		url	= _url;
		
		String url_str = url.toExternalForm();
		
		String id_key = "azcdid=";
		String dn_key = "azcddn=";
		
		int id_pos = url_str.indexOf( id_key );
		int dn_pos = url_str.indexOf( dn_key );

		int	min_pos = id_pos;
		if ( min_pos == -1 ){
			min_pos = dn_pos;
		}else{
			if ( dn_pos != -1 ){
				min_pos = Math.min( min_pos, dn_pos );
			}
		}
		
		if ( min_pos > 0 ){
			
			try{				
				url = new URL( url_str.substring( 0, min_pos-1 ) );
				
			}catch( Throwable e ){
				
				Debug.out( e );
			}
		}
		
		try{
			hash		= new SHA1Simple().calculateHash( ( "DiskManagerFileInfoURL" +  url.toExternalForm()).getBytes( "UTF-8" ));
			
		}catch( Throwable e ){
			
			Debug.out(e);
		}
		
		String file_name;
				
		if ( dn_pos != -1 ){
			
			String dn = url_str.substring( dn_pos + dn_key.length());
			
			dn_pos = dn.indexOf( '&' );
			
			if ( dn_pos != -1 ){
				
				dn = dn.substring( 0, dn_pos );
			}
		
			file_name = UrlUtils.decode( dn );
			
		}else{
			
			String path = url.getPath();
			
			int pos = path.lastIndexOf( "/" );
			
			if ( pos != -1 ){
				
				path = path.substring( pos+1 );
			}
			
			path = path.trim();
			
			if ( url_str.length() > 0 ){
				
				file_name = UrlUtils.decode( path );
				
			}else{
				
				file_name = Base32.encode( hash );
			}
		}
		
		file_name = FileUtil.convertOSSpecificChars( file_name, false );
		
		try{
			file = new File( AETemporaryFileHandler.createTempDir(), file_name );
			
		}catch( Throwable e ){
		
			file_name += ".tmp";
			
			file = new File( AETemporaryFileHandler.getTempDirectory(), file_name );
		}
	}
	
	public URL
	getURL()
	{
		return( url );
	}
	
	public void
	download()
	{
		synchronized( lock ){
			
			if ( file_cached ){
				
				return;
			}
			
			try{
				channel chan = createChannel();
				
				channel.request req = chan.createRequest();
				
				req.setAll();
				
				final FileOutputStream fos = new FileOutputStream( file );
				
				boolean	ok = false;
				
				try{
					req.addListener(
						new DiskManagerListener()
						{
							public void
							eventOccurred(
								DiskManagerEvent	event )
							{
								if ( event.getType() == DiskManagerEvent.EVENT_TYPE_FAILED ){
									
									throw( new RuntimeException( event.getFailure()));
								}
								
								PooledByteBuffer buffer = event.getBuffer();
								
								if ( buffer == null ){
									
									throw( new RuntimeException( "eh?" ));
								}
								
								try{
									
									fos.write( buffer.toByteArray());
									
								}catch( IOException e ){
									
									throw( new RuntimeException( "Failed to write to " + file, e ));
									
								}finally{
									
									buffer.returnToPool();
								}
							}
						});
				
					req.run();
					
					ok = true;
					
				}finally{
					
					try{
						fos.close();
						
					}catch( Throwable e ){
						
						Debug.out( e );
					}
					
					if ( !ok ){
						
						file.delete();
						
					}else{
						
						file_cached = true;
					}
				}
			}catch( Throwable e ){
				
				Debug.out( "Failed to cache file from " + url, e );
			}
		}
	}
	
	public void 
	setPriority(
		boolean b )
	{
	}
	
	public void 
	setSkipped(
		boolean b )
	{
		throw( new RuntimeException( "Not supported" ));
	}
	
	public int 
	getNumericPriority() 
	{
		return( 0 );
	}
	
	public int 
	getNumericPriorty() 
	{
		return( 0 );
	}
	
	public void 
	setNumericPriority(
		int priority) 
	{
		throw( new RuntimeException( "Not supported" ));
	}
	
	public void
	setDeleted(boolean b)
	{
	}	
	
	public void
	setLink(
		File	link_destination )
	{	
		throw( new RuntimeException( "Not supported" ));
	}
	
	public File
	getLink()
	{
		return( null );
	}
		 	
	public int 
	getAccessMode()
	{
		return( READ );
	}
	
	public long 
	getDownloaded()
	{
		return( getLength());
	}
	
	public long
	getLength()
	{
		if ( file_cached ){
			
			long len = file.length();
			
			if ( len > 0 ){
				
				return( len );
			}
		}
		
		return( -1 );
	}
	
	public File 
	getFile()
	{
		return( file );
	}
		
	public File
	getFile(
		boolean	follow_link )
	{
		return( file );
	}
	
	public int
	getIndex()
	{
		return( 0 );
	}
	
	public int 
	getFirstPieceNumber()
	{
		return( 0 );
	}
	
	public long
	getPieceSize()
	{
		return( 32*1024 );
	}
	
	public int 
	getNumPieces()
	{
		return( -1 );
	}
		
	public boolean 
	isPriority()
	{
		return( false );
	}
	
	public boolean 
	isSkipped()
	{
		return( false );
	}
	
	public boolean
	isDeleted()
	{
		return( false );
	}
	
	public byte[] 
	getDownloadHash() 
	
		throws DownloadException 
	{
		return( hash );
	}
	
	public Download 
	getDownload()
	
         throws DownloadException
    {
		throw( new DownloadException( "Not supported" ));
    }
	
	public channel
	createChannel()
	
		throws DownloadException
	{
		return( new channel());
	}
	
	public DiskManagerRandomReadRequest
	createRandomReadRequest(
		long						file_offset,
		long						length,
		boolean						reverse_order,
		DiskManagerListener			listener )
	
		throws DownloadException
	{
		throw( new DownloadException( "Not supported" ));
	}
	
	public PasswordAuthentication
	getAuthentication(
		String		realm,
		URL			tracker )
	{
		return( null );
	}
	
	public void
	setAuthenticationOutcome(
		String		realm,
		URL			tracker,
		boolean		success )
	{
	}
	
	public void
	clearPasswords()
	{
	}

	protected class
	channel
		implements DiskManagerChannel
	{
		private volatile boolean	channel_destroyed;
		private volatile long		channel_position;
		
		public request
		createRequest()
		{
			return( new request());
		}
		
		public DiskManagerFileInfo
		getFile()
		{
			return( DiskManagerFileInfoURL.this );
		}
		
		public long 
		getPosition() 
		{
			return( channel_position );
		}
		
		public boolean 
		isDestroyed() 
		{
			return( channel_destroyed );
		}
		
		public void
		destroy()
		{
			channel_destroyed	= true;
		}
		
		protected class
		request
			implements DiskManagerRequest
		{
			private long		offset;
			private long		length;
			private boolean		do_all_file;
			
			private long		position;
			
			private int			max_read_chunk = 128*1024;;
			
			private volatile boolean	request_cancelled;
			
			private CopyOnWriteList<DiskManagerListener>		listeners = new CopyOnWriteList<DiskManagerListener>();
			
			public void
			setType(
				int			type )
			{
				if ( type != DiskManagerRequest.REQUEST_READ ){
					
					throw( new RuntimeException( "Not supported" ));
				}
			}
			
			public void
			setAll()
			{
				do_all_file = true;
				offset		= 0;
				setLength( -1 );
			}
			
			public void
			setOffset(
				long		_offset )
			{
				offset		= _offset;
			}
			
			public void
			setLength(
				long		_length )
			{
					// length can be -1 here meaning 'to the end'
				
				length		= _length==-1?Long.MAX_VALUE:_length;
			}
				
			public void
			setMaximumReadChunkSize(
				int 	size )
			{
				if ( size > 16*1024 ){
				
					max_read_chunk = size;
				}
			}
			
			public long
			getAvailableBytes()
			{
				return( getRemaining());
			}
						
			public long
			getRemaining()
			{
				return( length==Long.MAX_VALUE?length:(offset + length - position ));
			}
			
			public void
			run()
			{
				try{				
					byte[] buffer = new byte[max_read_chunk];
					
					long	rem		= length;
					long	pos 	= offset;
						
					InputStream	is = null;
					
					try{
						SESecurityManager.setThreadPasswordHandler( DiskManagerFileInfoURL.this );
						
						// 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	target = redirected_url==null?url:redirected_url;
								
								for ( int ssl_loop=0; ssl_loop<2; ssl_loop++ ){
									
									try{
										connection = (HttpURLConnection)target.openConnection();
										
										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" );
									
									if ( !do_all_file ){
									
										connection.setRequestProperty( "Range", "bytes=" + offset + "-" + (offset+length-1));
									}
									
									connection.setConnectTimeout( 20*1000 );
																
									connection.connect();
									
									connection.setReadTimeout( 10*1000 );
																					
									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( target ) != null ){
											
												// certificate has been installed
											
											continue;	// retry with new certificate
										}
									}
				
									throw( e );
								}
								
									// don't need another SSL loop
								
								break;
							}
						}
						
						URL final_url = connection.getURL();
						
						if ( consec_redirect_fails < 10 && !url.toExternalForm().equals( final_url.toExternalForm())){
							
							redirected_url = final_url;
						}
			            
						is = connection.getInputStream();
						
						while( rem > 0 ){

							if ( request_cancelled ){
								
								throw( new Exception( "Cancelled" ));
								
							}else if ( channel_destroyed ){
								
								throw( new Exception( "Destroyed" ));
							}
							
							int	len = is.read( buffer );
							
							if ( len == -1 ){
								
								if ( length == Long.MAX_VALUE ){
									
									break;
									
								}else{
									
									throw( new Exception( "Premature end of stream (complete)" ));
								}
							}else if ( len == 0 ){
								
								sendEvent( new event( pos ));
								
							}else{
																
								sendEvent( new event( new PooledByteBufferImpl( buffer, 0, len ), pos, len ));
								
								rem -= len;
								pos	+= len;
							}
						}
					}finally{
						
						SESecurityManager.unsetThreadPasswordHandler();

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

						if ( is != null ){
							
							try{
								is.close();
								
							}catch( Throwable e ){
								
							}
						}
					}							
				}catch( Throwable e ){
					
					sendEvent( new event( e ));
				}
			}
			
			public void
			cancel()
			{
				request_cancelled = true;
			}
			
			public void
			setUserAgent(
				String		agent )
			{	
			}
			
			protected void
			sendEvent(
				event		ev )
			{					
				for ( DiskManagerListener l: listeners ){
					
					l.eventOccurred( ev );
				}
			}
			
			public void
			addListener(
				DiskManagerListener	listener )
			{
				listeners.add( listener );
			}
			
			public void
			removeListener(
				DiskManagerListener	listener )
			{
				listeners.remove( listener );
			}
		
			protected class
			event
				implements DiskManagerEvent
			{
				private int					event_type;
				private Throwable			error;
				private PooledByteBuffer	buffer;
				private long				event_offset;
				private int					event_length;
				
				protected
				event(
					Throwable		_error )
				{
					event_type	= DiskManagerEvent.EVENT_TYPE_FAILED;
					error		= _error;
				}
					
				protected 
				event(
					long				_offset )
				{
					event_type		= DiskManagerEvent.EVENT_TYPE_BLOCKED;

					event_offset	= _offset;	
					
					channel_position	= _offset;
				}
				
				protected
				event(
					PooledByteBuffer	_buffer,
					long				_offset,
					int					_length )
				{
					event_type		= DiskManagerEvent.EVENT_TYPE_SUCCESS;
					buffer			= _buffer;
					event_offset	= _offset;
					event_length	= _length;
					
					channel_position	= _offset + _length - 1;
				}
				
				public int
				getType()
				{
					return( event_type );
				}
				
				public DiskManagerRequest
				getRequest()
				{
					return( request.this );
				}
				
				public long
				getOffset()
				{
					return( event_offset );
				}
				
				public int
				getLength()
				{
					return( event_length );
				}
				
				public PooledByteBuffer
				getBuffer()
				{
					return( buffer );
				}
				
				public Throwable
				getFailure()
				{
					return( error );
				}
			}
		}
	}
}
