/** 
 * 
 * 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.service.boundedvm;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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.Filter;
import org.activemq.io.util.MemoryBoundedQueue;
import org.activemq.io.util.MemoryBoundedQueueManager;
import org.activemq.io.util.MemoryManageable;
import org.activemq.message.ActiveMQDestination;
import org.activemq.message.ActiveMQMessage;
import org.activemq.message.ConsumerInfo;
import org.activemq.message.MessageAck;
import org.activemq.service.DeadLetterPolicy;
import org.activemq.service.MessageContainer;
import org.activemq.service.MessageContainerAdmin;
import org.activemq.service.MessageIdentity;
import org.activemq.service.QueueListEntry;
import org.activemq.service.RedeliveryPolicy;
import org.activemq.service.Service;
import org.activemq.service.impl.DefaultQueueList;

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

/**
 * A MessageContainer for transient queues
 * 
 * @version $Revision: 1.1.1.1 $
 */
public class TransientQueueBoundedMessageContainer implements MessageContainer, Service, Runnable, MessageContainerAdmin {
    private MemoryBoundedQueueManager queueManager;
    private ActiveMQDestination destination;
    private SynchronizedBoolean started;
    private SynchronizedBoolean running;
    private MemoryBoundedQueue queue;
    private DefaultQueueList subscriptions;
    private Executor threadPool;
    private Log log;
    private long idleTimestamp; //length of time (ms) there have been no active subscribers
    private RedeliveryPolicy redeliveryPolicy;
    private DeadLetterPolicy deadLetterPolicy;
	private final Object dispatchMutex = new Object();
    private final Object subscriptionMutex = new Object();
	
    /**
     * Construct this beast
     * 
     * @param threadPool
     * @param queueManager
     * @param destination
     * @param redeliveryPolicy
     * @param deadLetterPolicy
     */
    public TransientQueueBoundedMessageContainer(Executor threadPool, MemoryBoundedQueueManager queueManager,
            ActiveMQDestination destination,RedeliveryPolicy redeliveryPolicy, DeadLetterPolicy deadLetterPolicy) {
        this.threadPool = threadPool;
        this.queueManager = queueManager;
        this.destination = destination;
        this.redeliveryPolicy = redeliveryPolicy;
        this.deadLetterPolicy = deadLetterPolicy;
        this.queue = queueManager.getMemoryBoundedQueue("TRANSIENT_QUEUE:-" + destination.getPhysicalName());
        this.started = new SynchronizedBoolean(false);
        this.running = new SynchronizedBoolean(false);
        this.subscriptions = new DefaultQueueList();
        this.log = LogFactory.getLog("TransientQueueBoundedMessageContainer:- " + destination);
    }

    
    /**
     * @return true if there are subscribers waiting for messages
     */
    public boolean isActive(){
        return !subscriptions.isEmpty();
    }
    
    /**
     * @return true if no messages are enqueued
     */
    public boolean isEmpty(){
        return queue.isEmpty();
    }
    
    /**
     * @return the timestamp (ms) from the when the last active subscriber stopped
     */
    public long getIdleTimestamp(){
        return idleTimestamp;
    }
    
    

    /**
     * Add a consumer to dispatch messages to
     * 
     * @param filter
     * @param info
     * @param client
     * @return TransientQueueSubscription
     * @throws JMSException
     */
    public TransientQueueSubscription addConsumer(Filter filter,ConsumerInfo info, BrokerClient client) throws JMSException {
        synchronized (subscriptionMutex) {
            TransientQueueSubscription ts = findMatch(info);
            if (ts == null) {
                MemoryBoundedQueue queue = queueManager
                        .getMemoryBoundedQueue("TRANSIENT_QUEUE_SUB:-"
                                + info.getConsumerId());
                MemoryBoundedQueue ackQueue = queueManager
                        .getMemoryBoundedQueue("TRANSIENT_QUEUE_SUB_ACKED:-"
                                + info.getConsumerId());
                ts = new TransientQueueSubscription(client, queue, ackQueue,
                        filter, info);

                idleTimestamp = 0;
                subscriptions.add(ts);
                if (started.get()) {
                    synchronized (running) {
                        if (running.commit(false, true)) {
                            try {
                                threadPool.execute(this);
                            } catch (InterruptedException e) {
                                JMSException jmsEx = new JMSException(
                                        toString()
                                                + " Failed to start running dispatch thread");
                                jmsEx.setLinkedException(e);
                                throw jmsEx;
                            }
                        }
                    }
                }

            }
            return ts;
        }
    }

