/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.qpid.server;

import org.apache.log4j.Logger;

import org.apache.qpid.AMQException;
import org.apache.qpid.configuration.Configured;
import org.apache.qpid.framing.AMQShortString;
import org.apache.qpid.framing.BasicContentHeaderProperties;
import org.apache.qpid.framing.ContentBody;
import org.apache.qpid.framing.ContentHeaderBody;
import org.apache.qpid.framing.FieldTable;
import org.apache.qpid.framing.abstraction.MessagePublishInfo;
import org.apache.qpid.server.ack.UnacknowledgedMessage;
import org.apache.qpid.server.ack.UnacknowledgedMessageMap;
import org.apache.qpid.server.ack.UnacknowledgedMessageMapImpl;
import org.apache.qpid.server.configuration.Configurator;
import org.apache.qpid.server.exchange.NoRouteException;
import org.apache.qpid.server.exchange.Exchange;
import org.apache.qpid.server.protocol.AMQProtocolSession;
import org.apache.qpid.server.queue.*;
import org.apache.qpid.server.store.MessageStore;
import org.apache.qpid.server.store.StoreContext;
import org.apache.qpid.server.txn.LocalTransactionalContext;
import org.apache.qpid.server.txn.NonTransactionalContext;
import org.apache.qpid.server.txn.TransactionalContext;

import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;

public class AMQChannel
{
    public static final int DEFAULT_PREFETCH = 5000;

    private static final Logger _log = Logger.getLogger(AMQChannel.class);

    private final int _channelId;

    // private boolean _transactional;

    private long _prefetch_HighWaterMark;

    private long _prefetch_LowWaterMark;

    private long _prefetchSize;

    /**
     * The delivery tag is unique per channel. This is pre-incremented before putting into the deliver frame so that
     * value of this represents the <b>last</b> tag sent out
     */
    private long _deliveryTag = 0;

    /** A channel has a default queue (the last declared) that is used when no queue name is explictily set */
    private AMQQueue _defaultQueue;

    /** This tag is unique per subscription to a queue. The server returns this in response to a basic.consume request. */
    private int _consumerTag;

    /**
     * The current message - which may be partial in the sense that not all frames have been received yet - which has
     * been received by this channel. As the frames are received the message gets updated and once all frames have been
     * received the message can then be routed.
     */
    private AMQMessage _currentMessage;

    /** Maps from consumer tag to queue instance. Allows us to unsubscribe from a queue. */
    private final Map<AMQShortString, AMQQueue> _consumerTag2QueueMap = new ConcurrentHashMap<AMQShortString, AMQQueue>();

    private final MessageStore _messageStore;

    private UnacknowledgedMessageMap _unacknowledgedMessageMap = new UnacknowledgedMessageMapImpl(DEFAULT_PREFETCH);

    private final AtomicBoolean _suspended = new AtomicBoolean(false);

    private TransactionalContext _txnContext, _nonTransactedContext;

    /**
     * A context used by the message store enabling it to track context for a given channel even across thread
     * boundaries
     */
    private final StoreContext _storeContext;

    private final List<RequiredDeliveryException> _returnMessages = new LinkedList<RequiredDeliveryException>();

    private MessageHandleFactory _messageHandleFactory = new MessageHandleFactory();

    private Set<Long> _browsedAcks = new HashSet<Long>();

    // Why do we need this reference ? - ritchiem
    private final AMQProtocolSession _session;
    private boolean _closing;

    @Configured(path = "advanced.enableJMSXUserID",
                defaultValue = "false")
    public boolean ENABLE_JMSXUserID;


    public AMQChannel(AMQProtocolSession session, int channelId, MessageStore messageStore)
            throws AMQException
    {
        //Set values from configuration
        Configurator.configure(this);

        _session = session;
        _channelId = channelId;
        _storeContext = new StoreContext("Session: " + session.getClientIdentifier() + "; channel: " + channelId);
        _prefetch_HighWaterMark = DEFAULT_PREFETCH;
        _prefetch_LowWaterMark = _prefetch_HighWaterMark / 2;
        _messageStore = messageStore;

        // by default the session is non-transactional
        _txnContext = new NonTransactionalContext(_messageStore, _storeContext, this, _returnMessages);
    }

