/*
 * ====================================================================
 * ======== The Apache Software License, Version 1.1
 * ==================
 * ==========================================================
 * Copyright (C) 2002 The Apache Software Foundation. All rights
 * reserved. Redistribution and use in source and binary forms, with
 * or without modifica- tion, are permitted provided that the
 * following conditions are met: 1. Redistributions of source code
 * must retain the above copyright notice, this list of conditions and
 * the following disclaimer. 2. Redistributions in binary form must
 * reproduce the above copyright notice, this list of conditions and
 * the following disclaimer in the documentation and/or other
 * materials provided with the distribution. 3. The end-user
 * documentation included with the redistribution, if any, must
 * include the following acknowledgment: "This product includes
 * software developed by SuperBonBon Industries
 * (http://www.sbbi.net/)." Alternately, this acknowledgment may
 * appear in the software itself, if and wherever such third-party
 * acknowledgments normally appear. 4. The names "UPNPLib" and
 * "SuperBonBon Industries" must not be used to endorse or promote
 * products derived from this software without prior written
 * permission. For written permission, please contact info@sbbi.net.
 * 5. Products derived from this software may not be called
 * "SuperBonBon Industries", nor may "SBBI" appear in their name,
 * without prior written permission of SuperBonBon Industries. THIS
 * SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR ITS
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLU- DING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE. This software consists of voluntary contributions made
 * by many individuals on behalf of SuperBonBon Industries. For more
 * information on SuperBonBon Industries, please see
 * <http://www.sbbi.net/>.
 */

package net.tomp2p.upnp;

import java.io.IOException;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;


/**
 * This class can be used to access some functionalities on the
 * InternetGatewayDevice on your network without having to know anything about
 * the required input/output parameters. All device functions are not provided.
 * 
 * @author <a href="mailto:superbonbon@sbbi.net">SuperBonBon</a>
 * @version 1.0
 */
public class InternetGatewayDevice
{

	private RootDevice igd;

	private UPNPMessageFactory msgFactory;

