/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2004 Bull S.A.
 * Contact: jonas-team@ow2.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.
 * --------------------------------------------------------------------------
 * $Id: DiscoveryComm.java 15428 2008-10-07 11:20:29Z sauthieg $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.discovery.internal.comm;

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.ow2.jonas.discovery.DiscoveryState;
import org.ow2.jonas.discovery.base.comm.DiscEvent;
import org.ow2.jonas.discovery.base.comm.DiscGreeting;
import org.ow2.jonas.discovery.base.comm.DiscMessage;
import org.ow2.jonas.discovery.internal.manager.DiscoveryManager;
import org.ow2.jonas.discovery.internal.utils.DiscoveryHelper;
import org.ow2.jonas.lib.util.Log;
import org.ow2.jonas.lib.util.NetUtils;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

/**
 * @author <a href="mailto:Takoua.Abdellatif@inria.fr">Takoua Abdellatif </a>
 * @version 1.0
 */
public class DiscoveryComm implements Runnable {
    /**
     * Size of buffer to read incoming packets into.
     */
    public static final int              RECEIVE_BUFFER_SIZE = 1024;
    /**
     * Used to multicast a discovery event on run() execution
     */
    protected MulticastSocket  multicastSocket;
    /**
     * Uset to send a discovery event as response to a discovey message
     */
    protected DatagramSocket unicastSocket;

    /**
     * My manager's listening port
     */
    private int port;
    /**
     * My manager's multicast IP address
     */
    private InetAddress destAddress;
    /**
     * Set to false if the thread is stopped
     */
    protected boolean notStopped = true;
    /**
     * Time to live for multicatst packets
     */
    private int ttl = 1; // why 1 ??

    /**
     * Name for this jonas instance.
     */
    protected String jonasName = null;
    /**
     * Domain name that this instance belongs to.
     */
    protected String domainName = null;

    /**
     * The server ID of the jonas instance.
     */
    protected String serverId = null;


    /**
     * MBean server connection URLs for this server.
     */
    protected String[] urls = null;

    /**
     * logger
     */
    private static Logger logger = Log.getLogger(Log.JONAS_DISCOVERY_PREFIX);
    /**
     * Constructs a DiscoveryComm associated to the DiscoveryManager
     * @param dm DiscoveryManager to which this thread is associated
     */
    public DiscoveryComm(DiscoveryManager dm) {
        this.port = dm.getListeningPort();
        try {
            this.destAddress = InetAddress.getByName(dm.getListeningIp());
            this.ttl = dm.getTimeToLive();
            this.jonasName = dm.getJonasName();
            this.domainName = dm.getDomainName();
            this.urls = dm.getUrls();
            this.serverId = dm.getServerId();
        } catch (UnknownHostException e) {
            logger.log(BasicLevel.ERROR, "Unknown Host", e);
        }
    }

