/*
 * ============================================================================
 *                 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.sbbi.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.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import net.sbbi.upnp.devices.UPNPRootDevice;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * 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
 */
@SuppressWarnings("unchecked")
public class Discovery {

  final static Log log = LogFactory.getLog( 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 UPNPRootDevice[] 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 UPNPRootDevice[] 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 UPNPRootDevice[] 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 UPNPRootDevice[] 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 UPNPRootDevice[] discover( int timeOut, int ttl, int mx, String searchTarget, NetworkInterface ni ) throws IOException {
    return discoverDevices( timeOut, ttl, mx, searchTarget, ni );
  }
 
  private static UPNPRootDevice[] 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 devices = new HashMap();
    
    DiscoveryResultsHandler handler = new DiscoveryResultsHandler() {

      public void discoveredDevice( String usn, String udn, String nt, String maxAge, URL location, String firmware ) {
        synchronized( devices ) {
          if ( ! devices.containsKey( usn ) ) {
            try {
              UPNPRootDevice device = new UPNPRootDevice( location, maxAge, firmware, usn, udn );
              devices.put( usn, device );
            } catch ( Exception ex ) {
              log.error( "Error occured during upnp root device object creation from location " + location, ex );
            }
          }
        }
      }
    };
    
    DiscoveryListener.getInstance().registerResultsHandler( handler, searchTarget );
    if ( ni == null ) {
      for ( Enumeration e = NetworkInterface.getNetworkInterfaces(); e.hasMoreElements(); ) {
        NetworkInterface intf = (NetworkInterface)e.nextElement();
        for ( Enumeration adrs = intf.getInetAddresses(); adrs.hasMoreElements(); ) {
          InetAddress adr = (InetAddress)adrs.nextElement();
          if ( adr instanceof Inet4Address && !adr.isLoopbackAddress()  ) {
            sendSearchMessage( adr, ttl, mx, searchTarget );
          }
        }
      }
    } else {
      for ( Enumeration adrs = ni.getInetAddresses(); adrs.hasMoreElements(); ) {
        InetAddress adr = (InetAddress)adrs.nextElement();
        if ( adr instanceof Inet4Address && !adr.isLoopbackAddress()  ) {
          sendSearchMessage( adr, ttl, mx, searchTarget );
        }
      }
    }

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

    DiscoveryListener.getInstance().unRegisterResultsHandler( handler, searchTarget );
    
    if ( devices.size() == 0 ) {
      return null;
    }
    int j = 0;
    UPNPRootDevice[] rootDevices = new UPNPRootDevice[devices.size()];
    for ( Iterator i = devices.values().iterator(); i.hasNext(); ) {
      rootDevices[j++] = (UPNPRootDevice)i.next();
    }
    return rootDevices;
   
  }
  
  /**
   * 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 );
    StringBuffer packet = new StringBuffer();
    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" );
    if ( log.isDebugEnabled() ) log.debug( "Sending discovery message on 239.255.255.250:1900 multicast address form ip " + src.getHostAddress() + ":\n" + packet.toString() );
    String toSend = packet.toString();
    byte[] pk = toSend.getBytes();
    skt.send( new DatagramPacket( pk, pk.length, adr ) );
    skt.disconnect();
    skt.close();
  }
 
}