/** 
 * 
 * Copyright 2004 Protique Ltd
 * Copyright 2005 Hiram Chirino
 * 
 * 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.service.boundedvm;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.jms.JMSException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.broker.BrokerClient;
import org.activemq.filter.AndFilter;
import org.activemq.filter.DestinationMap;
import org.activemq.filter.Filter;
import org.activemq.filter.FilterFactory;
import org.activemq.filter.FilterFactoryImpl;
import org.activemq.filter.NoLocalFilter;
import org.activemq.io.util.MemoryBoundedQueueManager;
import org.activemq.message.ActiveMQDestination;
import org.activemq.message.ActiveMQMessage;
import org.activemq.message.ActiveMQQueue;
import org.activemq.message.ConsumerInfo;
import org.activemq.message.MessageAck;
import org.activemq.service.DeadLetterPolicy;
import org.activemq.service.MessageContainer;
import org.activemq.service.MessageContainerManager;
import org.activemq.service.RedeliveryPolicy;
import org.activemq.service.TransactionManager;
import org.activemq.service.TransactionTask;
import org.activemq.store.MessageStore;
import org.activemq.store.PersistenceAdapter;

import EDU.oswego.cs.dl.util.concurrent.ConcurrentHashMap;
import EDU.oswego.cs.dl.util.concurrent.Executor;
import EDU.oswego.cs.dl.util.concurrent.PooledExecutor;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedBoolean;
import EDU.oswego.cs.dl.util.concurrent.ThreadFactory;

/**
 * A MessageContainerManager for Durable queues
 * 
 * @version $Revision: 1.1.1.1 $
 */
public class DurableQueueBoundedMessageManager implements MessageContainerManager, Runnable {
    private static final int DEFAULT_GARBAGE_COLLECTION_CAPACITY_LIMIT = 10;
    private static final long DEFAULT_INACTIVE_TIMEOUT = 30 * 1000;
    private static final Log log = LogFactory.getLog(DurableQueueBoundedMessageManager.class);
    private MemoryBoundedQueueManager queueManager;
    private ConcurrentHashMap containers;
    private ConcurrentHashMap subscriptions;
    private FilterFactory filterFactory;
    private SynchronizedBoolean started;
    private SynchronizedBoolean doingGarbageCollection;
    private Map destinations;
    private DestinationMap destinationMap;
    private PooledExecutor threadPool;
    private long inactiveTimeout;
    private int garbageCoolectionCapacityLimit;
    private RedeliveryPolicy redeliveryPolicy;
    private DeadLetterPolicy deadLetterPolicy;
    private final PersistenceAdapter persistenceAdapter;
    
    protected static class DurableQueueThreadFactory implements ThreadFactory {
        /**
         * @param command - command to run
         * @return a new thread
         */
        public Thread newThread(Runnable command) {
            Thread result = new Thread(command, "Durable Queue Worker");
            result.setPriority(Thread.NORM_PRIORITY + 1);
            result.setDaemon(true);
            return result;
        }
    }

    /**
     * Constructor for DurableQueueBoundedMessageManager
     * 
     * @param mgr
     * @param redeliveryPolicy
     * @param deadLetterPolicy
     */
    public DurableQueueBoundedMessageManager(PersistenceAdapter persistenceAdapter, MemoryBoundedQueueManager mgr, RedeliveryPolicy redeliveryPolicy,
            DeadLetterPolicy deadLetterPolicy) {
        this.persistenceAdapter = persistenceAdapter;
        this.queueManager = mgr;
        this.redeliveryPolicy = redeliveryPolicy;
        this.deadLetterPolicy = deadLetterPolicy;
        this.containers = new ConcurrentHashMap();
        this.destinationMap = new DestinationMap();
        this.destinations = new ConcurrentHashMap();
        this.subscriptions = new ConcurrentHashMap();
        this.filterFactory = new FilterFactoryImpl();
        this.started = new SynchronizedBoolean(false);
        this.doingGarbageCollection = new SynchronizedBoolean(false);
        this.threadPool = new PooledExecutor();
        this.threadPool.setThreadFactory(new DurableQueueThreadFactory());
        this.inactiveTimeout = DEFAULT_INACTIVE_TIMEOUT;
        this.garbageCoolectionCapacityLimit = DEFAULT_GARBAGE_COLLECTION_CAPACITY_LIMIT;
    }

    /**
     * @return Returns the garbageCoolectionCapacityLimit.
     */
    public int getGarbageCoolectionCapacityLimit() {
        return garbageCoolectionCapacityLimit;
    }

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

