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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.jms.IllegalStateException;
import javax.jms.JMSException;

import org.apache.activemq.DestinationDoesNotExistException;
import org.apache.activemq.advisory.AdvisorySupport;
import org.apache.activemq.broker.ConnectionContext;
import org.apache.activemq.broker.ConsumerBrokerExchange;
import org.apache.activemq.broker.ProducerBrokerExchange;
import org.apache.activemq.broker.region.policy.PolicyEntry;
import org.apache.activemq.broker.region.virtual.CompositeDestinationFilter;
import org.apache.activemq.command.ActiveMQDestination;
import org.apache.activemq.command.ConsumerControl;
import org.apache.activemq.command.ConsumerId;
import org.apache.activemq.command.ConsumerInfo;
import org.apache.activemq.command.Message;
import org.apache.activemq.command.MessageAck;
import org.apache.activemq.command.MessageDispatchNotification;
import org.apache.activemq.command.MessagePull;
import org.apache.activemq.command.ProducerInfo;
import org.apache.activemq.command.RemoveSubscriptionInfo;
import org.apache.activemq.command.Response;
import org.apache.activemq.filter.DestinationFilter;
import org.apache.activemq.filter.DestinationMap;
import org.apache.activemq.security.SecurityContext;
import org.apache.activemq.thread.TaskRunnerFactory;
import org.apache.activemq.usage.SystemUsage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 *
 */
public abstract class AbstractRegion implements Region {

    private static final Logger LOG = LoggerFactory.getLogger(AbstractRegion.class);

    protected final Map<ActiveMQDestination, Destination> destinations = new ConcurrentHashMap<ActiveMQDestination, Destination>();
    protected final DestinationMap destinationMap = new DestinationMap();
    protected final Map<ConsumerId, Subscription> subscriptions = new ConcurrentHashMap<ConsumerId, Subscription>();
    protected final SystemUsage usageManager;
    protected final DestinationFactory destinationFactory;
    protected final DestinationStatistics destinationStatistics;
    protected final RegionStatistics regionStatistics = new RegionStatistics();
    protected final RegionBroker broker;
    protected boolean autoCreateDestinations = true;
    protected final TaskRunnerFactory taskRunnerFactory;
    protected final ReentrantReadWriteLock destinationsLock = new ReentrantReadWriteLock();
    protected final Map<ConsumerId, Object> consumerChangeMutexMap = new HashMap<ConsumerId, Object>();
    protected boolean started;

    public AbstractRegion(RegionBroker broker, DestinationStatistics destinationStatistics, SystemUsage memoryManager,
            TaskRunnerFactory taskRunnerFactory, DestinationFactory destinationFactory) {
        if (broker == null) {
            throw new IllegalArgumentException("null broker");
        }
        this.broker = broker;
        this.destinationStatistics = destinationStatistics;
        this.usageManager = memoryManager;
        this.taskRunnerFactory = taskRunnerFactory;
        if (destinationFactory == null) {
            throw new IllegalArgumentException("null destinationFactory");
        }
        this.destinationFactory = destinationFactory;
    }

    @Override
    public final void start() throws Exception {
        started = true;

        Set<ActiveMQDestination> inactiveDests = getInactiveDestinations();
        for (Iterator<ActiveMQDestination> iter = inactiveDests.iterator(); iter.hasNext();) {
            ActiveMQDestination dest = iter.next();

            ConnectionContext context = new ConnectionContext();
            context.setBroker(broker.getBrokerService().getBroker());
            context.setSecurityContext(SecurityContext.BROKER_SECURITY_CONTEXT);
            context.getBroker().addDestination(context, dest, false);
        }
        destinationsLock.readLock().lock();
        try{
            for (Iterator<Destination> i = destinations.values().iterator(); i.hasNext();) {
                Destination dest = i.next();
                dest.start();
            }
        } finally {
            destinationsLock.readLock().unlock();
        }
    }

