/**
 *
 * 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.broker.impl;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.jms.DeliveryMode;
import javax.jms.JMSException;
import javax.jms.ObjectMessage;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.advisories.TempDestinationAdvisoryEvent;
import org.activemq.broker.BrokerClient;
import org.activemq.message.ActiveMQDestination;
import org.activemq.message.ActiveMQMessage;
import org.activemq.message.ActiveMQObjectMessage;
import org.activemq.message.ConnectionInfo;
import org.activemq.message.ConsumerInfo;
import org.activemq.message.Packet;
import org.activemq.message.ProducerInfo;
import org.activemq.util.IdGenerator;
import EDU.oswego.cs.dl.util.concurrent.ConcurrentHashMap;
import EDU.oswego.cs.dl.util.concurrent.CopyOnWriteArraySet;

/**
 * Manages advisory subscriptions and messages
 * 
 * @version $Revision: 1.1.1.1 $
 */
class AdvisorySupport {
    private static final Log log = LogFactory.getLog(AdvisorySupport.class);
    private Set advisoryConsumers = new CopyOnWriteArraySet();
    private Set consumers = new CopyOnWriteArraySet();
    private Set producers = new CopyOnWriteArraySet();
    private Set connections = new CopyOnWriteArraySet();
    private Set tempDestConsumers = new CopyOnWriteArraySet();
    private IdGenerator idGen = new IdGenerator();
    private Map tempDestinations = new ConcurrentHashMap();//client ids = keys, Set of TempDestinationAdvisoryEvents =
    // values
    private DefaultBroker broker;

    AdvisorySupport(DefaultBroker broker) {
        this.broker = broker;
    }

    /**
     * Add an advisory Consumer
     * 
     * @param advisory
     * @param sender
     */
    void addAdvisory(BrokerClient sender, ConsumerInfo advisory) {
        if (advisory != null && advisory.isAdvisory()) {
            advisoryConsumers.add(advisory);
            for (Iterator i = consumers.iterator();i.hasNext();) {
                ConsumerInfo info = (ConsumerInfo) i.next();
                dispatchToTarget(sender, generateAdvisory(advisory, info));
            }
            for (Iterator i = producers.iterator();i.hasNext();) {
                ProducerInfo info = (ProducerInfo) i.next();
                dispatchToTarget(sender, generateAdvisory(advisory, info));
            }
            for (Iterator i = connections.iterator();i.hasNext();) {
                ConnectionInfo info = (ConnectionInfo) i.next();
                dispatchToTarget(sender, generateAdvisory(advisory, info));
            }
            for (Iterator i = tempDestinations.values().iterator();i.hasNext();) {
                Set set = (Set) i.next();
                for (Iterator si = set.iterator();si.hasNext();) {
                    TempDestinationAdvisoryEvent event = (TempDestinationAdvisoryEvent) si.next();
                    dispatchToTarget(sender, generateAdvisory(advisory, event));
                }
            }
        }
        addConsumer(sender, advisory);
    }

    /**
     * remove an advisory Consumer
     * 
     * @param sender
     * @param info
     */
    void removeAdvisory(BrokerClient sender, ConsumerInfo info) {
        advisoryConsumers.remove(info);
        removeConsumer(sender, info);
    }

    /**
     * Add a Consumer
     * 
     * @param sender
     * @param info
     */
    private void addConsumer(BrokerClient sender, ConsumerInfo info) {
        consumers.remove(info);
        consumers.add(info);
        dispatchToBroker(sender, generateAdvisoryMessage(info, info.getDestination().getTopicForConsumerAdvisory()));
    }

    /**
     * Remove a Consumer
     * 
     * @param sender
     * @param info
     */
    private void removeConsumer(BrokerClient sender, ConsumerInfo info) {
        consumers.remove(info);
        dispatchToBroker(sender, generateAdvisoryMessage(info, info.getDestination().getTopicForConsumerAdvisory()));
    }

    /**
     * Add a Producer
     * 
     * @param sender
     * @param info
     */
    void addProducer(BrokerClient sender, ProducerInfo info) {
        producers.remove(info);
        producers.add(info);
        if (info.getDestination() != null) {
            dispatchToBroker(sender, generateAdvisoryMessage(info, info.getDestination().getTopicForProducerAdvisory()));
        }
    }

    /**
     * Remove a Producer
     * 
     * @param sender
     * @param info
     */
    void removeProducer(BrokerClient sender, ProducerInfo info) {
        producers.remove(info);
        if (info.getDestination() != null) {
            dispatchToBroker(sender, generateAdvisoryMessage(info, info.getDestination().getTopicForProducerAdvisory()));
        }
    }

