package de.renew.plugin;

import java.io.File;
import java.io.PrintStream;
import java.io.Serializable;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.Vector;
import java.util.concurrent.CompletableFuture;

import org.apache.log4j.Logger;

import de.renew.plugin.DependencyCheckList.DependencyElement;
import de.renew.plugin.command.CLCommand;
import de.renew.plugin.command.ExitCommand;
import de.renew.plugin.command.GCCommand;
import de.renew.plugin.command.GetPropertyCommand;
import de.renew.plugin.command.InfoCommand;
import de.renew.plugin.command.ListCommand;
import de.renew.plugin.command.LoadCommand;
import de.renew.plugin.command.NoOpCommand;
import de.renew.plugin.command.ScriptCommand;
import de.renew.plugin.command.SetPropertyCommand;
import de.renew.plugin.command.SleepCommand;
import de.renew.plugin.command.UnloadCommand;
import de.renew.plugin.di.Container;
import de.renew.plugin.di.MissingDependencyException;
import de.renew.plugin.di.ServiceContainer;
import de.renew.plugin.jpms.impl.ModuleManager;
import de.renew.plugin.load.DIPluginLoader;
import de.renew.plugin.load.PluginLoaderComposition;
import de.renew.plugin.load.SimplePluginLoader;
import de.renew.plugin.locate.PluginJarLocationFinder;
import de.renew.plugin.locate.PluginLocationFinders;
import de.renew.plugin.locate.PluginSubDirFinder;

/**
 * This class is the central management facility for providing Plugins with
 * access to Renew. When the Singleton instance of this class is created, the
 * statical plugins will be loaded. Furthermore, this class will be the
 * interface for dynamical plugins that can log into it at run-time.
 */
public class PluginManager implements Serializable, CommandsProvider {
    /**
     * Simple separator for readability purposes
     */
    public static final String COMMAND_SEPERATOR = "---";
    /**
     * Logger for logging purposes as specified by Apache Log4j
     */
    public static final Logger LOGGER = Logger.getLogger(PluginManager.class);

    /**
     * The module manager.
     */
    private final ModuleManager _moduleManager = new ModuleManager();

    /**
     * The timeout for cleaning up a plugin, in microseconds. The value is fixed
     * to {@value} microseconds.
     **/
    public static final int CLEANUP_TIMEOUT = 20000;

    /**
     * The name of the system property, which define the path for the plugins.
     */
    public static final String PLUGIN_LOCATIONS_PROPERTY = "pluginLocations";

    /** Singleton member instance. **/
    protected static PluginManager _instance;
    /** The list managing the dependencies among the plugins. **/
    DependencyCheckList<IPlugin> _dependencyList = new DependencyCheckList<>();

    /**
     * The modular plugins.
     */
    private final List<IPlugin> _modularPlugins = new ArrayList<>();

    /**
     * The map of recognized commands and their associated executors. Maps from
     * <code>String</code> to {@link CLCommand} objects.
     **/
    private Map<String, CLCommand> _commands = Collections.synchronizedMap(new TreeMap<>());

    /**
     * Contains the listeners that register for PluginManager events.
     */
    private Set<IPluginManagerListener> _managerListener;

    /**
     * The set of plugins preventing the system from automatically terminating.
     **/
    private Set<IPlugin> _blockers = Collections.synchronizedSet(new HashSet<>());

    /**
     * This flag indicates that the system termination thread is running. It is
     * set by the {@link #stopSynchronized()} method only.
     **/
    private boolean _terminating = false;

    /**
     * The plugin location finder.
     */
    protected PluginLocationFinders _locationFinder = PluginLocationFinders.getInstance();

    /**
     * The plugin loader.
     */
    private PluginLoaderComposition _loader;

    /**
     * The class loader manager.
     */
    private final ClassLoaderManager _classLoaderManager;

    /**
     * The location of loader.jar
     */
    protected static URL _loaderLocation;

    /**
     * The log strategy.
     */
    private LogStrategy _logStrategy;

    // Location of the preferences (expected in home directory).
    // use with method getPreferenceLocation.
    private static final String PREF_DIR = ".renew";

    /**
     * The commands listener.
     */
    private ArrayList<CommandsListener> _commandsListener = new ArrayList<>();

    /**
     * The service container.
     */
    private final ServiceContainer _container;

    /**
     * Get the location of loader.jar
     *
     * @return url of loader.jar
     */
    public static URL getLoaderLocation() {
        if (_loaderLocation == null) { // to prevent NullPointerException
            _loaderLocation = getDefaultLoaderLocation();
        }
        return _loaderLocation;
    }