    @Override
    public void stop() throws Exception {
        started = false;
        destinationsLock.readLock().lock();
        try{
            for (Iterator<Destination> i = destinations.values().iterator(); i.hasNext();) {
                Destination dest = i.next();
                dest.stop();
            }
        } finally {
            destinationsLock.readLock().unlock();
        }

        destinationsLock.writeLock().lock();
        try {
            destinations.clear();
            regionStatistics.getAdvisoryDestinations().reset();
            regionStatistics.getDestinations().reset();
            regionStatistics.getAllDestinations().reset();
        } finally {
            destinationsLock.writeLock().unlock();
        }
    }

    @Override
    public Destination addDestination(ConnectionContext context, ActiveMQDestination destination,
            boolean createIfTemporary) throws Exception {

        destinationsLock.writeLock().lock();
        try {
            Destination dest = destinations.get(destination);
            if (dest == null) {
                if (destination.isTemporary() == false || createIfTemporary) {
                    // Limit the number of destinations that can be created if
                    // maxDestinations has been set on a policy
                    validateMaxDestinations(destination);

                    LOG.debug("{} adding destination: {}", broker.getBrokerName(), destination);
                    dest = createDestination(context, destination);
                    // intercept if there is a valid interceptor defined
                    DestinationInterceptor destinationInterceptor = broker.getDestinationInterceptor();
                    if (destinationInterceptor != null) {
                        dest = destinationInterceptor.intercept(dest);
                    }
                    dest.start();
                    addSubscriptionsForDestination(context, dest);
                    destinations.put(destination, dest);
                    updateRegionDestCounts(destination, 1);
                    destinationMap.put(destination, dest);
                }
                if (dest == null) {
                    throw new DestinationDoesNotExistException(destination.getQualifiedName());
                }
            }
            return dest;
        } finally {
            destinationsLock.writeLock().unlock();
        }
    }

    public Map<ConsumerId, Subscription> getSubscriptions() {
        return subscriptions;
    }


    /**
     * Updates the counts in RegionStatistics based on whether or not the destination
     * is an Advisory Destination or not
     *
     * @param destination the destination being used to determine which counters to update
     * @param count the count to add to the counters
     */
    protected void updateRegionDestCounts(ActiveMQDestination destination, int count) {
        if (destination != null) {
            if (AdvisorySupport.isAdvisoryTopic(destination)) {
                regionStatistics.getAdvisoryDestinations().add(count);
            } else {
                regionStatistics.getDestinations().add(count);
            }
            regionStatistics.getAllDestinations().add(count);
        }
    }

    /**
     * This method checks whether or not the destination can be created based on
     * {@link PolicyEntry#getMaxDestinations}, if it has been set. Advisory
     * topics are ignored.
     *
     * @param destination
     * @throws Exception
     */
    protected void validateMaxDestinations(ActiveMQDestination destination)
            throws Exception {
        if (broker.getDestinationPolicy() != null) {
            PolicyEntry entry = broker.getDestinationPolicy().getEntryFor(destination);
            // Make sure the destination is not an advisory topic
            if (entry != null && entry.getMaxDestinations() >= 0
                    && !AdvisorySupport.isAdvisoryTopic(destination)) {
                // If there is an entry for this destination, look up the set of
                // destinations associated with this policy
                // If a destination isn't specified, then just count up
                // non-advisory destinations (ie count all destinations)
                int destinationSize = (int) (entry.getDestination() != null ?
                        destinationMap.get(entry.getDestination()).size() : regionStatistics.getDestinations().getCount());
                if (destinationSize >= entry.getMaxDestinations()) {
                    if (entry.getDestination() != null) {
                        throw new IllegalStateException(
                                "The maxmimum number of destinations allowed ("+ entry.getMaxDestinations() +
                                ") for the policy " + entry.getDestination() + " has already been reached.");
                    // No destination has been set (default policy)
                    } else {
                        throw new IllegalStateException("The maxmimum number of destinations allowed ("
                                        + entry.getMaxDestinations() + ") has already been reached.");
                    }
                }
            }
        }
    }