	private InternetGatewayDevice(RootDevice igd, boolean WANIPConnection, boolean WANPPPConnection)
			throws UnsupportedOperationException
	{
		this.igd = igd;
		Device myIGDWANConnDevice = igd.getChildDevice("urn:schemas-upnp-org:device:WANConnectionDevice:1");
		if (myIGDWANConnDevice == null)
		{
			throw new UnsupportedOperationException(
					"device urn:schemas-upnp-org:device:WANConnectionDevice:1 not supported by IGD device "
							+ igd.modelName);
		}

		Service wanIPSrv = myIGDWANConnDevice.getService("urn:schemas-upnp-org:service:WANIPConnection:1");
		Service wanPPPSrv = myIGDWANConnDevice.getService("urn:schemas-upnp-org:service:WANPPPConnection:1");

		if (WANIPConnection && WANPPPConnection && wanIPSrv == null && wanPPPSrv == null)
		{
			throw new UnsupportedOperationException(
					"Unable to find any urn:schemas-upnp-org:service:WANIPConnection:1 or urn:schemas-upnp-org:service:WANPPPConnection:1 service");
		}
		else if (WANIPConnection && !WANPPPConnection && wanIPSrv == null)
		{
			throw new UnsupportedOperationException(
					"Unable to find any urn:schemas-upnp-org:service:WANIPConnection:1 service");
		}
		else if (!WANIPConnection && WANPPPConnection && wanPPPSrv == null)
		{
			throw new UnsupportedOperationException(
					"Unable to find any urn:schemas-upnp-org:service:WANPPPConnection:1 service");
		}

		if (wanIPSrv != null && wanPPPSrv == null)
		{
			msgFactory = new UPNPMessageFactory(wanIPSrv);
		}
		else if (wanPPPSrv != null && wanIPSrv == null)
		{
			msgFactory = new UPNPMessageFactory(wanPPPSrv);
		}
		else
		{
			// discover the active WAN interface using the
			// WANCommonInterfaceConfig specs
			Device wanDevice = igd.getChildDevice("urn:schemas-upnp-org:device:WANDevice:1");
			Service configService = wanDevice.getService("urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1");

			if (configService != null)
			{ // retrieve the first active connection
				Action act = configService.getUPNPServiceAction("GetActiveConnection");
				if (act != null)
				{ // this action is optional - definitely not supported by
					// the NetGear WGR614v9
					UPNPMessageFactory msg = new UPNPMessageFactory(configService);
					String deviceContainer = null;
					String serviceID = null;
					try
					{ // always lookup for the first index of active
						// connections.
						ActionResponse resp = msg.getMessage("GetActiveConnection")
								.setInputParameter("NewActiveConnectionIndex", 0).service();
						deviceContainer = resp.getOutActionArgumentValue("NewActiveConnDeviceContainer");
						serviceID = resp.getOutActionArgumentValue("NewActiveConnectionServiceID");
					}
					catch (IOException ex)
					{ // no response returned
					}
					catch (UPNPResponseException respEx)
					{ // should never happen unless the damn thing is
						// bugged
					}
					if (deviceContainer != null && deviceContainer.trim().length() > 0 && serviceID != null
							&& serviceID.trim().length() > 0)
					{
						for (Iterator<Device> i = igd.getChildDevices().iterator(); i.hasNext();)
						{
							Device dv = i.next();
							if (deviceContainer.startsWith(dv.UDN)
									&& dv.deviceType.indexOf(":WANConnectionDevice:") != -1)
							{
								myIGDWANConnDevice = dv;
								break;
							}
						}
						msgFactory = new UPNPMessageFactory(myIGDWANConnDevice.getServiceByID(serviceID));
					}
				}
				else
				{
					// Lets look at the required WANAccessType variable
					StateVariable wat = configService.getUPNPServiceStateVariable("WANAccessType");
					assert wat != null : "bugged upnp implementation";
					try
					{
						String accessType = wat.getValue();
						System.out.println(accessType);
						if (accessType.equals("DSL") || accessType.equals("POTS"))
						{
							msgFactory = new UPNPMessageFactory(wanPPPSrv);
						}
						else if (accessType.equals("Cable") || accessType.equals("Ethernet"))
						{
							msgFactory = new UPNPMessageFactory(wanIPSrv);
						}
						else
						{
							assert false : "Illegal access type : " + accessType;
						}
					}
					catch (UPNPResponseException e)
					{
						e.printStackTrace();
					}
					catch (IOException e)
					{
						e.printStackTrace();
					}
				}
			}

			if (msgFactory == null)
			{ // last-ditch attempt
				// Doing a tricky test with external IP address, the
				// unactive
				// interface should return a null value or none
				if (testWANInterface(wanIPSrv))
				{
					msgFactory = new UPNPMessageFactory(wanIPSrv);
				}
				else if (testWANInterface(wanPPPSrv))
				{
					msgFactory = new UPNPMessageFactory(wanPPPSrv);
				}
			}

			if (msgFactory == null)
			{
				// Nothing found using WANCommonInterfaceConfig! IP by
				// default
				// log
				// .warn(
				// "Unable to detect active WANIPConnection, dfaulting to urn:schemas-upnp-org:service:WANIPConnection:1"
				// );
				msgFactory = new UPNPMessageFactory(wanIPSrv);
			}
		}
	}

	private boolean testWANInterface(Service srv)
	{
		UPNPMessageFactory tmp = new UPNPMessageFactory(srv);

		ActionMessage msg = tmp.getMessage("GetExternalIPAddress");
		String ipToParse = null;
		try
		{
			ipToParse = msg.service().getOutActionArgumentValue("NewExternalIPAddress");
		}
		catch (UPNPResponseException ex)
		{
			// ok probably not the IP interface
		}
		catch (IOException ex)
		{
			// not really normal
			// log.warn( "IOException occurred during device detection",
			// ex );
		}
		if (ipToParse != null && ipToParse.length() > 0 && !ipToParse.equals("0.0.0.0"))
		{
			try
			{
				return InetAddress.getByName(ipToParse) != null;
			}
			catch (UnknownHostException ex)
			{
				// ok a crappy IP provided, definitely the wrong
				// interface..
			}
		}
		return false;
	}