    /** Sets this channel to be part of a local transaction */
    public void setLocalTransactional()
    {
        _txnContext = new LocalTransactionalContext(_messageStore, _storeContext, _returnMessages);
    }

    public boolean isTransactional()
    {
        // this does not look great but there should only be one "non-transactional"
        // transactional context, while there could be several transactional ones in
        // theory
        return !(_txnContext instanceof NonTransactionalContext);
    }

    public int getChannelId()
    {
        return _channelId;
    }

    public long getPrefetchCount()
    {
        return _prefetch_HighWaterMark;
    }

    public void setPrefetchCount(long prefetchCount)
    {
        _prefetch_HighWaterMark = prefetchCount;
    }

    public long getPrefetchSize()
    {
        return _prefetchSize;
    }

    public void setPrefetchSize(long prefetchSize)
    {
        _prefetchSize = prefetchSize;
    }

    public long getPrefetchLowMarkCount()
    {
        return _prefetch_LowWaterMark;
    }

    public void setPrefetchLowMarkCount(long prefetchCount)
    {
        _prefetch_LowWaterMark = prefetchCount;
    }

    public long getPrefetchHighMarkCount()
    {
        return _prefetch_HighWaterMark;
    }

    public void setPrefetchHighMarkCount(long prefetchCount)
    {
        _prefetch_HighWaterMark = prefetchCount;
    }

    public void setPublishFrame(MessagePublishInfo info, AMQProtocolSession publisher, final Exchange e) throws AMQException
    {

        _currentMessage = new AMQMessage(_messageStore.getNewMessageId(), info, _txnContext);
        _currentMessage.setPublisher(publisher);
        _currentMessage.setExchange(e);
    }

    public void publishContentHeader(ContentHeaderBody contentHeaderBody, AMQProtocolSession protocolSession)
            throws AMQException
    {
        if (_currentMessage == null)
        {
            throw new AMQException("Received content header without previously receiving a BasicPublish frame");
        }
        else
        {
            if (_log.isDebugEnabled())
            {
                _log.debug(debugIdentity() + "Content header received on channel " + _channelId);
            }

            if (ENABLE_JMSXUserID)
            {
                //Set JMSXUserID
                BasicContentHeaderProperties properties = (BasicContentHeaderProperties) contentHeaderBody.properties;
                //fixme: fudge for QPID-677
                properties.getHeaders().keySet();

                properties.setUserId(protocolSession.getAuthorizedID().getName());
            }

            _currentMessage.setContentHeaderBody(contentHeaderBody);
            _currentMessage.setExpiration();

            routeCurrentMessage();
            _currentMessage.routingComplete(_messageStore, _storeContext, _messageHandleFactory);

            // check and deliver if header says body length is zero
            if (contentHeaderBody.bodySize == 0)
            {
                _txnContext.messageProcessed(protocolSession);
                _currentMessage = null;
            }
        }
    }

    public void publishContentBody(ContentBody contentBody, AMQProtocolSession protocolSession) throws AMQException
    {
        if (_currentMessage == null)
        {
            throw new AMQException("Received content body without previously receiving a JmsPublishBody");
        }

        if (_log.isDebugEnabled())
        {
            _log.debug(debugIdentity() + "Content body received on channel " + _channelId);
        }

        try
        {

            // returns true iff the message was delivered (i.e. if all data was
            // received
            if (_currentMessage.addContentBodyFrame(_storeContext,
                        protocolSession.getMethodRegistry().getProtocolVersionMethodConverter().convertToContentChunk(
                            contentBody)))
            {
                // callback to allow the context to do any post message processing
                // primary use is to allow message return processing in the non-tx case
                _txnContext.messageProcessed(protocolSession);
                _currentMessage = null;
            }
        }
        catch (AMQException e)
        {
            // we want to make sure we don't keep a reference to the message in the
            // event of an error
            _currentMessage = null;
            throw e;
        }
    }

    protected void routeCurrentMessage() throws AMQException
    {
        try
        {
            _currentMessage.route();            
        }
        catch (NoRouteException e)
        {
            _returnMessages.add(e);
        }
    }

    public long getNextDeliveryTag()
    {
        return ++_deliveryTag;
    }