    /**
     * Add a Connection
     * 
     * @param sender
     * @param info
     */
    void addConnection(BrokerClient sender, ConnectionInfo info) {
        connections.remove(info);
        connections.add(info);
        ActiveMQDestination dest = ActiveMQDestination.createDestination(ActiveMQDestination.ACTIVEMQ_TOPIC,
                ActiveMQDestination.CONNECTION_ADVISORY_PREFIX);
        dispatchToBroker(sender, generateAdvisoryMessage(info, dest));
    }

    /**
     * Remove a Connection
     * 
     * @param sender
     * @param info
     */
    void removeConnection(BrokerClient sender, ConnectionInfo info) {
        connections.remove(info);
        removeAllTempDestinations(sender, info.getClientId());
        ActiveMQDestination dest = ActiveMQDestination.createDestination(ActiveMQDestination.ACTIVEMQ_TOPIC,
                ActiveMQDestination.CONNECTION_ADVISORY_PREFIX);
        dispatchToBroker(sender, generateAdvisoryMessage(info, dest));
    }

    /**
     * @param sender
     * @param message
     * @throws JMSException
     */
    void processTempDestinationAdvisory(BrokerClient sender, ActiveMQMessage message) throws JMSException {
        TempDestinationAdvisoryEvent event = (TempDestinationAdvisoryEvent) ((ObjectMessage) message).getObject();
        processTempDestinationAdvisory(event);
    }

    /**
     * @param advisory
     * @param info
     * @return an advisory message or null
     */
    private ActiveMQMessage generateAdvisory(ConsumerInfo advisory, ConsumerInfo info) {
        if (matchConsumer(advisory, info)) {
            return generateAdvisoryMessage(advisory, info, info.getDestination().getTopicForConsumerAdvisory());
        }
        return null;
    }

    /**
     * @param advisory
     * @param info
     * @return an advisory message or null
     */
    private ActiveMQMessage generateAdvisory(ConsumerInfo advisory, ProducerInfo info) {
        if (matchProducer(advisory, info)) {
            return generateAdvisoryMessage(advisory, info, info.getDestination().getTopicForProducerAdvisory());
        }
        return null;
    }

    /**
     * @param advisory
     * @param info
     * @return an advisory message or null
     */
    private ActiveMQMessage generateAdvisory(ConsumerInfo advisory, ConnectionInfo info) {
        if (matchConnection(advisory, info)) {
            String destName = advisory.getDestination().getPhysicalName();
            ActiveMQDestination dest = ActiveMQDestination.createDestination(advisory.getDestination()
                    .getDestinationType(), destName);
            return generateAdvisoryMessage(advisory, info, dest);
        }
        return null;
    }

    /**
     * Generate a TempDestinationAdvisoryEvent if the advisory is a match
     * 
     * @param advisory
     * @param event
     * @return an advisory message or null
     */
    private ActiveMQMessage generateAdvisory(ConsumerInfo advisory, TempDestinationAdvisoryEvent event) {
        if (matchTempDestinationAdvisory(advisory, event.getDestination())) {
            return generateAdvisoryMessage(advisory, event, event.getDestination().getTopicForTempAdvisory());
        }
        return null;
    }

    boolean matchConsumer(ConsumerInfo advisory, ConsumerInfo info) {
        boolean result = false;
        if (advisory != null && advisory.getDestination() != null && info != null && info.getDestination() != null) {
            ActiveMQDestination advisoryDestination = advisory.getDestination();
            ActiveMQDestination destination = info.getDestination();
            if (advisoryDestination.isConsumerAdvisory()) {
                ActiveMQDestination match = advisoryDestination.getDestinationBeingAdvised();
                return match.matches(destination) || matchGeneralAdvisory(advisory, destination);
            }
        }
        return result;
    }

    boolean matchProducer(ConsumerInfo advisory, ProducerInfo info) {
        boolean result = false;
        if (advisory != null && advisory.getDestination() != null && info != null && info.getDestination() != null) {
            ActiveMQDestination advisoryDestination = advisory.getDestination();
            ActiveMQDestination destination = info.getDestination();
            if (advisoryDestination.isProducerAdvisory()) {
                ActiveMQDestination match = advisoryDestination.getDestinationBeingAdvised();
                return match.matches(destination) || matchGeneralAdvisory(advisory, destination);
            }
        }
        return result;
    }