	/**
	 * Retrieves the IDG UNPNRootDevice object
	 * 
	 * @return the UNPNRootDevie object bound to this object
	 */
	public RootDevice getIGDRootDevice()
	{
		return igd;
	}

	/**
	 * Lookup all the IGD (IP or PPP) devices on the network. If a device
	 * implements both IP and PPP, the active service will be used for nat
	 * mappings.
	 * 
	 * @param timeout
	 *            the timeout in ms to listen for devices response, -1 for
	 *            default value
	 * @return an array of devices to play with or null if nothing found.
	 * @throws IOException
	 *             if some IO Exception occurs during discovery
	 */
	public static Collection<InternetGatewayDevice> getDevices(int timeout) throws IOException
	{
		return lookupDeviceDevices(timeout, Discovery.DEFAULT_TTL, Discovery.DEFAULT_MX, true, true, null);
	}

	/**
	 * Lookup all the IGD (IP urn:schemas-upnp-org:service:WANIPConnection:1, or
	 * PPP urn:schemas-upnp-org:service:WANPPPConnection:1) devices for a given
	 * network interface. If a device implements both IP and PPP, the active
	 * service will be used for nat mappings.
	 * 
	 * @param timeout
	 *            the timeout in ms to listen for devices response, -1 for
	 *            default value
	 * @param ttl
	 *            the discovery ttl such as
	 *            {@link net.tomp2p.upnp.Discovery#DEFAULT_TTL}
	 * @param mx
	 *            the discovery mx such as
	 *            {@link net.tomp2p.upnp.Discovery#DEFAULT_MX}
	 * @param ni
	 *            the network interface where to lookup IGD devices
	 * @return an array of devices to play with or null if nothing found.
	 * @throws IOException
	 *             if some IO Exception occurs during discovery
	 */
	public static Collection<InternetGatewayDevice> getDevices(int timeout, int ttl, int mx, NetworkInterface ni)
			throws IOException
	{
		return lookupDeviceDevices(timeout, ttl, mx, true, true, ni);
	}

	private static Collection<InternetGatewayDevice> lookupDeviceDevices(int timeout, int ttl, int mx,
			boolean WANIPConnection, boolean WANPPPConnection, NetworkInterface ni) throws IOException
	{
		Collection<RootDevice> devices = Discovery.discover(timeout == -1 ? Discovery.DEFAULT_TIMEOUT : timeout, ttl,
				mx, "urn:schemas-upnp-org:device:InternetGatewayDevice:1", ni);

		if (devices == null) return null;

		Set<InternetGatewayDevice> valid = new HashSet<InternetGatewayDevice>();
		for (RootDevice device : devices)
		{
			try
			{
				valid.add(new InternetGatewayDevice(device, WANIPConnection, WANPPPConnection));
			}
			catch (UnsupportedOperationException ex)
			{
				// the device is either not IP or PPP

				// log.debug(
				// "UnsupportedOperationException during discovery " +
				// ex.getMessage() );
			}
		}
		if (valid.size() == 0)
		{
			return null;
		}
		return valid;
	}

	/**
	 * Retrieves the external IP address
	 * 
	 * @return a String representing the external IP
	 * @throws UPNPResponseException
	 *             if the devices returns an error code
	 * @throws IOException
	 *             if some error occurs during communication with the device
	 */
	public String getExternalIPAddress() throws UPNPResponseException, IOException
	{
		ActionMessage msg = msgFactory.getMessage("GetExternalIPAddress");
		return msg.service().getOutActionArgumentValue("NewExternalIPAddress");
	}

