/** 
 * 
 * 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.codehaus.activemq;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.activemq.management.JMSConsumerStatsImpl;
import org.codehaus.activemq.management.StatsCapable;
import org.codehaus.activemq.message.ActiveMQDestination;
import org.codehaus.activemq.message.ActiveMQMessage;
import org.codehaus.activemq.message.util.MemoryBoundedQueue;
import org.codehaus.activemq.selector.SelectorParser;

import javax.jms.IllegalStateException;
import javax.jms.InvalidDestinationException;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageConsumer;
import javax.jms.MessageListener;
import javax.management.j2ee.statistics.Stats;

/**
 * A client uses a <CODE>MessageConsumer</CODE> object to receive messages from a destination. A <CODE>
 * MessageConsumer</CODE> object is created by passing a <CODE>Destination</CODE> object to a message-consumer
 * creation method supplied by a session.
 * <P>
 * <CODE>MessageConsumer</CODE> is the parent interface for all message consumers.
 * <P>
 * A message consumer can be created with a message selector. A message selector allows the client to restrict the
 * messages delivered to the message consumer to those that match the selector.
 * <P>
 * A client may either synchronously receive a message consumer's messages or have the consumer asynchronously deliver
 * them as they arrive.
 * <P>
 * For synchronous receipt, a client can request the next message from a message consumer using one of its <CODE>
 * receive</CODE> methods. There are several variations of <CODE>receive</CODE> that allow a client to poll or wait
 * for the next message.
 * <P>
 * For asynchronous delivery, a client can register a <CODE>MessageListener</CODE> object with a message consumer. As
 * messages arrive at the message consumer, it delivers them by calling the <CODE>MessageListener</CODE>'s<CODE>
 * onMessage</CODE> method.
 * <P>
 * It is a client programming error for a <CODE>MessageListener</CODE> to throw an exception.
 *
 * @version $Revision: 1.7 $
 * @see javax.jms.MessageConsumer
 * @see javax.jms.QueueReceiver
 * @see javax.jms.TopicSubscriber
 * @see javax.jms.Session
 */
public class ActiveMQMessageConsumer implements MessageConsumer, StatsCapable, Closeable {
    private static final Log log = LogFactory.getLog(ActiveMQMessageConsumer.class);
    protected ActiveMQSession session;
    protected String consumerId;
    protected MemoryBoundedQueue messageQueue;
    protected String messageSelector;
    private MessageListener messageListener;
    protected String consumerName;
    protected ActiveMQDestination destination;
    private boolean closed;
    protected int consumerNumber;
    protected int prefetchNumber;
    protected long startTime;
    protected boolean noLocal;
    protected boolean browser;
    private Thread accessThread;
    private Object messageListenerGuard;
    private JMSConsumerStatsImpl stats;

    /**
     * Create a MessageConsumer
     *
     * @param theSession
     * @param dest
     * @param name
     * @param selector
     * @param cnum
     * @param prefetch
     * @param noLocalValue
     * @param browserValue
     * @throws JMSException
     */
    protected ActiveMQMessageConsumer(ActiveMQSession theSession, ActiveMQDestination dest, String name,
                                      String selector, int cnum, int prefetch, boolean noLocalValue, boolean browserValue) throws JMSException {
        if (dest == null) {
            throw new InvalidDestinationException("Do not understand a null destination");
        }
        if (dest.isTemporary() && theSession.connection.isJ2EEcompliant()) {
            //validate that the destination comes from this Connection
            String physicalName = dest.getPhysicalName();
            if (physicalName == null) {
                throw new IllegalArgumentException("Physical name of Destination should be valid: " + dest);
            }
            String clientID = theSession.connection.getInitializedClientID();
            if (physicalName.indexOf(clientID) < 0) {
                throw new InvalidDestinationException("Cannot use a Temporary destination from another Connection");
            }
            if( dest.isDeleted() ) {
                throw new InvalidDestinationException("Cannot use a Temporary destination that has been deleted");
            }
        }
        dest.incrementConsumerCounter();
        if (selector != null) {
            selector = selector.trim();
            if (selector.length() > 0) {
                // Validate that the selector
                new SelectorParser().parse(selector);
            }
        }
        this.session = theSession;
        this.destination = dest;
        this.consumerName = name;
        this.messageSelector = selector;

        this.consumerNumber = cnum;
        this.prefetchNumber = prefetch;
        this.noLocal = noLocalValue;
        this.browser = browserValue;
        this.startTime = System.currentTimeMillis();
        this.messageListenerGuard = new Object();
        String queueName = theSession.connection.clientID + ":" + name;
        queueName += ":" + cnum;
        this.messageQueue = theSession.connection.getMemoryBoundedQueue(queueName);
        this.stats = new JMSConsumerStatsImpl(theSession.getSessionStats(), dest);
        this.session.addConsumer(this);
    }
    
    /**
     * @return the memory used by the internal queue for this MessageConsumer
     */
    public long getLocalMemoryUsage(){
        return this.messageQueue.getLocalMemoryUsedByThisQueue();
    }
    