    /**
     * Remove a consumer
     * 
     * @param info
     * @throws JMSException
     */
    public void removeConsumer(ConsumerInfo info) throws JMSException {
        synchronized (subscriptionMutex) {
            TransientQueueSubscription ts = findMatch(info);
            if (ts != null) {

                subscriptions.remove(ts);
                if (subscriptions.isEmpty()) {
                    running.commit(true, false);
                    idleTimestamp = System.currentTimeMillis();
                }

                //get unacknowledged messages and re-enqueue them
                List list = ts.getUndeliveredMessages();
                for (int i = list.size() - 1; i >= 0; i--) {
                    queue.enqueueFirstNoBlock((MemoryManageable) list.get(i));
                }

                // If it is a queue browser, then re-enqueue the browsed
                // messages.
                if (ts.isBrowser()) {
                    list = ts.listAckedMessages();
                    for (int i = list.size() - 1; i >= 0; i--) {
                        queue.enqueueFirstNoBlock((MemoryManageable) list
                                .get(i));
                    }
                    ts.removeAllAckedMessages();
                }

                ts.close();
            }
        }
    }
    
    
    /**
     * start working
     * 
     * @throws JMSException
     */
    public void start() throws JMSException {
        if (started.commit(false, true)) {
            if (!subscriptions.isEmpty()) {
                synchronized (running) {
                    if (running.commit(false, true)) {
                        try {
                            threadPool.execute(this);
                        }
                        catch (InterruptedException e) {
                            JMSException jmsEx = new JMSException(toString() + " Failed to start");
                            jmsEx.setLinkedException(e);
                            throw jmsEx;
                        }
                    }
                }
            }
        }
    }

    /**
     * enqueue a message for dispatching
     * 
     * @param message
     */
    public void enqueue(ActiveMQMessage message) {
        if (message.isAdvisory()) {
            doAdvisoryDispatchMessage(message);
        }
        else {
            queue.enqueue(message);
            startRunning();
        }
    }

    /**
     * re-enqueue a message for dispatching
     * 
     * @param message
     */
    public void redeliver(ActiveMQMessage message) {
        queue.enqueueFirstNoBlock(message);
        startRunning();
    }

    public void redeliver(List messages) {
        queue.enqueueAllFirstNoBlock(messages);
        startRunning();
    }

    /**
     * stop working
     */
    public void stop() {
        started.set(false);
        running.set(false);
        queue.clear();
    }

    /**
     * close down this container
     * 
     * @throws JMSException
     */
    public void close() throws JMSException {
        if (started.get()) {
            stop();
        }
        queue.close();
        synchronized (subscriptionMutex) {
            QueueListEntry entry = subscriptions.getFirstEntry();
            while (entry != null) {
                TransientQueueSubscription ts = (TransientQueueSubscription) entry
                        .getElement();
                ts.close();
                entry = subscriptions.getNextEntry(entry);
            }
            subscriptions.clear();
        }
    }

    /**
     * do some dispatching
     */
    public void run() {
        // Only allow one thread at a time to dispatch.
        synchronized (dispatchMutex) {
            boolean dispatched = false;
            boolean targeted = false;
            ActiveMQMessage message = null;
            int notDispatchedCount = 0;
            int sleepTime = 250;
            int iterationsWithoutDispatchingBeforeStopping = 10000 / sleepTime;// ~10
                                                                                // seconds
            Map messageParts = new HashMap();
            try {
                while (started.get() && running.get()) {
                    dispatched = false;
                    targeted = false;
                    synchronized (subscriptionMutex) {
                        if (!subscriptions.isEmpty()) {
                            message = (ActiveMQMessage) queue
                                    .dequeue(sleepTime);
                            if (message != null) {
                                if (!message.isExpired()) {
                                    QueueListEntry entry = subscriptions.getFirstEntry();
                                    while (entry != null) {
                                        TransientQueueSubscription ts = (TransientQueueSubscription) entry.getElement();
                                        if (ts.isTarget(message)) {
                                            targeted = true;
                                            if (message.isMessagePart()) {
                                                TransientQueueSubscription sameTarget = (TransientQueueSubscription) messageParts
                                                        .get(message.getParentMessageID());
                                                if (sameTarget == null) {
                                                    sameTarget = ts;
                                                    messageParts.put(message.getParentMessageID(),sameTarget);
                                                }
                                                sameTarget.doDispatch(message);
                                                if (message.isLastMessagePart()) {
                                                    messageParts.remove(message.getParentMessageID());
                                                }
                                                message = null;
                                                dispatched = true;
                                                notDispatchedCount = 0;
                                                break;
                                            } else if (ts.canAcceptMessages()) {
                                                ts.doDispatch(message);
                                                message = null;
                                                dispatched = true;
                                                notDispatchedCount = 0;
                                                subscriptions.rotate();
                                                break;
                                            }
                                        }
                                        entry = subscriptions
                                                .getNextEntry(entry);
                                    }
                                } else {
                                    // expire message
                                    if (log.isDebugEnabled()) {
                                        log.debug("expired message: "+ message);
                                    }
                                    deadLetterPolicy.sendToDeadLetter(message);
                                    message = null;
                                }
                            }
                        }
                    }
                    if (!dispatched) {
                        if (message != null) {
                            if (targeted) {
                                queue.enqueueFirstNoBlock(message);
                            } else {
                                //no matching subscribers - dump to end and hope one shows up ...
                                queue.enqueueNoBlock(message);

                            }
                        }
                        if (running.get()) {
                            if (notDispatchedCount++ > iterationsWithoutDispatchingBeforeStopping
                                    && queue.isEmpty()) {
                                synchronized (running) {
                                    running.commit(true, false);
                                }
                            } else {
                                Thread.sleep(sleepTime);
                            }
                        }
                    }
                }
            } catch (InterruptedException ie) {
                //someone is stopping us from another thread
            } catch (Throwable e) {
                log.warn("stop dispatching", e);
                stop();
            }
        }
    }