	/**
	 * Retrieves a generic port mapping entry.
	 * 
	 * @param newPortMappingIndex
	 *            the index to lookup in the nat table of the upnp device
	 * @return an action response Object containing the following fields :
	 *         NewRemoteHost, NewExternalPort, NewProtocol, NewInternalPort,
	 *         NewInternalClient, NewEnabled, NewPortMappingDescription,
	 *         NewLeaseDuration or null if the index does not exists
	 * @throws IOException
	 *             if some error occurs during communication with the device
	 * @throws UPNPResponseException
	 *             if some unexpected error occurs on the UPNP device
	 */
	public ActionResponse getGenericPortMappingEntry(int newPortMappingIndex) throws IOException, UPNPResponseException
	{

		ActionMessage msg = msgFactory.getMessage("GetGenericPortMappingEntry");
		msg.setInputParameter("NewPortMappingIndex", newPortMappingIndex);

		try
		{
			return msg.service();
		}
		catch (UPNPResponseException ex)
		{
			if (ex.getDetailErrorCode() == 714)
			{
				return null;
			}
			throw ex;
		}

	}

	/**
	 * Retrieves information about a specific port mapping
	 * 
	 * @param remoteHost
	 *            the remote host ip to check, null if wildcard
	 * @param externalPort
	 *            the port to check
	 * @param protocol
	 *            the protocol for the mapping, either TCP or UDP
	 * @return an action response Object containing the following fields :
	 *         NewInternalPort, NewInternalClient, NewEnabled,
	 *         NewPortMappingDescription, NewLeaseDuration or null if no such
	 *         entry exists in the device NAT table
	 * @throws IOException
	 *             if some error occurs during communication with the device
	 * @throws UPNPResponseException
	 *             if some unexpected error occurs on the UPNP device
	 */
	public ActionResponse getSpecificPortMappingEntry(String remoteHost, int externalPort, String protocol)
			throws IOException, UPNPResponseException
	{
		remoteHost = remoteHost == null ? "" : remoteHost;
		checkPortMappingProtocol(protocol);
		checkPortRange(externalPort);

		ActionMessage msg = msgFactory.getMessage("GetSpecificPortMappingEntry");
		msg.setInputParameter("NewRemoteHost", remoteHost).setInputParameter("NewExternalPort", externalPort)
				.setInputParameter("NewProtocol", protocol);

		try
		{
			return msg.service();
		}
		catch (UPNPResponseException ex)
		{
			if (ex.getDetailErrorCode() == 714)
			{
				return null;
			}
			throw ex;
		}
	}
	
	public boolean addPortMapping(String description, String protocol, String internalHost, int externalPort,
			int internalPort) throws IOException, UPNPResponseException
	{
		//we need to be sure that igd.getLocalIP().getHostAddress() is in the network of internalHost
		return addPortMapping(description, protocol, null, externalPort, internalHost, internalPort, 0);
	}

	/**
	 * Configures a nat entry on the UPNP device.
	 * 
	 * @param description
	 *            the mapping description, null for no description
	 * @param protocol
	 *            the protocol, either TCP or UDP
	 * @param remoteHost
	 *            the remote host ip for this entry, null for a wildcard value
	 * @param externalPort
	 *            the external port to open on the UPNP device an map on the
	 *            internal client, 0 for a wildcard value
	 * @param internalClient
	 *            the internal client ip where data should be redirected
	 * @param internalPort
	 *            the internal client port where data should be redirected
	 * @param leaseDuration
	 *            the lease duration in seconds 0 for an infinite time
	 * @return true if the port is mapped false if the mapping is already done
	 *         for another internal client
	 * @throws IOException
	 *             if some error occurs during communication with the device
	 * @throws UPNPResponseException
	 *             if the device does not accept some settings :<br/>
	 *             402 Invalid Args See UPnP Device Architecture section on
	 *             Control<br/>
	 *             501 Action Failed See UPnP Device Architecture section on
	 *             Control<br/>
	 *             715 WildCardNotPermittedInSrcIP The source IP address cannot
	 *             be wild-carded<br/>
	 *             716 WildCardNotPermittedInExtPort The external port cannot be
	 *             wild-carded <br/>
	 *             724 SamePortValuesRequired Internal and External port values
	 *             must be the same<br/>
	 *             725 OnlyPermanentLeasesSupported The NAT implementation only
	 *             supports permanent lease times on port mappings<br/>
	 *             726 RemoteHostOnlySupportsWildcard RemoteHost must be a
	 *             wildcard and cannot be a specific IP address or DNS name<br/>
	 *             727 ExternalPortOnlySupportsWildcard ExternalPort must be a
	 *             wildcard and cannot be a specific port value
	 */
	public boolean addPortMapping(String description, String protocol, String remoteHost, int externalPort,
			String internalClient, int internalPort, int leaseDuration) throws IOException, UPNPResponseException
	{
		remoteHost = remoteHost == null ? "" : remoteHost;
		checkPortMappingProtocol(protocol);
		if (externalPort != 0)
		{
			checkPortRange(externalPort);
		}
		checkPortRange(internalPort);
		description = description == null ? "" : description;
		if (leaseDuration < 0)
		{
			throw new IllegalArgumentException("Invalid leaseDuration (" + leaseDuration + ") value");
		}

		ActionMessage msg = msgFactory.getMessage("AddPortMapping");
		msg.setInputParameter("NewRemoteHost", remoteHost).setInputParameter("NewExternalPort", externalPort)
				.setInputParameter("NewProtocol", protocol).setInputParameter("NewInternalPort", internalPort)
				.setInputParameter("NewInternalClient", internalClient).setInputParameter("NewEnabled", true)
				.setInputParameter("NewPortMappingDescription", description)
				.setInputParameter("NewLeaseDuration", leaseDuration);
		try
		{
			msg.service();
			return true;
		}
		catch (UPNPResponseException ex)
		{
			if (ex.getDetailErrorCode() == 718)
			{
				return false;
			}
			throw ex;
		}
	}