    /**
     * @return Returns the inactiveTimeout.
     */
    public long getInactiveTimeout() {
        return inactiveTimeout;
    }

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

    /**
     * start the manager
     * 
     * @throws JMSException
     */
    public void start() throws JMSException {
        if (started.commit(false, true)) {
            for (Iterator i = containers.values().iterator();i.hasNext();) {
                DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
                container.start();
            }
            try {
                threadPool.execute(this);//start garbage collection
            }
            catch (InterruptedException e) {
                JMSException jmsEx = new JMSException("Garbage collection interupted on start()");
                jmsEx.setLinkedException(e);
                throw jmsEx;
            }
        }
    }

    /**
     * stop the manager
     * 
     * @throws JMSException
     */
    public void stop() throws JMSException {
        if (started.commit(true, false)) {
            for (Iterator i = containers.values().iterator();i.hasNext();) {
                DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
                container.stop();
            }
            threadPool.interruptAll();
			threadPool.shutdownNow();
        }
    }

    /**
     * collect expired messages
     */
    public void run() {
        while (started.get()) {
            doGarbageCollection();
            try {
                Thread.sleep(2000);
            }
            catch (InterruptedException e) {
            }
        }
    }

    /**
     * Add a consumer if appropiate
     * 
     * @param client
     * @param info
     * @throws JMSException
     */
    public synchronized void addMessageConsumer(BrokerClient client, ConsumerInfo info) throws JMSException {
        ActiveMQDestination destination = info.getDestination();
        if ( !destination.isQueue() || destination.isTemporary() )
            return;
            
        DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) containers
                .get(destination);
        if (container == null) {
            container = createContainer(destination,false);
        }
        if (log.isDebugEnabled()) {
            log.debug("Adding consumer: " + info);
        }
        
