/** 
 * 
 * Copyright 2004 Protique Ltd
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 * 
 **/

package org.activemq.transport.multicast;
import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Iterator;
import java.util.Map;
import javax.jms.JMSException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.io.impl.DefaultWireFormat;
import org.activemq.message.ActiveMQMessage;
import org.activemq.message.ActiveMQObjectMessage;
import org.activemq.message.Packet;
import org.activemq.message.PacketListener;
import org.activemq.transport.DiscoveryAgentSupport;
import org.activemq.transport.DiscoveryEvent;
import org.activemq.util.IdGenerator;
import EDU.oswego.cs.dl.util.concurrent.ConcurrentHashMap;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedBoolean;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedLong;


/**
 * An agent used to discover other instances of a service
 * 
 * @version $Revision: 1.1.1.1 $
 */
public class MulticastDiscoveryAgent extends DiscoveryAgentSupport implements PacketListener, Runnable {
    private static final Log log = LogFactory.getLog(MulticastDiscoveryAgent.class);
    /**
     * default URI used for discovery
     */
    public static final String DEFAULT_DISCOVERY_URI = "multicast://224.1.2.3:6066";
    private static final String KEEP_ALIVE_TYPE = "KEEP_ALIVE";
    private static final String SERVICE_TYPE = "SERVICE";
    private static final String ALIVE_TYPE = "ALIVE_TYPE";
    private static final String SERVICE_NAME = "SERVICE_NAME";
    private static final String CHANNEL_NAME = "CHANNEL_NAME";
    private static final long DEFAULT_KEEP_ALIVE_TIMEOUT = 5000;
    private static final int DEFAULT_TIMEOUT_COUNT = 2;
    private ConcurrentHashMap services;
    private ConcurrentHashMap keepAliveMap;
    private SynchronizedBoolean started;
    private MulticastTransportChannel channel;
    private Thread runner;
    private IdGenerator idGen;
    private String localId;
    private URI uri;
    private int timeoutCount;
    private long keepAliveTimeout;
    private long timeoutExpiration;
    //private ActiveMQMessage keepAliveMessage;
    private ActiveMQObjectMessage serviceMessage;
    private String serviceName = "";
    private int timeToLive = 1;
    private String channelName = "defaultChannel";

    /**
     * Construct a discovery agent that uses multicast
     * 
     * @param channelName
     * @throws JMSException
     */
    public MulticastDiscoveryAgent(String channelName) throws JMSException {
        init();
        this.channelName = channelName;
        try {
            setUri(new URI(DEFAULT_DISCOVERY_URI));
        }
        catch (URISyntaxException e) {
            JMSException jmsEx = new JMSException("URI Syntax exception: " + e.getMessage());
            jmsEx.setLinkedException(e);
            throw jmsEx;
        }
    }

    public MulticastDiscoveryAgent(URI uri) {
        init();
        this.uri = uri;
    }

    private void init() {
        this.started = new SynchronizedBoolean(false);
        this.services = new ConcurrentHashMap();
        this.keepAliveMap = new ConcurrentHashMap();
        this.idGen = new IdGenerator();
        this.localId = idGen.generateId();
        this.keepAliveTimeout = DEFAULT_KEEP_ALIVE_TIMEOUT;
        this.timeoutCount = DEFAULT_TIMEOUT_COUNT;
        this.timeoutExpiration = this.keepAliveTimeout * timeoutCount;
    }


    /**
     * @return Returns the keepAliveTimeout.
     */
    public long getKeepAliveTimeout() {
        return keepAliveTimeout;
    }

    /**
     * @param keepAliveTimeout The keepAliveTimeout to set.
     */
    public void setKeepAliveTimeout(long keepAliveTimeout) {
        this.keepAliveTimeout = keepAliveTimeout;
    }

    /**
     * @return Returns the timeoutCount.
     */
    public int getTimeoutCount() {
        return timeoutCount;
    }

    /**
     * @param timeoutCount The timeoutCount to set.
     */
    public void setTimeoutCount(int timeoutCount) {
        this.timeoutCount = timeoutCount;
    }

