package de.renew.plugin.load;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Collection;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.concurrent.Callable;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import de.renew.plugin.IPlugin;
import de.renew.plugin.PluginAdapter;
import de.renew.plugin.PluginClassLoader;
import de.renew.plugin.PluginProperties;
import de.renew.plugin.annotations.Provides;
import de.renew.plugin.di.FactoryDefinition;
import de.renew.plugin.di.ServiceContainer;


/**
 * Represents the abstract plugin loader.
 * @author Konstantin Simon Maria Moellers
 * @version 2015-10-11
 */
public abstract class AbstractPluginLoader implements PluginLoader {
    /**
     * Logger for logging purposes as specified by Apache Log4j
     */
    public static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(SimplePluginLoader.class);
    /**
     * Plugin Loader to be used in this class
     */
    protected final PluginClassLoader _loader;
    /**
     * Collection used to store service definitions
     */
    protected final ServiceContainer _container;

    /**
     * Constructor to initialize AbstractPluginLoader
     * @param loader Plugin loader to be used in this AbstractPluginLoader
     * @param container Service container to be used in this AbstractPluginLoader
     */
    public AbstractPluginLoader(PluginClassLoader loader, ServiceContainer container) {
        this._loader = loader;
        this._container = container;
    }

    /**
     * Retrieves all JAR urls from a directory specified by provided URL.
     *
     * @param url URL specifying the directory.
     * @return An array of URLs referencing JARs.
     */
    public static URL[] unifyURL(URL url) {
        File dir = null;
        try {
            dir = new File(url.toURI());
        } catch (Exception e) {
            LOGGER.warn("Unable to search for JAR files in " + url + ": " + e);
        }

        URL[] jarURLs = getURLsFromDirectory(dir);
        if (jarURLs == null || jarURLs.length == 0) {
            LOGGER.warn("No JAR found in " + url + ", resorting to given URL.");
            return new URL[] { url };
        }

        return jarURLs;
    }

    private static URL[] getURLsFromDirectory(File location) {
        // changed (06.05.2004) to support more than one jar in a plugin folder
        if (location == null) {
            return null;
        } else if (location.getName().endsWith(".jar")) {
            try {
                // look into jar file if there are additional lib jars
                // includes
                Vector<URL> urls = new Vector<URL>();
                urls.add(location.toURI().toURL());

                String baseURL = "jar:" + location.toURI().toURL() + "!/";
                JarFile jar = new JarFile(location);
                Enumeration<JarEntry> e = jar.entries();
                while (e.hasMoreElements()) {
                    JarEntry entry = e.nextElement();
                    if ((entry.getName().startsWith("libs/"))
                        && (entry.getName().endsWith(".jar"))) {
                        urls.add(new URL(baseURL + entry.getName()));
                    }
                }

                return urls.toArray(new URL[urls.size()]);
            } catch (MalformedURLException e) {
                LOGGER.error(
                    "SimplePluginLoader: Could not convert to URL: " + location + " ("
                        + e.getMessage() + ").");
            } catch (IOException e1) {
                LOGGER.error("Error while opening/reading jar file: " + location, e1);
            }
        }

        if (!location.isDirectory()) {
            location = location.getParentFile();
        }

        final Vector<URL> result = getJarsRecursiveFromDir(location);
        return result.toArray(new URL[result.size()]);
    }

    private static Vector<URL> getJarsRecursiveFromDir(File dir) {
        Vector<URL> v = new Vector<URL>();

        File[] files = dir.listFiles(new FileFilter() {
            @Override
            public boolean accept(File f) {
                return f.isDirectory() || f.getName().endsWith(".jar");
            }
        });

        // No JARs or directories found?
        if (files == null) {
            return v;
        }

        for (File file : files) {
            if (file.isFile()) {
                try {
                    v.add(file.toURI().toURL());
                } catch (MalformedURLException e) {
                    LOGGER.debug("Can't convert file location to URL: " + e.getMessage(), e);
                }
            } else if (file.isDirectory()) {
                v.addAll(getJarsRecursiveFromDir(file));
            }
        }

        return v;
    }

