/*
 * Created on 14-Feb-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 org.gudy.azureus2.core3.tracker.client.impl;

import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.logging.*;
import org.gudy.azureus2.core3.peer.PEPeerSource;
import org.gudy.azureus2.core3.torrent.TOTorrent;
import org.gudy.azureus2.core3.torrent.TOTorrentException;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncer;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerException;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerListener;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerResponse;
import org.gudy.azureus2.core3.tracker.client.TRTrackerAnnouncerResponsePeer;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.LightHashMap;
import org.gudy.azureus2.core3.util.ListenerManager;
import org.gudy.azureus2.core3.util.ListenerManagerDispatcher;
import org.gudy.azureus2.plugins.clientid.ClientIDException;
import org.gudy.azureus2.plugins.download.DownloadAnnounceResultPeer;
import org.gudy.azureus2.pluginsimpl.local.clientid.ClientIDManagerImpl;

import com.aelitis.azureus.core.tracker.TrackerPeerSource;
import com.aelitis.azureus.core.tracker.TrackerPeerSourceAdapter;

/**
 * @author parg
 *
 */

public abstract class 
TRTrackerAnnouncerImpl
	implements TRTrackerAnnouncer
{
  // Used to be componentID 2
	public final static LogIDs LOGID = LogIDs.TRACKER;

	// 	listener
	
	protected static final int LDT_TRACKER_RESPONSE		= 1;
	protected static final int LDT_URL_CHANGED			= 2;
	protected static final int LDT_URL_REFRESH			= 3;
	
	private static final String chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";

	private static final int	   	key_id_length	= 8;

	private static String
	createKeyID()
	{
		String	key_id = "";
		
		for (int i = 0; i < key_id_length; i++) {
			int pos = (int) ( Math.random() * chars.length());
		    key_id +=  chars.charAt(pos);
		}
		
		return( key_id );
	}
	
	protected ListenerManager<TRTrackerAnnouncerListener>	listeners 	= ListenerManager.createManager(
			"TrackerClient:ListenDispatcher",
			new ListenerManagerDispatcher<TRTrackerAnnouncerListener>()
			{
				public void
				dispatch(
					TRTrackerAnnouncerListener		listener,
					int								type,
					Object							value )
				{
					if ( type == LDT_TRACKER_RESPONSE ){
						
						listener.receivedTrackerResponse((TRTrackerAnnouncerResponse)value);
						
					}else if ( type == LDT_URL_CHANGED ){
						
						Object[]	x = (Object[])value;
						
						URL			old_url 	= (URL)x[0];
						URL			new_url 	= (URL)x[1];
						boolean		explicit	= ((Boolean)x[2]).booleanValue();
						
						listener.urlChanged( TRTrackerAnnouncerImpl.this, old_url, new_url, explicit );
						
					}else{
						
						listener.urlRefresh();
					}
				}
			});

	private Map	tracker_peer_cache		= new LinkedHashMap();	// insertion order - most recent at end
	private AEMonitor tracker_peer_cache_mon 	= new AEMonitor( "TRTrackerClientClassic:PC" );
	private int	cache_peers_used;
	
	final private TOTorrent						torrent;
	final private byte[]						peer_id;
	final private String						tracker_key;
	final private int							udp_key;
		
	
	protected
	TRTrackerAnnouncerImpl(
		TOTorrent	_torrent )
	
		throws TRTrackerAnnouncerException
	{
		torrent	= _torrent;
		
		tracker_key	= createKeyID();
	    
		udp_key	= (int)(Math.random() *  0xFFFFFFFFL );
		
		try{
			peer_id		= ClientIDManagerImpl.getSingleton().generatePeerID( torrent, false );
		
		}catch( ClientIDException e ){

			 throw( new TRTrackerAnnouncerException( "TRTrackerAnnouncer: Peer ID generation fails", e ));
		}
	}

	public TOTorrent
	getTorrent()
	{
		return( torrent );
	}
	
	public Helper
	getHelper()
	{
		return(
			new Helper()
			{
				public byte[]
				getPeerID()
				{
					return( peer_id );
				}
				
				public String
				getTrackerKey()
				{
					return( tracker_key );
				}
				
				public int
				getUDPKey()
				{
					return( udp_key );
				}
				
				public void
				addToTrackerCache(
					TRTrackerAnnouncerResponsePeerImpl[]		peers )
				{
					TRTrackerAnnouncerImpl.this.addToTrackerCache( peers );
				}

				public TRTrackerAnnouncerResponsePeer[]
		      	getPeersFromCache(
		      		int			num_want )
				{
					return( TRTrackerAnnouncerImpl.this.getPeersFromCache(num_want));
				}
				
				public void
				setTrackerResponseCache(
					Map 		map	)
				{
					TRTrackerAnnouncerImpl.this.setTrackerResponseCache( map );
				}
					
				public void 
				removeFromTrackerResponseCache(
					String ip, int tcpPort )
				{
					TRTrackerAnnouncerImpl.this.removeFromTrackerResponseCache( ip,tcpPort );
				}
					
				public Map 
				getTrackerResponseCache()
				{
					return( TRTrackerAnnouncerImpl.this.getTrackerResponseCache());
				}
					
				public void
				informResponse(
					TRTrackerAnnouncerHelper		helper,
					TRTrackerAnnouncerResponse		response )
				{
					TRTrackerAnnouncerImpl.this.informResponse( helper, response );
				}
				
				public void
				informURLChange(
					URL		old_url,
					URL		new_url,
					boolean	explicit )
				{
					listeners.dispatch(	LDT_URL_CHANGED,
							new Object[]{old_url, new_url, new Boolean(explicit)});
				}
				
				public void
				informURLRefresh()
				{
					TRTrackerAnnouncerImpl.this.informURLRefresh();
				}
				
			 	public void
				addListener(
					TRTrackerAnnouncerListener	l )
			 	{
			 		TRTrackerAnnouncerImpl.this.addListener( l );
			 	}
					
				public void
				removeListener(
					TRTrackerAnnouncerListener	l )
				{
					TRTrackerAnnouncerImpl.this.removeListener( l );
				}
			});
	}
	
	public byte[]
	getPeerId()
	{
		return( peer_id );
	}
	
 	public static byte[]
	getAnonymousPeerId(
		String	my_ip,
		int		my_port )
	{
  		byte[] anon_peer_id = new byte[20];
	
  		// unique initial two bytes to identify this as fake

  		anon_peer_id[0] = (byte)'[';
  		anon_peer_id[1] = (byte)']';

  		try{
	  		byte[]	ip_bytes 	= my_ip.getBytes( Constants.DEFAULT_ENCODING );
	  		int		ip_len		= ip_bytes.length;
	
	  		if ( ip_len > 18 ){
		
	  			ip_len = 18;
	  		}
	
	  		System.arraycopy( ip_bytes, 0, anon_peer_id, 2, ip_len );
									
	  		int	port_copy = my_port;
		
	  		for (int j=2+ip_len;j<20;j++){
			
	  			anon_peer_id[j] = (byte)(port_copy&0xff);
			
	  			port_copy >>= 8;
	  		}
  		}catch( UnsupportedEncodingException e ){
  			
  			Debug.printStackTrace( e );
  		}
  		
  		return( anon_peer_id );
   }
					
		// NOTE: tracker_cache is cleared out in DownloadManager when opening a torrent for the
		// first time as a DOS prevention measure
	
	public Map
	getTrackerResponseCache()
	{				
		return( exportTrackerCache());
	}
	
	
	public void
	setTrackerResponseCache(
		Map		map )
	{
		int	num = importTrackerCache( map );
		
		if (Logger.isEnabled())
			Logger.log(new LogEvent(getTorrent(), LOGID, "TRTrackerClient: imported "
					+ num + " cached peers"));
	}
	
	protected Map
	exportTrackerCache()
	{
		Map	res = new LightHashMap(1);
		
		List	peers = new ArrayList();
		
		res.put( "tracker_peers", peers );
		
		try{
			tracker_peer_cache_mon.enter();
			
			Iterator it = tracker_peer_cache.values().iterator();
			
			while( it.hasNext()){
				
				TRTrackerAnnouncerResponsePeer	peer = (TRTrackerAnnouncerResponsePeer)it.next();		
	
				LightHashMap entry = new LightHashMap();
				
				entry.put( "ip", peer.getAddress().getBytes());
				entry.put( "src", peer.getSource().getBytes());
				entry.put( "port", new Long(peer.getPort()));
				
				int	udp_port = peer.getUDPPort();
				if ( udp_port != 0 ){
					entry.put( "udpport", new Long( udp_port));
				}
				int	http_port = peer.getHTTPPort();
				if ( http_port != 0 ){
					entry.put( "httpport", new Long( http_port));
				}
				
				entry.put( "prot", new Long(peer.getProtocol()));
				
				byte	az_ver = peer.getAZVersion();
				
				if ( az_ver != TRTrackerAnnouncer.AZ_TRACKER_VERSION_1 ){
					entry.put( "azver", new Long( az_ver ));
				}
				
				entry.compactify(0.9f);
				
				peers.add( entry );
			}
		
			if (Logger.isEnabled())
				Logger.log(new LogEvent(getTorrent(), LOGID,
						"TRTrackerClient: exported " + tracker_peer_cache.size()
								+ " cached peers"));
		}finally{
			
			tracker_peer_cache_mon.exit();
		}
		
		return( res );
	}
	
	protected int
	importTrackerCache(
		Map		map )
	{
		if ( !COConfigurationManager.getBooleanParameter("File.save.peers.enable")){
			
			return( 0 );
		}
		
		try{
			if ( map == null ){
				
				return( 0 );
			}
			
			List	peers = (List)map.get( "tracker_peers" );
	
			if ( peers == null ){
				
				return( 0 );
			}
			
			try{
				tracker_peer_cache_mon.enter();
				
				for (int i=0;i<peers.size();i++){
					
					Map	peer = (Map)peers.get(i);
					
					byte[]	src_bytes = (byte[])peer.get("src");
					String	peer_source = src_bytes==null?PEPeerSource.PS_BT_TRACKER:new String(src_bytes);
					String	peer_ip_address = new String((byte[])peer.get("ip"));
					int		peer_tcp_port	= ((Long)peer.get("port")).intValue();
					byte[]	peer_peer_id	= getAnonymousPeerId( peer_ip_address, peer_tcp_port );
					Long	l_protocol		= (Long)peer.get( "prot" );
					short	protocol		= l_protocol==null?DownloadAnnounceResultPeer.PROTOCOL_NORMAL:l_protocol.shortValue();
					Long	l_udp_port		= (Long)peer.get("udpport");
					int		peer_udp_port	= l_udp_port==null?0:l_udp_port.intValue();
					Long	l_http_port		= (Long)peer.get("httpport");
					int		peer_http_port	= l_http_port==null?0:l_http_port.intValue();
					Long	l_az_ver		= (Long)peer.get("azver");
					byte	az_ver			= l_az_ver==null?TRTrackerAnnouncer.AZ_TRACKER_VERSION_1:l_az_ver.byteValue();
				
					//System.out.println( "recovered " + ip_address + ":" + port );
	
					TRTrackerAnnouncerResponsePeerImpl	entry =
						new TRTrackerAnnouncerResponsePeerImpl(
							peer_source, 
							peer_peer_id, 
							peer_ip_address, 
							peer_tcp_port,
							peer_udp_port,
							peer_http_port,
							protocol,
							az_ver,
							(short)0 );
					
					tracker_peer_cache.put( entry.getKey(), entry );
				}
				
				return( tracker_peer_cache.size());
				
			}finally{
				
				tracker_peer_cache_mon.exit();
			}
		}catch( Throwable e ){
			
			Debug.printStackTrace( e );
			
			return( tracker_peer_cache.size());
		}
	}
	
	protected void
	addToTrackerCache(
		TRTrackerAnnouncerResponsePeerImpl[]		peers )
	{
		if ( !COConfigurationManager.getBooleanParameter("File.save.peers.enable")){
			
			return;
		}
		
		int	max = COConfigurationManager.getIntParameter( "File.save.peers.max", DEFAULT_PEERS_TO_CACHE );
		
		// System.out.println( "max peers= " + max );
		
		try{
			tracker_peer_cache_mon.enter();
			
			for (int i=0;i<peers.length;i++){
				
				TRTrackerAnnouncerResponsePeerImpl	peer = peers[i];
				
					// remove and reinsert to maintain most recent last
				
				tracker_peer_cache.remove( peer.getKey());
				
				tracker_peer_cache.put( peer.getKey(), peer );
			}
			
			Iterator	it = tracker_peer_cache.keySet().iterator();
			
			if ( max > 0 ){
					
				while ( tracker_peer_cache.size() > max ){
						
					it.next();
					
					it.remove();
				}
			}
		}finally{
			
			tracker_peer_cache_mon.exit();
		}
	}
	
	public void
	removeFromTrackerResponseCache(
		String		ip,
		int			tcp_port )
	{
		try{
			tracker_peer_cache_mon.enter();
		
				// create a fake peer so we can get the key
			
			TRTrackerAnnouncerResponsePeerImpl peer = 
				new TRTrackerAnnouncerResponsePeerImpl( "", new byte[0], ip, tcp_port, 0, 0, (short)0, (byte)0, (short)0 );
			
			if ( tracker_peer_cache.remove( peer.getKey()) != null ){
				
				if (Logger.isEnabled())
					Logger.log(new LogEvent( getTorrent(), LOGID, "Explicit removal of peer cache for " + ip + ":" + tcp_port ));
			}
			
		}finally{
			
			tracker_peer_cache_mon.exit();
		}
	}
	
	public static Map
	mergeResponseCache(
		Map		map1,
		Map		map2 )
	{
		if ( map1 == null && map2 == null ){
			return( new HashMap());
		}else if ( map1 == null ){
			return( map2 );
		}else if ( map2 == null ){
			return( map1 );
		}
		
		Map	res = new HashMap();
				
		List	peers = (List)map1.get( "tracker_peers" );
		
		if ( peers == null ){
			
			peers = new ArrayList();
		}
		
		List	p2 = (List)map2.get( "tracker_peers" );
		
		if ( p2 != null ){
			
			if (Logger.isEnabled())
				Logger.log(new LogEvent(LOGID,
						"TRTrackerClient: merged peer sets: p1 = " + peers.size()
								+ ", p2 = " + p2.size()));
		
			for (int i=0;i<p2.size();i++){
				
				peers.add( p2.get( i ));
			}
		}
		
		res.put( "tracker_peers", peers );
		
		return( res );
	}
	
	protected abstract int
	getPeerCacheLimit();
	
	protected TRTrackerAnnouncerResponsePeer[]
	getPeersFromCache(
		int	num_want )
	{
		int	limit = getPeerCacheLimit();
		
		if ( limit <= 0 ){
			
			return( new TRTrackerAnnouncerResponsePeer[0] );
		}
		
			// limit peers returned to avoid multi-tracker torrents from getting swamped
			// by out-of-date peers from a failed tracker
		
		num_want = Math.min( limit, num_want );
		
		try{
			tracker_peer_cache_mon.enter();
	
			TRTrackerAnnouncerResponsePeerImpl[]	res;
			
			if ( tracker_peer_cache.size() <= num_want ){
				
				res = new TRTrackerAnnouncerResponsePeerImpl[tracker_peer_cache.size()];
				
				tracker_peer_cache.values().toArray( res );
				
			}else{
			
				res = new TRTrackerAnnouncerResponsePeerImpl[num_want];
				
				Iterator	it = tracker_peer_cache.keySet().iterator();
				
					// take 'em out and put them back in so we cycle through the peers
					// over time
				
				for (int i=0;i<num_want;i++){
					
					String	key = (String)it.next();
					
					res[i] = (TRTrackerAnnouncerResponsePeerImpl)tracker_peer_cache.get(key);
				
					it.remove();
				}
				
				for (int i=0;i<num_want;i++){
					
					tracker_peer_cache.put( res[i].getKey(), res[i] );
				}
			}
			
			if (Logger.isEnabled()){
				
				for (int i=0;i<res.length;i++){
						
					Logger.log(new LogEvent(getTorrent(), LOGID, "CACHED PEER: " + res[i].getString()));
				}
			
				Logger.log(new LogEvent(getTorrent(), LOGID,
						"TRTrackerClient: returned " + res.length + " cached peers"));
			}
		    
			cache_peers_used += res.length;
			
			return( res );
			
		}finally{
			
			tracker_peer_cache_mon.exit();
		}
	} 
	
	public TrackerPeerSource 
	getCacheTrackerPeerSource()
	{
		return(
			new TrackerPeerSourceAdapter()
			{
				public String
				getName()
				{
					return( MessageText.getString( "tps.tracker.cache1", new String[]{ String.valueOf( cache_peers_used )}));
				}
				
				public int
				getPeers()
				{
					return( tracker_peer_cache.size() );
				}
			});
	}

	protected void
	informResponse(
		TRTrackerAnnouncerHelper		helper,
		TRTrackerAnnouncerResponse		response )
	{
		listeners.dispatch( LDT_TRACKER_RESPONSE, response );
	}

	protected void
	informURLRefresh()
	{
		listeners.dispatch( LDT_URL_REFRESH, null );
	}
	
 	public void
	addListener(
		TRTrackerAnnouncerListener	l )
	{
		listeners.addListener( l );
	}
		
	public void
	removeListener(
		TRTrackerAnnouncerListener	l )
	{
		listeners.removeListener(l);
	}

	public interface
	Helper
	{
		public byte[]
		getPeerID();
		
		public String
		getTrackerKey();
		
		public int
		getUDPKey();
		
		public void
		addToTrackerCache(
			TRTrackerAnnouncerResponsePeerImpl[]		peers );

		public TRTrackerAnnouncerResponsePeer[]
      	getPeersFromCache(
      		int			num_want );
		
		public void 
		setTrackerResponseCache(
			Map map	);
		
		public void 
		removeFromTrackerResponseCache(
			String ip, int tcpPort );
		
		public Map 
		getTrackerResponseCache();
		
		public void
		informResponse(
			TRTrackerAnnouncerHelper		helper,
			TRTrackerAnnouncerResponse		response );
		
		public void
		informURLChange(
			URL			old_url,
			URL			new_url,
			boolean		explicit );
		
		public void
		informURLRefresh();
		
	 	public void
		addListener(
			TRTrackerAnnouncerListener	l );
			
		public void
		removeListener(
			TRTrackerAnnouncerListener	l );
	}
}
