/**
 * 
 * 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.service.impl;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedInt;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.activemq.broker.BrokerClient;
import org.codehaus.activemq.broker.BrokerConnector;
import org.codehaus.activemq.filter.Filter;
import org.codehaus.activemq.message.ActiveMQDestination;
import org.codehaus.activemq.message.ActiveMQMessage;
import org.codehaus.activemq.message.BrokerInfo;
import org.codehaus.activemq.message.ConsumerInfo;
import org.codehaus.activemq.message.MessageAck;
import org.codehaus.activemq.service.DeadLetterPolicy;
import org.codehaus.activemq.service.Dispatcher;
import org.codehaus.activemq.service.MessageContainer;
import org.codehaus.activemq.service.MessageIdentity;
import org.codehaus.activemq.service.QueueList;
import org.codehaus.activemq.service.QueueListEntry;
import org.codehaus.activemq.service.SubscriberEntry;
import org.codehaus.activemq.service.Subscription;
import org.codehaus.activemq.service.RedeliveryPolicy;
import javax.jms.JMSException;
import java.util.ArrayList;
import java.util.List;

/**
 * A Subscription holds messages to be dispatched to a a Client Consumer
 * 
 * @version $Revision: 1.3 $
 */
public class SubscriptionImpl implements Subscription {
    private static final Log log = LogFactory.getLog(SubscriptionImpl.class);
    private String clientId;
    private String subscriberName;
    private ActiveMQDestination destination;
    private String selector;
    private int prefetchLimit;
    private boolean noLocal;
    private boolean active;
    private int consumerNumber;
    private String consumerId;
    private boolean browser;
    protected Dispatcher dispatch;
    protected String brokerName;
    protected String clusterName;
    private MessageIdentity lastMessageIdentity;
    Filter filter;
    protected SynchronizedInt unconsumedMessagesDispatched = new SynchronizedInt(0);
    QueueList messagePtrs = new DefaultQueueList();
    private boolean usePrefetch = false;
    private SubscriberEntry subscriberEntry;
    private ConsumerInfo activeConsumer;
    private BrokerClient activeClient;
    private RedeliveryPolicy redeliveryPolicy;
    private DeadLetterPolicy deadLetterPolicy;

    /**
     * Create a Subscription object that holds messages to be dispatched to a Consumer
     * 
     * @param dispatcher
     * @param client
     * @param info
     * @param filter
     * @param redeliveryPolicy
     * @param deadLetterPolicy
     */
    public SubscriptionImpl(Dispatcher dispatcher, BrokerClient client, ConsumerInfo info, Filter filter,
            RedeliveryPolicy redeliveryPolicy, DeadLetterPolicy deadLetterPolicy) {
        this.dispatch = dispatcher;
        this.filter = filter;
        this.redeliveryPolicy = redeliveryPolicy;
        this.deadLetterPolicy = deadLetterPolicy;
        setActiveConsumer(client, info);
    }

    /**
     * Set the active consumer info
     * 
     * @param client
     * @param info
     */
    public void setActiveConsumer(BrokerClient client, ConsumerInfo info) {
        if (info != null) {
            this.clientId = info.getClientId();
            this.subscriberName = info.getConsumerName();
            this.noLocal = info.isNoLocal();
            this.destination = info.getDestination();
            this.selector = info.getSelector();
            this.prefetchLimit = info.getPrefetchNumber();
            this.consumerNumber = info.getConsumerNo();
            this.consumerId = info.getConsumerId();
            this.browser = info.isBrowser();
        }
        this.activeClient = client;
        this.activeConsumer = info;
        if (client != null) {
            BrokerConnector brokerConnector = client.getBrokerConnector();
            if (brokerConnector != null) {
                BrokerInfo brokerInfo = brokerConnector.getBrokerInfo();
                if (brokerInfo != null) {
                    brokerName = brokerInfo.getBrokerName();
                    clusterName = brokerInfo.getClusterName();
                }
            }
        }
    }