    private static URL getDefaultLoaderLocation() {
        URL url = Loader.class.getProtectionDomain().getCodeSource().getLocation();
        String base = url.toExternalForm();
        base = base.substring(0, base.lastIndexOf("/"));
        try {
            return new URL(base + "/");
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        return null;
    }

    private static LogStrategy getDefaultLogStrategy() {
        return new DefaultLogStrategy();
    }

    private static ClassLoaderManager getDefaultClassLoaderManager() {
        return new DefaultClassLoaderManager();
    }

    /**
     * Constructor for PluginManager
     * @param url Location of Loader
     * @param logStrategy LogStrategy used to configure logging
     * @param classLoaderManager Manger to get system dependant class loader
     */
    protected PluginManager(
        URL url, LogStrategy logStrategy, ClassLoaderManager classLoaderManager)
    {
        if (logStrategy == null) {
            logStrategy = getDefaultLogStrategy();
        }
        _logStrategy = logStrategy;
        if (classLoaderManager == null) {
            classLoaderManager = getDefaultClassLoaderManager();
        }
        _classLoaderManager = classLoaderManager;

        _logStrategy.configureLogging();
        _classLoaderManager.initClassLoaders();

        _managerListener = new HashSet<>();
        _moduleManager.registerLayerListener(
            ServiceLookupInfrastructure.getInstance().new LayerServicesListener());

        initLocationFinders();

        _container = new Container();
        _container.set(PluginManager.class, this);
        _container.set(ServiceContainer.class, _container);
        _container.set(ClassLoaderManager.class, _classLoaderManager);

        // initialize the loaders
        _loader = new PluginLoaderComposition(_moduleManager);
        final PluginClassLoader loader = getPluginClassLoader();
        _loader.addLoader(new SimplePluginLoader(loader, _container));
        _loader.addLoader(new DIPluginLoader(loader, _container));

        _commands.put("", new NoOpCommand());
        _commands.put("help", new HelpCommand());
        _commands.put("exit", new ExitCommand());
        _commands.put("load", new LoadCommand());
        _commands.put("list", new ListCommand());
        _commands.put("unload", new UnloadCommand());
        _commands.put("info", new InfoCommand());
        _commands.put("script", new ScriptCommand());
        _commands.put("gc", new GCCommand());
        _commands.put(GetPropertyCommand.COMMAND_NAME, new GetPropertyCommand());
        _commands.put(SetPropertyCommand.COMMAND_NAME, new SetPropertyCommand());
        _commands.put(
            "packageCount", _classLoaderManager.getPluginClassLoader().new PackageCountCommand());
        _commands.put(SleepCommand.CMD, new SleepCommand());
    }

    /**
     * Creates PluginManager instance with default configuration if none was given at startup
     * @return Functioning PluginManager
     */
    public static synchronized PluginManager getInstance() {
        if (_instance == null) {
            createInstance(
                getDefaultLoaderLocation(), getDefaultLogStrategy(),
                getDefaultClassLoaderManager());
        }

        return _instance;
    }

    private static synchronized PluginManager createInstance(
        URL url, LogStrategy logStrategy, ClassLoaderManager classLoaderManager)
    {
        if (_instance != null) {
            throw new IllegalStateException(
                "Cannot create PluginManager singleton, is already there.");
        }
        _instance = new PluginManager(url, logStrategy, classLoaderManager);
        _instance.initPlugins();
        return _instance;
    }

    /**
     * Adds a Listener to the PluginManager
     *
     * @param l
     *            the listener
     */
    public void addPluginManagerListener(IPluginManagerListener l) {
        _managerListener.add(l);
    }

    /**
     * Removes a Listener from the PluginManager
     *
     * @param l
     *            the listener
     */
    public void removePluginManagerListener(IPluginManagerListener l) {
        _managerListener.remove(l);
    }

    /**
     * Returns service container of plugin manager
     * @return service container
     */
    public ServiceContainer getServiceContainer() {
        return _container;
    }

    private void serviceAdded(Collection<String> services, IPlugin provider) {
        for (IPluginManagerListener listener : _managerListener) {
            for (String service : services) {
                listener.serviceAdded(service, provider);
            }
        }
    }

    private void serviceRemoved(Collection<String> services, IPlugin provider) {
        for (IPluginManagerListener listener : _managerListener) {
            for (String service : services) {
                listener.serviceRemoved(service, provider);
            }
        }
    }

    /**
     * Adds a Plugin to module manager
     * @param plugin Plugin to be added to module manager
     * @param isModular determines whether a plugin is treated as modular or not
     * @throws DependencyNotFulfilledException if given plugins dependencies can not be resolved
     */
    public void addPlugin(IPlugin plugin, boolean isModular)
        throws DependencyNotFulfilledException
    {
        if (!checkDependenciesFulfilled(plugin)) {
            throw new DependencyNotFulfilledException("Cannot add " + plugin);
        }

        try {
            LOGGER.debug("************ INITIALIZING " + plugin.getName() + " ********");
            plugin.init();
            _dependencyList.addElement(DependencyElement.create(plugin));
            if (isModular) {
                registerPluginAsModular(plugin);
            }
            serviceAdded(plugin.getProperties().getProvisions(), plugin);
        } catch (RuntimeException | LinkageError e) {
            LOGGER.error(
                "PluginManager: adding of " + plugin + " failed: " + e + "\n Plugin location: "
                    + plugin.getProperties().getURL());
            LOGGER.debug(e.toString(), e);
            if (serviceContainerContainsMainClass(plugin)) {
                _container.unbind(plugin.getClass());
            }
            if (isModular) {
                _moduleManager.removeLayerOfPlugin(plugin.getName());
                unregisterModularPlugin(plugin);
            }
        }
    }

    private boolean serviceContainerContainsMainClass(IPlugin plugin) {
        if (plugin.getProperties().getMainClass().isBlank()) {
            return false;
        }
        return _container.has(plugin.getClass());
    }

    /**
     * Checks whether the dependencies specified by the given
     * <code>PluginProperties</code> object are fulfilled by the current set of
     * loaded plugins.
     *
     * @param props
     *            the properties that specify the dependencies.
     *
     * @return <code>true</code>, if a plugin with the given <code>props</code>
     *         could be added to the set of plugins. Returns <code>false</code>,
     *         if some dependency of the plugin would not be fulfilled.
     **/
    public synchronized boolean checkDependenciesFulfilled(PluginProperties props) {
        return _dependencyList.dependencyFulfilled(DependencyElement.create(props));
    }

    /**
     * Checks whether the dependencies specified by the given
     * <code>IPlugin</code> object are fulfilled by the current set of loaded
     * plugins.
     *
     * @param plugin
     *            the plugin that specifies the dependencies.
     *
     * @return <code>true</code>, if the given <code>plugin</code> could be
     *         added to the set of plugins. Returns <code>false</code>, if some
     *         dependency of the plugin would not be fulfilled.
     **/
    public synchronized boolean checkDependenciesFulfilled(IPlugin plugin) {
        return _dependencyList.dependencyFulfilled(DependencyElement.create(plugin));
    }

    /**
     * Returns the plugin with the given name, null if no such plugin is
     * present.
     * @param pluginName Name of plugin to be returned
     * @return Return plugin specified by given name
     */
    public IPlugin getPluginByName(String pluginName) {
        IPlugin found = null;
        pluginName = pluginName.replaceAll("_", " ").trim();
        LOGGER.debug("PluginManager looking for " + pluginName);
        List<IPlugin> plugins = getPlugins();
        for (IPlugin plugin : plugins) {
            if (plugin.getName().equals(pluginName)) {
                found = plugin;
                break;
            }
        }
        if (found == null) {
            found = getPluginByAlias(pluginName);
        }
        return found;
    }

    /**
     * Returns the plugin of the given class, null if no such plugin is
     * present.
     * @param clazz Class to search plugin by
     * @return Plugin as specified by given class
     */
    public IPlugin getPluginByClass(Class<?> clazz) {
        LOGGER.debug("PluginManager looking for " + clazz.getName());
        try {
            final Object o = _container.get(clazz);

            if (o instanceof IPlugin) {
                return (IPlugin) o;
            }

            return null;
        } catch (MissingDependencyException e) {
            return null;
        }
    }

    /**
     * Returns the plugin with the given alias, null if no such plugin is
     * present.
     * @param pluginAlias Alias to search plugin by
     * @return Plugin as specified by given alias
     */
    public IPlugin getPluginByAlias(String pluginAlias) {
        IPlugin found = null;
        LOGGER.debug("PluginManager looking for " + pluginAlias);
        List<IPlugin> plugins = getPlugins();
        for (int i = 0; i < plugins.size(); i++) {
            IPlugin current = plugins.get(i);

            if (pluginAlias.equals(current.getAlias())) {
                found = current;
                break;
            }
        }
        return found;
    }

    /**
     * Returns a Collection containing the Plugins that provide the given
     * service.
     * @param service used to look for plugins
     * @return Plugins that provide given service
     */
    public Collection<IPlugin> getPluginsProviding(String service) {
        Collection<IPlugin> result = new Vector<>();
        for (IPlugin plugin : getPlugins()) {
            if (plugin.getProperties().getProvisions().contains(service)) {
                result.add(plugin);
            }
        }
        return result;
    }

    /**
     * Returns a collection of plug-ins which require the given <b>service</b>.<br>
     * <span style="color:red;">NOT safe for concurrent access.</span>
     *
     * @param service
     *            The required service
     * @return Collection&lt;IPlugin&gt;
     */
    public Collection<IPlugin> getPluginsRequiring(String service) {
        Collection<IPlugin> result = new Vector<>();
        Iterator<IPlugin> it = getPlugins().iterator();
        while (it.hasNext()) {
            IPlugin p = it.next();
            if (p.getProperties().getRequirements().contains(service)) {
                result.add(p);
                continue;
            }
            Iterator<String> requirements = p.getProperties().getRequirements().iterator();
            while (requirements.hasNext()) {
                String requirement = requirements.next();
                if (requirement.startsWith(service)) {
                    result.add(p);
                    break;
                }
            }
        }
        return result;
    }

    /**
     * Iterate through all plugins and initialize them.
     */
    public void initPlugins() {
        Iterator<IPlugin> plugins = getPlugins().iterator();
        while (plugins.hasNext()) {
            IPlugin p = plugins.next();
            LOGGER.debug(
                "initializing " + p.getName() + ", loaded from " + p.getClass().getClassLoader());
            LOGGER.debug("************ INITIALIZING " + p.getName() + " ********");
            p.init();
            serviceAdded(p.getProperties().getProvisions(), p);
        }
    }

    /**
     * Return the ClassLoader instance used to load the plugins.
     * @return ClassLoader to load the plugins
     */
    public PluginClassLoader getPluginClassLoader() {
        return _classLoaderManager.getPluginClassLoader();
    }

    /**
     * Return the ClassLoader instance used to load the class.
     * @param name Name of the class to look for
     * @return String representation of classloader the class belongs to
     */
    public ClassLoader getModuleClassLoaderForClass(String name) {
        return _moduleManager.getModuleClassLoaderForClass(name);
    }

    /**
     * Returns the ClassLoader instance used to load the user defined .
     * @return ClassLoader instance used to load the user defined .
     */
    public ClassLoader getBottomClassLoader() {
        return _classLoaderManager.getBottomClassLoader();
    }

    /**
     * Returns a new ClassLoader instance, which can be used to load user defined content.
     * @return new ClassLoader instance to load user defined content
     */
    public ClassLoader getNewBottomClassLoader() {
        return _classLoaderManager.getNewBottomClassLoader();
    }

    /**
     * Returns the system classloader
     * @return system classloader
     */
    public ClassLoader getSystemClassLoader() {
        return _classLoaderManager.getSystemClassLoader();
    }

    /**
     * Get the PluginLoader used to create plugin instances
     */
    PluginLoaderComposition getPluginLoader() {
        return _loader;
    }

    /**
     * Finds all candidate locations and tries to load a plugin from each
     * location. The currently configured <code>PluginLoader</code> is used for
     * the job.
     **/
    public synchronized void loadPlugins() {
        _loader.loadPlugins();
    }

    /**
     * Loads a plugin from the given URL, if possible. The currently configured
     * <code>PluginLoader</code> is used for the job.
     *
     * @param url the plugin's location as <code>URL</code>.
     *
     * @return Plugin that was specified in URL
     */
    public synchronized IPlugin loadPlugin(URL url) {
        return _loader.loadPluginFromURL(url);
    }

    /**
     * Return a list of all loaded Plugins.
     *
     * @return List&lt;{@link IPlugin}&gt;
     */
    public List<IPlugin> getPlugins() {
        return _dependencyList.getFulfilledObjects();
    }

    /**
     * Adds the plugin to the list of currently loaded
     * plugins that were loaded from a modular Jar file.
     * @param plugin the plugin to register
     */
    private void registerPluginAsModular(IPlugin plugin) {
        _modularPlugins.add(plugin);
    }

    /**
     * Removes the plugin from the list of currently loaded
     * modular plugins.
     * @param plugin the plugin to unregister
     */
    private void unregisterModularPlugin(IPlugin plugin) {
        _modularPlugins.remove(plugin);
    }

    /**
     * Checks if a plugin was registered as being modular.
     * @param plugin the plugin to be checked
     * @return true if the plugin is modular, else false
     */
    private boolean isModularPlugin(IPlugin plugin) {
        return _modularPlugins.contains(plugin);
    }

    /**
     * Register the given command. It will be called when the user types the
     * given String into the console. It is not checked whether the String
     * already exists, so an existing command can be overridden!
     *
     * @param command
     *            The String identifying
     * @param cLCommand
     *            The command to add
     */
    public void addCLCommand(String command, CLCommand cLCommand) {
        _commands.put(command, cLCommand);
        notifyCommandAdded(command, cLCommand);
    }

    /**
     * Unregister the command identified by the given String.
     * @param command String representation of the command to be unregistered
     */
    public void removeCLCommand(String command) {
        _commands.remove(command);
        notifyCommandRemoved(command);
    }

    /**
     * Return a Map containing all commands as values, with the identifying
     * Strings as keys.
     * @return a Map containing all commands
     */
    public Map<String, CLCommand> getCLCommands() {
        return _commands;
    }

    /**
     * Adds the given plugin to the set of exit blockers. If the exit blocker
     * set becomes non-empty, the plugin system will not be automatically
     * terminated.
     *
     * @param blocker
     *            the plugin that prevents automatic termination of the plugin
     *            system.
     **/
    public void blockExit(IPlugin blocker) {
        if (blocker != null) {
            LOGGER.debug("PluginManager: registering exit blocker " + blocker);
            _blockers.add(blocker);
        }
    }

    /**
     * Removes the given plugin from the set of exit blockers. If the exit
     * blocker set becomes empty, the plugin system is automatically terminated.
     *
     * @param blocker
     *            the plugin that should not any longer prevent automatic
     *            termination of the plugin system. If this plugin object is not
     *            included in the set of exit blockers, the set will not be
     *            modified. Nevertheless, the set will be checked for emptiness.
     **/
    public void exitOk(IPlugin blocker) {
        synchronized (_blockers) {
            if (blocker != null) {
                LOGGER.debug("PluginManager: unregistering exit blocker " + blocker);
                _blockers.remove(blocker);
            }
            checkExit();
        }
    }

    /**
     * Checks whether there are any exit blockers registered, and shuts down the
     * plugin system, if not.
     *
     * @return Existence of exit blockers as boolean representation
     */
    public boolean checkExit() {
        synchronized (_blockers) {
            if (_blockers.isEmpty()) {
                LOGGER.debug("PluginManager: no active plugins, shutting down.");

                // Since there might still be AWT threads running,
                // shut down the Java VM manually.
                // The asynchronous stop method is used to decouple the system
                // termination from the plugin that unknowingly initiated the
                // process. This also unrolls the endless loop when a plugin on
                // termination unregisters itself as exit blocker.
                stop();
                return true;
            } else if (LOGGER.isDebugEnabled()) {
                LOGGER
                    .debug("Active plugins blocking exit: " + CollectionLister.toString(_blockers));
            }
        }
        return false;
    }

    /**
     * Terminates all plugins and the whole Java system. Whenever the
     * termination of any plugin fails, the termination process is cancelled,
     * too.
     * <p>
     * This method works asynchronously, it returns to the caller immediately
     * after initiating the termination process. Plugins are then terminated in
     * sequence (with respect to plugin dependencies), but concurrently to the
     * calling thread. Multiple concurrent calls to this method will be
     * sequential because each termination process is synchronized on the
     * PluginManager object.
     * </p>
     * There is no feedback to the calling thread, instead messages will be
     * printed to <code>System.out</code>.
     **/
    public void stop() {
        LOGGER.debug("Initiating PluginManager termination.");
        Thread terminationThread = new Thread() {
            @Override
            public void run() {
                if (stopSynchronized()) {
                    System.exit(0);
                }
            }
        };
        terminationThread.start();
    }

    /**
     * Stops the given plugin. If the plugin is modular, removes its layer
     * and unregisters it from the {@link ModuleManager}.
     * <p>
     * This method works asynchronously, it returns to the caller immediately
     * after initiating the termination process. The plugin is then terminated
     * (with respect to plugin dependencies) concurrently to the calling thread.
     * Multiple concurrent calls to this method will be sequential because
     * each termination process is synchronized on the PluginManager object.
     * </p>
     * There is no feedback to the calling thread, instead messages will be
     * printed to <code>System.out</code>.
     *
     * @param p
     *            the plugin to remove from the plugin system.
     **/
    public void stop(final IPlugin p) {
        LOGGER.debug("Initiating termination of " + p + ".");
        Thread terminationThread = new Thread() {
            @Override
            public void run() {
                stopSynchronized(p);
            }
        };
        terminationThread.start();
    }

    /**
     * <p>
     * Stops the given plugins. If some of the plugins are modular, removes
     * their layers and unregisters them from the {@link ModuleManager}.
     * </p>
     * Works through the list <em>from back to
     * front</em>. If any plugin cannot be stopped due to some other dependent
     * plugin (it does not matter whether it is or is not included in the list
     * in front of the plugin it depends on), the list is not further processed.
     * <p>
     * Example: Assume that plugin B depends on plugin A. A call with the list
     * <code>[A, B]</code> will succeed. But a call with the list
     * <code>[B, A]</code> will fail because plugin A cannot be terminated due
     * to the dependent plugin B.
     * </p>
     * <p>
     * The reason for the backward processing is that lists of recursively
     * dependent plugins are easier to generate with the most basic plugin at
     * first position.
     * </p>
     * <p>
     * This method works asynchronously, it returns to the caller immediately
     * after initiating the termination process. The plugins are then terminated
     * (with respect to plugin dependencies) concurrently to the calling thread.
     * Multiple concurrent calls to this method will be sequential because
     * each termination process is synchronized on the PluginManager object.
     * </p>
     * There is no feedback to the calling thread, instead messages will be
     * printed to <code>System.out</code>.
     *
     * @param plugins
     *            the plugins to remove from the plugin system.
     * @return a future {@code true} if the unloading was successful, future {@code false} otherwise.
     **/
    public CompletableFuture<Boolean> stop(final List<IPlugin> plugins) {
        LOGGER.debug("Initiating termination of " + plugins + ".");
        CompletableFuture<Boolean> completableFuture = new CompletableFuture<>();
        Thread terminationThread = new Thread() {
            @Override
            public void run() {
                boolean result = stopSynchronized(plugins);
                completableFuture.complete(result);
            }
        };
        terminationThread.start();
        return completableFuture;
    }

    /**
     * Stops the given plugins. If some plugins are modular, it removes
     * their layers and unregisters them from the {@link ModuleManager}.
     *
     * If any plugin cannot be stopped due to some other dependent
     * plugin (it does not matter whether it is or is not included in the list
     * in front of the plugin it depends on), the list is not further processed.
     *
     * Plugins are unloaded sequentially and not concurrent.
     *
     * @param plugins the list of plugins to be terminated.
     */
    public void blockingStop(final List<IPlugin> plugins) {
        LOGGER.debug("Initiating termination of " + plugins + ".");
        stopSynchronized(plugins);
    }

    /**
     * Tells whether the plugin system is about to terminate. This method only
     * returns true, if the {@link #stop()} method has been used to trigger the
     * system termination.
     * <p>
     * Please be aware that this method's result may be out of date before you
     * are able to interpret it. It is intended to be used by terminated plugins
     * that want to know whether it's just them or the whole system...
     * </p>
     *
     * @return <code>true</code> if the concurrent system termination thread is
     *         running.
     **/
    public boolean isStopping() {
        return _terminating;
    }

    /**
     * Terminates all plugins. This method is called internally by
     * {@link #stop()}.
     *
     * @return List of stopped plugins
     */
    public synchronized boolean stopSynchronized() {
        LOGGER.debug("Stopping plugin system asynchronously.");
        _terminating = true;
        try {
            List<IPlugin> pls = _dependencyList.getFulfilledObjects();
            return stopSynchronized(pls);
        } finally {
            _terminating = false;
        }
    }

    /**
     * Terminates all plugins in the given list. This method is called
     * internally by {@link #stopSynchronized()} and {@link #stop(List)}.
     **/
    private synchronized boolean stopSynchronized(List<IPlugin> pls) {
        // int limit = pls.size();
        for (int i = pls.size() - 1; i >= 0; i--) {
            // ask all plugins if they can be stopped.
            final IPlugin toStop = pls.get(i);
            Object sync = new Object();
            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("Preparing stop of " + toStop);
            }
            int waitSeconds =
                toStop.getProperties().getIntProperty("cleanupTimeout", CLEANUP_TIMEOUT);
            int firstWait = waitSeconds;
            int secondWait = 1;
            if (waitSeconds > 10) {
                firstWait = waitSeconds / 10;
                secondWait = waitSeconds - firstWait;
            }
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace(
                    "canShutDown timeout is " + waitSeconds + " milliseconds (" + firstWait + "/"
                        + secondWait + ").");
            }
            SynchronizedThread ct = new SynchronizedThread(() -> toStop.canShutDown(), sync);
            try {
                ct.start();
                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("Started canShutDown thread, syncing on " + sync);
                }
                synchronized (sync) {
                    // has thread already been finished?
                    if (ct.didFinish()) {
                        // do nothing
                    } else if (waitSeconds == -1) {
                        // stay a while...
                        // stay... forever!
                        if (LOGGER.isTraceEnabled()) {
                            LOGGER.trace(
                                "Waiting (unlimited) for canShutDown thread to finish, synced on "
                                    + sync);
                        }
                        sync.wait();
                    } else {
                        if (LOGGER.isTraceEnabled()) {
                            LOGGER.trace(
                                "Waiting (first part) for canShutDown thread to finish, synced on "
                                    + sync);
                        }
                        sync.wait(firstWait);
                        if (!ct.didFinish()) {
                            LOGGER.info("Waiting for " + toStop + " to confirm termination...");
                            sync.wait(secondWait);
                        }
                        if (LOGGER.isTraceEnabled()) {
                            LOGGER.trace(
                                "Done waiting, canShutDown thread "
                                    + (ct.didFinish() ? "finished" : "did not finish")
                                    + ", synced on " + sync);
                        }
                    }
                }
            } catch (InterruptedException e) {
                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("Interrupted timeout waiting on " + sync);
                }
            }
            if (!ct.hadSuccess()) {
                LOGGER.warn(toStop + " did not confirm termination in time.");
                return false;
            } else {
                LOGGER.debug(toStop + " said it can shut down.");
            }
        }
        for (int i = pls.size() - 1; i >= 0; i--) {
            IPlugin toStop = pls.get(i);
            try {
                _dependencyList.removeElement(toStop);
                LOGGER.info(toStop.getName() + " was unloaded.");
            } catch (DependencyNotFulfilledException e) {
                LOGGER.error(e.getMessage());
                LOGGER.error("list of dependent plugins:");
                LOGGER.error(CollectionLister.toString(e.getElements()));
                return false;
            }

            if (!stopSynchronized(toStop)) {
                LOGGER.warn("stop cancelled by plugin " + toStop);
                return false;
            }
        }
        return true;
    }

    /**
     * Stops the given plugin. If the plugin is modular, removes its
     * layer and unregisters it from the {@link ModuleManager}.
     * <br>
     * This method is called internally by {@link #stop(IPlugin)}
     * and {@link #stopSynchronized(List)}.
     **/
    private synchronized boolean stopSynchronized(final IPlugin p) {
        try {
            synchronized (_dependencyList) {
                _dependencyList.removeElement(p);
            }
        } catch (DependencyNotFulfilledException e) {
            LOGGER.error(e.getMessage());
            LOGGER.error("list of dependent plugins:");
            LOGGER.error(CollectionLister.toString(e.getElements()));
            synchronized (_dependencyList) {
                _dependencyList.addElement(DependencyElement.create(p));
            }
            return false;
        }

        LOGGER.debug("stopping " + p);
        boolean result;
        Object sync = new Object();
        int waitSeconds = p.getProperties().getIntProperty("cleanupTimeout", CLEANUP_TIMEOUT);
        if (LOGGER.isTraceEnabled()) {
            LOGGER.trace("Cleanup timeout is " + waitSeconds + " milliseconds.");
        }
        SynchronizedThread ct = new SynchronizedThread(new Command() {
            @Override
            public boolean execute() {
                return p.cleanup();
            }
        }, sync);

        // prevent stop from being called by the manager because
        // it just shut down the last plugin with an exit block.
        IPlugin blockLock = new PluginAdapter(PluginProperties.getUserProperties());
        if (!p.getName().equals("Renew Gui")) {
            _blockers.add(blockLock);
        }
        try {
            ct.start();
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("Started cleanup thread, syncing on " + sync);
            }
            synchronized (sync) {
                if (!ct.didFinish()) {
                    // stay a while...
                    if (waitSeconds == -1) {
                        // stay... forever!
                        if (LOGGER.isTraceEnabled()) {
                            LOGGER.trace(
                                "Waiting (unlimited) for cleanup thread to finish, synced on "
                                    + sync);
                        }
                        sync.wait();
                    } else {
                        if (LOGGER.isTraceEnabled()) {
                            LOGGER.trace("Waiting for cleanup thread to finish, synced on " + sync);
                        }
                        sync.wait(waitSeconds);
                    }
                }
                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace(
                        "Done waiting, cleanup thread "
                            + (ct.didFinish() ? "finished" : "did not finish") + ", synced on "
                            + sync);
                }
            }
        } catch (InterruptedException e) {
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("Interrupted timeout waiting on " + sync);
            }
        }

        if (!p.getName().equals("Renew Gui")) {
            _blockers.remove(blockLock);
        }
        LOGGER.debug(p + " stopped");
        result = ct.hadSuccess();
        if (!result) {
            synchronized (_dependencyList) {
                _dependencyList.addElement(DependencyElement.create(p));
            }
        } else {
            // success:
            serviceRemoved(p.getProperties().getProvisions(), p);
            if (serviceContainerContainsMainClass(p)) {
                // The plugin has at least one bound service, release it
                _container.unbind(p.getClass());
            }
            if (isModularPlugin(p)) {
                _moduleManager.removeLayerOfPlugin(p.getName());
                unregisterModularPlugin(p);
            }
        }
        return result;
    }

    /**
     * Start the Renew Plugin System
     *
     * @param args the command line parameters
     */
    public static void main(String[] args) {
        main(args, null);
    }

    /**
     * Start the Renew Plugin System
     *
     * @param args the command line parameters
     * @param url the url of the Loader.class needs a trailing slash
     */
    public static void main(String[] args, URL url) {
        main(args, url, null, null);
    }

    /**
     * Start the Renew Plugin System. Call {@link #main(String[], URL)} instead
     * of this method.
     * @param args the command line parameters
     * @param url the url of the Loader.class
     * @param logStrategy Log strategy to configure renews logging
     * @param classLoaderManager Classloader given to start renews plugin system
     */
    public static void main(
        String[] args, URL url, LogStrategy logStrategy, ClassLoaderManager classLoaderManager)
    {
        for (String s : args) {
            System.out.println(s);
        }


        if (url != null) {
            _loaderLocation = url;
        }
        PluginManager pm = createInstance(url, logStrategy, classLoaderManager); // logging initialized

        pm.loadPlugins();
        for (IPlugin plugin : pm.getPlugins()) {
            plugin.startUpComplete(true);
            LOGGER.info("loaded plugin: " + plugin.getName());
        }

        if (args.length > 0) {
            ArrayList<List<String>> cmds = new ArrayList<>();
            cmds.add(new ArrayList<>());

            for (int i = 0; i < args.length; i++) {
                String arg = args[i];

                if (arg.equals(COMMAND_SEPERATOR)) {
                    cmds.add(new ArrayList<>());
                    continue;
                } else if (arg.matches("^\\s*$")) {
                    continue;
                }

                while (arg.contains(COMMAND_SEPERATOR)) {
                    int indexOf = arg.indexOf(COMMAND_SEPERATOR);
                    String left = arg.substring(0, indexOf);
                    String right = arg.substring(indexOf + COMMAND_SEPERATOR.length());
                    cmds.get(cmds.size() - 1).add(left);
                    cmds.add(new ArrayList<>());
                    arg = right;
                }
                cmds.get(cmds.size() - 1).add(arg);
            }

            for (List<String> cmd : cmds) {
                if (cmd.size() == 0) {
                    continue;
                }
                CLCommand c = pm._commands.get(cmd.get(0));
                if (c == null) {
                    for (String s : cmd) {
                        System.out.println("#" + s + "#");
                    }

                    LOGGER.warn("Unknown initial command (ignored): " + cmd);
                } else {
                    cmd.remove(0);
                    String[] nc = new String[cmd.size()];
                    nc = cmd.toArray(nc);
                    LOGGER.debug("Executing initial command: " + cmd);
                    c.execute(nc, System.out);
                }
            }
        }
        pm.checkExit();
    }

    /**
     * This is a shortcut to configure the logging environment
     * without initializing the complete plug-in management system.
     *
     */
    public static synchronized void configureLogging() {
        if (_instance != null) {
            _instance._logStrategy.configureLogging();
        } else {
            getDefaultLogStrategy().configureLogging();
        }
    }

    private void initLocationFinders() {
        URL url = getLoaderLocation(); // getClass().getProtectionDomain().getCodeSource().getLocation();
        try {
            url = new URL(url, "plugins/");
        } catch (MalformedURLException e) {
            LOGGER.error("Could not deduce plugins directory near plugin loader: " + e);
            try {
                url = new URL(new File(System.getProperty("user.dir")).toURI().toURL(), "plugins/");
            } catch (MalformedURLException e2) {
                LOGGER.error("Could not deduce plugins directory near current directory: " + e2);
                return;
            }
        }
        _locationFinder.addLocationFinder(new PluginSubDirFinder(url));
        _locationFinder.addLocationFinder(new PluginJarLocationFinder(url));
        Iterator<URL> fromFile = getLocations();
        while (fromFile.hasNext()) {
            url = fromFile.next();
            _locationFinder.addLocationFinder(new PluginSubDirFinder(url));
            _locationFinder.addLocationFinder(new PluginJarLocationFinder(url));
        }
    }

    /**
     * Returns a List of URLs to be added to the location finder.
     *
     * @return a List of URLs to be added to the location finder
     */
    public Iterator<URL> getLocations() {
        Vector<URL> result = new Vector<>();
        Properties userProps = PluginProperties.getUserProperties();
        if (userProps.getProperty(PLUGIN_LOCATIONS_PROPERTY) == null) {
            LOGGER.info("no additional plugin locations set.");
        }
        Collection<String> locations = PropertyHelper
            .parsePathListString(userProps.getProperty(PLUGIN_LOCATIONS_PROPERTY, ""));
        Iterator<String> files = locations.iterator();
        while (files.hasNext()) {
            String file = files.next();
            LOGGER.debug("location: " + file);
            URL url = createURLFromString(file);
            if (url != null) {
                LOGGER.debug("as URL: " + url);
                result.add(url);
            }
        }
        return result.iterator();
    }

    private URL createURLFromString(String str) {
        URL url = null;

        // first check if str is a filename of an existing file
        File file = new File(str);
        if (file.exists()) {
            try {
                url = file.toURI().toURL();
                return url;
            } catch (MalformedURLException e) {
                LOGGER.error(e.getMessage(), e);
            }
        }

        // then, try to create an url brute force
        try {
            url = new URI(str).toURL();
        } catch (URISyntaxException | MalformedURLException | IllegalArgumentException
            | NullPointerException e) {
            if (LOGGER.isDebugEnabled()) {
                LOGGER.warn("Neither file nor url: " + str + "(" + e + ")", e);
            } else {
                LOGGER.warn("Neither file nor url: " + str);
            }
        }
        return url;
    }

    /**
     * Gets the missing dependencies specified by the given
     * <code>PluginProperties</code> object
     *
     * @param props the properties that specify the dependencies.
     *
     * @return List of missing dependencies. Can be empty
     **/
    public List<String> getMissingDependencies(PluginProperties props) {
        List<String> dependencyList = new ArrayList<>(_dependencyList.getFulfilledProvisions());
        DependencyElement dependencyElement = DependencyElement.create(props);

        return dependencyElement.getMissingRequirements(dependencyList);
    }

    /**
     * This command prints a list of all available commands.
     */
    public class HelpCommand implements CLCommand {
        /*
         * print all available commands
         */
        @Override
        public void execute(String[] args, PrintStream response) {
            response.println("usage: {command {args}* " + COMMAND_SEPERATOR + "}*");
            response.println("available commands:");

            int largestKeySize = 0;
            for (String key : _commands.keySet()) {
                largestKeySize = Math.max(largestKeySize, key.length());
            }

            String space = stringRepeat(" ", (largestKeySize + 5));

            for (String key : _commands.keySet()) {
                CLCommand command = _commands.get(key);
                String additionalSpace = stringRepeat(" ", largestKeySize - key.length() + 2);
                response.print(key + additionalSpace + "-  ");
                String description = command.getDescription();
                description = description.replaceAll("\n", "\n" + space);
                response.println(description);
            }
        }

        /**
         * Repeat a string a specified number of times.
         */
        private String stringRepeat(String str, int times) {
            String result = "";
            if (times > 0) {
                for (int i = 0; i < times; i++) {
                    result += str;
                }
            }
            return result;
        }

        @Override
        public String getDescription() {
            return "print a list of all commands";
        }

        @Override
        public String getArguments() {
            return null;
        }
    }

    /**
     * This class represents a timeout when trying to clean up a plugin. An
     * object must be given to synchronize on; this will be notified when the
     * cleanup worked.
     */
    private static class SynchronizedThread extends Thread {
        private boolean _success = false;
        private boolean _finished = false;
        private Object _toNotify;
        private Command _toExecute;

        public SynchronizedThread(Command toExecute, Object toNotify) {
            _toExecute = toExecute;
            _toNotify = toNotify;
        }

        @Override
        public void run() {
            _success = _toExecute.execute();
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("Execution finished: 1) syncing on " + _toNotify);
            }
            synchronized (_toNotify) {
                if (LOGGER.isTraceEnabled()) {
                    LOGGER.trace("Execution finished: 2) notifying on " + _toNotify);
                }
                _finished = true;
                _toNotify.notify();
            }
            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("Execution finished: 3) done on " + _toNotify);
            }
        }

        public boolean hadSuccess() {
            return _success;
        }

        public boolean didFinish() {
            return _finished;
        }
    }

    private interface Command {
        boolean execute();
    }

    /**
     * Returns URLs of used SystemClassLoader
     * @return The URLs of the used SystemClassLoader
     */
    public URL[] getLibs() {
        URLClassLoader urlCL;
        try {
            urlCL = (URLClassLoader) getSystemClassLoader();
        } catch (ClassCastException e) {
            LOGGER.warn("Could not extract URL from SystemClassLoader, no URLClassLoader.", e);
            return null;
        }
        return urlCL.getURLs();
    }

    /**
     * Gets a list of directly dependent plugins on a given plugin.
     *
     * @param plugin The IPlugin to check the dependent plugins for
     * @return a list of plugins that directly depend on the given plugin
     */
    public List<String> getDependentPlugins(IPlugin plugin) {
        List<String> dependentPlugins = new ArrayList<>();

        for (DependencyElement dependency : _dependencyList.getDependentPlugins(plugin)) {
            dependentPlugins.add(dependency.getPluginName());
        }

        return dependentPlugins;
    }

    /**
     * User preferences location is usually set to ~/.renew.
     *
     * @return folder of user preferences location
     */
    public static File getPreferencesLocation() {
        //FIXME: fallback, if home folder does not exist (temp dir?)
        File dir = new File(System.getProperty("user.home") + File.separator + PREF_DIR);
        if (!dir.exists()) {
            if (!dir.mkdir()) {
                return null;
            }
        }
        return dir;
    }

    @Override
    public void addCommandListener(CommandsListener listener) {
        _commandsListener.add(listener);
    }

    @Override
    public void removeCommandListener(CommandsListener listener) {
        _commandsListener.remove(listener);
    }

    @Override
    public void notifyCommandAdded(String name, CLCommand command) {
        for (CommandsListener cListener : _commandsListener) {
            cListener.commandAdded(name, command);
        }
    }

    @Override
    public void notifyCommandRemoved(String name) {
        for (CommandsListener cListener : _commandsListener) {
            cListener.commandRemoved(name);
        }
    }
}