    public int getNextConsumerTag()
    {
        return ++_consumerTag;
    }

    /**
     * Subscribe to a queue. We register all subscriptions in the channel so that if the channel is closed we can clean
     * up all subscriptions, even if the client does not explicitly unsubscribe from all queues.
     *
     * @param tag       the tag chosen by the client (if null, server will generate one)
     * @param queue     the queue to subscribe to
     * @param session   the protocol session of the subscriber
     * @param noLocal   Flag stopping own messages being receivied.
     * @param exclusive Flag requesting exclusive access to the queue
     * @param acks      Are acks enabled for this subscriber
     * @param filters   Filters to apply to this subscriber
     *
     * @return the consumer tag. This is returned to the subscriber and used in subsequent unsubscribe requests
     *
     * @throws ConsumerTagNotUniqueException if the tag is not unique
     * @throws AMQException                  if something goes wrong
     */
    public AMQShortString subscribeToQueue(AMQShortString tag, AMQQueue queue, AMQProtocolSession session, boolean acks,
                                           FieldTable filters, boolean noLocal, boolean exclusive) throws AMQException, ConsumerTagNotUniqueException
    {
        if (tag == null)
        {
            tag = new AMQShortString("sgen_" + getNextConsumerTag());
        }

        if (_consumerTag2QueueMap.containsKey(tag))
        {
            throw new ConsumerTagNotUniqueException();
        }

        // We add before we register as the Async Delivery process may AutoClose the subscriber
        // so calling _cT2QM.remove before we have done put which was after the register succeeded.
        // So to keep things straight we put before the call and catch all exceptions from the register and tidy up.
        _consumerTag2QueueMap.put(tag, queue);

        try
        {
            queue.registerProtocolSession(session, _channelId, tag, acks, filters, noLocal, exclusive);
        }
        catch (AMQException e)
        {
            _consumerTag2QueueMap.remove(tag);
            throw e;
        }

        return tag;
    }

    /**
     * Unsubscribe a consumer from a queue.
     * @param session
     * @param consumerTag
     * @return true if the consumerTag had a mapped queue that could be unregistered.
     * @throws AMQException
     */
    public boolean unsubscribeConsumer(AMQProtocolSession session, AMQShortString consumerTag) throws AMQException
    {
        if (_log.isDebugEnabled())
        {
            _log.debug("Unacked Map Dump size:" + _unacknowledgedMessageMap.size());
            _unacknowledgedMessageMap.visit(new UnacknowledgedMessageMap.Visitor()
            {

                public boolean callback(UnacknowledgedMessage message) throws AMQException
                {
                    _log.debug(message);

                    return true;
                }

                public void visitComplete()
                {
                }
            });
        }

        AMQQueue q = _consumerTag2QueueMap.remove(consumerTag);
        if (q != null)
        {
            q.unregisterProtocolSession(session, _channelId, consumerTag);
            return true;
        }
        return false;
    }

    /**
     * Called from the protocol session to close this channel and clean up. T
     *
     * @param session The session to close
     *
     * @throws AMQException if there is an error during closure
     */
    public void close(AMQProtocolSession session) throws AMQException
    {
        _txnContext.rollback();
        unsubscribeAllConsumers(session);
        try
        {
            requeue();
        }
        catch (AMQException e)
        {
            _log.error("Caught AMQException whilst attempting to reque:" + e);        
        }

        setClosing(true);
    }

    private void setClosing(boolean closing)
    {
        _closing = closing;
    }

    private void unsubscribeAllConsumers(AMQProtocolSession session) throws AMQException
    {
        if (_log.isInfoEnabled())
        {
            if (!_consumerTag2QueueMap.isEmpty())
            {
                _log.info("Unsubscribing all consumers on channel " + toString());
            }
            else
            {
                _log.info("No consumers to unsubscribe on channel " + toString());
            }
        }

        for (Map.Entry<AMQShortString, AMQQueue> me : _consumerTag2QueueMap.entrySet())
        {
            if (_log.isInfoEnabled())
            {
                _log.info("Unsubscribing consumer '" + me.getKey() + "' on channel " + toString());
            }

            me.getValue().unregisterProtocolSession(session, _channelId, me.getKey());
        }

        _consumerTag2QueueMap.clear();
    }