    boolean matchConnection(ConsumerInfo advisory, ConnectionInfo info) {
        boolean result = false;
        if (advisory != null && advisory.getDestination() != null && info != null) {
            result = (advisory.getDestination().isConnectionAdvisory() && advisory.getDestination().matches(
                    ActiveMQDestination.createDestination(advisory.getDestination().getDestinationType(),
                            ActiveMQDestination.CONNECTION_ADVISORY_PREFIX)))
                    || matchGeneralAdvisory(advisory, advisory.getDestination());
        }
        return result;
    }

    /**
     * A consumer could listen for all advisories
     * 
     * @param advisory
     * @param destination
     * @return true if a general 'catch-all' advisory subscriber
     */
    private boolean matchGeneralAdvisory(ConsumerInfo advisory, ActiveMQDestination destination) {
        boolean result = advisory.getDestination() != null && advisory.getDestination().isAdvisory();
        if (result) {
            ActiveMQDestination match = advisory.getDestination().getDestinationBeingAdvised();
            result = match.matches(destination);
        }
        return result;
    }

    boolean matchTempDestinationAdvisory(ConsumerInfo advisory, ActiveMQDestination destination) {
        boolean result = false;
        if (advisory != null && advisory.getDestination() != null) {
            ActiveMQDestination advisoryDestination = advisory.getDestination();
            if (advisoryDestination.isTempDestinationAdvisory()) {
                ActiveMQDestination match = advisoryDestination.getDestinationBeingAdvised();
                return match.matches(destination) || matchGeneralAdvisory(advisory, destination);
            }
        }
        return result;
    }

    private void processTempDestinationAdvisory(TempDestinationAdvisoryEvent event) {
        String clientId = ActiveMQDestination.getClientId(event.getDestination());
        Set set = (Set) tempDestinations.get(clientId);
        if (event.isStarted()) {
            if (set == null) {
                set = new HashSet();
                tempDestinations.put(clientId, set);
            }
            set.add(event);
        }
        else {
            if (set != null) {
                set.remove(event);
                if (set.isEmpty()) {
                    tempDestinations.remove(clientId);
                }
            }
        }
    }

    private void removeAllTempDestinations(BrokerClient sender, String clientId) {
        Set set = (Set) tempDestinations.remove(clientId);
        if (set != null) {
            for (Iterator i = set.iterator();i.hasNext();) {
                TempDestinationAdvisoryEvent event = (TempDestinationAdvisoryEvent) i.next();
                event.setStarted(false);
                processTempDestinationAdvisory(event);
                for (Iterator it = advisoryConsumers.iterator();it.hasNext();) {
                    ConsumerInfo advisory = (ConsumerInfo) it.next();
                    dispatchToTarget(sender, generateAdvisory(advisory, event));
                }
            }
        }
    }

    /**
     * Generate an advisory message
     * 
     * @param payload
     * @param destination
     * @return create ActiveMQMessage
     */
    private ActiveMQMessage generateAdvisoryMessage(Packet payload, ActiveMQDestination destination) {
        return generateAdvisoryMessage(null, payload, destination);
    }

    /**
     * Generate an advisory message
     * 
     * @param advisoryTarget
     * @param payload
     * @param destination
     * @return create ActiveMQMessage
     */
    private ActiveMQMessage generateAdvisoryMessage(final ConsumerInfo advisoryTarget, final Packet payload,
            final ActiveMQDestination destination) {
        ActiveMQObjectMessage advisoryMsg = null;
        try {
            advisoryMsg = new ActiveMQObjectMessage();
            advisoryMsg.setJMSMessageID(idGen.generateId());
            advisoryMsg.setJMSDestination(destination);
            advisoryMsg.setExternalMessageId(true);
            advisoryMsg.setDeliveryCount(DeliveryMode.NON_PERSISTENT);
            advisoryMsg.setObject((Serializable) payload);
            if (advisoryTarget != null) {
                advisoryMsg.setConsumerNos(new int[]{advisoryTarget.getConsumerNo()});
            }
        }
        catch (JMSException e) {
            advisoryMsg = null;
            log.warn("caught an exception generating an advisory", e);
        }
        return advisoryMsg;
    }

    private void dispatchToTarget(BrokerClient target, ActiveMQMessage message) {
        if (target != null && message != null) {
            target.dispatch(message);
        }
    }

    private void dispatchToBroker(BrokerClient sender, ActiveMQMessage message) {
        if (sender != null && message != null) {
            try {
                broker.sendMessage(sender, message);
            }
            catch (JMSException e) {
                log.warn("caught an exception sending an advisory", e);
            }
        }
    }
}