/*
 * Created on 04-Jan-2006
 * 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 org.gudy.azureus2.core3.util;

import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.bouncycastle.util.encoders.Base64;

import com.aelitis.azureus.core.AzureusCoreFactory;
import com.aelitis.azureus.core.instancemanager.AZInstance;
import com.aelitis.azureus.core.instancemanager.AZInstanceManager;
import com.aelitis.azureus.core.proxy.AEProxyFactory;

public class 
AddressUtils 
{
	public static final byte LAN_LOCAL_MAYBE	= 0;
	public static final byte LAN_LOCAL_YES		= 1;
	public static final byte LAN_LOCAL_NO		= 2;
	
	private static AZInstanceManager	instance_manager;
	
	private static Map	host_map = null;
	
	public static URL
	adjustURL(
		URL		url )
	{
		url = AEProxyFactory.getAddressMapper().internalise( url );

		if ( host_map != null ){
			
			String	rewrite = (String)host_map.get( url.getHost());
			
			if ( rewrite != null ){
		
				String str = url.toExternalForm();
				
				try{
					int pos = str.indexOf( "//" ) + 2;
					
					int pos2 = str.indexOf( "/", pos );
					
					String	host_bit = str.substring( pos, pos2 );
					
					int	pos3 = host_bit.indexOf(':');
					
					String	port_bit;
					
					if ( pos3 == -1 ){
						
						port_bit = "";
						
					}else{
						
						port_bit = host_bit.substring(pos3);
					}
					
					String new_str = str.substring(0,pos) + rewrite + port_bit + str.substring( pos2 );
										
					url = new URL( new_str );
					
				}catch( Throwable e ){
					
					Debug.printStackTrace(e);
				}
			}
		}
		
		return( url );
	}
	
	public static synchronized void
	addHostRedirect(
		String	from_host,
		String	to_host )
	{
		System.out.println( "AddressUtils::addHostRedirect - " + from_host + " -> " + to_host );
		
		Map	new_map;
		
		if ( host_map == null ){
			
			new_map = new HashMap();
		}else{
			
			new_map = new HashMap( host_map );
		}
		
		new_map.put( from_host, to_host );
		
		host_map = new_map;
	}
	
	public static InetSocketAddress
	adjustTCPAddress(
		InetSocketAddress	address,
		boolean				ext_to_lan )
	{
		return( adjustAddress( address, ext_to_lan, AZInstanceManager.AT_TCP ));
	}
	
	public static InetSocketAddress
	adjustUDPAddress(
		InetSocketAddress	address,
		boolean				ext_to_lan )
	{
		return( adjustAddress( address, ext_to_lan, AZInstanceManager.AT_UDP ));
	}
	
	public static InetSocketAddress
	adjustDHTAddress(
		InetSocketAddress	address,
		boolean				ext_to_lan )
	{
		return( adjustAddress( address, ext_to_lan, AZInstanceManager.AT_UDP_NON_DATA ));
	}
	
	private static InetSocketAddress
	adjustAddress(
		InetSocketAddress	address,
		boolean				ext_to_lan,
		int					port_type )
	{
		if ( instance_manager == null ){
			
			try{
				instance_manager = AzureusCoreFactory.getSingleton().getInstanceManager();
				
			}catch( Throwable e ){
				
				// Debug.printStackTrace(e);
			}
		}
		
		if ( instance_manager == null || !instance_manager.isInitialized()){
			
			return( address );
		}
		
		InetSocketAddress	adjusted_address;
		
		if ( ext_to_lan ){
			
			adjusted_address	= instance_manager.getLANAddress( address, port_type );
			
		}else{

			adjusted_address	= instance_manager.getExternalAddress( address, port_type );
		}
		
		if ( adjusted_address == null ){
			
			adjusted_address	= address;
			
		}else{
		
			// System.out.println( "adj: " + address + "/" + ext_to_lan + " -> " + adjusted_address );
		}
		
		return( adjusted_address );
	}
	
	public static List
	getLANAddresses(
		String		address )
	{
		List	result = new ArrayList();
		
		result.add( address );
		
		try{
			InetAddress ad = InetAddress.getByName( address );

			if ( isLANLocalAddress( address ) != LAN_LOCAL_NO ){

				if ( instance_manager == null ){
					
					try{
						instance_manager = AzureusCoreFactory.getSingleton().getInstanceManager();
						
					}catch( Throwable e ){
						
						//Debug.printStackTrace(e);
					}
				}
				
				if ( instance_manager == null || !instance_manager.isInitialized()){
					
					return( result );
				}
			
				AZInstance[] instances = instance_manager.getOtherInstances();
				
				for (int i=0;i<instances.length;i++){
					
					AZInstance instance = instances[i];
					
					List addresses = instance.getInternalAddresses();
					
					if ( addresses.contains( ad )){
						
						for ( int j=0; j<addresses.size();j++){
							
							InetAddress ia = (InetAddress)addresses.get(j);
							
							String str = ia.getHostAddress();
							
							if ( !result.contains( str )){
								
								result.add( str );
							}
						}
					}
				}
			}
		}catch( Throwable e ){
			
		}
		
		return( result );
	}
	
	public static byte
	isLANLocalAddress(
		InetSocketAddress	socket_address )
	{
		InetAddress address = socket_address.getAddress();
		
		return( isLANLocalAddress( address ));
	}
	
	public static byte
	isLANLocalAddress(
		InetAddress	address )

	{
			// if someone passes us an unresolved address then handle sensibly
		
		if ( address == null ){
			
			return( LAN_LOCAL_NO );
		}
		
		if ( instance_manager == null ){
			
			if ( AzureusCoreFactory.isCoreAvailable()){
				
				try{
					instance_manager = AzureusCoreFactory.getSingleton().getInstanceManager();
					
				}catch( Throwable e ){
					
					// Debug.printStackTrace(e);
				}
			}
		}
		
		if ( instance_manager == null || !instance_manager.isInitialized()){
			
			return( LAN_LOCAL_MAYBE );
		}
		
		return( instance_manager.isLANAddress( address )? LAN_LOCAL_YES:LAN_LOCAL_NO);
	}
	
	
	public static byte 
	isLANLocalAddress( 
		String address ) 
	{
		byte is_lan_local = LAN_LOCAL_MAYBE;
		
		try {
			is_lan_local = isLANLocalAddress( HostNameToIPResolver.syncResolve( address ));
			
		}catch( UnknownHostException e ){
			
		}catch( Throwable t ){
			
			t.printStackTrace();  
		}
		
		return is_lan_local;
	}
	
	/**
	 * checks if the provided address is a global-scope ipv6 unicast address
	 */
	public static boolean isGlobalAddressV6(InetAddress addr) {
		return addr instanceof Inet6Address && !addr.isAnyLocalAddress() && !addr.isLinkLocalAddress() && !addr.isLoopbackAddress() && !addr.isMulticastAddress() && !addr.isSiteLocalAddress() && !((Inet6Address)addr).isIPv4CompatibleAddress();
	}
	
	public static boolean isTeredo(InetAddress addr)
	{
		if(!(addr instanceof Inet6Address))
			return false;
		byte[] bytes = addr.getAddress();
		// check for the 2001:0000::/32 prefix, i.e. teredo
		return bytes[0] == 0x20 && bytes[1] == 0x01 && bytes[2] == 0x00 && bytes[3] == 0x00;
	}
	
	public static boolean is6to4(InetAddress addr)
	{
		if(!(addr instanceof Inet6Address))
			return false;
		byte[] bytes = addr.getAddress();
		// check for the 2002::/16 prefix, i.e. 6to4
		return bytes[0] == 0x20 && bytes[1] == 0x02;
	}
	
	/**
	 * picks 1 global-scoped address out of a list based on the heuristic
	 * "true" ipv6/tunnel broker > 6to4 > teredo
	 * 
	 * @return null if no proper v6 address is found, best one otherwise
	 */
	public static InetAddress pickBestGlobalV6Address(List<InetAddress> addrs)
	{
		InetAddress bestPick = null;
		int currentRanking = 0;
		for(InetAddress addr : addrs)
		{
			if(!isGlobalAddressV6(addr))
				continue;
			int ranking = 3;
			if(isTeredo(addr))
				ranking = 1;
			else if(is6to4(addr))
				ranking = 2;
			
			if(ranking > currentRanking)
			{
				bestPick = addr;
				currentRanking = ranking;
			}
		}
		
		return bestPick;
	}
	
	public static InetAddress
	getByName(
		String		host )
		
		throws UnknownHostException
	{
		if ( AENetworkClassifier.categoriseAddress( host ) == AENetworkClassifier.AT_PUBLIC ){
		
			return( InetAddress.getByName( host ));
		}
		
		throw( new UnknownHostException( host ));
	}
	
	public static InetAddress[]
	getAllByName(
		String		host )
		
		throws UnknownHostException
	{
		if ( AENetworkClassifier.categoriseAddress( host ) == AENetworkClassifier.AT_PUBLIC ){
		
			return( InetAddress.getAllByName( host ));
		}
		
		throw( new UnknownHostException( host ));
	}
	
	public static byte[]
	getAddressBytes(
		InetSocketAddress	address )
	{
		if ( address.isUnresolved()){
			
			try{
				return( address.getHostName().getBytes( "ISO8859-1" ));
				
			}catch( Throwable e ){
				
				Debug.out( e );
				
				return( null );
			}
		}else{
			
			return( address.getAddress().getAddress());
		}
	}
	
	public static String
	getHostAddress(
		InetSocketAddress	address )
	{
		if ( address.isUnresolved()){
			
			return( address.getHostName());
			
		}else{
			
			return( address.getAddress().getHostAddress());
		}
	}
	
	public static String
	getHostNameNoResolve(
		InetSocketAddress	address )
	{
		InetAddress i_address = address.getAddress();
		
		if ( i_address == null ){
			
			return( address.getHostName());
			
		}else{
			
				// only way I can see (short of reflection) of getting access to unresolved host name
				// toString returns (hostname or "")/getHostAddress()
			
			String str = i_address.toString();
			
			int	pos = str.indexOf( '/' );

			if ( pos == -1 ){
				
				// darn it, borkage
				
				System.out.println( "InetAddress::toString not returning expected result: " + str );
				
				return( i_address.getHostAddress());
			}
			
			if ( pos > 0  ){
				
				return( str.substring( 0, pos ));
				
			}else{
				
				return( str.substring( pos+1 ));
			}
		}
	}
	
	public static String
	convertToShortForm(
		String		address )
	{
		int	address_length = address.length();
		
		if ( address_length > 256  ){
			
			String to_decode;
			
			if ( address.endsWith( ".i2p" )){
				
				to_decode = address.substring( 0, address.length() - 4 );
				
			}else if ( address.indexOf( '.' ) == -1 ){
				
				to_decode = address;
				
			}else{
				
				return( address );
			}
			
			try{
					// unfortunately we have an incompatible base64 standard in i2p, they replaced / with ~ and + with -
				
				char[]	encoded = to_decode.toCharArray();
				
				for ( int i=0;i<encoded.length;i++){
					
					char c = encoded[i];
					
					if ( c == '~' ){
						encoded[i] = '/';
					}else if( c == '-' ){
						encoded[i] = '+';
					}
				}
				
				byte[] decoded = Base64.decode( encoded );
				
				byte[] hash = MessageDigest.getInstance( "SHA-256" ).digest( decoded );
				
				return( Base32.encode( hash ).toLowerCase( Locale.US ) + ".b32.i2p" );
				
			}catch( Throwable e ){
				
			}
		}
			
		return( address );
	}
}