	/**
	 * Deletes a port mapping on the IDG device
	 * 
	 * @param remoteHost
	 *            the host ip for which the mapping was done, null value for a
	 *            wildcard value
	 * @param externalPort
	 *            the port to close
	 * @param protocol
	 *            the protocol for the mapping, TCP or UDP
	 * @return true if the port has been unmapped correctly otherwise false (
	 *         entry does not exists ).
	 * @throws IOException
	 *             if some error occurs during communication with the device
	 * @throws UPNPResponseException
	 *             if the devices returns an error message
	 */
	public boolean deletePortMapping(String remoteHost, int externalPort, String protocol) throws IOException,
			UPNPResponseException
	{

		remoteHost = remoteHost == null ? "" : remoteHost;
		checkPortMappingProtocol(protocol);
		checkPortRange(externalPort);
		ActionMessage msg = msgFactory.getMessage("DeletePortMapping");
		msg.setInputParameter("NewRemoteHost", remoteHost).setInputParameter("NewExternalPort", externalPort)
				.setInputParameter("NewProtocol", protocol);
		try
		{
			msg.service();
			return true;
		}
		catch (UPNPResponseException ex)
		{
			if (ex.getDetailErrorCode() == 714)
			{
				return false;
			}
			throw ex;
		}
	}

	/**
	 * Retrieves the current number of mapping in the NAT table
	 * 
	 * @return the nat table current number of mappings or null if the device
	 *         does not allow to query state variables
	 * @throws IOException
	 *             if some error occurs during communication with the device
	 * @throws UPNPResponseException
	 *             if the devices returns an error message with error code other
	 *             than 404
	 */
	public Integer getNatMappingsCount() throws IOException, UPNPResponseException
	{

		Integer rtrval = null;
		StateVariableMessage natTableSize = msgFactory.getStateVariableMessage("PortMappingNumberOfEntries");
		try
		{
			StateVariableResponse resp = natTableSize.service();
			rtrval = new Integer(resp.getStateVariableValue());
		}
		catch (UPNPResponseException ex)
		{
			// 404 can happen if device do not implement state variables
			// queries
			if (ex.getDetailErrorCode() != 404)
			{
				throw ex;
			}
		}
		return rtrval;
	}

	private void checkPortMappingProtocol(String prot) throws IllegalArgumentException
	{
		if (prot == null || !prot.equals("TCP") && !prot.equals("UDP"))
		{
			throw new IllegalArgumentException("PortMappingProtocol must be either TCP or UDP");
		}
	}

	private void checkPortRange(int port) throws IllegalArgumentException
	{
		if (port < 1 || port > 65535)
		{
			throw new IllegalArgumentException("Port range must be between 1 and 65535");
		}
	}

}
