/*
 * Created on 20-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.core.instancemanager.impl;

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

import org.gudy.azureus2.core3.config.COConfigurationListener;
import org.gudy.azureus2.core3.config.COConfigurationManager;
import org.gudy.azureus2.core3.util.*;

import com.aelitis.azureus.core.instancemanager.AZInstanceManagerAdapter;
import com.aelitis.azureus.core.networkmanager.admin.NetworkAdmin;
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 AZInstanceManagerAdapter	adapter;
	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(
		AZInstanceManagerAdapter	_adapter,
		AZInstanceManagerImpl		_manager )

	{
		adapter	= _adapter;
		manager	= _manager;
		
		id	= adapter.getID();
		
		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 );
		
		adapter.addListener(
			new AZInstanceManagerAdapter.StateListener()
			{
				public void
				started()
				{		        	
				    DHTPlugin dht = adapter.getDHTPlugin();
				    
			    	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 );
			        				}
			        			}
			        		});
			    	}
				}
				
				public void
				stopped()
				{
				}
			});
	}
	
	private 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[]	ports = adapter.getPorts();
		
		int	new_tcp_port			= ports[0];
		int	new_udp_port			= ports[1];
		int	new_udp_non_data_port	= ports[2];
		
		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 );
		}
	}
	
	private 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 );
		}
		        	
	    DHTPlugin dht = adapter.getDHTPlugin();
	    	    
	    	// 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()){
	    	
	    	AZInstanceManagerAdapter.VCPublicAddress a = adapter.getVCPublicAddress();

	    	if ( a != null ){
	    		
	    		long cache_time = a.getCacheTime(); 
	    		
		    	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

	    	AZInstanceManagerAdapter.VCPublicAddress a = adapter.getVCPublicAddress();
	    			
			if ( a != null ){
				
				try{
					
					external_address	= InetAddress.getByName( a.getAddress());
					
				}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{
				   	UPnPPlugin upnp = adapter.getUPnPPlugin();
				 
				   	if ( upnp != null ){
				   		
				    	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 = adapter.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 );
	}
	
	private boolean
	sameFamily(
		InetAddress	a1,
		InetAddress a2 )
	{		
		return( a1 instanceof Inet4Address == a2 instanceof Inet4Address );
	}
			
	public String
	getID()
	{
		return( id );
	}
	
	public String
	getApplicationID()
	{
		return( SystemProperties.getApplicationIdentifier() + "_" + SystemProperties.getApplicationVersion());
	}
	
	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 );
	}
	
	public Map<String, Object> 
	getProperties() 
	{
		return((Map<String, Object>)COConfigurationManager.getMapParameter( "instance.manager.props", null ));
	}
}