    /**
     * @return pretty print of the Subscription
     */
    public String toString() {
        String str = "SubscriptionImpl(" + super.hashCode() + ")[" + consumerId + "]" + clientId + ": "
                + subscriberName + " : " + destination;
        return str;
    }

    /**
     * Called when the Subscription is discarded
     * 
     * @throws JMSException
     */
    public synchronized void clear() throws JMSException {
        QueueListEntry entry = messagePtrs.getFirstEntry();
        while (entry != null) {
            MessagePointer pointer = (MessagePointer) entry.getElement();
            pointer.clear();
            entry = messagePtrs.getNextEntry(entry);
        }
        messagePtrs.clear();
    }

    /**
     * Called when an active subscriber has closed. This resets all MessagePtrs
     */
    public synchronized void reset() throws JMSException {
        QueueListEntry entry = messagePtrs.getFirstEntry();
        while (entry != null) {
            MessagePointer pointer = (MessagePointer) entry.getElement();
            if (pointer.isDispatched()) {
                pointer.reset();
                pointer.setRedelivered(true);
            }
            else {
                break;
            }
            entry = messagePtrs.getNextEntry(entry);
        }
    }

    /**
     * @return Returns the clientId.
     */
    public String getClientId() {
        return clientId;
    }

    /**
     * @param clientId The clientId to set.
     */
    public void setClientId(String clientId) {
        this.clientId = clientId;
    }

    /**
     * @return Returns the filter.
     */
    public Filter getFilter() {
        return filter;
    }

    /**
     * @param filter The filter to set.
     */
    public void setFilter(Filter filter) {
        this.filter = filter;
    }

    public boolean isWildcard() {
        return filter.isWildcard();
    }

    public String getPersistentKey() {
        // not required other than for persistent topic subscriptions
        return null;
    }

    public boolean isSameDurableSubscription(ConsumerInfo info) throws JMSException {
        if (isDurableTopic()) {
            return equal(clientId, info.getClientId()) && equal(subscriberName, info.getConsumerName());
        }
        return false;
    }

    /**
     * @return Returns the noLocal.
     */
    public boolean isNoLocal() {
        return noLocal;
    }

    /**
     * @param noLocal The noLocal to set.
     */
    public void setNoLocal(boolean noLocal) {
        this.noLocal = noLocal;
    }

    /**
     * @return Returns the subscriberName.
     */
    public String getSubscriberName() {
        return subscriberName;
    }

    /**
     * @param subscriberName The subscriberName to set.
     */
    public void setSubscriberName(String subscriberName) {
        this.subscriberName = subscriberName;
    }

    public RedeliveryPolicy getRedeliveryPolicy() {
        return redeliveryPolicy;
    }

    public void setRedeliveryPolicy(RedeliveryPolicy redeliveryPolicy) {
        this.redeliveryPolicy = redeliveryPolicy;
    }

    /**
     * determines if the Subscription is interested in the message
     * 
     * @param message
     * @return true if this Subscription will accept the message
     * @throws JMSException
     */
    public boolean isTarget(ActiveMQMessage message) throws JMSException {
        boolean result = false;
        if (message != null) {
            if (activeClient == null || brokerName == null || clusterName == null
                    || !activeClient.isClusteredConnection() || !message.isEntryCluster(clusterName)
                    || message.isEntryBroker(brokerName)) {
                result = message.isDispatchedFromDLQ() || filter.matches(message);
                // lets check that we don't have no-local enabled
                if (noLocal && result) {
                    if (clientIDsEqual(message)) {
                        result = false;
                    }
                }
            }
        }
        return result;
    }

    /**
     * If the Subscription is a target for the message, the subscription will add a reference to the message and
     * register an interest in the message to the container
     * 
     * @param container
     * @param message
     * @throws JMSException
     */
    public synchronized void addMessage(MessageContainer container, ActiveMQMessage message) throws JMSException {
        //log.info("###### Adding to subscription: " + this + " message: " + message);
        if (log.isDebugEnabled()) {
            log.debug("Adding to subscription: " + this + " message: " + message);
        }
        MessagePointer pointer = new MessagePointer(container, message.getJMSMessageIdentity());
        messagePtrs.add(pointer);
        dispatch.wakeup(this);
        lastMessageIdentity = message.getJMSMessageIdentity();
    }

