/*
 * Created on 16-Jun-2004
 * 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.plugins.upnp;

/**
 * @author parg
 *
 */

import java.net.URL;
import java.util.*;

import org.gudy.azureus2.core3.internat.*;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.plugins.logging.*;
import org.gudy.azureus2.plugins.ui.config.BooleanParameter;

import com.aelitis.net.upnp.services.*;

public class 
UPnPPluginService 
{
	protected UPnPWANConnection		connection;
	protected BooleanParameter 		alert_success;
	protected BooleanParameter 		grab_ports;
	protected BooleanParameter 		alert_other_port_param;
	protected BooleanParameter		release_mappings;
	
	protected List<serviceMapping>	service_mappings = new ArrayList<serviceMapping>();
	
	protected AEMonitor	this_mon 	= new AEMonitor( "UPnPPluginService" );
	   
	protected
	UPnPPluginService(
		UPnPWANConnection				_connection,
		UPnPWANConnectionPortMapping[]	_ports,
		BooleanParameter				_alert_success,
		BooleanParameter				_grab_ports,
		BooleanParameter				_alert_other_port_param,
		BooleanParameter				_release_mappings)
	{
		connection				= _connection;
		alert_success			= _alert_success;
		grab_ports				= _grab_ports;
		alert_other_port_param	= _alert_other_port_param;
		release_mappings		= _release_mappings;
		
		for (int i=0;i<_ports.length;i++){

			service_mappings.add( new serviceMapping( _ports[i]));
		}
	}
	
	public String
	getName()
	{
		return( connection.getGenericService().getDevice().getRootDevice().getDevice().getFriendlyName());
	}
	
	public String
	getInfo()
	{
		return( connection.getGenericService().getDevice().getRootDevice().getInfo());
	}
	public String
	getAddress()
	{
		return( connection.getGenericService().getDevice().getRootDevice().getLocation().getHost());
	}
	
	public int
	getPort()
	{
		URL	url = connection.getGenericService().getDevice().getRootDevice().getLocation();
		
		int	port = url.getPort();
		
		if ( port == -1 ){
			
			port = url.getDefaultPort();
		}
		
		return( port );
	}
	
	public String
	getExternalAddress()
	{
		try{
			return( connection.getExternalIPAddress());
			
		}catch( Throwable e ){
			
			return( null );
		}
	}
	
	public UPnPWANConnection
	getService()
	{
		return( connection );
	}
	
	protected String
	getOldDescriptionForPort(
		int		port )
	{
			// Remove one day - port name was changed as some routers use name uniqueness
			// to manage ports, hence UDP and TCP with same name failed
		
		return( "Azureus UPnP " + port );
	}
	
	protected String
	getDescriptionForPort(
		boolean	TCP,
		int		port )
	{
		return( "Azureus UPnP " + port + " " + (TCP?"TCP":"UDP"));
	}
	
	protected void
	checkMapping(
		LoggerChannel		log,
		UPnPMapping			mapping )
	{
		try{
			this_mon.enter();
		
			if ( mapping.isEnabled()){
				
					// check for change of port number and delete old value if so
				
				for (int i=0;i<service_mappings.size();i++){
					
					serviceMapping	sm = (serviceMapping)service_mappings.get(i);
					
					if ( sm.getMappings().contains( mapping )){
				
						if ( sm.getPort() != mapping.getPort()){
							
							removeMapping( log, mapping, sm, false );
						}
					}
				}
			
				serviceMapping	grab_in_progress	= null;
				
				String local_address = connection.getGenericService().getDevice().getRootDevice().getLocalAddress().getHostAddress();
				
				for (int i=0;i<service_mappings.size();i++){
					
					serviceMapping	sm = (serviceMapping)service_mappings.get(i);
					
					if ( 	sm.isTCP() 		== mapping.isTCP() &&
							sm.getPort() 	== mapping.getPort()){				
				
						if ( sm.getInternalHost().equals( local_address )){
							
								// make sure we tie this to the mapping in case it
								// was external to begin with
							
							sm.addMapping( mapping  );
							
							if ( !sm.getLogged(mapping)){
								
								sm.setLogged(mapping);
								
								log.log( "Mapping " + mapping.getString() + " already established" );
							}
							
							return;
							
						}else{
							
							if ( !grab_ports.getValue() ){
		
								if ( !sm.getLogged(mapping)){
									
									sm.setLogged(mapping);
								
									String	text = 
										MessageText.getString( 
											"upnp.alert.differenthost", 
											new String[]{ mapping.getString(), sm.getInternalHost()});
																
									if ( alert_other_port_param.getValue()){
									
										log.logAlertRepeatable( LoggerChannel.LT_WARNING, text );
									}else{
										
										log.log( text);
									}
								}
								
								return;
								
							}else{
								
									// we're going to grab it
								
								sm.addMapping( mapping  );
	
								grab_in_progress	= sm;
							}
						}
					}
				}
				
					// not found - try and establish it + add entry even if we fail so
					// that we don't retry later
				
				String	error_text = null;
				
				try{
					connection.addPortMapping( 
						mapping.isTCP(), mapping.getPort(), 
						getDescriptionForPort( mapping.isTCP(), mapping.getPort()));
								
					String	text;
					
					if ( grab_in_progress != null ){
						
						text = MessageText.getString( 
								"upnp.alert.mappinggrabbed", 
								new String[]{ mapping.getString(), grab_in_progress.getInternalHost()});
					}else{
						
						text = MessageText.getString( 
								"upnp.alert.mappingok", 
								new String[]{ mapping.getString()});
					}
					
					log.log( text );
					
					if ( alert_success.getValue()){
						
						log.logAlertRepeatable( LoggerChannel.LT_INFORMATION, text );
					}
										
				}catch( Throwable e ){
					
					error_text = Debug.getNestedExceptionMessage( e );
					
					String	text = 
						MessageText.getString( 
								"upnp.alert.mappingfailed", 
								new String[]{ mapping.getString()});
					
					log.log( text );
				
					if ( alert_other_port_param.getValue()){
					
						log.logAlertRepeatable( LoggerChannel.LT_ERROR, text );
					}
				}
				
				if ( grab_in_progress == null ){
					
					serviceMapping	new_mapping = new serviceMapping( mapping );
				
					new_mapping.setError( error_text );

					service_mappings.add( new_mapping );
					
				}else{
					
					grab_in_progress.setError( error_text );
				}
				
			}else{
					// mapping is disabled
				
				removeMapping( log, mapping, false );
			}
		}finally{
			
			this_mon.exit();
		}
	}
	
	protected void
	removeMapping(
		LoggerChannel		log,
		UPnPMapping			mapping, 
		boolean				end_of_day )
	{
		try{
			this_mon.enter();
					
			for (int i=0;i<service_mappings.size();i++){
				
				serviceMapping	sm = (serviceMapping)service_mappings.get(i);
				
				if ( 	sm.isTCP() == mapping.isTCP() &&
						sm.getPort() == mapping.getPort() &&
						sm.getMappings().contains( mapping )){
					
					removeMapping( log, mapping, sm, end_of_day );
	
					return;
				}
			}
		}finally{
			
			this_mon.exit();
		}
	}
							
	protected void
	removeMapping(
		LoggerChannel		log,
		UPnPMapping			upnp_mapping,
		serviceMapping		service_mapping, 
		boolean				end_of_day )
	{
		if ( service_mapping.isExternal()){
		
			log.log( "Mapping " + service_mapping.getString() + " not removed as not created by Azureus" );
			
		}else{
			int	persistent	=  UPnPMapping.PT_DEFAULT;
			
			List	mappings = service_mapping.getMappings();
			
			for (int i=0;i<mappings.size();i++){
				
				UPnPMapping	map = (UPnPMapping)mappings.get(i); 
					
				int	p = map.getPersistent(); 
				
				if ( p == UPnPMapping.PT_DEFAULT ){
				
						// default - leave as is
					
				}else if ( p == UPnPMapping.PT_TRANSIENT ){
					
						// transient overrides default
					
					if ( persistent == UPnPMapping.PT_DEFAULT ){
						
						persistent	= p;
					}
				}else{
					
						// persistent overrides all others
					
					persistent	= UPnPMapping.PT_PERSISTENT;
				}
			}
			
				// set effective persistency
			
			if ( persistent == UPnPMapping.PT_DEFAULT ){
				
				persistent = release_mappings.getValue()?UPnPMapping.PT_TRANSIENT:UPnPMapping.PT_PERSISTENT;
			}
			
				// only time we take note of whether or not to release the mapping is
				// at closedown
			
			if ( end_of_day && persistent == UPnPMapping.PT_PERSISTENT ){

				log.log( "Mapping " + service_mapping.getString() + " not removed as mapping is persistent" );
	
			}else{
				
					// get the name here for the deletion case so that the subsequent message makes 
					// sense (as the name is derived from the current mappings, so getting it after
					// deleting it results in a name of <external>)
				
				String	service_name = service_mapping.getString();
				
				service_mapping.removeMapping( upnp_mapping );
				
				if ( service_mapping.getMappings().size() == 0 ){
				
					try{
						connection.deletePortMapping( 
								service_mapping.isTCP(), service_mapping.getPort());
				
						log.log( "Mapping " + service_name + " removed" );
						
					}catch( Throwable e ){
						
						log.log( "Mapping " + service_name + " failed to delete", e );
					}
					
					service_mappings.remove(service_mapping);
					
				}else{
					
					log.log( "Mapping " + service_mapping.getString() + " not removed as interest remains" );
				}
			}
		}
	}
	
	public serviceMapping[]
	getMappings()
	{
		try{
			this_mon.enter();

			return( service_mappings.toArray( new serviceMapping[service_mappings.size()]));
			
		}finally{
			
			this_mon.exit();
		}
	}
	
	public String
	getString()
	{
		String str = "name=" + getName() + ",info=" + getInfo() + ",int=" + getAddress() + ":" + getPort() + ",ext=" + getExternalAddress();
		
		serviceMapping[] sms = getMappings();
		
		for ( serviceMapping sm: sms ){
			
			String error = sm.getError();
			
			if ( error != null ){
				
				str += ":" + sm.getString() + " -> " + error;
			}
		}
		
		return( str );
	}
	
	public class
	serviceMapping
	{
		private List		mappings	= new ArrayList();
		
		private boolean		tcp;
		private int			port;
		private String		internal_host;
		
		private boolean		external;		// true -> not defined by us
		
		private List		logged_mappings = new ArrayList();
		
		private String		error;
		
		protected
		serviceMapping(
			UPnPWANConnectionPortMapping		device_mapping )
		{
			tcp				= device_mapping.isTCP();
			port			= device_mapping.getExternalPort();
			internal_host	= device_mapping.getInternalHost();
			
			String	desc = device_mapping.getDescription();
			
			if ( 	desc == null || 
					!(	desc.equalsIgnoreCase( getOldDescriptionForPort( port )) ||
						desc.equalsIgnoreCase( getDescriptionForPort( tcp, port )))){
				
				external		= true;
			}
		}
	
		protected
		serviceMapping(
			UPnPMapping		_mapping )
		{
			mappings.add( _mapping );
			
			tcp				= _mapping.isTCP();
			port			= _mapping.getPort();
			internal_host	= connection.getGenericService().getDevice().getRootDevice().getLocalAddress().getHostAddress();
		}
		
		public boolean
		isExternal()
		{
			return( external );
		}
		
		protected List
		getMappings()
		{
			return( mappings );
		}
		
		protected void
		addMapping(
			UPnPMapping	_mapping )
		{
			if ( !mappings.contains( _mapping )){
				
				mappings.add( _mapping );
			}
		}
		
		protected void
		removeMapping(
			UPnPMapping	_mapping )
		{
			mappings.remove( _mapping );
		}
		
		protected boolean
		getLogged(
			UPnPMapping	mapping )
		{
			return( logged_mappings.contains( mapping ));
		}
		
		protected void
		setLogged(
			UPnPMapping	mapping )
		{
			if ( !logged_mappings.contains( mapping )){
				
				logged_mappings.add( mapping );
			}
		}
		
		public boolean
		isTCP()
		{
			return( tcp );
		}
		
		public int
		getPort()
		{
			return( port );
		}
		
		public String
		getInternalHost()
		{
			return( internal_host );
		}
		
		private String
		getError()
		{
			return( error );
		}
		
		private void
		setError(
			String	_error )
		{
			error = _error;
		}
		
		public String
		getString()
		{
			if ( mappings.size() == 0 ){
				
				return( "<external> (" + (isTCP()?"TCP":"UDP")+"/"+getPort()+")" ); 
				
			}else{
				
				String	str = "";
				
				for (int i=0;i<mappings.size();i++){
					str += (i==0?"":",")+ ((UPnPMapping)mappings.get(i)).getString( getPort());
				}
				
				return( str );
			}
		}
	}
}