    /**
     * Creates a MulticastSocket and joins the group of multicas host
     * identified by the InetAddress <code>destAddress</code>
     *
      */
    protected void join() {
        try {
            multicastSocket = new MulticastSocket(port);
            multicastSocket.setTimeToLive(ttl);
            multicastSocket.joinGroup(destAddress);
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "multicast ip address is "
                        + destAddress);
                logger.log(BasicLevel.DEBUG, "multicast port is " + port);
            }
        } catch (IOException e) {
            logger.log(BasicLevel.ERROR, "io problem", e);
        }
    }

    /**
     * sends (multicasts) a Discovery Message to the group.
     * @param msg The message to send.
     */
    public void sendNotif(DiscMessage msg) {
        try {
            //send it on the multicast address
            //after transforming the object to a datagram
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, msg);
            }
            byte[] messageBytes = DiscoveryHelper.objectToBytes(msg);
            multicastSocket.send(new DatagramPacket(messageBytes,
                    messageBytes.length, destAddress, port));
        } catch (IOException e1) {
            logger.log(BasicLevel.ERROR, "DiscoveryComm: Error to send notification", e1);
        }

    }
    /**
     * Send response to a DiscoveryMessage
     * @param msg Containes a DiscoveryMessage allowing to inform about the responder
     * (name, state, URLs)
     * @param destAddress the destination address picked up from the request
     * @param port the destination port picked up from the request
     */
    protected void sendResponse(DiscMessage msg, InetAddress destAddress, int port) {
        if (logger.isLoggable(BasicLevel.DEBUG)) {
            logger.log(BasicLevel.DEBUG, "DiscoveryComm : The message to send is "
                    + msg + "Sending it to: " + destAddress + " and port is: " + port);
        }
        try {
            byte[] messageBytes = DiscoveryHelper.objectToBytes(msg);
            if (messageBytes != null) {
                // send the unicast response to the discovery client
                unicastSocket.send(new DatagramPacket(messageBytes,
                        messageBytes.length, destAddress, port));
            }
        } catch (IOException e) {
            logger.log(BasicLevel.ERROR, "DiscoveryComm: Error to send response to discovery message", e);
        }
    }

    /**
     * Create a discovery event to notify about a state change of the
     * event sender
     * @param state
     *         - RUNNING if the sender notifies that it gets running
     *         - STOPPING if the sender notifies that it stops running
     * @return a Discovery event (notification)
     * @throws Exception
     *           is thrown if the jmx service is not reached.
     */
    public DiscEvent createNotifMessage(String state) throws Exception {
        String theHostAddress;
        try {
            theHostAddress = NetUtils.getLocalAddress();
        } catch (UnknownHostException e) {
            logger.log(BasicLevel.ERROR, "Unknown host", e);
            return null;
        }

        if (!state.equals(DiscoveryState.RUNNING)) {
            urls = null;
        }
        // In the case of a notification, the field port is not important since the
        // notifier is not waiting for an acknowledfement.
        DiscEvent resp = new DiscEvent(theHostAddress, port, jonasName, domainName, serverId, urls);
        resp.setState(state);
        return resp;
    }
    /**
     * Construct a new datagram.
     * @param length packets length to be received
     * @return the created datagram
     */
    protected DatagramPacket getDatagram(int length) {
        return new DatagramPacket(new byte[length], length);
    }

    /**
     *
     * @see java.lang.Runnable#run()
     */
    public void run()  {
        // Join the group in order to receive multicast messages
        join();
        // Create notification message containing a discovery event with state RUNNING
        DiscEvent discEventMsg = null;
        try {
            discEventMsg = createNotifMessage(DiscoveryState.RUNNING);
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR,
                "DiscoveryComm:  Unable to create a notification message", e);
        }
        if (discEventMsg != null) {
            // Multicast the message
            sendNotif(discEventMsg);
        }
        // Create the socket to be used for responding
        try {
            unicastSocket = new DatagramSocket();
        } catch (SocketException e3) {
            logger.log(BasicLevel.ERROR, " Socket exception", e3);
            return;
        }
        try {
            while (notStopped) {
                DatagramPacket datagram = null;
                Object objReceived = null;
                try {
                    datagram = getDatagram(RECEIVE_BUFFER_SIZE);
                    multicastSocket.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) {
                    // The DiscEvents are ignored
                        if ((objReceived instanceof DiscEvent) || (objReceived instanceof DiscGreeting) ) {
                            if (logger.isLoggable(BasicLevel.DEBUG)) {
                                logger.log(BasicLevel.DEBUG,
                                        "This discovery event/greeting is ignored " + objReceived);
                            }
                        } else {
                            DiscMessage request = (DiscMessage) objReceived;
                            if (logger.isLoggable(BasicLevel.DEBUG)) {
                                logger.log(BasicLevel.DEBUG,
                                        "A discovery message is received "
                                        + objReceived);
                            }
                            if (discEventMsg != null) {
                                // Use the address in the datagram packet instead of trusting the message.
                                InetAddress destAddress = datagram.getAddress();
                                int destPort = request.getSourcePort();
                                sendResponse(discEventMsg, destAddress, destPort);
                            }
                        }
                }
                datagram = null;
            }
        } catch (ClassNotFoundException e) {
            logger.log(BasicLevel.ERROR, e);
        }
    }
    /**
     * sends a notification message to notify that the server is stopping.
     *
     */
    public void stop() {
        // send a notification message of type STOPPING
        DiscEvent msg = null;
        try {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Sending a STOPPING DiscEvent.");
            }
            msg = createNotifMessage(DiscoveryState.STOPPING);
        } catch (Exception e) {
            logger.log(BasicLevel.ERROR, e);
        }
        if (msg != null) {
            sendNotif(msg);
        }
        Thread.interrupted();
    }

    /**
     * @param jonasName The jonasName to set.
     */
    protected void setJonasName(String jonasName) {
        this.jonasName = jonasName;
    }
    /**
     * @param domainName The domainName to set.
     */
    protected void setDomainName(String domainName) {
        this.domainName = domainName;
    }
    /**
     * @param urls The urls to set.
     */
    protected void setUrls(String[] urls) {
        this.urls = urls;
    }

    public String getServerId() {
        return serverId;
    }

    public void setServerId(String serverId) {
        this.serverId = serverId;
    }
}