    /**
     * Indicates a message has been delivered to a MessageConsumer
     * 
     * @param ack
     * @throws JMSException
     */
    public synchronized void messageConsumed(MessageAck ack) throws JMSException {
        doMessageConsume(ack, true);
    }

    public synchronized void onAcknowledgeTransactedMessageBeforeCommit(MessageAck ack) throws JMSException {
        doMessageConsume(ack, false);
    }

    public synchronized void redeliverMessage(MessageContainer container, MessageAck ack) throws JMSException {
        QueueListEntry entry = messagePtrs.getFirstEntry();
        while (entry != null) {
            MessagePointer pointer = (MessagePointer) entry.getElement();
            if (pointer.getMessageIdentity().getMessageID().equals(ack.getMessageID())) {
                break;
            }
            entry = messagePtrs.getNextEntry(entry);
        }
        if (entry != null) {
            MessagePointer pointer = (MessagePointer) entry.getElement();
            if (pointer != null) {
                unconsumedMessagesDispatched.increment();
                pointer.reset();
                pointer.setRedelivered(true);
                dispatch.wakeup(this);
            }
        }
    }

    /**
     * Retrieve messages to dispatch
     * 
     * @return the messages to dispatch
     * @throws JMSException
     */
    public synchronized ActiveMQMessage[] getMessagesToDispatch() throws JMSException {
        if (usePrefetch) {
            return getMessagesWithPrefetch();
        }
        List tmpList = new ArrayList();
        QueueListEntry entry = messagePtrs.getFirstEntry();
        while (entry != null) {
            MessagePointer pointer = (MessagePointer) entry.getElement();
            if (!pointer.isDispatched()) {
                ActiveMQMessage msg = pointer.getContainer().getMessage(pointer.getMessageIdentity());
                if (msg != null) {
                    if (pointer.isDispatched() || pointer.isRedelivered()) {
                        //already dispatched - so mark as redelivered
                        msg.setJMSRedelivered(true);
                        if (redeliveryPolicy.isBackOffMode()
                                && msg.getDeliveryCount() < redeliveryPolicy.getMaximumRetryCount()) {
                            long sleepTime = redeliveryPolicy.getInitialRedeliveryTimeout();
                            sleepTime *= (msg.getDeliveryCount() * redeliveryPolicy.getBackOffIncreaseRate());
                            try {
                                Thread.sleep(sleepTime);
                            }
                            catch (InterruptedException e) {
                            }
                        }
                        //incremenent delivery count
                        msg.incrementDeliveryCount();
                    }
                    if (!pointer.getContainer().isDeadLetterQueue()
                            && (msg.isExpired() || msg.getDeliveryCount() >= redeliveryPolicy.getMaximumRetryCount())) {
                        if (msg.isExpired()) {
                            log.warn("Message: " + msg + " has expired");
                        }
                        else {
                            log.warn("Message: " + msg + " exceeded retry count: " + msg.getDeliveryCount());
                        }
                        deadLetterPolicy.sendToDeadLetter(msg);
                        QueueListEntry discarded = entry;
                        entry = messagePtrs.getPrevEntry(discarded);
                        messagePtrs.remove(discarded);
                    }
                    else {
                        pointer.setDispatched(true);
                        msg.setDispatchedFromDLQ(pointer.getContainer().isDeadLetterQueue());
                        tmpList.add(msg);
                    }
                }
                else {
                    //the message is probably expired
                    log.info("Message probably expired: " + msg);
                    QueueListEntry discarded = entry;
                    entry = messagePtrs.getPrevEntry(discarded);
                    messagePtrs.remove(discarded);
                    if (msg != null) {
                        deadLetterPolicy.sendToDeadLetter(msg);
                    }
                }
            }
            entry = messagePtrs.getNextEntry(entry);
        }
        ActiveMQMessage[] messages = new ActiveMQMessage[tmpList.size()];
        return (ActiveMQMessage[]) tmpList.toArray(messages);
    }