    /**
     * @return the number of messages enqueued by this consumer awaiting dispatch
     */
    public int size(){
        return this.messageQueue.size();
    }
   

    /**
     * @return Stats for this MessageConsumer
     */
    public Stats getStats() {
        return stats;
    }

    /**
     * @return Stats for this MessageConsumer
     */
    public JMSConsumerStatsImpl getConsumerStats() {
        return stats;
    }

    /**
     * @return pretty print of this consumer
     */
    public String toString() {
        return "MessageConsumer: " + consumerId;
    }

    /**
     * @return Returns the prefetchNumber.
     */
    public int getPrefetchNumber() {
        return prefetchNumber;
    }

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

    /**
     * Gets this message consumer's message selector expression.
     *
     * @return this message consumer's message selector, or null if no message selector exists for the message consumer
     *         (that is, if the message selector was not set or was set to null or the empty string)
     * @throws JMSException if the JMS provider fails to receive the next message due to some internal error.
     */
    public String getMessageSelector() throws JMSException {
        checkClosed();
        return this.messageSelector;
    }

    /**
     * Gets the message consumer's <CODE>MessageListener</CODE>.
     *
     * @return the listener for the message consumer, or null if no listener is set
     * @throws JMSException if the JMS provider fails to get the message listener due to some internal error.
     * @see javax.jms.MessageConsumer#setMessageListener(javax.jms.MessageListener)
     */
    public MessageListener getMessageListener() throws JMSException {
        checkClosed();
        return this.messageListener;
    }

    /**
     * Sets the message consumer's <CODE>MessageListener</CODE>.
     * <P>
     * Setting the message listener to null is the equivalent of unsetting the message listener for the message
     * consumer.
     * <P>
     * The effect of calling <CODE>MessageConsumer.setMessageListener</CODE> while messages are being consumed by an
     * existing listener or the consumer is being used to consume messages synchronously is undefined.
     *
     * @param listener the listener to which the messages are to be delivered
     * @throws JMSException if the JMS provider fails to receive the next message due to some internal error.
     * @see javax.jms.MessageConsumer#getMessageListener()
     */
    public void setMessageListener(MessageListener listener) throws JMSException {
        checkClosed();
        this.messageListener = listener;
        if (listener != null) {
            session.setSessionConsumerDispatchState(ActiveMQSession.CONSUMER_DISPATCH_ASYNC);
        }
    }

    /**
     * Receives the next message produced for this message consumer.
     * <P>
     * This call blocks indefinitely until a message is produced or until this message consumer is closed.
     * <P>
     * If this <CODE>receive</CODE> is done within a transaction, the consumer retains the message until the
     * transaction commits.
     *
     * @return the next message produced for this message consumer, or null if this message consumer is concurrently
     *         closed
     * @throws JMSException
     */
    public Message receive() throws JMSException {
        checkClosed();
        session.setSessionConsumerDispatchState(ActiveMQSession.CONSUMER_DISPATCH_SYNC);
        try {
            this.accessThread = Thread.currentThread();
            ActiveMQMessage message = (ActiveMQMessage) messageQueue.dequeue();
            this.accessThread = null;
            if (message != null) {
                boolean expired = message.isExpired();
                messageDelivered(message, true,expired);
                if (!expired){
                message = message.shallowCopy();
                }else {
                    message = (ActiveMQMessage) receiveNoWait(); //this will remove any other expired messages held in the queue
                }
            }
            return message;
        }
        catch (InterruptedException ioe) {
            return null;
        }
    }

    /**
     * Receives the next message that arrives within the specified timeout interval.
     * <P>
     * This call blocks until a message arrives, the timeout expires, or this message consumer is closed. A <CODE>
     * timeout</CODE> of zero never expires, and the call blocks indefinitely.
     *
     * @param timeout the timeout value (in milliseconds)
     * @return the next message produced for this message consumer, or null if the timeout expires or this message
     *         consumer is concurrently closed
     * @throws JMSException
     */
    public Message receive(long timeout) throws JMSException {
        checkClosed();
        session.setSessionConsumerDispatchState(ActiveMQSession.CONSUMER_DISPATCH_SYNC);
        try {
            if (timeout == 0) {
                return this.receive();
            }
            this.accessThread = Thread.currentThread();
            ActiveMQMessage message = (ActiveMQMessage) messageQueue.dequeue(timeout);
            this.accessThread = null;
            if (message != null) {
                boolean expired = message.isExpired();
                messageDelivered(message, true,expired);
                if (!expired){
                message = message.shallowCopy();
                }else {
                    message = (ActiveMQMessage) receiveNoWait(); //this will remove any other expired messages held in the queue
                }
            }
            return message;
        }
        catch (InterruptedException ioe) {
            return null;
        }
    }

    /**
     * Receives the next message if one is immediately available.
     *
     * @return the next message produced for this message consumer, or null if one is not available
     * @throws JMSException if the JMS provider fails to receive the next message due to some internal error.
     */
    public Message receiveNoWait() throws JMSException {
        checkClosed();
        session.setSessionConsumerDispatchState(ActiveMQSession.CONSUMER_DISPATCH_SYNC);
        try {
            ActiveMQMessage message = null;
            //iterate through an scrub delivered but expired messages
            while ((message = (ActiveMQMessage)messageQueue.dequeueNoWait())!= null){
                boolean expired = message.isExpired();
                messageDelivered(message,true,expired);
                if (!expired){
                    return message.shallowCopy();
                }
            }
        }
        catch (InterruptedException ioe) {
            throw new JMSException("Queue is interrupted: " + ioe.getMessage());
        }
        return null;
    }