    protected List<Subscription> addSubscriptionsForDestination(ConnectionContext context, Destination dest) throws Exception {
        List<Subscription> rc = new ArrayList<Subscription>();
        // Add all consumers that are interested in the destination.
        for (Iterator<Subscription> iter = subscriptions.values().iterator(); iter.hasNext();) {
            Subscription sub = iter.next();
            if (sub.matches(dest.getActiveMQDestination())) {
                try {
                    ConnectionContext originalContext = sub.getContext() != null ? sub.getContext() : context;
                    dest.addSubscription(originalContext, sub);
                    rc.add(sub);
                } catch (SecurityException e) {
                    if (sub.isWildcard()) {
                        LOG.debug("Subscription denied for " + sub + " to destination " +
                            dest.getActiveMQDestination() +  ": " + e.getMessage());
                    } else {
                        throw e;
                    }
                }
            }
        }
        return rc;

    }

    @Override
    public void removeDestination(ConnectionContext context, ActiveMQDestination destination, long timeout)
            throws Exception {

        // No timeout.. then try to shut down right way, fails if there are
        // current subscribers.
        if (timeout == 0) {
            for (Iterator<Subscription> iter = subscriptions.values().iterator(); iter.hasNext();) {
                Subscription sub = iter.next();
                if (sub.matches(destination) ) {
                    throw new JMSException("Destination still has an active subscription: " + destination);
                }
            }
        }

        if (timeout > 0) {
            // TODO: implement a way to notify the subscribers that we want to
            // take the down
            // the destination and that they should un-subscribe.. Then wait up
            // to timeout time before
            // dropping the subscription.
        }

        LOG.debug("{} removing destination: {}", broker.getBrokerName(), destination);

        destinationsLock.writeLock().lock();
        try {
            Destination dest = destinations.remove(destination);
            if (dest != null) {
                updateRegionDestCounts(destination, -1);

                // timeout<0 or we timed out, we now force any remaining
                // subscriptions to un-subscribe.
                for (Iterator<Subscription> iter = subscriptions.values().iterator(); iter.hasNext();) {
                    Subscription sub = iter.next();
                    if (sub.matches(destination)) {
                        dest.removeSubscription(context, sub, 0l);
                    }
                }
                destinationMap.remove(destination, dest);
                dispose(context, dest);
                DestinationInterceptor destinationInterceptor = broker.getDestinationInterceptor();
                if (destinationInterceptor != null) {
                    destinationInterceptor.remove(dest);
                }

            } else {
                LOG.debug("Cannot remove a destination that doesn't exist: {}", destination);
            }
        } finally {
            destinationsLock.writeLock().unlock();
        }
    }

    /**
     * Provide an exact or wildcard lookup of destinations in the region
     *
     * @return a set of matching destination objects.
     */
    @Override
    @SuppressWarnings("unchecked")
    public Set<Destination> getDestinations(ActiveMQDestination destination) {
        destinationsLock.readLock().lock();
        try{
            return destinationMap.get(destination);
        } finally {
            destinationsLock.readLock().unlock();
        }
    }