    public synchronized SubscriberEntry getSubscriptionEntry() {
        if (subscriberEntry == null) {
            subscriberEntry = createSubscriptionEntry();
        }
        return subscriberEntry;
    }

    // Implementation methods
    //-------------------------------------------------------------------------
    protected SubscriberEntry createSubscriptionEntry() {
        SubscriberEntry answer = new SubscriberEntry();
        answer.setClientID(clientId);
        answer.setConsumerName(subscriberName);
        answer.setDestination(destination.getPhysicalName());
        answer.setSelector(selector);
        return answer;
    }

    protected synchronized ActiveMQMessage[] getMessagesWithPrefetch() throws JMSException {
        List tmpList = new ArrayList();
        QueueListEntry entry = messagePtrs.getFirstEntry();
        int count = 0;
        int maxNumberToDispatch = prefetchLimit - unconsumedMessagesDispatched.get();
        while (entry != null && count < maxNumberToDispatch) {
            MessagePointer pointer = (MessagePointer) entry.getElement();
            if (!pointer.isDispatched()) {
                ActiveMQMessage msg = pointer.getContainer().getMessage(pointer.getMessageIdentity());
                if (msg != null && !msg.isExpired()) {
                    if (pointer.isDispatched() || pointer.isRedelivered()) {
                        //already dispatched - so mark as redelivered
                        msg.setJMSRedelivered(true);
                    }
                    pointer.setDispatched(true);
                    tmpList.add(msg);
                    unconsumedMessagesDispatched.increment();
                    count++;
                }
                else {
                    //the message is probably expired
                    log.info("Message probably expired: " + msg);
                    QueueListEntry discarded = entry;
                    entry = messagePtrs.getPrevEntry(discarded);
                    messagePtrs.remove(discarded);
                    if (msg != null) {
                        deadLetterPolicy.sendToDeadLetter(msg);
                    }
                }
            }
            entry = messagePtrs.getNextEntry(entry);
        }
        /**
         * if (tmpList.isEmpty() && ! messagePtrs.isEmpty()) { System.out.println("### Nothing to dispatch but
         * messagePtrs still has: " + messagePtrs.size() + " to dispatch, prefetchLimit: " + prefetchLimit + "
         * unconsumedMessagesDispatched: " + unconsumedMessagesDispatched.get() + " maxNumberToDispatch: " +
         * maxNumberToDispatch); MessagePointer first = (MessagePointer) messagePtrs.getFirst(); System.out.println("###
         * First: " + first + " dispatched: " + first.isDispatched() + " id: " + first.getMessageIdentity()); } else {
         * if (! tmpList.isEmpty()) { System.out.println("### dispatching: " + tmpList.size() + " items = " + tmpList); } }
         */
        ActiveMQMessage[] messages = new ActiveMQMessage[tmpList.size()];
        return (ActiveMQMessage[]) tmpList.toArray(messages);
    }

    /**
     * Indicates the Subscription it's reached it's pre-fetch limit
     * 
     * @return true/false
     * @throws JMSException
     */
    public synchronized boolean isAtPrefetchLimit() throws JMSException {
        if (usePrefetch) {
            int underlivedMessageCount = messagePtrs.size() - unconsumedMessagesDispatched.get();
            return underlivedMessageCount >= prefetchLimit;
        }
        else {
            return false;
        }
    }

    /**
     * Indicates if this Subscription has more messages to send to the Consumer
     * 
     * @return true if more messages available to dispatch
     */
    public synchronized boolean isReadyToDispatch() throws JMSException {
        /** TODO we may have dispatched messags inside messagePtrs */
        boolean answer = active && messagePtrs.size() > 0;
        return answer;
    }

