/*
 * ============================================================================
 *                 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 de.avm.android.tr064.discovery;

/* BASED on UPNP by <sbbi.net>. Modified by AVM GmbH <info@avm.de> */

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.ArrayList;
import android.content.Context;
import de.avm.android.tr064.sax.SAXTr064DescHandler;

/**
 * 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 FritzBoxDiscovery
{
	public interface OnDiscoveredDeviceListener
	{
		/**
		 * Box device found
		 * @param isTR064
		 * 		true for TR-064 UDN
		 * @param udn
		 * 		the UDN
		 * @param location
		 *		the URL of device description
		 * @param server
		 * 		the server info string
		 */
		void onDiscoveredDevice(boolean isTR064, String udn,
				URL location, String server);
	}
	
	public interface OnGetAddressByNameListener
	{
		InetAddress getAddressByNameListener(String host)
				throws UnknownHostException;
	}
	
	private final static String SEARCH_TR064 = SAXTr064DescHandler.DEVICE_TYPE_IGD;
	private final static String SEARCH_UPNP = "urn:schemas-upnp-org:device:InternetGatewayDevice:1";

	private static final int DEFAULT_MX = 5;
	public static final int DEFAULT_TTL = 4;
	public static final int DEFAULT_TIMEOUT = 1500;
	private static final int RETRY_TIMEOUT = 500;
	public static final int DEFAULT_SSDP_SEARCH_PORT = 1901;

	public final static String SSDP_IP = "239.255.255.250";
	private final static int SSDP_PORT = 1900;

	/**
	 * Overload InetAddress.getByName(host) 
	 * 
	 * @param listener
	 * 		listener to perform getByName
	 */
	public static void setOnGetAddressByNameListener(
			OnGetAddressByNameListener listener)
	{
		FritzBoxDiscoveryListener.getInstance()
				.setOnGetAddressByNameListener(listener);
	}
	
	/**
	 * Devices discovering on all network interfaces with a given root device to
	 * search.
	 * 
	 * @param c
	 *            a valid context
	 *            
	 * @param timeOut
	 *            the time out
	 * @param listener
	 *            called for every discovered UPNP Root device that matches
	 *            the search (might be called more than once per device)
	 * 
	 * @throws IOException
	 *             if some IOException occurs during discovering
	 */
	public static void discover(Context c, int timeout,
			ArrayList<InetAddress> interfaceAddresses,
			OnDiscoveredDeviceListener listener)
			throws IOException
	{
		discoverDevices(timeout, DEFAULT_TTL, DEFAULT_MX, c,
				interfaceAddresses, listener);
	}

	/**
	 * Discover devices.
	 * 
	 * @param timeOut
	 *            the time out
	 * @param ttl
	 *            the time to live
	 * @param mx
	 *            the mx field
	 * @param searchTarget
	 *            the search target
	 * @param c
	 *            a valid context
	 * @param listener
	 *            called for every discovered device (might be called more
	 *            than once er device
	 * 
	 * @throws IOException
	 *             Signals that an I/O exception has occurred.
	 */
	private static void discoverDevices(int timeOut, int ttl, int mx, Context c,
			ArrayList<InetAddress> interfaceAddresses,
			final OnDiscoveredDeviceListener listener)
			throws IOException
	{
		FritzBoxDiscoveryResultsHandler handlerTr64 =
				new FritzBoxDiscoveryResultsHandler()
		{
			public void discoveredDevice(String usn, String udn, String nt,
					String maxAge, URL location, String server)
			{
				if (listener != null)
					listener.onDiscoveredDevice(true, udn, location, server);
			}
		};

		FritzBoxDiscoveryResultsHandler handlerUpnp =
				new FritzBoxDiscoveryResultsHandler()
		{
			public void discoveredDevice(String usn, String udn, String nt,
					String maxAge, URL location, String server)
			{
				if (listener != null)
					listener.onDiscoveredDevice(false, udn, location, server);
			}
		};

		FritzBoxDiscoveryListener.getInstance().registerResultsHandler(handlerTr64,
				SEARCH_TR064);
		FritzBoxDiscoveryListener.getInstance().registerResultsHandler(handlerUpnp,
				SEARCH_UPNP);

		if (interfaceAddresses.size() > 0)
		{
			// sometimes response packets on the network are not received here
			// so send multiple searches as workaround
			int doneTimeout = 0;
			while (doneTimeout < timeOut)
			{
				for (InetAddress adr : interfaceAddresses)
				{
					sendSearchMessage(adr, ttl, mx, SEARCH_TR064);
					sendSearchMessage(adr, ttl, mx, SEARCH_UPNP);
				}
				int nextTimeout = timeOut - doneTimeout;
				if (nextTimeout > RETRY_TIMEOUT) nextTimeout = RETRY_TIMEOUT; 
				try { Thread.sleep(nextTimeout); }
				catch (InterruptedException ex) { /* don't care */ }
				doneTimeout += nextTimeout;
			}
		}

		FritzBoxDiscoveryListener.getInstance().unRegisterResultsHandler(
				handlerTr64, SEARCH_TR064);
		FritzBoxDiscoveryListener.getInstance().unRegisterResultsHandler(
				handlerUpnp, SEARCH_UPNP);
	}

	/**
	 * 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 {

		InetSocketAddress adr = new InetSocketAddress(FritzBoxDiscoveryListener.getInstance()
				.getByName(SSDP_IP), SSDP_PORT);

		java.net.MulticastSocket skt = new java.net.MulticastSocket(null);
		skt.bind(new InetSocketAddress(src, DEFAULT_SSDP_SEARCH_PORT));
		// due to issue 9813 of Android project's bug tracking this is broken with
		// Android 2.0, packet is always sent with TTL==1
		// a fix is scheduled for Versions following Android 2.2 
		skt.setTimeToLive(ttl);
		StringBuffer packet = new StringBuffer();
		packet.append("M-SEARCH * HTTP/1.1\r\n");
		packet.append("HOST: " + SSDP_IP + ":" + Integer.toString(SSDP_PORT) + "\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();
	}

}