        DurableQueueSubscription ts = container.addConsumer(createFilter(info), info, client);
        if (ts != null) {
            subscriptions.put(info.getConsumerId(), ts);
        }
        String name = destination.getPhysicalName();
        if (!destinations.containsKey(name)) {
            destinations.put(name, destination);
        }
    }

    /**
     * @param client
     * @param destination
     * @param isDeadLetterQueue is this queue a dead letter queue 
     * @return the container
     * @throws JMSException
     */
    private DurableQueueBoundedMessageContainer createContainer(ActiveMQDestination destination, boolean isDeadLetterQueue) throws JMSException {
        MessageStore messageStore = persistenceAdapter.createQueueMessageStore(destination.getPhysicalName());
        DurableQueueBoundedMessageContainer container = new DurableQueueBoundedMessageContainer(messageStore, threadPool, queueManager, destination, isDeadLetterQueue ? null : redeliveryPolicy, deadLetterPolicy);
        addContainer(container);
        if (started.get()) {
            container.start();            
        }
        return container;
    }

    /**
     * @param client
     * @param info
     * @throws JMSException
     */
    public synchronized void removeMessageConsumer(BrokerClient client, ConsumerInfo info) throws JMSException {
        ActiveMQDestination destination = info.getDestination();
        if ( !destination.isQueue() || destination.isTemporary() )
            return;
        
        for (Iterator i = containers.values().iterator();i.hasNext();) {
            DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
            if (container != null) {
                container.removeConsumer(info);
            }
        }
        subscriptions.remove(info.getConsumerId());
    }

    /**
     * Delete a durable subscriber
     * 
     * @param clientId
     * @param subscriberName
     * @throws JMSException if the subscriber doesn't exist or is still active
     */
    public void deleteSubscription(String clientId, String subscriberName) throws JMSException {
    }

    /**
     * @param client
     * @param message
     * @throws JMSException
     */
    public void sendMessage(final BrokerClient client, final ActiveMQMessage message) throws JMSException {
        ActiveMQDestination destination = message.getJMSActiveMQDestination();
        if (!destination.isQueue() || destination.isTemporary() || !message.isPersistent())
            return;

        if (queueManager.getCurrentCapacity() <= garbageCoolectionCapacityLimit) {
            doGarbageCollection();
        }
        DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) containers.get(destination);
        if (container == null) {
            container = createContainer(destination, false);
        }
        
        Set set = destinationMap.get(message.getJMSActiveMQDestination());
        for (Iterator i = set.iterator();i.hasNext();) {
            container = (DurableQueueBoundedMessageContainer) i.next();
            container.enqueue(message);
        }
    }

    /**
     * @param client
     * @param ack
     * @throws JMSException
     */
    public void acknowledgeMessage(final BrokerClient client, final MessageAck ack) throws JMSException {
        
        ActiveMQDestination destination = ack.getDestination();
        if (!destination.isQueue() || destination.isTemporary() || !ack.isPersistent())
            return;

        final DurableQueueSubscription ts = (DurableQueueSubscription) subscriptions.get(ack.getConsumerId());
        if (ts == null)
            return;

        final DurableMessagePointer messagePointer = ts.acknowledgeMessage(ack.getMessageID());
        if (messagePointer == null )
            return;
        
        if( ts.isBrowser() ) {
            ts.addAckedMessage(messagePointer);
            return;
        }
        
        if (!ack.isMessageRead() || ack.isExpired()) {
            redeliverMessage(ts, ack, messagePointer);
            return;
        } 
        
        // Let the message store ack the message.
        messagePointer.getMessageStore().removeMessage(ack);        
        if (TransactionManager.isCurrentTransaction()) {
            // Hook in a callbacks on first acked message
            if (!ts.hasAckedMessage()) {
                TransactionManager.getContexTransaction().addPostRollbackTask(new TransactionTask() {
                    public void execute() throws Throwable {
                        
                        List ackList = ts.listAckedMessages();
                        HashMap redeliverMap = new HashMap();
                       //for (int x = ackList.size()-1; x >= 0 ; x--){    
                       for (int x = 0; x < ackList.size(); x++){    
                            DurableMessagePointer messagePointer = (DurableMessagePointer) ackList.get(x);
                            ActiveMQMessage message = messagePointer.getMessage();
                            message.setJMSRedelivered(true);
                            if (message.incrementDeliveryCount() >= redeliveryPolicy.getMaximumRetryCount()) {
                                if (log.isDebugEnabled()){
                                    log.debug("Message: " + message + " has exceeded its retry count");
                                }
                                // TODO: see if we can use the deadLetterPolicy of the container that dispatched the message.
                                deadLetterPolicy.sendToDeadLetter(message);
                            }
                            else if (ack.isExpired()) {
                                if (log.isDebugEnabled()){
                                    log.debug("Message: " + message + " has expired");
                                }
                                // TODO: see if we can use the deadLetterPolicy of the container that dispatched the message.
                                deadLetterPolicy.sendToDeadLetter(message);
                            }
                            else {
                                Set containers = destinationMap.get(message.getJMSActiveMQDestination());
                                for (Iterator i = containers.iterator();i.hasNext();) {
                                    DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
                                    LinkedList l = (LinkedList) redeliverMap.get(container);
                                    if( l==null ) {
                                        l = new LinkedList();
                                        redeliverMap.put(container, l);
                                    }
                                    l.add(messagePointer);
                                }
                            }
                        }
                        
                        for (Iterator i = redeliverMap.keySet().iterator();i.hasNext();) {
                            DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
                            List l = (List) redeliverMap.get(container);
                            container.redeliver(l);
                        }
                        
                        ts.removeAllAckedMessages();
                    }
                });
                TransactionManager.getContexTransaction().addPostCommitTask(new TransactionTask() {
                    public void execute() throws Throwable {
                        ts.removeAllAckedMessages();
                    }
                });
            }
            // We need to keep track of messages that were acked. If we
            // rollback.
            ts.addAckedMessage(messagePointer);
        }
    }

    /**
     * @param client
     * @param ack
     * @param message 
     * @throws JMSException
     */
    private void redeliverMessage(DurableQueueSubscription ts, MessageAck ack, DurableMessagePointer message) throws JMSException {
        message.getMessage().setJMSRedelivered(true);
        if (message.incrementDeliveryCount() >= redeliveryPolicy.getMaximumRetryCount()) {
            if (log.isDebugEnabled()){
                log.debug("Message: " + message + " has exceeded its retry count");
            }
            deadLetterPolicy.sendToDeadLetter(message.getMessage());
        }
        else if (ack.isExpired()) {
            if (log.isDebugEnabled()){
                log.debug("Message: " + message + " has expired");
            }
            deadLetterPolicy.sendToDeadLetter(message.getMessage());
        }
        else {
            Set set = destinationMap.get(message.getMessage().getJMSActiveMQDestination());
            for (Iterator i = set.iterator();i.hasNext();) {
                DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
                container.redeliver(message);
                break;
            }
        }
    }
    
    /**
     * @throws JMSException
     */
    public void poll() throws JMSException {
    }

    /**
     * @param physicalName
     * @return MessageContainer
     * @throws JMSException
     */
    public MessageContainer getContainer(String physicalName) throws JMSException {
        ActiveMQDestination key = (ActiveMQDestination) destinations.get(physicalName);
        if (key != null) {
            return (MessageContainer) containers.get(key);
        }
        return null;
    }

    /**
     * @return a map of destinations
     */
    public Map getDestinations() {
        return Collections.unmodifiableMap(containers);
    }

    /**
     * Returns an unmodifiable map, indexed by String name, of all the {@link javax.jms.Destination}
     * objects used by non-broker consumers directly connected to this container
     *
     * @return
     */
    public Map getLocalDestinations() {
        Map localDestinations = new HashMap();
        for (Iterator iter = subscriptions.values().iterator(); iter.hasNext();) {
            DurableSubscription sub = (DurableSubscription) iter.next();
            if (sub.isLocalSubscription()) {
                final ActiveMQDestination dest = sub.getDestination();
                localDestinations.put(dest.getPhysicalName(), dest);
            }
        }
        return Collections.unmodifiableMap(localDestinations);
    }

    /**
     * @return the DeadLetterPolicy for this Container Manager
     */
    public DeadLetterPolicy getDeadLetterPolicy() {
        return deadLetterPolicy;
    }

    /**
     * Set the DeadLetterPolicy for this Container Manager
     * 
     * @param policy
     */
    public void setDeadLetterPolicy(DeadLetterPolicy policy) {
        this.deadLetterPolicy = policy;
    }

    /**
     * Create filter for a Consumer
     * 
     * @param info
     * @return the Fitler
     * @throws javax.jms.JMSException
     */
    protected Filter createFilter(ConsumerInfo info) throws JMSException {
        Filter filter = filterFactory.createFilter(info.getDestination(), info.getSelector());
        if (info.isNoLocal()) {
            filter = new AndFilter(filter, new NoLocalFilter(info.getClientId()));
        }
        return filter;
    }

    private void doGarbageCollection() {
        
        if (doingGarbageCollection.commit(true, false)) {
            
            if (queueManager.getCurrentCapacity() <= garbageCoolectionCapacityLimit) {
                for (Iterator i = containers.values().iterator();i.hasNext();) {
                    DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
                    container.removeExpiredMessages();
                }
            }
            
            //if still below the limit - clear queues with no subscribers
            //which have been inactive for a while
            if (queueManager.getCurrentCapacity() <= garbageCoolectionCapacityLimit) {
                for (Iterator i = containers.values().iterator();i.hasNext();) {
                    DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
                    if (!container.isActive() && (container.getIdleTimestamp() < (System.currentTimeMillis() - inactiveTimeout))) {
                        removeContainer(container);
                        log.warn("memory limit low - forced to remove inactive and idle queue: "
                                + container.getDestinationName());
                    }
                }
            }
            //now close any inactive queues
            for (Iterator i = containers.values().iterator();i.hasNext();) {
                DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) i.next();
                if (!container.isActive() && !container.isEmpty()) {
                    removeContainer(container);
                }
            }
            doingGarbageCollection.set(false);
        }
    }

    private synchronized void addContainer(DurableQueueBoundedMessageContainer container) {
        containers.put(container.getDestination(), container);
        destinationMap.put(container.getDestination(), container);
    }

    private synchronized void removeContainer(DurableQueueBoundedMessageContainer container) {
        try {
            container.close();
            log.info("closed inactive Durable queue container: " + container.getDestinationName());
        }
        catch (JMSException e) {
            log.warn("failure closing container", e);
        }
        containers.remove(container.getDestination());
        destinationMap.remove(container.getDestination(), container);
    }

    protected Executor getThreadPool() {
        return threadPool;
    }

    public void createMessageContainer(ActiveMQDestination dest) throws JMSException {
        createContainer(dest, false);
    }
    
    public Map getMessageContainerAdmins() throws JMSException {
        return Collections.unmodifiableMap(containers);
    }


    public void destroyMessageContainer(ActiveMQDestination dest) throws JMSException {
        if ( !dest.isQueue() )
            return;

        DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) containers.remove(dest);
        if( container != null ) {
            container.empty();
            container.stop();
        }
        destinationMap.removeAll(dest);
    }

    public void sendToDeadLetterQueue(String deadLetterName, ActiveMQMessage message) throws JMSException {
        
        ActiveMQQueue destination = new ActiveMQQueue(deadLetterName);
        DurableQueueBoundedMessageContainer container = (DurableQueueBoundedMessageContainer) containers.get(destination);
        if (container == null) {
            container = createContainer(destination, true);
        }        
        container.enqueue(message);
        
    }
}