/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2004 Bull S.A.
 * Contact: jonas-team@objectweb.org
 *
 * This library is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as published by the
 * Free Software Foundation; either version 2.1 of the License, or any later
 * version.
 *
 * This library 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 Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, write to the Free Software Foundation,
 * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
 *
 */

package org.objectweb.jonas.discovery;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.SocketException;
import java.net.UnknownHostException;

import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.jmx.JonasObjectName;
import org.objectweb.jonas.management.j2eemanagement.J2EEDomain;
import org.objectweb.jonas.management.monitoring.DomainMonitor;
import org.objectweb.jonas.service.ServiceException;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.objectweb.jonas.discovery.DiscoveryHelper;

/**
 * @author <a href="mailto:Takoua.Abdellatif@inria.fr">Takoua Abdellatif </a>
 * @author Adriana Danes
 * @version 1.0
 */
public class DiscoveryClientListener implements Runnable {

    /**
     * Management notification type for <i>discovery</i> events
     */
    public static final String DISCOVERY_TYPE = "jonas.management.discovery";

    private static int RECEIVE_BUFFER_SIZE = 1024;

    /**
     * Used to multicast a discovery message on run() execution
     */
    private MulticastSocket multicastSocket;

    /**
     * Port associated to the multicast socket
     */
    private int port;

    /**
     * IP address for the multicast socket
     */
    private InetAddress groupAddress;

    /**
     * Time to live for multicatst packets
     */
    private int ttl;

    /**
     * Uset to receive a discovery event as response to the sent discovey message
     */
    private DatagramSocket unicastSocket;

    /**
     * Ip address which identify the sender of the discovery message
     */
    private String sourceIp;

    /**
     * Port which identify by the sender of the discovery message
     */
    private int sourcePort;

    /**
     * State information
     */
    private boolean notStopped = true;

    /**
     * Time to wait for a response
     */
    private long timeout = 1000;

    /**
     * Logger
     */
    private static Logger logger = Log.getLogger(Log.JONAS_DISCOVERY_PREFIX);

    /**
     * Constructs a DiscoveryClientListener associated with a DiscoveryClient
     * @param discoveryClient DiscoveryClient to which this thread is associated
     */
    public DiscoveryClientListener(DiscoveryClient discoveryClient) {
        this.port = discoveryClient.getListeningPort();
        try {
            this.groupAddress = InetAddress.getByName(discoveryClient.getListeningIp());
            this.ttl = discoveryClient.getTimeToLive();
        } catch (UnknownHostException e) {
            logger.log(BasicLevel.ERROR, "Invalid host", e);
        }
        this.timeout = discoveryClient.getTimeout();
        this.sourcePort = discoveryClient.getSourcePort();
        this.sourceIp = discoveryClient.getSourceIp();

        // Create a unicast socket to receive responses
        // Do this in constructor so the exception can be caught
        // and acted on before a new thread is created.
        try {
            unicastSocket = new DatagramSocket(sourcePort);
        } catch (SocketException e2) {
            logger.log(BasicLevel.ERROR, "DiscoveryClient : Unable to create a Datagram socket", e2);
            // Could not create datagram socket, so throw a ServiceException.
            throw new ServiceException(
                    "Could not create socket to listen for discovery "
                            + "messages at port: " + sourcePort
                            + ". The port might be in use.");
        }
    }

    /**
     * Sends a discovery message to the server group.
     */
    public void sendDiscoveryMessage(DiscMessage msg) {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "DiscoveryClient : The message to send is " + msg);
        }
        // send the message on the multicast socket
        // after packing it into a datagram
        byte[] messageBytes = null;
        try {
            messageBytes = DiscoveryHelper.objectToBytes(msg);
             if (messageBytes != null) {
                 multicastSocket.send(new DatagramPacket(messageBytes,
                         messageBytes.length, groupAddress, port));
             }
        } catch (IOException e) {
            logger.log(BasicLevel.ERROR, "DiscoveryClient :  Error to send discovery message", e);
        }
    }

    /**
     * @see java.lang.Runnable#run()
     */
    public void run() {
        // Create a multicast socket
        try {
            multicastSocket = new MulticastSocket(port);
            multicastSocket.setTimeToLive(ttl);
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

        // Prepare a discovery message
        DiscMessage msg = new DiscMessage(sourceIp, sourcePort);
        // First send a discovery message
        sendDiscoveryMessage(msg);
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, " DiscoveryClient: Sent Message is" + msg);
        }

        // wait for responses during a Timeout period on the unicast socket.
        long lastTime = timeout + System.currentTimeMillis();
        DiscEvent event = null;
        try {
            while ((notStopped) && System.currentTimeMillis() <= lastTime) {
                DatagramPacket datagram = null;
                Object objReceived = null;
                try {
                    datagram = getDatagram(RECEIVE_BUFFER_SIZE);
                    unicastSocket.receive(datagram);
                    objReceived = DiscoveryHelper.bytesToObject(datagram.getData());
                } catch (RuntimeException e) {
                    logger.log(BasicLevel.DEBUG, "Host received other packet than DataGramSocket packet. That caused following Exception: \n"+e);
                    objReceived = null;
                }catch (IOException e) {
                    logger.log(BasicLevel.DEBUG, "Host received other packet than DataGramSocket packet. That caused following Exception: \n"+e);
                    objReceived = null;
                }
                if (objReceived != null) {
                    if (objReceived instanceof DiscEvent) {
                        event = (DiscEvent) objReceived;
                        if (event.getDomainName().equals(JonasObjectName.getDomain())) {
                            // Trust the datagram packet instead of the encoded URL
                            event.setSourceAddress(datagram.getAddress().getHostAddress());
                            DomainMonitor dm = J2EEDomain.getInstance().getDomainMonitor();
                            if (dm == null) {
                                logger.log(BasicLevel.WARN, "No DomainMonitor");
                            } else {
                                dm.discoveryNotification(event);
                            }
                        }
                    }
                }
                datagram = null;
            }
        } catch (ClassNotFoundException e) {
            logger.log(BasicLevel.ERROR, "DiscoveryClient ClassNotFoundException ", e);
        }
    }

    protected DatagramPacket getDatagram(int length) {
        return new DatagramPacket(new byte[length], length);
    }

    /**
     * Stops the current thread
     */
    public void stop() {
        notStopped = false;
        Thread.interrupted();
    }
}