/**
 *  Licensed under GPL. For more information, see
 *    http://jaxodraw.sourceforge.net/license.html
 *  or the LICENSE file in the jaxodraw distribution.
 */
package net.sf.jaxodraw.plugin;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;

import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import net.sf.jaxodraw.util.JaxoLog;


/**
 * Loads JaxoDraw plugins.
 *
 * @since 2.0
 */
public class JaxoPluginLoader {

    /**
     * Load plugins. Scans the given directory for jar files,
     * each jar file for a class that ends with "JaxoPlugin.class"
     * and if this class implements JaxoPlugin, adds the jar to the classpath.
     *
     * @param pluginDirName the directory to scan for plugins.
     * @return A list of JaxoPlugins that have been loaded.
     */
    public List<JaxoPlugin> loadPlugins(final String pluginDirName) {
        final List<JaxoPlugin> plugins = new ArrayList<JaxoPlugin>(8);

        final String[] children = getJars(pluginDirName);

        if (children == null) {
            JaxoLog.debug("No plugin directory found, no plugins loaded!");
            return plugins;
        }

        // loop over jars in plugin dir
        for (int i = 0; i < children.length; i++) {
            final String filename = new File(pluginDirName, children[i]).toString();
            loadPluginFromJar(filename, plugins);
        }

        // load plugin properties from corresponding files
        for (final Iterator<JaxoPlugin> it = plugins.iterator(); it.hasNext();) {
            (it.next()).loadProperties();
        }

        return plugins;
    }

    /**
     * Invalidates the class loader responsible for plugin loading
     * and re-scans the plugin dir, loading any found classes.
     * This is the only way to remove plugins from the class loader.
     *
     * @param pluginDirName the directory to scan for plugins.
     * @return A list of JaxoPlugins that have been loaded.
     * @see #loadPlugins(java.lang.String)
     */
    public List<JaxoPlugin> reValidate(final String pluginDirName) {
        jarLoader().reset();

        return loadPlugins(pluginDirName);
    }

    /**
     * Loads plugins from a jar file. If the jar contains
     * any classes that end with "JaxoPlugin.class" and that
     * implement JaxoPlugin, the jar is added to the classpath.
     *
     * @param filename absolute path to a jar file that contains a JaxoPlugin.
     *
     * @return A List of JaxoPlugins that have been loaded,
     * or null if there was a problem or the jar didn't contain a JaxoPlugin.
     */
    public List<JaxoPlugin> loadPluginsFromJar(final String filename) {
        final List<JaxoPlugin> plugins = new ArrayList<JaxoPlugin>(8);
        loadPluginFromJar(filename, plugins);

        if (plugins.isEmpty()) {
            return null;
        }

        for (final Iterator<JaxoPlugin> it = plugins.iterator(); it.hasNext();) {
            final JaxoPlugin plugin = it.next();
            plugin.loadProperties();
        }

        return plugins;
    }

    /**
     * Tries to find the jar source file for a JaxoPlugin
     * in the given directory. The search is done via the pluginId() of the
     * plugin, ie the first jar that contains a class whose name matches
     * the pluginId is returned.
     *
     * @param plugin the plugin whose source jar to find.
     * @param pluginDir the directory where jars are listed.
     * @return a File whose jar contains the given plugin,
     *      or null if nothing is found.
     */
    public static File getPluginJar(final JaxoPlugin plugin, final String pluginDir) {
        final String[] children = getJars(pluginDir);

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

        // loop over jars in plugin dir
        for (int i = 0; i < children.length; i++) {
            final File file = new File(pluginDir, children[i]);
            final JarFile jarFile = getJarFile(file.toString());
            final Enumeration<JarEntry> entries = jarFile.entries();

            // loop over jar entries
            while (entries.hasMoreElements()) {
                final JarEntry entry = entries.nextElement();
                final String name = entry.getName();
                if (name.endsWith("JaxoPlugin.class")) {

                    final String clas =
                        name.substring(0, name.lastIndexOf(".class"))
                            .replace('/', '.');
                    if (clas.equals(plugin.pluginId())) {
                        return file;
                    }

                }
            }
        }

        // nothing found
        return null;
    }

      //
     // private
    //

    private JarLoader jarLoader() {
        return JarLoader.getInstance();
    }

    private static JarFile getJarFile(final String filename) {
        try {
            return new JarFile(filename);
        } catch (IOException e) {
            JaxoLog.debug(filename + " cannot be loaded, ignoring!", e);
            return null;
        }
    }

