/*
 * Created on Jan 29, 2007
 * Created by Paul Gardner
 * Copyright (C) 2007 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.plugins.tracker.peerauth;

import java.io.BufferedInputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.*;
import java.util.*;


import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.BDecoder;
import org.gudy.azureus2.core3.util.BEncoder;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.ThreadPool;
import org.gudy.azureus2.plugins.Plugin;
import org.gudy.azureus2.plugins.PluginInterface;
import org.gudy.azureus2.plugins.download.Download;
import org.gudy.azureus2.plugins.download.DownloadAnnounceResult;
import org.gudy.azureus2.plugins.download.DownloadAnnounceResultPeer;
import org.gudy.azureus2.plugins.download.DownloadManagerListener;
import org.gudy.azureus2.plugins.download.DownloadPeerListener;
import org.gudy.azureus2.plugins.download.DownloadScrapeResult;
import org.gudy.azureus2.plugins.download.DownloadTrackerListener;
import org.gudy.azureus2.plugins.logging.LoggerChannel;
import org.gudy.azureus2.plugins.logging.LoggerChannelListener;
import org.gudy.azureus2.plugins.peers.Peer;
import org.gudy.azureus2.plugins.peers.PeerManager;
import org.gudy.azureus2.plugins.peers.PeerManagerListener;
import org.gudy.azureus2.plugins.torrent.Torrent;
import org.gudy.azureus2.plugins.torrent.TorrentAttribute;
import org.gudy.azureus2.plugins.ui.UIManager;
import org.gudy.azureus2.plugins.ui.config.ConfigSection;
import org.gudy.azureus2.plugins.ui.model.BasicPluginConfigModel;
import org.gudy.azureus2.plugins.ui.model.BasicPluginViewModel;

import com.aelitis.azureus.core.util.bloom.BloomFilter;
import com.aelitis.azureus.core.util.bloom.BloomFilterFactory;

public class 
TrackerPeerAuthPlugin 
	implements Plugin, DownloadManagerListener
{
	private static final String	PLUGIN_NAME	= "Tracker Peer Auth";
	private static final String PLUGIN_CONFIGSECTION_ID = "Plugin.trackerpeerauth.name";

	private static final int DEFAULT_CHECK_PERIOD	= 30*1000;
	
	private static final String	STATE_ENABLED	= "enabled";
	private static final String	STATE_DISABLED	= "disabled";
	
	private static final int	TIMER_PERIOD	= 10*1000;
	
	private PluginInterface		plugin_interface;
	
	private TorrentAttribute 	ta_state ;
	private LoggerChannel 		log;
		
	private Map					dt_map	= new HashMap();
	
	private ThreadPool			thread_pool = new ThreadPool("TrackerPeerAuthPlugin",8, true );
	
	public void
	initialize(
		PluginInterface 	_plugin_interface )
	{
		plugin_interface = _plugin_interface;
		
		plugin_interface.getPluginProperties().setProperty( "plugin.version", 	"1.0" );
		plugin_interface.getPluginProperties().setProperty( "plugin.name", 		PLUGIN_NAME );

		ta_state 	= plugin_interface.getTorrentManager().getPluginAttribute( "state" );
		
		log = plugin_interface.getLogger().getTimeStampedChannel(PLUGIN_NAME);
		
		UIManager	ui_manager = plugin_interface.getUIManager();
		
		BasicPluginConfigModel	config = ui_manager.createBasicPluginConfigModel( ConfigSection.SECTION_PLUGINS, PLUGIN_CONFIGSECTION_ID );

		config.addLabelParameter2( "Plugin.trackerpeerauth.info" );		
		
		final BasicPluginViewModel	view_model = 
			plugin_interface.getUIManager().createBasicPluginViewModel( "Plugin.trackerpeerauth.name" );

		view_model.setConfigSectionID(PLUGIN_CONFIGSECTION_ID);
		view_model.getActivity().setVisible( false );
		view_model.getProgress().setVisible( false );
		
		log.addListener(
				new LoggerChannelListener()
				{
					public void
					messageLogged(
						int		type,
						String	content )
					{
						view_model.getLogArea().appendText( content + "\n" );
					}
					
					public void
					messageLogged(
						String		str,
						Throwable	error )
					{
						if ( str.length() > 0 ){
							view_model.getLogArea().appendText( str + "\n" );
						}
						
						StringWriter sw = new StringWriter();
						
						PrintWriter	pw = new PrintWriter( sw );
						
						error.printStackTrace( pw );
						
						pw.flush();
						
						view_model.getLogArea().appendText( sw.toString() + "\n" );
					}
				});
		
		System.out.println( "**** tracker peer auth disabled ****" );
		
		/*

		plugin_interface.getDownloadManager().addListener( this );
		
		SimpleTimer.addPeriodicEvent(
			"TrackerPeerAuthPlugin:checker",
			TIMER_PERIOD,
			new TimerEventPerformer()
			{
				private long	tick_count = 0;
				
				public void
				perform(
					TimerEvent	event )
				{
					tick_count++;
					
					synchronized( dt_map ){
						
						Iterator	it = dt_map.values().iterator();
						
						while( it.hasNext()){
							
							((DownloadTracker)it.next()).checkPeers( tick_count );
						}
					}
				}
			});
			*/
	}
	
	public void
	downloadAdded(
		final Download	download )
	{
		Torrent	torrent = download.getTorrent();
		
		if ( torrent != null && torrent.isPrivate()){
			
			download.addTrackerListener( 
				new DownloadTrackerListener()
				{
					public void 
					scrapeResult(
						DownloadScrapeResult result )
					{
						
					}

					public void 
					announceResult(
						DownloadAnnounceResult result )
					{
						if ( result.getResponseType() == DownloadAnnounceResult.RT_SUCCESS ){
							
							Map	ext = result.getExtensions();
							
							boolean	enabled = true;
							
							int		check_period	= DEFAULT_CHECK_PERIOD;
							
							if ( ext != null ){
								
								// TODO: detect enabled state
								
								// TODO: get check period
							}
							
							download.setAttribute( ta_state, enabled?STATE_ENABLED:STATE_DISABLED );

							setState( download, enabled, check_period );
						}
					}
				});
			
			String state = download.getAttribute( ta_state );
			

			if ( state != null ){
				
				boolean	enabled = state.equals( STATE_ENABLED );
				
				setState( download, enabled, DEFAULT_CHECK_PERIOD );
			}
		}
	}
	
	public void
	downloadRemoved(
		Download	download )
	{
		synchronized( dt_map ){
		
			dt_map.remove( download );
		}
	}
	
	protected void
	setState(
		Download	download,
		boolean		enabled,
		int			check_period )
	{
		synchronized( dt_map ){

			if ( enabled ){
						
				DownloadTracker existing = (DownloadTracker)dt_map.get( download );
			
				if ( existing == null ){
			
					DownloadTracker	dt = new DownloadTracker( download, check_period );
				
					dt_map.put( download, dt );
					
				}else{
					
					existing.setCheckPeriod( check_period );
				}
			}else{
							
				dt_map.remove( download );
			}
		}
	}
	
	protected void
	log(
		Download	download,
		String		str )
	{
		log.log( "Download '" + download.getName() + "' - " + str );
	}
	
	protected class
	DownloadTracker
		implements DownloadPeerListener, PeerManagerListener, DownloadTrackerListener
	{
		private static final int 	BACKOFF_TICK_COUNT	= 60*1000 / TIMER_PERIOD;	// TODO:
		
		private static final int 	MAX_PEERS_PER_QUERY	= 100;
		
		private static final int	OK_BLOOM_INITIAL	= 16*1024;
		private static final int	OK_BLOOM_INC		= 16*1024;
		private static final int	OK_BLOOM_MAX		= 128*1024;
		
		private static final int	BAD_BLOOM_INITIAL	= 4*1024;
		private static final int	BAD_BLOOM_INC		= 4*1024;
		private static final int	BAD_BLOOM_MAX		= 64*1024;
		
		private Download		download;
		
		private BloomFilter		ok_bloom 	= BloomFilterFactory.createAddOnly( OK_BLOOM_INITIAL );
		private BloomFilter		bad_bloom 	= BloomFilterFactory.createAddOnly( BAD_BLOOM_INITIAL );
			
		private long			pending_check_peer_count	= 0; 
		
		private boolean			check_running;
		private int				check_tick_count;
		private int				backoff_tick_count;
		
		protected
		DownloadTracker(
			Download	_download,
			int			_min_check_period )
		{
			download	= _download;
			
			download.addTrackerListener( this );
			
			download.addPeerListener( this );
			
			setCheckPeriod( _min_check_period );
			
			log( "enabled, check period=" + _min_check_period );
		}
		
		protected void
		setCheckPeriod(
			int			_min_check_period )
		{
			check_tick_count	= _min_check_period / TIMER_PERIOD;
		}
		
		protected void
		recordPeer(
			String		source,
			byte[]		id,
			String		ip,
			int			port,
			boolean		ok )
		{
			if ( id == null ){
				
				return;
			}
			
			byte[]	key = getKey( id, ip );
			
			synchronized( this ){
				
				if ( ok ){
					
					int	entries = ok_bloom.getEntryCount();
					
					if ( entries > 0 && ( ok_bloom.getSize() / entries < 10 )){
						
						int	new_size = ok_bloom.getSize() + OK_BLOOM_INC;
						
						if ( new_size > OK_BLOOM_MAX ){
							
							new_size = OK_BLOOM_MAX;
						}
						
						log( "Expanding ok bloom to " + new_size + " entries" );
						
						BloomFilter new_ok_bloom 	= BloomFilterFactory.createAddOnly( new_size );
	
						PeerManager pm = download.getPeerManager();
						
						if ( pm != null ){
							
							Peer[] peers = pm.getPeers();
							
							for (int i=0;i<peers.length;i++){
								
								byte[]	peer_key = getKey( peers[i] );
								
								if ( peer_key != null && ok_bloom.contains( peer_key )){
									
									new_ok_bloom.add( peer_key );
								}
							}
						}
						
						ok_bloom = new_ok_bloom;
						
							// need to drop the bad bloom here too. we rely on the ok bloom 
							// to filter false positives on the bad bloom 
						
						bad_bloom 	= BloomFilterFactory.createAddOnly( bad_bloom.getSize());
					}
					
					ok_bloom.add( key );
					
				}else{
					
					int	entries = bad_bloom.getEntryCount();
					
					if ( entries > 0 && ( bad_bloom.getSize() / entries < 10 )){
						
						int	new_size = bad_bloom.getSize() + BAD_BLOOM_INC;
						
						if ( new_size > BAD_BLOOM_MAX ){
							
							new_size = BAD_BLOOM_MAX;
						}
						
						log( "Expanding bad bloom to " + new_size + " entries" );
						
						bad_bloom 	= BloomFilterFactory.createAddOnly( new_size );
					}
					
					bad_bloom.add( key );
				}
			}
		}
		
		protected void
		checkPeers(
			long	tick_count )
		{
			if ( backoff_tick_count > 0 ){
				
				backoff_tick_count--;
				
				return;
			}
			
			if ( tick_count % check_tick_count == 0 ){
				
				synchronized( this ){
					
					if ( pending_check_peer_count > 0 && !check_running){
						
						pending_check_peer_count	= 0;
						
						check_running				= true;
						
					}else{
						
						return;
					}
				}
				
				boolean	gone_async = false;
				
				try{
					
					PeerManager pm = download.getPeerManager();
					
					if ( pm != null ){
						
						Peer[] peers = pm.getPeers();
						
						final List	to_check = new ArrayList();
						
						for (int i=0;i<peers.length;i++){
							
							Peer	peer = peers[i];
							
							byte[]	peer_key = getKey( peer );
							
							if ( peer_key != null ){
								
								if ( ok_bloom.contains( peer_key )){
									
								}else if ( bad_bloom.contains( peer_key )){
									
									removePeer( peer );
									
								}else{
									
									to_check.add( peer );
								}
							}
						}
						
						if ( to_check.size() > 0 ){
																
							thread_pool.run(
								new AERunnable()
								{
									public void
									runSupport()
									{
										try{
											
											check( to_check );
											
										}finally{
											
											synchronized( DownloadTracker.this ){
											
												check_running	= false;
											}
										}
									}
								});
							
							gone_async = true;
						}
					}
				}finally{
					
					if ( !gone_async ){
						
						synchronized( this ){

							check_running = false;
						}
					}
				}
			}
		}
		
		protected void
		check(
			List		peers )
		{
			DownloadAnnounceResult	an = download.getLastAnnounceResult();
			
			URL	target = an==null?null:an.getURL();
			
			if ( target == null ){
				
				target = download.getTorrent().getAnnounceURL();
			}
							
			OutputStreamWriter 		out	= null;
			BufferedInputStream		in	= null;
			
			try{
				String	url_str = target.toString();
				
				int	pos = url_str.indexOf( "announce" );
				
				if ( pos == -1 ){
				
						// TODO: this should be logged once and checked earlier
					
					log( "announce URL '" + url_str + "' is non-conformant" );
					
					return;
				}
				
				url_str = url_str.substring(0,pos) + "testauth" + url_str.substring( pos + 8 );
				
				target = new URL( url_str );
			
				Map	map = new HashMap();
				
				String	peer_str = "";
				
				for (int i=0;i<peers.size() && i < MAX_PEERS_PER_QUERY; i++ ){
					
					Peer	peer = (Peer)peers.get(i);
					
					List	peer_data = new ArrayList();
					
					peer_data.add( download.getTorrent().getHash());
					peer_data.add( peer.getId());
					peer_data.add( peer.getIp());
					
					map.put( "peer" + i, peer_data );
					
					peer_str += (i==0?"":",") + peer.getIp();
				}

				log( "Checking " + url_str + " : peers=" + peer_str );
				

				byte[]	encoded = BEncoder.encode( map, true );				

				HttpURLConnection connection = (HttpURLConnection)target.openConnection();
				
			    String data = "authpeers=" + new String(encoded, "ISO-8859-1" );
			    
			    System.out.println( "sending '" + data + "'" );
			    
			    connection.setDoOutput(true);
			    
			    connection.setRequestMethod("POST");
			    
			    connection.setRequestProperty("User-Agent", Constants.AZUREUS_NAME + " " + Constants.AZUREUS_VERSION);     
				  
			    connection.setRequestProperty( "Connection", "close" );

			    connection.addRequestProperty( "Accept-Encoding", "gzip" );

			    out = new OutputStreamWriter(connection.getOutputStream());
			    
			    out.write(data);
			    
			    out.flush();
			    
			    in = new BufferedInputStream(connection.getInputStream());

			    Map	result_map = BDecoder.decode( in );
			    
				for (int i=0;i<peers.size() && i < MAX_PEERS_PER_QUERY; i++ ){

					Peer	peer = (Peer)peers.get(i);

					Long	enabled = (Long)result_map.get( "peer" + i );
					
					if ( enabled == null ){
						
						log( "No response for peer '" + peer.getIp() + "'" );
						
					}else{
						
						boolean	ok = enabled.longValue() != 0;
						
						recordPeer( "auth check", peer.getId(), peer.getIp(), peer.getPort(), ok );
				
						if ( !ok ){
							
							removePeer( peer );
						}
					}
				}
				
			}catch( Throwable e ){
				
				backoff_tick_count = BACKOFF_TICK_COUNT;
				
				e.printStackTrace();
				
			}finally{
				
				if ( out != null ){
					
					try{
						out.close();
						
					}catch( Throwable e ){
						
					}
				}
				
				if ( in != null ){
					
					try{
						in.close();
						
					}catch( Throwable e ){
						
					}
				}
			}
		}
		
		protected byte[]
		getKey(
			Peer		peer )
		{
			byte[]	peer_id = peer.getId();
			
			if ( peer_id == null ){
							
				return( null );
			}
						
			return( getKey( peer_id, peer.getIp()));
		}
		
		protected byte[]
		getKey(
			byte[]		peer_id,
			String		ip_str )
		{
			byte[]	ip = ip_str.getBytes();
			
			byte[]	key = new byte[peer_id.length + ip.length ];
			
			System.arraycopy( peer_id, 0, key, 0, peer_id.length );
			System.arraycopy( ip, 0, key, peer_id.length, ip.length );
			
			return( key );
		}
		
		protected boolean
		knownToBeOK(
			Peer		peer )
		{
			byte[]	peer_id = peer.getId();
			
			if ( peer_id == null ){
			
					// only happens on outbound connectas we don't retain the peer-id pending
					// outbound connect
				
				return( true );
			}
						
			byte[]	key = getKey( peer_id, peer.getIp());
			
			return( ok_bloom.contains( key ));
		}
		
		protected boolean
		knownToBeBad(
			Peer		peer )
		{
			byte[]	peer_id = peer.getId();
			
			if ( peer_id == null ){
				
					// shouldn't get here as we should check for OK first
				
				return( true );
			}
						
			byte[]	key = getKey( peer_id, peer.getIp());
			
			return( bad_bloom.contains( key ));
		}
		
		protected void
		peerMightBeBad(
			Peer		peer )
		{
			if ( knownToBeOK( peer )){
				
			}else if ( knownToBeBad( peer )){
				
				removePeer( peer );
				
			}else{
				
				// we just leave it here and pick up for checking periodically
				
				pending_check_peer_count++;
			}
		}
		
		protected void
		removePeer(
			Peer		peer )
		{
			log( "Disconnecting peer " + peer.getIp() + "/" + peer.getPort() + ": not authorized" );
			
			peer.close( "Tracker peer authorization failure", false, false );
		}
		
		public void 
		scrapeResult(
			DownloadScrapeResult result )
		{
			
		}


		public void 
		announceResult(
			DownloadAnnounceResult result )
		{
			DownloadAnnounceResultPeer[] peers = result.getPeers();
			
			if ( peers != null ){
				
				for (int i=0;i<peers.length;i++){
					
					DownloadAnnounceResultPeer	peer = peers[i];
					
					recordPeer( "Tracker", peer.getPeerID(), peer.getAddress(), peer.getPort(), true );
				}
			}
		}
		
		public void
		peerAdded(
			PeerManager	manager,
			Peer		peer )
		{
				// assume all outgoing connections are valid as we got them from the tracker
			
			if ( peer.isIncoming()){
				
				peerMightBeBad( peer );

			}else{
				
				recordPeer( "Outgoing", peer.getId(), peer.getIp(), peer.getPort(), true );
			}
		}
		
		public void
		peerRemoved(
			PeerManager	manager,
			Peer		peer )
		{
		}
		
		public void
		peerManagerAdded(
			Download		download,
			PeerManager		peer_manager )
		{
			peer_manager.addListener( this );
		}
		
		public void
		peerManagerRemoved(
			Download		download,
			PeerManager		peer_manager )
		{
			peer_manager.removeListener( this );
		}
		
		protected void
		log(
			String	str )
		{
			TrackerPeerAuthPlugin.this.log( download, str );
		}
	}
}
