/*
 * #%L
 * Alfresco Repository
 * %%
 * Copyright (C) 2005 - 2016 Alfresco Software Limited
 * %%
 * This file is part of the Alfresco software. 
 * If the software was purchased under a paid Alfresco license, the terms of 
 * the paid license agreement will prevail.  Otherwise, the software is 
 * provided under the following open source license terms:
 * 
 * Alfresco is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Alfresco is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 * #L%
 */
package org.alfresco.repo.management;

import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ApplicationEventMulticaster;
import org.springframework.context.event.ContextClosedEvent;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.GenericApplicationListenerAdapter;
import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.OrderComparator;
import org.springframework.core.ResolvableType;

/**
 * Abstract implementation of the {@link ApplicationEventMulticaster} interface,
 * providing the basic listener registration facility.
 * 
 * <p>
 * Doesn't permit multiple instances of the same listener by default, as it
 * keeps listeners in a linked Set. The collection class used to hold
 * ApplicationListener objects can be overridden through the "collectionClass"
 * bean property.
 * 
 * <p>
 * Implementing ApplicationEventMulticaster's actual {@link #multicastEvent}
 * method is left to subclasses. {@link org.springframework.context.event.SimpleApplicationEventMulticaster}
 * simply multicasts all events to all registered listeners, invoking them in
 * the calling thread. Alternative implementations could be more sophisticated
 * in those respects.
 * 
 * @author Juergen Hoeller
 * @since 1.2.3
 * @see #getApplicationListeners(ApplicationEvent)
 * @see org.springframework.context.event.SimpleApplicationEventMulticaster
 */
public class SafeApplicationEventMulticaster implements ApplicationEventMulticaster, ApplicationContextAware
{
    private final Log log = LogFactory.getLog(SafeApplicationEventMulticaster.class);
    private final ListenerRetriever defaultRetriever = new ListenerRetriever(false);

    private final Map<ListenerCacheKey, ListenerRetriever> retrieverCache = new ConcurrentHashMap<ListenerCacheKey, ListenerRetriever>();

    private ApplicationContext appContext;
    private Executor taskExecutor;

    /** Has the application started? */
    private boolean isApplicationStarted;

    /** The queued events that can't be broadcast until the application is started. */
    private List<ApplicationEvent> queuedEvents = new LinkedList<ApplicationEvent>();


    /**
     * Set the TaskExecutor to execute application listeners with.
     * <p>
     * Default is a SyncTaskExecutor, executing the listeners synchronously in
     * the calling thread.
     * <p>
     * Consider specifying an asynchronous TaskExecutor here to not block the
     * caller until all listeners have been executed. However, note that
     * asynchronous execution will not participate in the caller's thread
     * context (class loader, transaction association) unless the TaskExecutor
     * explicitly supports this.
     * 
     */
    public void setTaskExecutor(Executor taskExecutor)
    {
        this.taskExecutor = taskExecutor;
    }

    /**
     * Return the current TaskExecutor for this multicaster.
     */
    protected Executor getTaskExecutor()
    {
        return this.taskExecutor;
    }

    public void addApplicationListener(ApplicationListener listener)
    {
        synchronized (this.defaultRetriever)
        {
            this.defaultRetriever.applicationListeners.add(listener);
            this.retrieverCache.clear();
        }
    }

    public void addApplicationListenerBean(String listenerBeanName)
    {
        synchronized (this.defaultRetriever)
        {
            this.defaultRetriever.applicationListenerBeans.add(listenerBeanName);
            this.retrieverCache.clear();
        }
    }

    public void removeApplicationListener(ApplicationListener listener)
    {
        synchronized (this.defaultRetriever)
        {
            this.defaultRetriever.applicationListeners.remove(listener);
            this.retrieverCache.clear();
        }
    }

    public void removeApplicationListenerBean(String listenerBeanName)
    {
        synchronized (this.defaultRetriever)
        {
            this.defaultRetriever.applicationListenerBeans.remove(listenerBeanName);
            this.retrieverCache.clear();
        }
    }

    public void removeAllListeners()
    {
        synchronized (this.defaultRetriever)
        {
            this.defaultRetriever.applicationListeners.clear();
            this.defaultRetriever.applicationListenerBeans.clear();
            this.retrieverCache.clear();
        }
    }

    private BeanFactory getBeanFactory()
    {
        if (this.appContext == null)
        {
            throw new IllegalStateException("ApplicationEventMulticaster cannot retrieve listener beans "
                    + "because it is not associated with a BeanFactory");
        }
        return this.appContext;
    }

    @Override
    public void multicastEvent(ApplicationEvent event)
    {
        if (event instanceof ContextRefreshedEvent && event.getSource() == this.appContext)
        {
            this.isApplicationStarted = true;
            for (ApplicationEvent queuedEvent : this.queuedEvents)
            {
                multicastEventInternal(queuedEvent);
            }
            this.queuedEvents.clear();
            multicastEventInternal(event);
        }
        else if (event instanceof ContextClosedEvent && event.getSource() == this.appContext)
        {
            this.isApplicationStarted = false;
            multicastEventInternal(event);
        }
        else if (this.isApplicationStarted)
        {
            multicastEventInternal(event);
        }
        else
        {
            this.queuedEvents.add(event);
        }
    }

    @Override
    public void multicastEvent(ApplicationEvent event, ResolvableType eventType)
    {
        multicastEvent(event);
    }

    @SuppressWarnings("unchecked")
    protected void multicastEventInternal(final ApplicationEvent event) {
        for (final ApplicationListener listener : getApplicationListeners(event)) {
            Executor executor = getTaskExecutor();
            if (executor != null) {
                executor.execute(new Runnable() {
                    public void run() {
                        listener.onApplicationEvent(event);
                    }
                });
            }
            else {
                listener.onApplicationEvent(event);
            }
        }
    }