    /**
     * @return Returns the destination.
     */
    public ActiveMQDestination getDestination() {
        return destination;
    }

    /**
     * @return Returns the selector.
     */
    public String getSelector() {
        return selector;
    }

    /**
     * @return Returns the active.
     */
    public synchronized boolean isActive() {
        return active;
    }

    /**
     * @param active The active to set.
     */
    public synchronized void setActive(boolean active) throws JMSException {
        this.active = active;
        if (!active) {
            reset();
        }
    }

    /**
     * @return Returns the consumerNumber.
     */
    public int getConsumerNumber() {
        return consumerNumber;
    }

    /**
     * @return the consumer Id for the active consumer
     */
    public String getConsumerId() {
        return consumerId;
    }

    /**
     * Indicates the Subscriber is a Durable Subscriber
     * 
     * @return true if the subscriber is a durable topic
     * @throws JMSException
     */
    public boolean isDurableTopic() throws JMSException {
        return destination.isTopic() && subscriberName != null && subscriberName.length() > 0;
    }

    /**
     * Indicates the consumer is a browser only
     * 
     * @return true if a Browser
     * @throws JMSException
     */
    public boolean isBrowser() throws JMSException {
        return browser;
    }

    public MessageIdentity getLastMessageIdentity() throws JMSException {
        return lastMessageIdentity;
    }

    public void setLastMessageIdentifier(MessageIdentity messageIdentity) throws JMSException {
        this.lastMessageIdentity = messageIdentity;
    }

    /**
     * Consume a message. If we are inside a transaction then we just update the consumed messages dispatched counter
     * and we don't actually remove the message until a future call.
     * 
     * @param ack the ack command
     * @param remove whether we should actually remove the message (i.e. really consume the message) or should we just
     * update the counters for the dispatcher / prefetch logic to work
     * @throws JMSException
     */
    protected synchronized void doMessageConsume(MessageAck ack, boolean remove) throws JMSException {
        //remove up to this message
        int count = 0;
        boolean found = false;
        QueueListEntry entry = messagePtrs.getFirstEntry();
        while (entry != null) {
            MessagePointer pointer = (MessagePointer) entry.getElement();
            if (remove) {
                messagePtrs.remove(entry);
                if ((ack.isExpired() || ack.isMessageRead()) && !browser) {
                    if (ack.isExpired() && !pointer.getContainer().isDeadLetterQueue()) {
                        ActiveMQMessage msg = pointer.getContainer().getMessage(pointer.getMessageIdentity());
                        if (msg != null) {
                            deadLetterPolicy.sendToDeadLetter(msg);
                        }
                    }
                    pointer.delete(ack);//delete message from the container (if possible)
                }
            }
            count++;
            // in transactions, we decrement on the first call and then don't decrement
            // the second call when we really remove the pointer
            if (remove && !ack.isPartOfTransaction()) {
                unconsumedMessagesDispatched.decrement();
            }
            if (pointer.getMessageIdentity().equals(ack.getMessageIdentity())) {
                if (!remove && ack.isPartOfTransaction()) {
                    // only decrement by one on the last message
                    // as we will be keeping around all the unconsumedMessages
                    unconsumedMessagesDispatched.decrement();
                }
                found = true;
                break;
            }
            entry = messagePtrs.getNextEntry(entry);
        }
        if (!found && log.isDebugEnabled()) {
            log.debug("Did not find a matching message for identity: " + ack.getMessageIdentity());
        }
        dispatch.wakeup(this);
    }

    protected boolean clientIDsEqual(ActiveMQMessage message) {
        String msgClientID = message.getJMSClientID();
        String producerClientID = message.getProducerID();
        String subClientID = clientId;
        if (producerClientID != null && producerClientID.equals(subClientID)) {
            return true;
        }
        else if (msgClientID == null || subClientID == null) {
            return false;
        }
        else {
            return msgClientID.equals(subClientID);
        }
    }

    protected static final boolean equal(Object left, Object right) {
        return left == right || (left != null && right != null && left.equals(right));
    }
}