/**
 * 
 * Copyright 2004 Protique Ltd
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 * 
 **/

package org.activemq.service.impl;
import java.util.ArrayList;
import java.util.List;
import javax.jms.JMSException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.broker.BrokerClient;
import org.activemq.broker.BrokerConnector;
import org.activemq.filter.Filter;
import org.activemq.message.ActiveMQDestination;
import org.activemq.message.ActiveMQMessage;
import org.activemq.message.BrokerInfo;
import org.activemq.message.ConsumerInfo;
import org.activemq.message.MessageAck;
import org.activemq.service.DeadLetterPolicy;
import org.activemq.service.Dispatcher;
import org.activemq.service.MessageContainer;
import org.activemq.service.MessageIdentity;
import org.activemq.service.QueueList;
import org.activemq.service.QueueListEntry;
import org.activemq.service.RedeliveryPolicy;
import org.activemq.service.SubscriberEntry;
import org.activemq.service.Subscription;
import org.activemq.service.TransactionManager;
import org.activemq.service.TransactionTask;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedBoolean;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedInt;

/**
 * A Subscription holds messages to be dispatched to a a Client Consumer
 * 
 * @version $Revision: 1.1.1.1 $
 */
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 int consumerNumber;
    private String consumerId;
    private boolean browser;
    protected Dispatcher dispatch;
    protected String brokerName;
    protected String clusterName;
    protected MessageIdentity lastMessageIdentity;
    Filter filter;
    protected SynchronizedInt unconsumedMessagesDispatched = new SynchronizedInt(0);
    QueueList messagePtrs = new DefaultQueueList();
    private boolean usePrefetch = true;
    private SubscriberEntry subscriberEntry;
    private ConsumerInfo activeConsumer;
    private BrokerClient activeClient;
    private RedeliveryPolicy redeliveryPolicy;
    private DeadLetterPolicy deadLetterPolicy;
    private SynchronizedBoolean active = new SynchronizedBoolean(false);
    Object lock = new Object();

    /**
     * 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 void clear() throws JMSException {
        synchronized (lock) {
            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
     * 
     * @throws JMSException
     */
    public void reset() throws JMSException {
        synchronized (lock) {
            QueueListEntry entry = messagePtrs.getFirstEntry();
            while (entry != null) {
                MessagePointer pointer = (MessagePointer) entry.getElement();
                if (pointer.isDispatched() && !pointer.isDeleted()) {
                    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 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);
        synchronized (lock) {
            messagePtrs.add(pointer);
        }
        dispatch.wakeup(this);
        lastMessageIdentity = message.getJMSMessageIdentity();
    }

    /**
     * Indicates a message has been delivered to a MessageConsumer
     * 
     * @param ack
     * @throws JMSException
     */
    public void messageConsumed(final MessageAck ack) throws JMSException {
        //remove up to this message
        int count = 0;
        boolean found = false;
        synchronized (lock) {
            QueueListEntry entry = messagePtrs.getFirstEntry();
            while (entry != null) {
                final MessagePointer pointer = (MessagePointer) entry.getElement();
                count++;
                // If in transaction: only consume the message acked.
                // If not in transaction: consume all previously delivered messages.
                if (!ack.isPartOfTransaction() || pointer.getMessageIdentity().equals(ack.getMessageIdentity())) {
                    if ((ack.isExpired() || ack.isMessageRead()) && !browser) {
                        pointer.delete(ack);//delete message from the container (if possible)
                    }
                    if (!ack.isMessageRead() && !browser) {
                        // It was a NACK.
                        pointer.reset();
                        pointer.setRedelivered(true);
                    }
                    else {
                        unconsumedMessagesDispatched.decrement();
                        // We may have to undo the delivery..
                        TransactionManager.getContexTransaction().addPostRollbackTask(new TransactionTask() {
                            public void execute() throws Throwable {
                                unconsumedMessagesDispatched.increment();
                                pointer.reset();
                                pointer.setRedelivered(true);
                                dispatch.wakeup(SubscriptionImpl.this);
                            }
                        });
                        final QueueListEntry theEntry = entry;
                        TransactionManager.getContexTransaction().addPostCommitTask(new TransactionTask() {
                            public void execute() throws Throwable {
                                messagePtrs.remove(theEntry);
                                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);
                                        }
                                    }
                                }
                            }
                        });
                    }
                    if (pointer.getMessageIdentity().equals(ack.getMessageIdentity())) {
                        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);
    }

    /**
     * Retrieve messages to dispatch
     * 
     * @return the messages to dispatch
     * @throws JMSException
     */
    public ActiveMQMessage[] getMessagesToDispatch() throws JMSException {
        if (usePrefetch) {
            return getMessagesWithPrefetch();
        }
        List tmpList = new ArrayList();
        synchronized (lock) {
            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 SubscriberEntry getSubscriptionEntry() {
        if (subscriberEntry == null) {
            subscriberEntry = createSubscriptionEntry();
        }
        return subscriberEntry;
    }

    public boolean isLocalSubscription() {
        if (activeClient != null) {
            return !(activeClient.isClusteredConnection() || activeClient.isBrokerConnection());
        }
        return true;
    }

    // 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 ActiveMQMessage[] getMessagesWithPrefetch() throws JMSException {
        
        List tmpList = new ArrayList();
        synchronized (lock) {
            QueueListEntry entry = messagePtrs.getFirstEntry();
            int count = 0;
            boolean fragmentedMessages = false;
            int maxNumberToDispatch = prefetchLimit - unconsumedMessagesDispatched.get();
            while (entry != null && (count < maxNumberToDispatch || fragmentedMessages)) {
                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);
                        fragmentedMessages = msg.isMessagePart() && !msg.isLastMessagePart();
                        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 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 boolean isReadyToDispatch() throws JMSException {
        /** TODO we may have dispatched messags inside messagePtrs */
        boolean answer = active.get() && 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 boolean isActive() {
        return active.get();
    }

    /**
     * @param newActive The active to set.
     * @throws JMSException
     */
    public void setActive(boolean newActive) throws JMSException {
        synchronized (active.getLock()) {
            active.set(newActive);
        }
        if (!newActive) {
            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;
    }

    protected boolean clientIDsEqual(ActiveMQMessage message) {
        String msgClientID = message.getJMSClientID();
        String subClientID = clientId;
        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));
    }
}