/*
 * 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.log4j.plugins;

import org.apache.log4j.spi.LoggerRepository;
import org.apache.log4j.spi.LoggerRepositoryEventListener;
import org.apache.log4j.spi.LoggerRepositoryEx;

import java.util.*;


/**
 * This is a registry for Plugin instances. It provides methods to
 * start and stop plugin objects individually and to stop all
 * plugins for a repository.
 *
 * @author Mark Womack
 * @author Paul Smith
 */
public final class PluginRegistry {
    /**
     * The pluginMap is keyed by plugin name and contains plugins as values.
     * key=plugin.getName, value=plugin
     */
    private final Map<String, Plugin> pluginMap;
    /**
     * Logger repository.
     */
    private final LoggerRepositoryEx loggerRepository;

    /**
     * the listener used to listen for repository events.
     */
    private final RepositoryListener listener = new RepositoryListener();
    /**
     * List of listeners.
     */
    private final List listenerList =
        Collections.synchronizedList(new ArrayList());

    /**
     * Creates a new instance.
     *
     * @param repository logger repository.
     */
    public PluginRegistry(final LoggerRepositoryEx repository) {
        super();
        pluginMap = new HashMap<>();
        this.loggerRepository = repository;
        this.loggerRepository.addLoggerRepositoryEventListener(listener);
    }

    /**
     * Get logger repository.
     *
     * @return logger repository.
     */
    public LoggerRepositoryEx getLoggerRepository() {
        return loggerRepository;
    }


    /**
     * Returns true if the specified name is already taken by
     * an existing Plugin registered within the scope of the specified
     * LoggerRepository.
     *
     * @param name The name to check the repository for
     * @return true if the name is already in use, otherwise false
     */
    public boolean pluginNameExists(final String name) {
        synchronized (pluginMap) {
            return pluginMap.containsKey(name);
        }
    }


    /**
     * Adds a plugin to the plugin registry.
     * If a plugin with the same name exists
     * already, it is shutdown and removed.
     *
     * @param plugin the plugin to add.
     */
    public void addPlugin(final Plugin plugin) {
        // put plugin into the repository's reciever map
        synchronized (pluginMap) {
            String name = plugin.getName();

            // make sure the plugin has reference to repository
            plugin.setLoggerRepository(getLoggerRepository());

            Plugin existingPlugin = pluginMap.get(name);
            if (existingPlugin != null) {
                existingPlugin.shutdown();
            }

            // put the new plugin into the map
            pluginMap.put(name, plugin);
            firePluginStarted(plugin);
        }
    }


    /**
     * Calls the pluginStarted method on every registered PluginListener.
     *
     * @param plugin The plugin that has been started.
     */
    private void firePluginStarted(final Plugin plugin) {
        PluginEvent e = null;
        synchronized (listenerList) {
            for (Object aListenerList : listenerList) {
                PluginListener l = (PluginListener) aListenerList;
                if (e == null) {
                    e = new PluginEvent(plugin);
                }
                l.pluginStarted(e);
            }
        }
    }


    /**
     * Calls the pluginStopped method for every registered PluginListner.
     *
     * @param plugin The plugin that has been stopped.
     */
    private void firePluginStopped(final Plugin plugin) {
        PluginEvent e = null;
        synchronized (listenerList) {
            for (Object aListenerList : listenerList) {
                PluginListener l = (PluginListener) aListenerList;
                if (e == null) {
                    e = new PluginEvent(plugin);
                }
                l.pluginStopped(e);
            }
        }
    }


    /**
     * Returns all the plugins for a given repository.
     *
     * @return List list of plugins from the repository.
     */
    public List<Plugin> getPlugins() {
        synchronized (pluginMap) {
            List<Plugin> pluginList = new ArrayList<>(pluginMap.size());

            pluginList.addAll(pluginMap.values());
            return pluginList;
        }
    }


    /**
     * Returns all the plugins for a given repository that are instances
     * of a certain class.
     *
     * @param pluginClass the class the plugin must implement to be selected.
     * @return List list of plugins from the repository.
     */
    public List getPlugins(final Class pluginClass) {
        synchronized (pluginMap) {
            List pluginList = new ArrayList(pluginMap.size());

            for (Object plugin : pluginMap.values()) {
                if (pluginClass.isInstance(plugin)) {
                    pluginList.add(plugin);
                }
            }
            return pluginList;
        }
    }


    /**
     * Stops a plugin by plugin name and repository.
     *
     * @param pluginName the name of the plugin to stop.
     * @return Plugin the plugin, if stopped, or null if the
     * the plugin was not found in the registry.
     */
    public Plugin stopPlugin(final String pluginName) {
        synchronized (pluginMap) {
            Plugin plugin = pluginMap.get(pluginName);

            if (plugin == null) {
                return null;
            }

            // shutdown the plugin
            plugin.shutdown();

            // remove it from the plugin map
            pluginMap.remove(pluginName);
            firePluginStopped(plugin);

            // return it for future use
            return plugin;
        }
    }

    /**
     * Stops all plugins in the given logger repository.
     */
    public void stopAllPlugins() {
        synchronized (pluginMap) {
            // remove the listener for this repository
            loggerRepository.removeLoggerRepositoryEventListener(listener);

            for (Object o : pluginMap.values()) {
                Plugin plugin = (Plugin) o;
                plugin.shutdown();
                firePluginStopped(plugin);
            }
        }
    }


    /**
     * Adds a PluginListener to this registry to be notified
     * of PluginEvents.
     *
     * @param l PluginListener to add to this registry
     */
    public void addPluginListener(final PluginListener l) {
        listenerList.add(l);
    }


    /**
     * Removes a particular PluginListener from this registry
     * such that it will no longer be notified of PluginEvents.
     *
     * @param l PluginListener to remove
     */
    public void removePluginListener(final PluginListener l) {
        listenerList.remove(l);
    }

    /**
     * Internal class used to handle listener events from repositories.
     */
    private class RepositoryListener implements LoggerRepositoryEventListener {
        /**
         * Stops all plugins associated with the repository being reset.
         *
         * @param repository the repository that was reset.
         */
        public void configurationResetEvent(final LoggerRepository repository) {
            PluginRegistry.this.stopAllPlugins();
        }


        /**
         * Called when the repository configuration is changed.
         *
         * @param repository the repository that was changed.
         */
        public void configurationChangedEvent(
            final LoggerRepository repository) {
            // do nothing with this event
        }


        /**
         * Stops all plugins associated with the repository being shutdown.
         *
         * @param repository the repository being shutdown.
         */
        public void shutdownEvent(final LoggerRepository repository) {
            PluginRegistry.this.stopAllPlugins();
        }
    }
}
