/*
 *
 * 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.framing.AMQDataBlock;
import org.apache.qpid.framing.BasicPublishBody;
import org.apache.qpid.framing.ContentBody;
import org.apache.qpid.framing.ContentHeaderBody;
import org.apache.qpid.server.ack.TxAck;
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.exchange.MessageRouter;
import org.apache.qpid.server.protocol.AMQProtocolSession;
import org.apache.qpid.server.queue.AMQMessage;
import org.apache.qpid.server.queue.AMQQueue;
import org.apache.qpid.server.queue.NoConsumersException;
import org.apache.qpid.server.store.MessageStore;
import org.apache.qpid.server.txn.TxnBuffer;
import org.apache.qpid.server.txn.TxnOp;

import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;

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;

    /**
     * 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 AtomicLong _deliveryTag = new AtomicLong(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<String, AMQQueue> _consumerTag2QueueMap = new TreeMap<String, AMQQueue>();

    private final MessageStore _messageStore;

    private final Object _unacknowledgedMessageMapLock = new Object();

    private Map<Long, UnacknowledgedMessage> _unacknowledgedMessageMap = new LinkedHashMap<Long, UnacknowledgedMessage>(DEFAULT_PREFETCH);

    private long _lastDeliveryTag;

    private final AtomicBoolean _suspended = new AtomicBoolean(false);

    private final MessageRouter _exchanges;

    private final TxnBuffer _txnBuffer;

    private TxAck ackOp;

    private final List<AMQDataBlock> _returns = new LinkedList<AMQDataBlock>();

    public AMQChannel(int channelId, MessageStore messageStore, MessageRouter exchanges)
            throws AMQException
    {
        _channelId = channelId;
        _prefetch_HighWaterMark = DEFAULT_PREFETCH;
        _prefetch_LowWaterMark = _prefetch_HighWaterMark / 2;
        _messageStore = messageStore;
        _exchanges = exchanges;
        _txnBuffer = new TxnBuffer(_messageStore);
    }

    public int getChannelId()
    {
        return _channelId;
    }

    public boolean isTransactional()
    {
        return _transactional;
    }

    public void setTransactional(boolean transactional)
    {
        _transactional = transactional;
    }

    public long getPrefetchCount()
    {
        return _prefetch_HighWaterMark;
    }

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

    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(BasicPublishBody publishBody, AMQProtocolSession publisher) throws AMQException
    {
        _currentMessage = new AMQMessage(_messageStore, publishBody);
        _currentMessage.setPublisher(publisher);
    }

    public void publishContentHeader(ContentHeaderBody contentHeaderBody)
            throws AMQException
    {
        if (_currentMessage == null)
        {
            throw new AMQException("Received content header without previously receiving a BasicDeliver frame");
        }
        else
        {
            _currentMessage.setContentHeaderBody(contentHeaderBody);
            // check and route if header says body length is zero
            if (contentHeaderBody.bodySize == 0)
            {
                routeCurrentMessage();
            }
        }
    }

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

        _currentMessage.addContentBodyFrame(contentBody);
        if (_currentMessage.isAllContentReceived())
        {
            routeCurrentMessage();
        }
    }

    protected void routeCurrentMessage() throws AMQException
    {
        if (_transactional)
        {
            //don't create a transaction unless needed
            if (_currentMessage.isPersistent())
            {
                _txnBuffer.containsPersistentChanges();
            }

            //A publication will result in the enlisting of several
            //TxnOps. The first is an op that will store the message.
            //Following that (and ordering is important), an op will
            //be added for every queue onto which the message is
            //enqueued. Finally a cleanup op will be added to decrement
            //the reference associated with the routing.
            Store storeOp = new Store(_currentMessage);
            _txnBuffer.enlist(storeOp);
            _currentMessage.setTxnBuffer(_txnBuffer);
            try
            {
                _exchanges.routeContent(_currentMessage);
                _txnBuffer.enlist(new Cleanup(_currentMessage));
            }
            catch (RequiredDeliveryException e)
            {
                //Can only be due to the mandatory flag, as no attempt
                //has yet been made to deliver the message. The
                //message will thus not have been delivered to any
                //queue so we can return the message (without killing
                //the transaction) and for efficiency remove the store
                //operation from the buffer.
                _txnBuffer.cancel(storeOp);
                throw e;
            }
            finally
            {
                _currentMessage = null;
            }
        }
        else
        {
            try
            {
                _exchanges.routeContent(_currentMessage);
                //following check implements the functionality
                //required by the 'immediate' flag:
                _currentMessage.checkDeliveredToConsumer();
            }
            finally
            {
                _currentMessage.decrementReference();
                _currentMessage = null;
            }
        }
    }

    public long getNextDeliveryTag()
    {
        return _deliveryTag.incrementAndGet();
    }

    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
     * @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 String subscribeToQueue(String tag, AMQQueue queue, AMQProtocolSession session, boolean acks) throws AMQException, ConsumerTagNotUniqueException
    {
        if (tag == null)
        {
            tag = "sgen_" + getNextConsumerTag();
        }
        if (_consumerTag2QueueMap.containsKey(tag))
        {
            throw new ConsumerTagNotUniqueException();
        }

        queue.registerProtocolSession(session, _channelId, tag, acks);
        _consumerTag2QueueMap.put(tag, queue);
        return tag;
    }


    public void unsubscribeConsumer(AMQProtocolSession session, String consumerTag) throws AMQException
    {
        AMQQueue q = _consumerTag2QueueMap.remove(consumerTag);
        if (q != null)
        {
            q.unregisterProtocolSession(session, _channelId, consumerTag);
        }
    }

    /**
     * Called from the protocol session to close this channel and clean up.
     *
     * @throws AMQException if there is an error during closure
     */
    public void close(AMQProtocolSession session) throws AMQException
    {
        if (_transactional)
        {
            synchronized(_txnBuffer)
            {
                _txnBuffer.rollback();//releases messages
            }
        }
        unsubscribeAllConsumers(session);
        requeue();
    }

    private void unsubscribeAllConsumers(AMQProtocolSession session) throws AMQException
    {
        _log.info("Unsubscribing all consumers on channel " + toString());
        for (Map.Entry<String, AMQQueue> me : _consumerTag2QueueMap.entrySet())
        {
            me.getValue().unregisterProtocolSession(session, _channelId, me.getKey());
        }
        _consumerTag2QueueMap.clear();
    }

    /**
     * Add a message to the channel-based list of unacknowledged messages
     *
     * @param message
     * @param deliveryTag
     * @param queue
     */
    public void addUnacknowledgedMessage(AMQMessage message, long deliveryTag, String consumerTag, AMQQueue queue)
    {
        synchronized(_unacknowledgedMessageMapLock)
        {
            _unacknowledgedMessageMap.put(deliveryTag, new UnacknowledgedMessage(queue, message, consumerTag, deliveryTag));
            _lastDeliveryTag = deliveryTag;
            checkSuspension();
        }
    }

    /**
     * Called to attempt re-enqueue all outstanding unacknowledged messages on the channel.
     * May result in delivery to this same channel or to other subscribers.
     */
    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
        Map<Long, UnacknowledgedMessage> currentList;
        synchronized(_unacknowledgedMessageMapLock)
        {
            currentList = _unacknowledgedMessageMap;
            _unacknowledgedMessageMap = new LinkedHashMap<Long, UnacknowledgedMessage>(DEFAULT_PREFETCH);
        }

        for (UnacknowledgedMessage unacked : currentList.values())
        {
            if (unacked.queue != null)
            {
                unacked.queue.deliver(unacked.message);
            }
        }
    }

    /**
     * Called to resend all outstanding unacknowledged messages to this same channel.
     */
    public void resend(AMQProtocolSession session)
    {
        //messages go to this channel
        synchronized(_unacknowledgedMessageMapLock)
        {
            for (Map.Entry<Long, UnacknowledgedMessage> entry : _unacknowledgedMessageMap.entrySet())
            {
                long deliveryTag = entry.getKey();
                String consumerTag = entry.getValue().consumerTag;
                AMQMessage msg = entry.getValue().message;

                session.writeFrame(msg.getDataBlock(_channelId, consumerTag, 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 sine we may get an ack for a delivery tag that was generated from the
     * deleted queue.
     *
     * @param queue
     */
    public void queueDeleted(AMQQueue queue)
    {
        synchronized(_unacknowledgedMessageMapLock)
        {
            for (Map.Entry<Long, UnacknowledgedMessage> unacked : _unacknowledgedMessageMap.entrySet())
            {
                final UnacknowledgedMessage unackedMsg = unacked.getValue();
                // we can compare the reference safely in this case
                if (unackedMsg.queue == queue)
                {
                    unackedMsg.queue = null;
                    try
                    {
                        unackedMsg.message.decrementReference();
                    }
                    catch (AMQException e)
                    {
                        _log.error("Error decrementing ref count on message " + unackedMsg.message.getMessageId() + ": " +
                                   e, e);
                    }
                }
            }
        }
    }

    /**
     * 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
    {
        if (_transactional)
        {
            //check that the tag exists to give early failure
            if (!multiple || deliveryTag > 0)
            {
                checkAck(deliveryTag);
            }
            //we use a single txn op for all acks and update this op
            //as new acks come in. If this is the first ack in the txn
            //we will need to create and enlist the op.
            if (ackOp == null)
            {
                ackOp = new TxAck(new AckMap());
                _txnBuffer.enlist(ackOp);
            }
            //update the op to include this ack request
            if (multiple && deliveryTag == 0)
            {
                synchronized(_unacknowledgedMessageMapLock)
                {
                    //if have signalled to ack all, that refers only
                    //to all at this time
                    ackOp.update(_lastDeliveryTag, multiple);
                }
            }
            else
            {
                ackOp.update(deliveryTag, multiple);
            }
        }
        else
        {
            handleAcknowledgement(deliveryTag, multiple);
        }
    }

    private void checkAck(long deliveryTag) throws AMQException
    {
        synchronized(_unacknowledgedMessageMapLock)
        {
            if (!_unacknowledgedMessageMap.containsKey(deliveryTag))
            {
                throw new AMQException("Ack with delivery tag " + deliveryTag + " not known for channel");
            }
        }
    }

    private void handleAcknowledgement(long deliveryTag, boolean multiple) throws AMQException
    {
        if (multiple)
        {
            LinkedList<UnacknowledgedMessage> acked = new LinkedList<UnacknowledgedMessage>();
            synchronized(_unacknowledgedMessageMapLock)
            {
                if (deliveryTag == 0)
                {
                    //Spec 2.1.6.11 ... If the multiple field is 1, and the delivery tag is zero, tells the server to acknowledge all outstanding mesages.
                    _log.trace("Multiple ack on delivery tag 0. ACKing all messages. Current count:" + _unacknowledgedMessageMap.size());
                    acked = new LinkedList<UnacknowledgedMessage>(_unacknowledgedMessageMap.values());
                    _unacknowledgedMessageMap.clear();
                }
                else
                {
                    if (!_unacknowledgedMessageMap.containsKey(deliveryTag))
                    {
                        throw new AMQException("Multiple ack on delivery tag " + deliveryTag + " not known for channel");
                    }
                    Iterator<Map.Entry<Long, UnacknowledgedMessage>> i = _unacknowledgedMessageMap.entrySet().iterator();

                    while (i.hasNext())
                    {

                        Map.Entry<Long, UnacknowledgedMessage> unacked = i.next();

                        if (unacked.getKey() > deliveryTag)
                        {
                            //This should not occur now.
                            throw new AMQException("UnacknowledgedMessageMap is out of order:" + unacked.getKey() + " When deliveryTag is:" + deliveryTag + "ES:" + _unacknowledgedMessageMap.entrySet().toString());
                        }

                        i.remove();

                        acked.add(unacked.getValue());
                        if (unacked.getKey() == deliveryTag)
                        {
                            break;
                        }
                    }
                }
            }// synchronized

            if (_log.isTraceEnabled())
            {
                _log.trace("Received multiple ack for delivery tag " + deliveryTag + ". Removing " +
                           acked.size() + " items.");
            }

            for (UnacknowledgedMessage msg : acked)
            {
                msg.discard();
            }

        }
        else
        {
            UnacknowledgedMessage msg;
            synchronized(_unacknowledgedMessageMapLock)
            {
                msg = _unacknowledgedMessageMap.remove(deliveryTag);
            }

            if (msg == null)
            {
                _log.trace("Single ack on delivery tag " + deliveryTag + " not known for channel:" + _channelId);
                throw new AMQException("Single ack on delivery tag " + deliveryTag + " not known for channel:" + _channelId);
            }
            msg.discard();
            if (_log.isTraceEnabled())
            {
                _log.trace("Received non-multiple ack for messaging with delivery tag " + deliveryTag);
            }
        }

        checkSuspension();
    }

    /**
     * Used only for testing purposes.
     *
     * @return the map of unacknowledged messages
     */
    public Map<Long, UnacknowledgedMessage> getUnacknowledgedMessageMap()
    {
        return _unacknowledgedMessageMap;
    }

    private void checkSuspension()
    {
        boolean suspend;
        //noinspection SynchronizeOnNonFinalField
        synchronized(_unacknowledgedMessageMapLock)
        {
            suspend = _unacknowledgedMessageMap.size() >= _prefetch_HighWaterMark;
        }
        setSuspended(suspend);
    }

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

        if (isSuspended && !suspended)
        {
            synchronized(_unacknowledgedMessageMapLock)
            {
                // 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 (ackOp != null)
        {
            ackOp.consolidate();
            if (ackOp.checkPersistent())
            {
                _txnBuffer.containsPersistentChanges();
            }
            ackOp = null;//already enlisted, after commit will reset regardless of outcome
        }

        _txnBuffer.commit();
        //TODO: may need to return 'immediate' messages at this point
    }

    public void rollback() throws AMQException
    {
        //need to protect rollback and close from each other...
        synchronized(_txnBuffer)
        {
            _txnBuffer.rollback();
        }
    }

    public String toString()
    {
        StringBuilder sb = new StringBuilder(30);
        sb.append("Channel: id ").append(_channelId).append(", transaction mode: ").append(_transactional);
        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 void processReturns(AMQProtocolSession session)
    {
        for (AMQDataBlock block : _returns)
        {
            session.writeFrame(block);
        }
        _returns.clear();
    }

    //we use this wrapper to ensure we are always using the correct
    //map instance (its not final unfortunately)
    private class AckMap implements UnacknowledgedMessageMap
    {
        public void collect(long deliveryTag, boolean multiple, List<UnacknowledgedMessage> msgs)
        {
            impl().collect(deliveryTag, multiple, msgs);
        }

        public void remove(List<UnacknowledgedMessage> msgs)
        {
            impl().remove(msgs);
        }

        private UnacknowledgedMessageMap impl()
        {
            return new UnacknowledgedMessageMapImpl(_unacknowledgedMessageMapLock, _unacknowledgedMessageMap);
        }
    }

    private class Store implements TxnOp
    {
        //just use this to do a store of the message during the
        //prepare phase. Any enqueueing etc is done by TxnOps enlisted
        //by the queues themselves.
        private final AMQMessage _msg;

        Store(AMQMessage msg)
        {
            _msg = msg;
        }

        public void prepare() throws AMQException
        {
            _msg.storeMessage();
            //the routers reference can now be released
            _msg.decrementReference();
        }

        public void undoPrepare()
        {
        }

        public void commit()
        {
        }

        public void rollback()
        {
        }
    }

    private class Cleanup implements TxnOp
    {
        private final AMQMessage _msg;

        Cleanup(AMQMessage msg)
        {
            _msg = msg;
        }

        public void prepare() throws AMQException
        {
        }

        public void undoPrepare()
        {
            //don't need to do anything here, if the store's txn failed
            //when processing prepare then the message was not stored
            //or enqueued on any queues and can be discarded
        }

        public void commit()
        {
            //The routers reference can now be released.  This is done
            //here to ensure that it happens after the queues that
            //enqueue it have incremented their counts (which as a
            //memory only operation is done in the commit phase).
            try
            {
                _msg.decrementReference();
            }
            catch (AMQException e)
            {
                _log.error("On commiting transaction, failed to cleanup unused message: " + e, e);
            }
            try
            {
                _msg.checkDeliveredToConsumer();
            }
            catch (NoConsumersException e)
            {
                //TODO: store this for delivery after the commit-ok
                _returns.add(e.getReturnMessage(_channelId));
            }
        }

        public void rollback()
        {
        }
    }

}
