/*
 * ====================================================================
 * ======== 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.DatagramPacket;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.URL;
import java.util.Collection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import net.tomp2p.utils.Timings;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Class to discover an UPNP device on the network.</br> A multicast
 * socket will be created to discover devices, the binding port for
 * this socket is set to 1901, if this is causing a problem you can
 * use the net.sbbi.upnp.Discovery.bindPort system property to specify
 * another port. The discovery methods only accept matching device
 * description and broadcast message response IP to avoid a security
 * flaw with the protocol. If you are not happy with such behaviour
 * you can set the net.sbbi.upnp.ddos.matchip system property to false
 * to avoid this check.
 * 
 * @author <a href="mailto:superbonbon@sbbi.net">SuperBonBon</a>
 * @version 1.0
 */

public class Discovery
{
	final private static Logger logger = LoggerFactory.getLogger(Discovery.class);
	/***/
	public final static String ROOT_DEVICES = "upnp:rootdevice";

	/***/
	public final static String ALL_DEVICES = "ssdp:all";

	/***/
	public static final int DEFAULT_MX = 3;

	/***/
	public static final int DEFAULT_TTL = 4;

	/***/
	public static final int DEFAULT_TIMEOUT = 1500;

	/***/
	public static final String DEFAULT_SEARCH = ALL_DEVICES;

	/***/
	public static final int DEFAULT_SSDP_SEARCH_PORT = 1901;

	/***/
	public final static String SSDP_IP = "239.255.255.250";

	/***/
	public final static int SSDP_PORT = 1900;

	/**
	 * Devices discovering on all network interfaces with default
	 * values, all root devices will be searched
	 * 
	 * @return an array of UPNP Root device or null if nothing found
	 *         with the default timeout. Null does NOT means that no
	 *         UPNP device is available on the network. It only means
	 *         that for this default timeout no devices responded or
	 *         that effectively no devices are available at all.
	 * @throws IOException
	 *            if some IOException occurs during discovering
	 */
	public static Collection<RootDevice> discover() throws IOException
	{
		return discover( DEFAULT_TIMEOUT, DEFAULT_TTL, DEFAULT_MX, DEFAULT_SEARCH );
	}

	/**
	 * Devices discovering on all network interfaces with a given root
	 * device to search
	 * 
	 * @param searchTarget
	 *           the device URI to search
	 * @return an array of UPNP Root device that matches the search or
	 *         null if nothing found with the default timeout. Null
	 *         does NOT means that no UPNP device is available on the
	 *         network. It only means that for this given timeout no
	 *         devices responded or that effectively no devices are
	 *         available at all.
	 * @throws IOException
	 *            if some IOException occurs during discovering
	 */
	public static Collection<RootDevice> discover( String searchTarget ) throws IOException
	{
		return discover( DEFAULT_TIMEOUT, DEFAULT_TTL, DEFAULT_MX, searchTarget );
	}

	/**
	 * Devices discovering on all network interfaces with a given
	 * timeout and a given root device to search
	 * 
	 * @param timeOut
	 *           the time allowed for a device to give a response
	 * @param searchTarget
	 *           the device URI to search
	 * @return an array of UPNP Root device that matches the search or
	 *         null if nothing found with the given timeout. Null does
	 *         NOT means that no UPNP device is available on the
	 *         network. It only means that for this given timeout no
	 *         devices responded or that effectively no devices are
	 *         available at all.
	 * @throws IOException
	 *            if some IOException occurs during discovering
	 */
	public static Collection<RootDevice> discover( int timeOut, String searchTarget )
			throws IOException
	{
		return discover( timeOut, DEFAULT_TTL, DEFAULT_MX, searchTarget );
	}

	/**
	 * Devices discovering on all network interfaces with a given
	 * timeout and a given root device to search, as well as a ttl and
	 * mx param
	 * 
	 * @param timeOut
	 *           the timeout for the a device to give a reponse
	 * @param ttl
	 *           the UDP socket packets time to live
	 * @param mx
	 *           discovery message mx http header field value
	 * @param searchTarget
	 *           the device URI to search
	 * @return an array of UPNP Root device that matches the search or
	 *         null if nothing found within the given timeout. Null
	 *         return does NOT means that no UPNP device is available
	 *         on the network. It only means that for this given
	 *         timeout no devices responded or that effectively no
	 *         devices are available at all.
	 * @throws IOException
	 *            if some IOException occurs during discovering
	 */
	public static Collection<RootDevice> discover( int timeOut, int ttl, int mx, String searchTarget )
			throws IOException
	{
		return discoverDevices( timeOut, ttl, mx, searchTarget, null );
	}