    /**
     * Add a message to the channel-based list of unacknowledged messages
     *
     * @param entry       the record of the message on the queue that was delivered
     * @param deliveryTag the delivery tag used when delivering the message (see protocol spec for description of the
     *                    delivery tag)
     * @param consumerTag The tag for the consumer that is to acknowledge this message.
     */
    public void addUnacknowledgedMessage(QueueEntry entry, long deliveryTag, AMQShortString consumerTag)
    {
        if (_log.isDebugEnabled())
        {
            if (entry.getQueue() == null)
            {
                _log.debug("Adding unacked message with a null queue:" + entry.debugIdentity());
            }
            else
            {
                if (_log.isDebugEnabled())
                {
                    _log.debug(debugIdentity() + " Adding unacked message(" + entry.getMessage().toString() + " DT:" + deliveryTag
                               + ") with a queue(" + entry.getQueue() + ") for " + consumerTag);
                }
            }
        }

        synchronized (_unacknowledgedMessageMap.getLock())
        {
            _unacknowledgedMessageMap.add(deliveryTag, new UnacknowledgedMessage(entry, consumerTag, deliveryTag));
            checkSuspension();
        }
    }

    private final String id = "(" + System.identityHashCode(this) + ")";

    public String debugIdentity()
    {
        return _channelId + id;
    }

    /**
     * Called to attempt re-delivery all outstanding unacknowledged messages on the channel. May result in delivery to
     * this same channel or to other subscribers.
     *
     * @throws org.apache.qpid.AMQException if the requeue fails
     */
    public void requeue() throws AMQException
    {
        // we must create a new map since all the messages will get a new delivery tag when they are redelivered
        Collection<UnacknowledgedMessage> messagesToBeDelivered = _unacknowledgedMessageMap.cancelAllMessages();

        // Deliver these messages out of the transaction as their delivery was never
        // part of the transaction only the receive.
        TransactionalContext deliveryContext = null;

        if (!messagesToBeDelivered.isEmpty())
        {
            if (_log.isInfoEnabled())
            {
                _log.info("Requeuing " + messagesToBeDelivered.size() + " unacked messages. for " + toString());
            }

            if (!(_txnContext instanceof NonTransactionalContext))
            {
                // if (_nonTransactedContext == null)
                {
                    _nonTransactedContext =
                            new NonTransactionalContext(_messageStore, _storeContext, this, _returnMessages);
                }

                deliveryContext = _nonTransactedContext;
            }
            else
            {
                deliveryContext = _txnContext;
            }
        }

        for (UnacknowledgedMessage unacked : messagesToBeDelivered)
        {
            if (!unacked.isQueueDeleted())
            {
                // Ensure message is released for redelivery
                unacked.entry.release();

                // Mark message redelivered
                unacked.getMessage().setRedelivered(true);

                // Deliver Message
                deliveryContext.deliver(unacked.entry, false);

                // Should we allow access To the DM to directy deliver the message?
                // As we don't need to check for Consumers or worry about incrementing the message count?
                // unacked.queue.getDeliveryManager().deliver(_storeContext, unacked.queue.getName(), unacked.message, false);
            }
        }

    }