    @Override
    public Map<ActiveMQDestination, Destination> getDestinationMap() {
        return destinations;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Subscription addConsumer(ConnectionContext context, ConsumerInfo info) throws Exception {
        LOG.debug("{} adding consumer: {} for destination: {}", new Object[]{ broker.getBrokerName(), info.getConsumerId(), info.getDestination() });
        ActiveMQDestination destination = info.getDestination();
        if (destination != null && !destination.isPattern() && !destination.isComposite()) {
            // lets auto-create the destination
            lookup(context, destination,true);
        }

        Object addGuard;
        synchronized (consumerChangeMutexMap) {
            addGuard = consumerChangeMutexMap.get(info.getConsumerId());
            if (addGuard == null) {
                addGuard = new Object();
                consumerChangeMutexMap.put(info.getConsumerId(), addGuard);
            }
        }
        synchronized (addGuard) {
            Subscription o = subscriptions.get(info.getConsumerId());
            if (o != null) {
                LOG.warn("A duplicate subscription was detected. Clients may be misbehaving. Later warnings you may see about subscription removal are a consequence of this.");
                return o;
            }

            // We may need to add some destinations that are in persistent store
            // but not active
            // in the broker.
            //
            // TODO: think about this a little more. This is good cause
            // destinations are not loaded into
            // memory until a client needs to use the queue, but a management
            // agent viewing the
            // broker will not see a destination that exists in persistent
            // store. We may want to
            // eagerly load all destinations into the broker but have an
            // inactive state for the
            // destination which has reduced memory usage.
            //
            DestinationFilter.parseFilter(info.getDestination());

            Subscription sub = createSubscription(context, info);

            // At this point we're done directly manipulating subscriptions,
            // but we need to retain the synchronized block here. Consider
            // otherwise what would happen if at this point a second
            // thread added, then removed, as would be allowed with
            // no mutex held. Remove is only essentially run once
            // so everything after this point would be leaked.

            // Add the subscription to all the matching queues.
            // But copy the matches first - to prevent deadlocks
            List<Destination> addList = new ArrayList<Destination>();
            destinationsLock.readLock().lock();
            try {
                for (Destination dest : (Set<Destination>) destinationMap.get(info.getDestination())) {
                    addList.add(dest);
                }
                // ensure sub visible to any new dest addSubscriptionsForDestination
                subscriptions.put(info.getConsumerId(), sub);
            } finally {
                destinationsLock.readLock().unlock();
            }

            List<Destination> removeList = new ArrayList<Destination>();
            for (Destination dest : addList) {
                try {
                    dest.addSubscription(context, sub);
                    removeList.add(dest);
                } catch (SecurityException e){
                    if (sub.isWildcard()) {
                        LOG.debug("Subscription denied for " + sub + " to destination " +
                            dest.getActiveMQDestination() + ": " + e.getMessage());
                    } else {
                        // remove partial subscriptions
                        for (Destination remove : removeList) {
                            try {
                                remove.removeSubscription(context, sub, info.getLastDeliveredSequenceId());
                            } catch (Exception ex) {
                                LOG.error("Error unsubscribing " + sub + " from " + remove + ": " + ex.getMessage(), ex);
                            }
                        }
                        subscriptions.remove(info.getConsumerId());
                        removeList.clear();
                        throw e;
                    }
                }
            }
            removeList.clear();

            if (info.isBrowser()) {
                ((QueueBrowserSubscription) sub).destinationsAdded();
            }

            return sub;
        }
    }

    /**
     * Get all the Destinations that are in storage
     *
     * @return Set of all stored destinations
     */
    @SuppressWarnings("rawtypes")
    public Set getDurableDestinations() {
        return destinationFactory.getDestinations();
    }

    /**
     * @return all Destinations that don't have active consumers
     */
    protected Set<ActiveMQDestination> getInactiveDestinations() {
        Set<ActiveMQDestination> inactiveDests = destinationFactory.getDestinations();
        destinationsLock.readLock().lock();
        try {
            inactiveDests.removeAll(destinations.keySet());
        } finally {
            destinationsLock.readLock().unlock();
        }
        return inactiveDests;
    }

    @Override
    @SuppressWarnings("unchecked")
    public void removeConsumer(ConnectionContext context, ConsumerInfo info) throws Exception {
        LOG.debug("{} removing consumer: {} for destination: {}", new Object[]{ broker.getBrokerName(), info.getConsumerId(), info.getDestination() });

        Subscription sub = subscriptions.remove(info.getConsumerId());
        // The sub could be removed elsewhere - see ConnectionSplitBroker
        if (sub != null) {

            // remove the subscription from all the matching queues.
            List<Destination> removeList = new ArrayList<Destination>();
            destinationsLock.readLock().lock();
            try {
                for (Destination dest : (Set<Destination>) destinationMap.get(info.getDestination())) {
                    removeList.add(dest);
                }
            } finally {
                destinationsLock.readLock().unlock();
            }
            for (Destination dest : removeList) {
                dest.removeSubscription(context, sub, info.getLastDeliveredSequenceId());
            }

            destroySubscription(sub);
        }
        synchronized (consumerChangeMutexMap) {
            consumerChangeMutexMap.remove(info.getConsumerId());
        }
    }

    protected void destroySubscription(Subscription sub) {
        sub.destroy();
    }

    @Override
    public void removeSubscription(ConnectionContext context, RemoveSubscriptionInfo info) throws Exception {
        throw new JMSException("Invalid operation.");
    }

    @Override
    public void send(final ProducerBrokerExchange producerExchange, Message messageSend) throws Exception {
        final ConnectionContext context = producerExchange.getConnectionContext();

        if (producerExchange.isMutable() || producerExchange.getRegionDestination() == null) {
            final Destination regionDestination = lookup(context, messageSend.getDestination(),false);
            producerExchange.setRegionDestination(regionDestination);
        }

        producerExchange.getRegionDestination().send(producerExchange, messageSend);

        if (producerExchange.getProducerState() != null && producerExchange.getProducerState().getInfo() != null){
            producerExchange.getProducerState().getInfo().incrementSentCount();
        }
    }

    @Override
    public void acknowledge(ConsumerBrokerExchange consumerExchange, MessageAck ack) throws Exception {
        Subscription sub = consumerExchange.getSubscription();
        if (sub == null) {
            sub = subscriptions.get(ack.getConsumerId());
            if (sub == null) {
                if (!consumerExchange.getConnectionContext().isInRecoveryMode()) {
                    LOG.warn("Ack for non existent subscription, ack: {}", ack);
                    throw new IllegalArgumentException("The subscription does not exist: " + ack.getConsumerId());
                } else {
                    LOG.debug("Ack for non existent subscription in recovery, ack: {}", ack);
                    return;
                }
            }
            consumerExchange.setSubscription(sub);
        }
        sub.acknowledge(consumerExchange.getConnectionContext(), ack);
    }

    @Override
    public Response messagePull(ConnectionContext context, MessagePull pull) throws Exception {
        Subscription sub = subscriptions.get(pull.getConsumerId());
        if (sub == null) {
            throw new IllegalArgumentException("The subscription does not exist: " + pull.getConsumerId());
        }
        return sub.pullMessage(context, pull);
    }

    protected Destination lookup(ConnectionContext context, ActiveMQDestination destination,boolean createTemporary) throws Exception {
        Destination dest = null;

        destinationsLock.readLock().lock();
        try {
            dest = destinations.get(destination);
        } finally {
            destinationsLock.readLock().unlock();
        }

        if (dest == null) {
            if (isAutoCreateDestinations()) {
                // Try to auto create the destination... re-invoke broker
                // from the
                // top so that the proper security checks are performed.
                context.getBroker().addDestination(context, destination, createTemporary);
                dest = addDestination(context, destination, false);
                // We should now have the dest created.
                destinationsLock.readLock().lock();
                try {
                    dest = destinations.get(destination);
                } finally {
                    destinationsLock.readLock().unlock();
                }
            }

            if (dest == null) {
                throw new JMSException("The destination " + destination + " does not exist.");
            }
        }
        return dest;
    }

    @Override
    public void processDispatchNotification(MessageDispatchNotification messageDispatchNotification) throws Exception {
        Subscription sub = subscriptions.get(messageDispatchNotification.getConsumerId());
        if (sub != null) {
            sub.processMessageDispatchNotification(messageDispatchNotification);
        } else {
            throw new JMSException("Slave broker out of sync with master - Subscription: "
                    + messageDispatchNotification.getConsumerId() + " on "
                    + messageDispatchNotification.getDestination() + " does not exist for dispatch of message: "
                    + messageDispatchNotification.getMessageId());
        }
    }

    /*
     * For a Queue/TempQueue, dispatch order is imperative to match acks, so the
     * dispatch is deferred till the notification to ensure that the
     * subscription chosen by the master is used. AMQ-2102
     */
    protected void processDispatchNotificationViaDestination(MessageDispatchNotification messageDispatchNotification)
            throws Exception {
        Destination dest = null;
        destinationsLock.readLock().lock();
        try {
            dest = destinations.get(messageDispatchNotification.getDestination());
        } finally {
            destinationsLock.readLock().unlock();
        }

        if (dest != null) {
            dest.processDispatchNotification(messageDispatchNotification);
        } else {
            throw new JMSException("Slave broker out of sync with master - Destination: "
                    + messageDispatchNotification.getDestination() + " does not exist for consumer "
                    + messageDispatchNotification.getConsumerId() + " with message: "
                    + messageDispatchNotification.getMessageId());
        }
    }

    @Override
    public void gc() {
        for (Subscription sub : subscriptions.values()) {
            sub.gc();
        }

        destinationsLock.readLock().lock();
        try {
            for (Destination dest : destinations.values()) {
                dest.gc();
            }
        } finally {
            destinationsLock.readLock().unlock();
        }
    }

    protected abstract Subscription createSubscription(ConnectionContext context, ConsumerInfo info) throws Exception;

    protected Destination createDestination(ConnectionContext context, ActiveMQDestination destination)
            throws Exception {
        return destinationFactory.createDestination(context, destination, destinationStatistics);
    }

    public boolean isAutoCreateDestinations() {
        return autoCreateDestinations;
    }

    public void setAutoCreateDestinations(boolean autoCreateDestinations) {
        this.autoCreateDestinations = autoCreateDestinations;
    }

    @Override
    @SuppressWarnings("unchecked")
    public void addProducer(ConnectionContext context, ProducerInfo info) throws Exception {
        destinationsLock.readLock().lock();
        try {
            for (Destination dest : (Set<Destination>) destinationMap.get(info.getDestination())) {
                dest.addProducer(context, info);
            }
        } finally {
            destinationsLock.readLock().unlock();
        }
    }

    /**
     * Removes a Producer.
     *
     * @param context
     *            the environment the operation is being executed under.
     * @throws Exception
     *             TODO
     */
    @Override
    @SuppressWarnings("unchecked")
    public void removeProducer(ConnectionContext context, ProducerInfo info) throws Exception {
        destinationsLock.readLock().lock();
        try {
            for (Destination dest : (Set<Destination>) destinationMap.get(info.getDestination())) {
                dest.removeProducer(context, info);
            }
        } finally {
            destinationsLock.readLock().unlock();
        }
    }

    protected void dispose(ConnectionContext context, Destination dest) throws Exception {
        dest.dispose(context);
        dest.stop();
        destinationFactory.removeDestination(dest);
    }

    @Override
    public void processConsumerControl(ConsumerBrokerExchange consumerExchange, ConsumerControl control) {
        Subscription sub = subscriptions.get(control.getConsumerId());
        if (sub != null && sub instanceof AbstractSubscription) {
            ((AbstractSubscription) sub).setPrefetchSize(control.getPrefetch());
            if (broker.getDestinationPolicy() != null) {
                PolicyEntry entry = broker.getDestinationPolicy().getEntryFor(control.getDestination());
                if (entry != null) {
                    entry.configurePrefetch(sub);
                }
            }
            LOG.debug("setting prefetch: {}, on subscription: {}; resulting value: {}", new Object[]{ control.getPrefetch(), control.getConsumerId(), sub.getConsumerInfo().getPrefetchSize()});
            try {
                lookup(consumerExchange.getConnectionContext(), control.getDestination(),false).wakeup();
            } catch (Exception e) {
                LOG.warn("failed to deliver post consumerControl dispatch-wakeup, to destination: {}", control.getDestination(), e);
            }
        }
    }

    @Override
    public void reapplyInterceptor() {
        destinationsLock.writeLock().lock();
        try {
            DestinationInterceptor destinationInterceptor = broker.getDestinationInterceptor();
            Map<ActiveMQDestination, Destination> map = getDestinationMap();
            for (ActiveMQDestination key : map.keySet()) {
                Destination destination = map.get(key);
                if (destination instanceof CompositeDestinationFilter) {
                    destination = ((CompositeDestinationFilter) destination).next;
                }
                if (destinationInterceptor != null) {
                    destination = destinationInterceptor.intercept(destination);
                }
                getDestinationMap().put(key, destination);
                Destination prev = destinations.put(key, destination);
                if (prev == null) {
                    updateRegionDestCounts(key, 1);
                }
            }
        } finally {
            destinationsLock.writeLock().unlock();
        }
    }
}