    /**
     * Closes the message consumer.
     * <P>
     * Since a provider may allocate some resources on behalf of a <CODE>MessageConsumer</CODE> outside the Java
     * virtual machine, clients should close them when they are not needed. Relying on garbage collection to eventually
     * reclaim these resources may not be timely enough.
     * <P>
     * This call blocks until a <CODE>receive</CODE> or message listener in progress has completed. A blocked message
     * consumer <CODE>receive</CODE> call returns null when this message consumer is closed.
     *
     * @throws JMSException if the JMS provider fails to close the consumer due to some internal error.
     */
    public void close() throws JMSException {
        try {
            this.accessThread.interrupt();
        }
        catch (NullPointerException npe) {
        }
        catch (SecurityException se) {
        }
        if( destination !=null )
        	destination.decrementConsumerCounter();
        
        this.session.removeConsumer(this);
        messageQueue.close();
        closed = true;
    }

    /**
     * @return true if this is a durable topic subscriber
     */
    public boolean isDurableSubscriber() {
        return this instanceof ActiveMQTopicSubscriber && consumerName != null && consumerName.length() > 0;
    }

    /**
     * @throws IllegalStateException
     */
    protected void checkClosed() throws IllegalStateException {
        if (closed) {
            throw new IllegalStateException("The Consumer is closed");
        }
    }

    /**
     * Process a Message - passing either to the queue or message listener
     *
     * @param message
     */
    protected void processMessage(ActiveMQMessage message) {
        message.setConsumerId(this.consumerId);
        MessageListener listener = null;
        synchronized (messageListenerGuard) {
            listener = this.messageListener;
        }
        try {
            if (!closed) {
                if (listener != null) {
                    beforeMessageDelivered(message);
                    boolean expired = message.isExpired();
                    if (!expired) {
                        listener.onMessage(message.shallowCopy());
                    }
                    afterMessageDelivered(message, true, expired, true);
                }
                else {
                    this.messageQueue.enqueue(message);
                }
            }
            else {
                messageDelivered(message,false,false);
            }
        }
        catch (Exception e) {
            log.warn("could not process message: " + message, e);
            messageDelivered(message, false, false);
        }
    }

    /**
     * @return Returns the consumerId.
     */
    protected String getConsumerId() {
        return consumerId;
    }

    /**
     * @param consumerId The consumerId to set.
     */
    protected void setConsumerId(String consumerId) {
        this.consumerId = consumerId;
    }

    /**
     * @return the consumer name - used for durable consumers
     */
    protected String getConsumerName() {
        return this.consumerName;
    }

    /**
     * Set the name of the Consumer - used for durable subscribers
     *
     * @param value
     */
    protected void setConsumerName(String value) {
        this.consumerName = value;
    }

    /**
     * @return the locally unique Consumer Number
     */
    protected int getConsumerNumber() {
        return this.consumerNumber;
    }

    /**
     * Set the locally unique consumer number
     *
     * @param value
     */
    protected void setConsumerNumber(int value) {
        this.consumerNumber = value;
    }

    /**
     * @return true if this consumer does not accept locally produced messages
     */
    protected boolean isNoLocal() {
        return this.noLocal;
    }

    /**
     * Retrive is a browser
     *
     * @return true if a browser
     */
    protected boolean isBrowser() {
        return this.browser;
    }

    /**
     * Set true if only a Browser
     *
     * @param value
     * @see ActiveMQQueueBrowser
     */
    protected void setBrowser(boolean value) {
        this.browser = value;
    }

    /**
     * @return ActiveMQDestination
     */
    protected ActiveMQDestination getDestination() {
        return this.destination;
    }

    /**
     * @return the startTime
     */
    protected long getStartTime() {
        return startTime;
    }

    protected void clearMessagesInProgress() {
        messageQueue.clear();
    }

    private void messageDelivered(ActiveMQMessage message, boolean messageRead,boolean messageExpired) {
        afterMessageDelivered(message, messageRead, messageExpired, false);
    }

    private void beforeMessageDelivered(ActiveMQMessage message) {
        if( message == null )
            return;        
        this.session.beforeMessageDelivered( message );
    }

    private void afterMessageDelivered(ActiveMQMessage message, boolean messageRead,boolean messageExpired, boolean beforeCalled) {
        if( message == null )
            return;
        
        boolean consumed = browser ? false : messageRead;
        message.setTransientConsumed(!isDurableSubscriber() && message.getJMSActiveMQDestination().isTopic());
        this.session.afterMessageDelivered((isDurableSubscriber() || destination.isQueue()), message, consumed, messageExpired, beforeCalled);
        if (messageRead) {
            stats.onMessage(message);
        }
        
    }
    
}