    /**
     * Requeue a single message
     *
     * @param deliveryTag The message to requeue
     *
     * @throws AMQException If something goes wrong.
     */
    public void requeue(long deliveryTag) throws AMQException
    {
        UnacknowledgedMessage unacked = _unacknowledgedMessageMap.remove(deliveryTag);

        if (unacked != null)
        {

            // Ensure message is released for redelivery
            if (!unacked.isQueueDeleted())
            {
                unacked.entry.release();
            }

            // Mark message redelivered
            unacked.getMessage().setRedelivered(true);

            // Deliver these messages out of the transaction as their delivery was never
            // part of the transaction only the receive.
            TransactionalContext deliveryContext;
            if (!(_txnContext instanceof NonTransactionalContext))
            {
                // if (_nonTransactedContext == null)
                {
                    _nonTransactedContext =
                            new NonTransactionalContext(_messageStore, _storeContext, this, _returnMessages);
                }

                deliveryContext = _nonTransactedContext;
            }
            else
            {
                deliveryContext = _txnContext;
            }

            if (!unacked.isQueueDeleted())
            {
                // Redeliver the messages to the front of the queue
                deliveryContext.deliver(unacked.entry, true);
                // Deliver increments the message count but we have already deliverted this once so don't increment it again
                // this was because deliver did an increment changed this.
            }
            else
            {
                _log.warn(System.identityHashCode(this) + " Requested requeue of message(" + unacked.getMessage().debugIdentity()
                          + "):" + deliveryTag + " but no queue defined and no DeadLetter queue so DROPPING message.");
                // _log.error("Requested requeue of message:" + deliveryTag +
                // " but no queue defined using DeadLetter queue:" + getDeadLetterQueue());
                //
                // deliveryContext.deliver(unacked.message, getDeadLetterQueue(), false);
                //
            }
        }
        else
        {
            _log.warn("Requested requeue of message:" + deliveryTag + " but no such delivery tag exists."
                      + _unacknowledgedMessageMap.size());

            if (_log.isDebugEnabled())
            {
                _unacknowledgedMessageMap.visit(new UnacknowledgedMessageMap.Visitor()
                {
                    int count = 0;

                    public boolean callback(UnacknowledgedMessage message) throws AMQException
                    {
                        _log.debug(
                                (count++) + ": (" + message.getMessage().debugIdentity() + ")" + "[" + message.deliveryTag + "]");

                        return false; // Continue
                    }

                    public void visitComplete()
                    {
                    }
                });
            }
        }

    }

    /**
     * Called to resend all outstanding unacknowledged messages to this same channel.
     *
     * @param requeue Are the messages to be requeued or dropped.
     *
     * @throws AMQException When something goes wrong.
     */
    public void resend(final boolean requeue) throws AMQException
    {
        final List<UnacknowledgedMessage> msgToRequeue = new LinkedList<UnacknowledgedMessage>();
        final List<UnacknowledgedMessage> msgToResend = new LinkedList<UnacknowledgedMessage>();

        if (_log.isDebugEnabled())
        {
            _log.debug("unacked map Size:" + _unacknowledgedMessageMap.size());
        }

        // Process the Unacked-Map.
        // Marking messages who still have a consumer for to be resent
        // and those that don't to be requeued.
        _unacknowledgedMessageMap.visit(new UnacknowledgedMessageMap.Visitor()
        {
            public boolean callback(UnacknowledgedMessage message) throws AMQException
            {
                AMQShortString consumerTag = message.consumerTag;
                AMQMessage msg = message.getMessage();
                msg.setRedelivered(true);
                if (consumerTag != null)
                {
                    // Consumer exists
                    if (_consumerTag2QueueMap.containsKey(consumerTag))
                    {
                        msgToResend.add(message);
                    }
                    else // consumer has gone
                    {
                        msgToRequeue.add(message);
                    }
                }
                else
                {
                    // Message has no consumer tag, so was "delivered" to a GET
                    // or consumer no longer registered
                    // cannot resend, so re-queue.
                    if (!message.isQueueDeleted())
                    {
                        if (requeue)
                        {
                            msgToRequeue.add(message);
                        }
                        else
                        {
                            _log.info("No DeadLetter Queue and requeue not requested so dropping message:" + message);
                        }
                    }
                    else
                    {
                        _log.info("Message.queue is null and no DeadLetter Queue so dropping message:" + message);
                    }
                }

                // false means continue processing
                return false;
            }

            public void visitComplete()
            {
            }
        });

        // Process Messages to Resend
        if (_log.isDebugEnabled())
        {
            if (!msgToResend.isEmpty())
            {
                _log.debug("Preparing (" + msgToResend.size() + ") message to resend.");
            }
            else
            {
                _log.debug("No message to resend.");
            }
        }

        for (UnacknowledgedMessage message : msgToResend)
        {
            AMQMessage msg = message.getMessage();

            // Our Java Client will always suspend the channel when resending!
            // If the client has requested the messages be resent then it is
            // their responsibility to ensure that thay are capable of receiving them
            // i.e. The channel hasn't been server side suspended.
            // if (isSuspended())
            // {
            // _log.info("Channel is suspended so requeuing");
            // //move this message to requeue
            // msgToRequeue.add(message);
            // }
            // else
            // {
            // release to allow it to be delivered
            message.entry.release();

            // Without any details from the client about what has been processed we have to mark
            // all messages in the unacked map as redelivered.
            msg.setRedelivered(true);

            Subscription sub = message.entry.getDeliveredSubscription();

            if (sub != null)
            {
                // Get the lock so we can tell if the sub scription has closed.
                // will stop delivery to this subscription until the lock is released.
                // note: this approach would allow the use of a single queue if the
                // PreDeliveryQueue would allow head additions.
                // In the Java Qpid client we are suspended whilst doing this so it is all rather Mute..
                // needs guidance from AMQP WG Model SIG
                synchronized (sub.getSendLock())
                {
                    if (sub.isClosed())
                    {
                        if (_log.isDebugEnabled())
                        {
                            _log.debug("Subscription(" + System.identityHashCode(sub)
                                       + ") closed during resend so requeuing message");
                        }
                        // move this message to requeue
                        msgToRequeue.add(message);
                    }
                    else
                    {
                        if (_log.isDebugEnabled())
                        {
                            _log.debug("Requeuing " + msg.debugIdentity() + " for resend via sub:"
                                       + System.identityHashCode(sub));
                        }

                        sub.addToResendQueue(message.entry);
                        _unacknowledgedMessageMap.remove(message.deliveryTag);
                    }
                } // sync(sub.getSendLock)
            }
            else
            {

                if (_log.isInfoEnabled())
                {
                    _log.info("DeliveredSubscription not recorded so just requeueing(" + message.toString()
                              + ")to prevent loss");
                }
                // move this message to requeue
                msgToRequeue.add(message);
            }
        } // for all messages
        // } else !isSuspend

        if (_log.isInfoEnabled())
        {
            if (!msgToRequeue.isEmpty())
            {
                _log.info("Preparing (" + msgToRequeue.size() + ") message to requeue to.");
            }
        }

        // Deliver these messages out of the transaction as their delivery was never
        // part of the transaction only the receive.
        TransactionalContext deliveryContext;
        if (!(_txnContext instanceof NonTransactionalContext))
        {
            if (_nonTransactedContext == null)
            {
                _nonTransactedContext =
                        new NonTransactionalContext(_messageStore, _storeContext, this, _returnMessages);
            }

            deliveryContext = _nonTransactedContext;
        }
        else
        {
            deliveryContext = _txnContext;
        }

        // Process Messages to Requeue at the front of the queue
        for (UnacknowledgedMessage message : msgToRequeue)
        {
            message.entry.release();
            message.entry.setRedelivered(true);

            deliveryContext.deliver(message.entry, true);

            _unacknowledgedMessageMap.remove(message.deliveryTag);
        }
    }