    private void loadPluginFromJar(final String filename, final List<JaxoPlugin> plugins) {
        final JarFile jarFile = getJarFile(filename);

        if (jarFile == null) {
            return;
        }

        // check if a jar with the same mainclass is already loaded
        try {
            final String mainclass =
                    jarFile.getManifest().getMainAttributes().getValue("Main-Class");

            if (mainclass != null && jarLoader().isLoaded(mainclass)) {
                JaxoLog.warn(mainclass + ": A Main-Class with this name is already loaded! Ignoring.");
                return;
            }

            // TODO: try to load dependencies from manifests Class-Path?
            //String classPath =
            //        jarFile.getManifest().getMainAttributes().getValue("Class-Path");
        } catch (IOException ex) {
            JaxoLog.debug(ex);
            JaxoLog.warn("Failed to load plugin from: " + jarFile.getName());
            return;
        }

        final Enumeration<JarEntry> entries = jarFile.entries();

        // loop over jar entries
        while (entries.hasMoreElements()) {
            final JarEntry entry = entries.nextElement();
            final String name = entry.getName();
            if (name.endsWith("JaxoPlugin.class")) {
                jarLoader().addJar(filename);

                final String clas =
                    name.substring(0, name.lastIndexOf(".class"))
                        .replace('/', '.');

                if (jarLoader().isLoaded(clas)) {
                    JaxoLog.warn(clas + ": double loaded!");
                }

                loadPluginFromClass(clas, plugins);
            }
        }
    }

    private void loadPluginFromClass(final String clas, final List<JaxoPlugin> plugins) {
        Class<?> jarLoaderClass = null;

        try {
            jarLoaderClass = jarLoader().loadClass(clas);
        } catch (ClassNotFoundException e) {
            logIgnor(clas, " cannot be loaded", e);
            return;
        }

        JaxoLog.debug("Class " + clas + " successfully loaded!");

        Object pluginClass = null;

        try {
            pluginClass = jarLoaderClass.newInstance();
        } catch (IllegalAccessException e) {
            logIgnor(clas, " cannot be accessed", e);
            return;
        } catch (InstantiationException e) {
            logIgnor(clas, " cannot be instantiated", e);
            return;
        } catch (NoClassDefFoundError e) {
            logIgnor(clas, " is missing dependencies", new Exception(e));
            return;
        }

        if (pluginClass instanceof JaxoPlugin) {
            final JaxoPlugin plugin = (JaxoPlugin) pluginClass;

            if (plugin.pluginId() == null) {
                JaxoLog.warn(clas + " has null pluginId(), ignoring!");
            } else if (plugin.pluginId().equals(jarLoaderClass.getName())) {
                plugins.add(plugin);
                JaxoLog.info(clas + ": Plugin registered!");
            } else {
                JaxoLog.warn(clas + " has invalid pluginId(), ignoring!");
            }
        } else {
            JaxoLog.warn(clas + " is not a JaxoPlugin, ignoring!");
        }
    }

    private void logIgnor(final String clas, final String msg, final Exception e) {
        JaxoLog.warn("Plugin class " + clas + msg + ", ignoring!");
        JaxoLog.debug(e);
    }

    private static String[] getJars(final String pluginDirName) {
        final FilenameFilter filter =
            new FilenameFilter() {
                public boolean accept(final File dir, final String name) {
                    return name.endsWith(".jar");
                }
            };

        return new File(pluginDirName).list(filter);
    }

    /** Load jars into the classpath. */
    private static final class JarLoader extends URLClassLoader {
        private static JarLoader jarLoader;

        private JarLoader() {
            super(new URL[] {});
        }

        public static JarLoader getInstance() {
            synchronized (JarLoader.class) {
                if (jarLoader == null) {
                    jarLoader = new JarLoader();
                }

                return jarLoader;
            }
        }

        public boolean isLoaded(final String clazz) {
            return (findLoadedClass(clazz) != null);
        }

        public void reset() {
            // only way to remove classes (plugins) is to gc the classloader
            jarLoader = null;
        }

        public void addJar(final String path) {
            final String uri = new File(path).toURI().toString();
            final String urlPath = "jar:" + uri + "!/";

            JaxoLog.debug("Loading plugin from " + path + "...");

            try {
                addURL(new URL(urlPath));
            } catch (MalformedURLException e) {
                JaxoLog.debug("Could not load " + urlPath + ", ignoring!", e);
            }
        }
    }
}