    /**
     * Loads the plugin in the given directory.
     * For that, the plugin.cfg file is parsed and searched for
     * the pluginClassName entry.
     * This class will then be loaded and instantiated.
     */
    @Override
    public final IPlugin loadPlugin(PluginProperties props) {
        LOGGER.debug(getClass().getSimpleName() + " loading from " + props.getURL());
        try {
            // Find main class.
            Class<? extends IPlugin> mainClass = findMainClass(props);

            // If no main class exists, create a basic plugin adapter.
            if (mainClass == null) {
                LOGGER.debug("* no main class!");
                return new PluginAdapter(props);
            }

            // Create a plugin from properties.
            final IPlugin plugin = createPlugin(props, mainClass);
            bindPluginServices(mainClass, plugin);

            return plugin;
        } catch (PluginInstantiationException e) {
            LOGGER.debug(getClass().getSimpleName() + ": " + e.getMessage());
            if (LOGGER.isTraceEnabled()) {
                e.printStackTrace();
            }
        }

        return null;
    }

    @Override
    public final IPlugin loadPluginFromURL(URL url) {
        PluginProperties props = new PluginProperties(url);
        try {
            InputStream stream = SimplePluginLoader.PluginConfigFinder.getConfigInputStream(url);
            props.load(stream);
            return loadPlugin(props);
        } catch (Exception e) {
            LOGGER.error("SimplePluginLoader.loadPluginFromURL: " + e);
        }

        return null;
    }

    /**
     * Creates a Plugin Object from given properties and mainClass
     * @param props Plugin Properties used for creation of Plugin
     * @param mainClass MainClass of Plugin to be created
     * @return Plugin Object build from given mainClass and plugin properties
     * @throws PluginInstantiationException when an error occurs during creation of Plugin object
     */
    protected abstract IPlugin createPlugin(
        PluginProperties props, Class<? extends IPlugin> mainClass)
        throws PluginInstantiationException;

    /**
     * Finds the main class.
     * @param props The plugin properties to search in.
     * @return Main Class instance or null.
     */
    protected Class<? extends IPlugin> findMainClass(PluginProperties props) {
        String className = props.getProperty("mainClass");

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

        try {
            LOGGER.debug("* creating a " + className + " with cl " + _loader);
            Class<?> mainClass = _loader.loadClass(className);

            return mainClass.asSubclass(IPlugin.class);
        } catch (ClassNotFoundException e) {
            LOGGER.error(e.getMessage());
            return null;
        } catch (ClassCastException e) {
            LOGGER.error(e.getMessage());
            return null;
        }
    }

    /**
     * Converts a String into a list by separating by commas
     * @param list String to be converted into a list
     * @return List object containing values given as String
     */
    protected Collection<String> parseListString(String list) {
        StringTokenizer tok = new StringTokenizer(list, ",");
        Collection<String> result = new Vector<String>(tok.countTokens());
        try {
            while (tok.hasMoreTokens()) {
                String currentToken = tok.nextToken();
                result.add(currentToken);
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            LOGGER.error("PluginLoader: " + e + " when parsing " + list + " as list!");
        }
        return result;
    }

    /**
     * Binds a plugin to a service container
     * @param mainClass Main Class of plugin to be bound to service container
     * @param plugin Plugin to be bound to service container
     */
    private void bindPluginServices(Class<? extends IPlugin> mainClass, final IPlugin plugin) {
        // Save plugin in container.
        _container.set(mainClass, plugin);
        LOGGER.debug("Bound Service: " + mainClass);

        for (final Method method : plugin.getClass().getMethods()) {
            if (method.getAnnotation(Provides.class) != null) {
                final Class<?> service = method.getReturnType();
                _container
                    .addDefinition(new FactoryDefinition<Object>(service, new Callable<Object>()
                    {
                        @Override
                        public Object call() throws Exception {
                            return method.invoke(plugin);
                        }
                    }));
                LOGGER.debug("Bound Service: " + service.toString());
            }
        }
    }
}