    /**
     * Callback indicating that a queue has been deleted. We must update the structure of unacknowledged messages to
     * remove the queue reference and also decrement any message reference counts, without actually removing the item
     * since we may get an ack for a delivery tag that was generated from the deleted queue.
     *
     * @param queue the queue that has been deleted
     *
     * @throws org.apache.qpid.AMQException if there is an error processing the unacked messages
     */
    public void queueDeleted(final AMQQueue queue) throws AMQException
    {
        _unacknowledgedMessageMap.visit(new UnacknowledgedMessageMap.Visitor()
        {
            public boolean callback(UnacknowledgedMessage message) throws AMQException
            {
                if (message.getQueue() == queue)
                {
                    try
                    {
                        message.discard(_storeContext);
                        message.setQueueDeleted(true);

                    }
                    catch (AMQException e)
                    {
                        _log.error(
                                "Error decrementing ref count on message " + message.getMessage().getMessageId() + ": " + e, e);
                    }
                }

                return false;
            }

            public void visitComplete()
            {
            }
        });
    }

    /**
     * Acknowledge one or more messages.
     *
     * @param deliveryTag the last delivery tag
     * @param multiple    if true will acknowledge all messages up to an including the delivery tag. if false only
     *                    acknowledges the single message specified by the delivery tag
     *
     * @throws AMQException if the delivery tag is unknown (e.g. not outstanding) on this channel
     */
    public void acknowledgeMessage(long deliveryTag, boolean multiple) throws AMQException
    {
        synchronized (_unacknowledgedMessageMap.getLock())
        {
            if (_log.isDebugEnabled())
            {
                _log.debug("Unacked (PreAck) Size:" + _unacknowledgedMessageMap.size());
            }

            _unacknowledgedMessageMap.acknowledgeMessage(deliveryTag, multiple, _txnContext);

            if (_log.isDebugEnabled())
            {
                _log.debug("Unacked (PostAck) Size:" + _unacknowledgedMessageMap.size());
            }

        }

        checkSuspension();
    }