    /**
     * @return Returns the localId.
     */
    public String getLocalId() {
        return localId;
    }

    /**
     * @param localId The localId to set.
     */
    public void setLocalId(String localId) {
        this.localId = localId;
    }

    /**
     * @return Returns the uri.
     */
    public URI getUri() {
        return uri;
    }

    /**
     * @param uri The uri to set.
     */
    public void setUri(URI uri) {
        this.uri = uri;
    }

    /**
     * @return the timeToLive of multicast packets used for discovery
     */
    public int getTimeToLive() {
        return this.timeToLive;
    }

    /**
     * @param timeToLive The timeToLive for multicast packets used in discovery.
     * @throws IOException
     */
    public void setTimeToLive(int timeToLive) throws IOException {
        this.timeToLive = timeToLive;
        if (channel != null) {
            channel.setTimeToLive(timeToLive);
        }
    }

    /**
     * @return Returns the channelName.
     */
    public String getChannelName() {
        return channelName;
    }

    /**
     * @param channelName The channelName to set.
     */
    public void setChannelName(String channelName) {
        this.channelName = channelName;
    }

    /**
     * @return a pretty print of this instance
     */
    public String toString() {
        return "MulticastDiscoveryAgent:" + serviceName;
    }

    /**
     * @return the number of active services, including self
     */
    public int getServicesCount() {
        return (this.serviceMessage != null ? 1 : 0) + services.size();
    }

    /**
     * Register a service for other discover nodes
     * 
     * @param name
     * @param details
     * @throws JMSException
     */
    public void registerService(String name, Map details) throws JMSException {
        if (this.serviceMessage != null){
            //notify the old service has stopped
            this.serviceMessage.setBooleanProperty(ALIVE_TYPE, false);
            sendService();
        }
        this.serviceName = name;
        this.serviceMessage = new ActiveMQObjectMessage();
        this.serviceMessage.setJMSType(SERVICE_TYPE);
        this.serviceMessage.setStringProperty(SERVICE_NAME, name);
        this.serviceMessage.setStringProperty(CHANNEL_NAME, channelName);
        this.serviceMessage.setBooleanProperty(ALIVE_TYPE, true);
        this.serviceMessage.setObject((Serializable) details);
        sendService();
    }

    /**
     * start this discovery agent
     * 
     * @throws JMSException
     */
    public void start() throws JMSException {
        if (started.commit(false, true)) {
            this.timeoutExpiration = this.keepAliveTimeout * timeoutCount;
            channel = new MulticastTransportChannel(new DefaultWireFormat(), uri);

            channel.setClientID(localId);
            channel.setPacketListener(this);
            try {
                channel.setTimeToLive(getTimeToLive());
            }
            catch (IOException e) {
                JMSException jmsEx = new JMSException("Set time to live failed");
                jmsEx.setLinkedException(e);
                throw jmsEx;
            }
            log.info("Starting multicast discovery agent on URI: " + uri + " with clientID: " + channel.getClientID());

            channel.start();
            runner = new Thread(this);
            runner.setName(toString());
            runner.setDaemon(true);
            runner.setPriority(Thread.MAX_PRIORITY);
            runner.start();
            sendService();
            fireServiceStarted(serviceMessage);
        }
    }

    /**
     * stop this discovery agent
     * 
     * @throws JMSException
     */
    public void stop() throws JMSException {
        boolean doStop = false;
        synchronized (started) {
            doStop = started.get();
            if (doStop) {
                if (this.serviceMessage != null){
                    //notify the old service has stopped
                    this.serviceMessage.setBooleanProperty(ALIVE_TYPE, false);
                    sendService();
                }
                channel.stop();
                started.set(false);
            }
        }
        if (doStop) {
            fireServiceStopped(serviceMessage);
        }
    }

    /**
     * send a keep alive message
     */
    public void run() {
        try {
            int count = 0;
            while (started.get()) {
                sendService();
                log.debug(serviceName + " sent keep alive");
                if (++count >= timeoutCount) {
                    count = 0;
                    checkNodesAlive();
                }
                Thread.sleep(getKeepAliveTimeout());
            }
        }
        catch (Throwable e) {
            log.error(toString() + " run failed", e);
        }
    }