    private TransientQueueSubscription findMatch(ConsumerInfo info) throws JMSException {
        TransientQueueSubscription result = null;
        synchronized(subscriptionMutex){
            QueueListEntry entry = subscriptions.getFirstEntry();
            while (entry != null) {
                TransientQueueSubscription ts = (TransientQueueSubscription) entry.getElement();
                if (ts.getConsumerInfo().equals(info)) {
                    result = ts;
                    break;
                }
                entry = subscriptions.getNextEntry(entry);
            }
        }
        return result;
    }

    /**
     * @return the destination associated with this container
     */
    public ActiveMQDestination getDestination() {
        return destination;
    }

    /**
     * @return the destination name
     */
    public String getDestinationName() {
        return destination.getPhysicalName();
    }

    /**
     * @param msg
     * @return @throws JMSException
     */
    public void addMessage(ActiveMQMessage msg) throws JMSException {
    }

    /**
     * @param messageIdentity
     * @param ack
     * @throws JMSException
     */
    public void delete(MessageIdentity messageIdentity, MessageAck ack) throws JMSException {
    }

    /**
     * @param messageIdentity
     * @return @throws JMSException
     */
    public ActiveMQMessage getMessage(MessageIdentity messageIdentity) throws JMSException {
        return null;
    }

    /**
     * @param messageIdentity
     * @throws JMSException
     */
    public void registerMessageInterest(MessageIdentity messageIdentity) throws JMSException {
    }

    /**
     * @param messageIdentity
     * @param ack
     * @throws JMSException
     */
    public void unregisterMessageInterest(MessageIdentity messageIdentity) throws JMSException {
    }

    /**
     * @param messageIdentity
     * @return @throws JMSException
     */
    public boolean containsMessage(MessageIdentity messageIdentity) throws JMSException {
        return false;
    }


    protected void clear() {
        queue.clear();
    }

    protected void removeExpiredMessages() {
        long currentTime = System.currentTimeMillis();
        List list = queue.getContents();
        for (int i = 0;i < list.size();i++) {
            ActiveMQMessage msg = (ActiveMQMessage) list.get(i);
            if (msg.isExpired(currentTime)) {
                queue.remove(msg);
                if (log.isDebugEnabled()) {
                    log.debug("expired message: " + msg);
                }
            }
        }
    }
    
    protected void startRunning(){
        if (!running.get() && started.get() && !subscriptions.isEmpty()) {
            synchronized (running) {
                if (running.commit(false, true)) {
                    try {
                        threadPool.execute(this);
                    }
                    catch (InterruptedException e) {
                       log.error(this + " Couldn't start executing ",e);
                    }
                }
            }
        }
    }


    /**
     * @see org.activemq.service.MessageContainer#getMessageContainerAdmin()
     */
    public MessageContainerAdmin getMessageContainerAdmin() {
        return this;
    }

    /**
     * @see org.activemq.service.MessageContainerAdmin#empty()
     */
    public void empty() throws JMSException {
        // TODO implement me.
    }


    /**
     * @see org.activemq.service.MessageContainer#isDeadLetterQueue()
     */
    public boolean isDeadLetterQueue() {
        return false;
    }
    
    /**
     * Dispatch an Advisory Message
     * @param message
     */
    private synchronized void doAdvisoryDispatchMessage(ActiveMQMessage message) {
        try {
            if (message != null && message.isAdvisory() && !message.isExpired()) {
                synchronized (subscriptionMutex) {
                    QueueListEntry entry = subscriptions.getFirstEntry();
                    while (entry != null) {
                        TransientQueueSubscription ts = (TransientQueueSubscription) entry.getElement();
                        if (ts.isTarget(message)) {
                            ts.doDispatch(message);
                            break;
                        }
                        entry = subscriptions.getNextEntry(entry);
                    }
                }
            }
        } catch (JMSException jmsEx) {
            log.warn("Failed to dispatch advisory", jmsEx);
        }
    }

}