    /**
     * Used only for testing purposes.
     *
     * @return the map of unacknowledged messages
     */
    public UnacknowledgedMessageMap getUnacknowledgedMessageMap()
    {
        return _unacknowledgedMessageMap;
    }

    private void checkSuspension()
    {
        boolean suspend;

        suspend =
                ((_prefetch_HighWaterMark != 0) && (_unacknowledgedMessageMap.size() >= _prefetch_HighWaterMark))
                || ((_prefetchSize != 0) && (_prefetchSize < _unacknowledgedMessageMap.getUnacknowledgeBytes()));

        setSuspended(suspend);
    }

    public void setSuspended(boolean suspended)
    {
        boolean isSuspended = _suspended.get();

        if (isSuspended && !suspended)
        {
            // Continue being suspended if we are above the _prefetch_LowWaterMark
            suspended = _unacknowledgedMessageMap.size() > _prefetch_LowWaterMark;
        }

        boolean wasSuspended = _suspended.getAndSet(suspended);
        if (wasSuspended != suspended)
        {
            if (wasSuspended)
            {
                _log.debug("Unsuspending channel " + this);
                // may need to deliver queued messages
                for (AMQQueue q : _consumerTag2QueueMap.values())
                {
                    q.deliverAsync();
                }
            }
            else
            {
                _log.debug("Suspending channel " + this);
            }
        }
    }

    public boolean isSuspended()
    {
        return _suspended.get();
    }

    public void commit() throws AMQException
    {
        if (!isTransactional())
        {
            throw new AMQException("Fatal error: commit called on non-transactional channel");
        }

        _txnContext.commit();
    }

    public void rollback() throws AMQException
    {
        _txnContext.rollback();
    }

    public String toString()
    {
        StringBuilder sb = new StringBuilder(30);
        sb.append("Channel: id ").append(_channelId).append(", transaction mode: ").append(isTransactional());
        sb.append(", prefetch marks: ").append(_prefetch_LowWaterMark);
        sb.append("/").append(_prefetch_HighWaterMark);

        return sb.toString();
    }

    public void setDefaultQueue(AMQQueue queue)
    {
        _defaultQueue = queue;
    }

    public AMQQueue getDefaultQueue()
    {
        return _defaultQueue;
    }

    public StoreContext getStoreContext()
    {
        return _storeContext;
    }

    public void processReturns(AMQProtocolSession session) throws AMQException
    {
        if (!_returnMessages.isEmpty())
        {
            for (RequiredDeliveryException bouncedMessage : _returnMessages)
            {
                AMQMessage message = bouncedMessage.getAMQMessage();
                session.getProtocolOutputConverter().writeReturn(message, _channelId, bouncedMessage.getReplyCode().getCode(),
                                                                 new AMQShortString(bouncedMessage.getMessage()));

                message.decrementReference(_storeContext);
            }

            _returnMessages.clear();
        }
    }

    public boolean wouldSuspend(AMQMessage msg)
    {
        if (isSuspended())
        {
            return true;
        }
        else
        {
            boolean willSuspend =
                    ((_prefetch_HighWaterMark != 0) && ((_unacknowledgedMessageMap.size() + 1) > _prefetch_HighWaterMark));
            if (!willSuspend)
            {
                final long unackedSize = _unacknowledgedMessageMap.getUnacknowledgeBytes();

                willSuspend = (_prefetchSize != 0) && (unackedSize != 0) && (_prefetchSize < (msg.getSize() + unackedSize));
            }

            if (willSuspend)
            {
                setSuspended(true);
            }

            return willSuspend;
        }

    }

    public TransactionalContext getTransactionalContext()
    {
        return _txnContext;
    }

    public boolean isClosing()
    {
        return _closing;
    }
}