    /**
     * Return a Collection containing all ApplicationListeners.
     * 
     * @return a Collection of ApplicationListeners
     * @see org.springframework.context.ApplicationListener
     */
    protected Collection<ApplicationListener> getApplicationListeners()
    {
        return this.defaultRetriever.getApplicationListeners();
    }

    /**
     * Return a Collection of ApplicationListeners matching the given event
     * type. Non-matching listeners get excluded early.
     * 
     * @param event
     *            the event to be propagated. Allows for excluding non-matching
     *            listeners early, based on cached matching information.
     * @return a Collection of ApplicationListeners
     * @see org.springframework.context.ApplicationListener
     */
    protected Collection<ApplicationListener> getApplicationListeners(ApplicationEvent event)
    {
        Class<? extends ApplicationEvent> eventType = event.getClass();
        Class sourceType = event.getSource().getClass();
        ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);
        ListenerRetriever retriever = this.retrieverCache.get(cacheKey);
        if (retriever != null)
        {
            return retriever.getApplicationListeners();
        }
        else
        {
            retriever = new ListenerRetriever(true);
            LinkedList<ApplicationListener> allListeners = new LinkedList<ApplicationListener>();
            synchronized (this.defaultRetriever)
            {
                if (!this.defaultRetriever.applicationListenerBeans.isEmpty())
                {
                    BeanFactory beanFactory = getBeanFactory();
                    for (String listenerBeanName : this.defaultRetriever.applicationListenerBeans)
                    {
                        ApplicationListener listener = beanFactory.getBean(listenerBeanName, ApplicationListener.class);
                        if (supportsEvent(listener, eventType, sourceType))
                        {
                            retriever.applicationListenerBeans.add(listenerBeanName);
                            allListeners.add(listener);
                        }
                    }
                }
                for (ApplicationListener listener : this.defaultRetriever.applicationListeners)
                {
                    if (!allListeners.contains(listener) && supportsEvent(listener, eventType, sourceType))
                    {
                        retriever.applicationListeners.add(listener);
                        allListeners.add(listener);
                    }
                }
                OrderComparator.sort(allListeners);
                this.retrieverCache.put(cacheKey, retriever);
            }
            if (log.isDebugEnabled())
            {
                log.debug(allListeners.toString());
            }
            return allListeners;
        }
    }

    /**
     * Determine whether the given listener supports the given event.
     * <p>
     * The default implementation detects the {@link SmartApplicationListener}
     * interface. In case of a standard {@link ApplicationListener}, a
     * {@link GenericApplicationListenerAdapter} will be used to introspect the
     * generically declared type of the target listener.
     * 
     * @param listener
     *            the target listener to check
     * @param eventType
     *            the event type to check against
     * @param sourceType
     *            the source type to check against
     * @return whether the given listener should be included in the candidates
     *         for the given event type
     */
    protected boolean supportsEvent(ApplicationListener listener, Class<? extends ApplicationEvent> eventType,
            Class sourceType)
    {

        SmartApplicationListener smartListener = (listener instanceof SmartApplicationListener ? (SmartApplicationListener) listener
                : new GenericApplicationListenerAdapter(listener));
        return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));
    }

    /**
     * Cache key for ListenerRetrievers, based on event type and source type.
     */
    private static class ListenerCacheKey
    {

        private final Class eventType;

        private final Class sourceType;

        public ListenerCacheKey(Class eventType, Class sourceType)
        {
            this.eventType = eventType;
            this.sourceType = sourceType;
        }

        @Override
        public boolean equals(Object other)
        {
            if (other == null)
                return false;
            if (this == other)
            {
                return true;
            }
            ListenerCacheKey otherKey = (ListenerCacheKey) other;
            return (this.eventType.equals(otherKey.eventType) && this.sourceType.equals(otherKey.sourceType));
        }

        @Override
        public int hashCode()
        {
            return this.eventType.hashCode() * 29 + this.sourceType.hashCode();
        }
    }

    /**
     * Helper class that encapsulates a specific set of target listeners,
     * allowing for efficient retrieval of pre-filtered listeners.
     * <p>
     * An instance of this helper gets cached per event type and source type.
     */
    private class ListenerRetriever
    {

        public final Set<ApplicationListener> applicationListeners;

        public final Set<String> applicationListenerBeans;

        private final boolean preFiltered;

        public ListenerRetriever(boolean preFiltered)
        {
            this.applicationListeners = new LinkedHashSet<ApplicationListener>();
            this.applicationListenerBeans = new LinkedHashSet<String>();
            this.preFiltered = preFiltered;
        }

        public Collection<ApplicationListener> getApplicationListeners()
        {
            LinkedList<ApplicationListener> allListeners = new LinkedList<ApplicationListener>();
            if (!this.applicationListenerBeans.isEmpty())
            {
                BeanFactory beanFactory = getBeanFactory();
                for (String listenerBeanName : this.applicationListenerBeans)
                {
                    ApplicationListener listener = beanFactory.getBean(listenerBeanName, ApplicationListener.class);
                    allListeners.add(listener);
                }
            }
            for (ApplicationListener listener : this.applicationListeners)
            {
                if (this.preFiltered || !allListeners.contains(listener))
                {
                    allListeners.add(listener);
                }
            }
            OrderComparator.sort(allListeners);
            if (log.isDebugEnabled())
            {
                log.debug(allListeners.toString());
            }
            return allListeners;
        }
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
    {
        this.appContext = applicationContext;
    }

}