	/**
	 * Devices discovering with a given timeout and a given root device
	 * to search on an given network interface, as well as a ttl and mx
	 * param
	 * 
	 * @param timeOut
	 *           the timeout for the a device to give a reponse
	 * @param ttl
	 *           the UDP socket packets time to live
	 * @param mx
	 *           discovery message mx http header field value
	 * @param searchTarget
	 *           the device URI to search
	 * @param ni
	 *           the networkInterface where to search devices, null to
	 *           lookup all interfaces
	 * @return an array of UPNP Root device that matches the search or
	 *         null if nothing found within the given timeout. Null
	 *         return does NOT means that no UPNP device is available
	 *         on the network. It only means that for this given
	 *         timeout no devices responded or that effectively no
	 *         devices are available at all.
	 * @throws IOException
	 *            if some IOException occurs during discovering
	 */
	public static Collection<RootDevice> discover( int timeOut, int ttl, int mx,
			String searchTarget, NetworkInterface ni ) throws IOException
	{
		return discoverDevices( timeOut, ttl, mx, searchTarget, ni );
	}

	private static Collection<RootDevice> discoverDevices( int timeOut, int ttl, int mx,
			String searchTarget, NetworkInterface ni ) throws IOException
	{
		if( searchTarget == null || searchTarget.trim().length() == 0 )
		{
			throw new IllegalArgumentException( "Illegal searchTarget" );
		}

		final Map<String, RootDevice> devices = new HashMap<String, RootDevice>();

		DiscoveryResultsHandler handler = new DiscoveryResultsHandler() {

			@Override
			public void discoveredDevice( String usn, String udn, String nt, String maxAge,
					URL location, String firmware, InetAddress localIP )
			{
				synchronized( devices )
				{
					if( !devices.containsKey( usn ) )
					{
						try
						{
							RootDevice device =
									RootDevice.build( location, maxAge, firmware, usn, udn, localIP );
							devices.put( usn, device );
						}
						catch( Exception ex )
						{
							// log.error(
							// "Error occured during upnp root device object creation from location "
							// + location, ex );
							ex.printStackTrace();
						}
					}
				}
			}
		};

		DiscoveryListener.getInstance().registerResultsHandler( handler, searchTarget );
		if( ni == null )
		{
			for( Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces(); e
					.hasMoreElements(); )
			{
				NetworkInterface intf =  e.nextElement();
				sendViaInterface(ttl, mx, searchTarget, intf);
			}
		}
		else
		{
			sendViaInterface(ttl, mx, searchTarget, ni);
		}

		try
		{
			Timings.sleep( timeOut );
		}
		catch( InterruptedException ex )
		{
			// don't care
		}

		DiscoveryListener.getInstance().unRegisterResultsHandler( handler, searchTarget );

		if( devices.size() == 0 )
		{
			return null;
		}
		return devices.values();
	}

	private static void sendViaInterface(int ttl, int mx, String searchTarget, NetworkInterface intf)
	{
		for( Enumeration<InetAddress> adrs = intf.getInetAddresses(); adrs.hasMoreElements(); )
		{
			InetAddress adr =  adrs.nextElement();
			if( adr instanceof Inet4Address && !adr.isLoopbackAddress() )
			{
				try
				{
					sendSearchMessage( adr, ttl, mx, searchTarget );
				}
				catch (IOException ex)
				{
					logger.warn("error sending message" +ex);
				}
			}
		}
	}

	/**
	 * Sends an SSDP search message on the network
	 * 
	 * @param src
	 *           the sender ip
	 * @param ttl
	 *           the time to live
	 * @param mx
	 *           the mx field
	 * @param searchTarget
	 *           the search target
	 * @throws IOException
	 *            if some IO errors occurs during search
	 */
	public static void sendSearchMessage( InetAddress src, int ttl, int mx,
			String searchTarget ) throws IOException
	{

		int bindPort = DEFAULT_SSDP_SEARCH_PORT;
		String port = System.getProperty( "net.sbbi.upnp.Discovery.bindPort" );
		if( port != null )
		{
			bindPort = Integer.parseInt( port );
		}
		InetSocketAddress adr =
				new InetSocketAddress( InetAddress.getByName( Discovery.SSDP_IP ),
						Discovery.SSDP_PORT );

		java.net.MulticastSocket skt = new java.net.MulticastSocket( null );
		skt.bind( new InetSocketAddress( src, bindPort ) );
		skt.setTimeToLive( ttl );
		StringBuilder packet = new StringBuilder();
		packet.append( "M-SEARCH * HTTP/1.1\r\n" );
		packet.append( "HOST: 239.255.255.250:1900\r\n" );
		packet.append( "MAN: \"ssdp:discover\"\r\n" );
		packet.append( "MX: " ).append( mx ).append( "\r\n" );
		packet.append( "ST: " ).append( searchTarget ).append( "\r\n" ).append( "\r\n" );

		String toSend = packet.toString();
		byte[] pk = toSend.getBytes();
		skt.send( new DatagramPacket( pk, pk.length, adr ) );
		skt.disconnect();
		skt.close();
	}

}