    /**
     * Consume multicast packets
     * 
     * @param packet
     */
    public void consume(Packet packet) {
        try {
            if (packet != null && packet.isJMSMessage()) {
                ActiveMQMessage msg = (ActiveMQMessage) packet;
                String receivedChannelName = msg.getStringProperty(CHANNEL_NAME);
                if (receivedChannelName != null && receivedChannelName.equals(channelName)) {
                    String type = msg.getJMSType();
                    if (type != null) {
                        if (type.equals(SERVICE_TYPE)) {
                            processService(msg);
                        }
                        else {
                            log.warn(toString() + " received Message of unknown type: " + type);
                        }
                    }
                    else {
                        log.error(toString() + " message type is null");
                    }
                }
                else {
                    if (log.isDebugEnabled()) {
                        log.debug("Discarded discovery message for channel: " + receivedChannelName + " in channel: " + channelName);
                    }
                }
            }
            else {
                log.warn(toString() + " received unexpected packet: " + packet);
            }
        }
        catch (Throwable e) {
            log.error(toString() + " couldn't process packet: " + packet, e);
        }
    }

    

    private void sendService() throws JMSException {
        if (started.get() && channel != null && !channel.isPendingStop() && serviceMessage != null) {
            channel.asyncSend(serviceMessage);
        }
    }

   
    private void processService(ActiveMQMessage message) throws JMSException {
        if (message != null) {
            ActiveMQObjectMessage objMsg = (ActiveMQObjectMessage) message;
            String name = objMsg.getStringProperty(SERVICE_NAME);

            if (log.isDebugEnabled()) {
                log.debug("Service message received for: " + name);
            }
            addService(name);
            ActiveMQObjectMessage oldMsg = (ActiveMQObjectMessage) services.get(name);
            services.put(name, objMsg);
            if (oldMsg == null) {
                fireServiceStarted(objMsg);
                //send out that we are here!
                sendService();
            }
            if (message.getBooleanProperty(ALIVE_TYPE)) {
                addService(name);
            }
            else {
                removeService(name);
            }
        }
    }

    private void fireServiceStarted(ActiveMQObjectMessage message) throws JMSException {
        if (message != null) {
            String name = message.getStringProperty(SERVICE_NAME);
            Map map = (Map) message.getObject();
            DiscoveryEvent event = new DiscoveryEvent(this, name, map);
            fireAddService(event);
        }
    }

    private void fireServiceStopped(ActiveMQObjectMessage message) throws JMSException {
        if (message != null) {
            String name = message.getStringProperty(SERVICE_NAME);
            Map map = (Map) message.getObject();
            DiscoveryEvent event = new DiscoveryEvent(this, name, map);
            fireRemoveService(event);
        }
    }

    private void addService(String name) {
        long timestamp = System.currentTimeMillis();
        SynchronizedLong activeTime = (SynchronizedLong) keepAliveMap.get(name);
        if (activeTime == null) {
            activeTime = new SynchronizedLong(0);
            keepAliveMap.put(name, activeTime);
        }
        activeTime.set(timestamp);
    }

    private void removeService(String name) throws JMSException {
        keepAliveMap.remove(name);
        ActiveMQObjectMessage message = (ActiveMQObjectMessage) services.remove(name);
        if (message != null) {
            fireServiceStopped(message);
        }
    }

    private void checkNodesAlive() throws JMSException {
        long timestamp = System.currentTimeMillis();
        long timeout = timestamp - timeoutExpiration;
        for (Iterator i = keepAliveMap.entrySet().iterator();i.hasNext();) {
            Map.Entry entry = (Map.Entry) i.next();
            SynchronizedLong activeTime = (SynchronizedLong) entry.getValue();
            if (activeTime.get() < timeout) {
                String name = entry.getKey().toString();
                removeService(name);
                log.warn(serviceName + " Expiring node: " + name);
            }
        }
    }
}