/*
 * Created on 20-Dec-2005
 * Created by Paul Gardner
 * Copyright (C) 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.azureus.core.instancemanager.impl;

import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;

import org.gudy.azureus2.core3.config.COConfigurationListener;
import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.util.ByteFormatter;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.core3.util.SHA1Simple;
import org.gudy.azureus2.core3.util.SystemTime;
import org.gudy.azureus2.plugins.PluginInterface;

import com.aelitis.azureus.core.AzureusCore;
import com.aelitis.azureus.core.AzureusCoreLifecycleAdapter;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin;
import com.aelitis.azureus.core.networkmanager.impl.tcp.TCPNetworkManager;
import com.aelitis.azureus.core.networkmanager.impl.udp.UDPNetworkManager;
import com.aelitis.azureus.core.versioncheck.VersionCheckClient;
import com.aelitis.azureus.plugins.dht.DHTPlugin;
import com.aelitis.azureus.plugins.dht.DHTPluginContact;
import com.aelitis.azureus.plugins.dht.DHTPluginListener;
import com.aelitis.azureus.plugins.upnp.UPnPPlugin;

public class 
AZMyInstanceImpl
	extends AZInstanceImpl
{
	public static final long	FORCE_READ_EXT_MIN	= 8*60*60*1000;
	public static final long	UPNP_READ_MIN		= 5*60*1000;
	
	private AzureusCore				core;
	private AZInstanceManagerImpl	manager;
	
	private String				id;
	private InetAddress			internal_address;
	private int					tcp_port;
	private int					udp_port;
	private int					udp_non_data_port;
	
	private long				last_upnp_read;
	
	private InetAddress			dht_address;
	private long				dht_address_time;
	
	private long				last_force_read_ext;
	private InetAddress			last_external_address;
	
	protected
	AZMyInstanceImpl(
		AzureusCore				_core,
		AZInstanceManagerImpl	_manager )

	{
		core	= _core;
		manager	= _manager;
		
		id	= COConfigurationManager.getStringParameter( "ID", "" );
		
		if ( id.length() == 0 ){
			
			id	= "" + SystemTime.getCurrentTime();
		}
		
		id = ByteFormatter.encodeString( new SHA1Simple().calculateHash( id.getBytes()));
		
		COConfigurationManager.addListener( 
			new COConfigurationListener()
			{
				public void
				configurationSaved()
				{
					readConfig( false );
				}
			});
		
		readConfig( true );
		
		core.addLifecycleListener(
			new AzureusCoreLifecycleAdapter()
			{
				public void
				started(
					AzureusCore		core )
				{
					core.removeLifecycleListener( this );
					
				    PluginInterface dht_pi = core.getPluginManager().getPluginInterfaceByClass( DHTPlugin.class );
		        	
				    DHTPlugin dht = null;
				    
				    if ( dht_pi != null ){
			    	
				    	dht = (DHTPlugin)dht_pi.getPlugin();
				    	
				    	if ( dht != null ){
				    		
				        	dht.addListener(
				        		new DHTPluginListener()
				        		{
				        			public void
				        			localAddressChanged(
				        				DHTPluginContact	local_contact )
				        			{
				        				InetAddress latest_dht_address = local_contact.getAddress().getAddress();
				        								        				
				        				if ( sameFamily( internal_address, latest_dht_address )){
				        				
					        				dht_address 		= latest_dht_address;
					        				dht_address_time	= SystemTime.getCurrentTime();
					        				
					        				manager.informChanged( AZMyInstanceImpl.this );
				        				}
				        			}
				        		});
				    	}
				    }
				}
			});
	}
	
	protected void
	readConfig(
		boolean	first_time )
	{
		InetAddress	new_internal_address	= NetworkAdmin.getSingleton().getSingleHomedServiceBindAddress();
		
		if ( new_internal_address == null ){
			
			try{
				new_internal_address = InetAddress.getByName( "0.0.0.0" );
				
			}catch( Throwable e ){			
			}
		}
				
		int	new_tcp_port 			= TCPNetworkManager.getSingleton().getTCPListeningPortNumber();
		int	new_udp_port 			= UDPNetworkManager.getSingleton().getUDPListeningPortNumber();
		int new_udp_non_data_port	= UDPNetworkManager.getSingleton().getUDPNonDataListeningPortNumber();
		
		boolean	same = true;
		
		if ( !first_time ){
			
			same = 	internal_address.equals( new_internal_address) &&
					tcp_port == new_tcp_port &&
					udp_port == new_udp_port &&
					udp_non_data_port == new_udp_non_data_port;
		}
		
		internal_address 	= new_internal_address;
		tcp_port			= new_tcp_port;
		udp_port			= new_udp_port;
		udp_non_data_port	= new_udp_non_data_port;
		
		if ( !same ){
			
			manager.informChanged( this );
		}
	}
	
	protected InetAddress
	readExternalAddress()
	{
		InetAddress	 external_address = null;

			// no point in kicking off any queries if we're closing
		
		if ( manager.isClosing()){
			
			external_address	= last_external_address;
			
			if ( external_address == null ){
				
				try{
					external_address = InetAddress.getByName("127.0.0.1");
					
				}catch( Throwable e ){
					
					Debug.printStackTrace(e);
				}
			}
			
			return( external_address );
		}
		
	    PluginInterface dht_pi = core.getPluginManager().getPluginInterfaceByClass( DHTPlugin.class );
        	
	    DHTPlugin dht = null;
	    
	    if ( dht_pi != null ){
    	
	    	dht = (DHTPlugin)dht_pi.getPlugin();
	    }
	    
	    	// if DHT has informed us of an address then we use this - most reliable up to date one
	    	// unless the version server cache time is more recent
	    
	    if ( dht_address != null && dht_address_time <= SystemTime.getCurrentTime()){
	    	
	    	long cache_time = VersionCheckClient.getSingleton().getCacheTime( false );
	    	 
	    	if ( cache_time <= dht_address_time ){
	    		
	    		external_address = dht_address;
	    	}
	    }

	    if ( 	external_address == null &&
	    		( dht == null || dht.getStatus() != DHTPlugin.STATUS_RUNNING )){
		
	    		// use cached version if available and the DHT isn't

			String	str_address = VersionCheckClient.getSingleton().getExternalIpAddress( true, false );
		
			if ( str_address != null ){
				
				try{
					
					external_address	= InetAddress.getByName( str_address );
					
				}catch( Throwable e ){
					
					Debug.printStackTrace(e);
				}
			}
		}
		
		if ( external_address == null && dht != null  ){
			
				// no cache, use DHT (this will hang during initialisation, hence the use of cached
				// version above
			
			try{
				InetAddress latest_dht_address = dht.getLocalAddress().getAddress().getAddress();
				
					// ignore any v6 addresses from DHT
				
				if ( sameFamily( internal_address, latest_dht_address )){
					
					external_address = latest_dht_address;
				}
	        	
			}catch( Throwable e ){
			}
		}
		
		long	now = SystemTime.getCurrentTime();
		
		if ( last_force_read_ext > now ){
			
			last_force_read_ext	= now;
		}
		
		boolean	ok_to_try_ext = now - last_force_read_ext > FORCE_READ_EXT_MIN;
		
	    	// try upnp - limit frequency unless external read is possible in which
			// case we try upnp first
			// currently we only use UPnP to validate our current external address, not
			// to deduce new ones (as for example there may be multiple upnp devices and
			// we don't know which one to believe
	   
		if ( external_address == null && last_external_address != null ){
			
			if ( last_upnp_read > now ){
				
				last_upnp_read = now;
			}
			
			if ( now - last_upnp_read > UPNP_READ_MIN || ok_to_try_ext ){
				
				last_upnp_read	= now;
				
				try{
				    PluginInterface upnp_pi = core.getPluginManager().getPluginInterfaceByClass( UPnPPlugin.class );
			        			    
				    if ( upnp_pi != null ){
			    	
				    	UPnPPlugin upnp = (UPnPPlugin)upnp_pi.getPlugin();
				    	
				    	String[]	addresses = upnp.getExternalIPAddresses();
				    	
				    	for (int i=0;i<addresses.length;i++){
				    		
				    		if ( addresses[i].equals( last_external_address.getHostAddress())){
				    			
				    			external_address = last_external_address; 
				    			
				    			break;
				    		}
				    	}
				    }
				}catch( Throwable e ){
				}
			}
		}
		
		if ( external_address == null ){
			
				// force read it
			
			if ( ok_to_try_ext ){
				
				last_force_read_ext	= now;
				
				external_address = core.getPluginManager().getDefaultPluginInterface().getUtilities().getPublicAddress();
			}
		}
		
			// no good address available		
		
		if ( external_address == null ){
				
			if ( last_external_address != null ){
				
				external_address = last_external_address;
				
			}else{
				try{
					external_address = InetAddress.getByName("127.0.0.1");
					
				}catch( Throwable e ){
					
					Debug.printStackTrace(e);
				}
			}
		}else{
			
			last_external_address	= external_address;
		}
		
		return( external_address );
	}
	
	protected boolean
	sameFamily(
		InetAddress	a1,
		InetAddress a2 )
	{		
		return( a1 instanceof Inet4Address == a2 instanceof Inet4Address );
	}
			
	public String
	getID()
	{
		return( id );
	}
	
	public InetAddress
	getInternalAddress()
	{
		return( internal_address );
	}
	
	public List 
	getInternalAddresses() 
	{
		List	l = new ArrayList();
		
		if ( internal_address != null ){
			
			l.add( internal_address );
		}
		
		return( l );
	}
	
	public InetAddress
	getExternalAddress()
	{
		return( readExternalAddress());
	}
	
	public int
	getTCPListenPort()
	{
		return( tcp_port );
	}
	
	public int
	getUDPListenPort()
	{
		return( udp_port );
	}
	
	public int 
	getUDPNonDataListenPort() 
	{
		return( udp_non_data_port );
	}
}
