/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This 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 (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.remoting.detection.multicast;

import org.jboss.remoting.InvokerLocator;
import org.jboss.remoting.InvokerRegistry;
import org.jboss.remoting.ServerInvoker;
import org.jboss.remoting.detection.AbstractDetector;
import org.jboss.remoting.detection.Detection;
import org.jboss.remoting.ident.Identity;
import org.jboss.remoting.loading.ClassUtil;

import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.util.ArrayList;
import java.util.List;

import EDU.oswego.cs.dl.util.concurrent.SynchronizedBoolean;

/**
 * MulticastDetector
 * 
 * @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
 * @author <a href="mailto:adrian.brock@happeningtimes.com">Adrian Brock</a>
 * @version $Revision: 1.7.4.5 $
 */
public class MulticastDetector extends AbstractDetector implements MulticastDetectorMBean
{

    public static final int DEFAULT_PORT = 2410;
    public static final String DEFAULT_IP = "224.1.9.1";

    private InetAddress addr;
    private InetAddress bindAddr;
    private int port = DEFAULT_PORT;
    private MulticastSocket socket;
    private Identity identity;
    private Listener listener = new Listener();
    private Detection lastDetection;
    private byte lastDetectionBuf[];
    private SynchronizedBoolean invalidClassError=new SynchronizedBoolean(false);

    /**
     * return the multicast address of the detector
     * 
     * @return 
     */
    public InetAddress getAddress()
    {
        return addr;
    }

    /**
     * set the interface address of the multicast
     * 
     * @param ip 
     */
    public void setAddress(InetAddress ip)
    {
        this.addr = ip;
    }

    /**
     * return the bind address of the detector
     * 
     * @return 
     */
    public InetAddress getBindAddress()
    {
        return bindAddr;
    }

    /**
     * set the bind address of the multicast
     * 
     * @param ip 
     */
    public void setBindAddress(InetAddress ip)
    {
        this.bindAddr = ip;
    }

    /**
     * get the port that the detector is multicasting to
     * 
     * @return 
     */
    public int getPort()
    {
        return port;
    }

    /**
     * set the port for detections to be multicast to
     * 
     * @param port 
     */
    public void setPort(int port)
    {
        this.port = port;
    }

    /**
     * called by MBeanServer to start the mbean lifecycle
     * 
     * @throws Exception 
     */
    public void start() throws Exception
    {
        if (addr == null)
        {
            this.addr = InetAddress.getByName(DEFAULT_IP);
        }
        // check to see if we're running on a machine with loopback and no NIC
        InetAddress localHost = InetAddress.getLocalHost();
        if (bindAddr == null && localHost.getHostAddress().equals("127.0.0.1"))
        {
            // use this to bind so multicast will work w/o network
            this.bindAddr = localHost;
        }
        identity = Identity.get(mbeanserver);
        socket = new MulticastSocket(port);
        if (bindAddr != null)
            socket.setInterface(bindAddr);
        socket.joinGroup(addr);
        if (listener == null)
        {
            listener = new Listener();
        }
        listener.start();
        super.start();
        if (log.isInfoEnabled())
        {
            log.info("Multicast Detector listening on " + addr + ":" + port);
        }
    }

    /**
     * called by the MBeanServer to stop the mbean lifecycle
     * 
     * @throws Exception 
     */
    public void stop() throws Exception
    {
        super.stop();
        if (listener != null)
        {
            listener.running = false;
            listener.interrupt();
            listener = null;
        }
        if (socket != null)
        {
            socket.leaveGroup(addr);
            socket.close();
            socket = null;
        }
        if (log.isInfoEnabled())
        {
            log.info("Multicast Detector shutdown on " + addr + ":" + port);
        }
    }

    /**
     * subclasses must implement to provide the specific heartbeat protocol
     * for this server to send out to other servers on the network
     */
    protected void heartbeat()
    {
        ServerInvoker invokers[] = InvokerRegistry.getServerInvokers();
        if (socket == null || invokers == null || invokers.length <= 0)
        {
            return;
        }
        List l = new ArrayList(invokers.length);
        for (int c = 0; c < invokers.length; c++)
        {
            if (invokers[c].isStarted())
            {
                l.add(invokers[c].getLocator());
            }
        }
        if (l.isEmpty())
        {
            return;
        }
        InvokerLocator locators[] = (InvokerLocator[]) l.toArray(new InvokerLocator[l.size()]);
        if (socket != null)
        {
            try
            {
                byte buf[] = null;
                Detection msg = new Detection(Identity.get(mbeanserver), locators);
                if (lastDetection != null && msg.equals(lastDetection))
                {
                    buf = lastDetectionBuf;
                }
                else
                {
                    // serialize and then remember it so we don't serialize each time
                    buf = ClassUtil.serialize(msg);
                    lastDetection = msg;
                    lastDetectionBuf = buf;
                }
                if (DETECTOR_DEBUG && log.isDebugEnabled())
                {
                    log.debug("sending heartbeat: " + msg);
                }
                DatagramPacket p = new DatagramPacket(buf, buf.length, addr, port);
                socket.send(p);
            }
            catch (Throwable ex)
            {
                if (isRunning())
                {
                    // its failed
                    log.debug("heartbeat failed", ex);
                }
            }
        }
    }

    private void listen(DatagramPacket p, byte[] buf)
    {
        if (socket != null && listener != null && listener.running)
        {
            try
            {
                // should block until we get a multicast
                socket.receive(p);

                // take the multicast, and deserialize into the detection event
                Detection msg = (Detection) ClassUtil.deserialize(buf);
                if (DETECTOR_DEBUG && log.isDebugEnabled())
                    log.debug("received detection: " + msg);

                // let the subclass do the hard work off handling detection
                detect(msg);
            }
            catch (Throwable e)
            {
                if (e instanceof java.io.InvalidClassException)
                {
                    synchronized(invalidClassError)
                    {
                        if (invalidClassError.get()==false)
                        {
                            invalidClassError.set(true);
                            log.warn("Invalid detection received from: "+p.getAddress()+":"+p.getPort()+", class looks invalid/errored - you will not see this error again ... surpressing if received again",e);
                        }
                    }
                    return;
                }
                if (socket != null && listener != null && listener.running && isRunning())
                {
                    log.debug("Error receiving detection", e);
                }
            }
        }
    }

    private final class Listener extends Thread
    {
        boolean running = true;

        Listener()
        {
            super("MulticastDetector - Detection Receiver");
        }

        public void run()
        {
            byte[] buf = new byte[4000];
            DatagramPacket p = new DatagramPacket(buf, 0, buf.length);
            while (isRunning() && running)
            {
                try
                {
                    listen(p, buf);
                }
                catch (Exception ex)
                {
                    if (!running && isRunning() == false)
                    {
                        break;
                    }
                    if (log.isDebugEnabled()) log.debug("exception received during listen", ex);
                }
            }
        }
    }
}
