//   Copyright 2012 Georg-August-Universität Göttingen, Germany
//
//   Licensed 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 de.ugoe.cs.autoquest.plugin;

import java.io.File;
import java.io.FilenameFilter;
import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

/**
 * <p>
 * This class provides the functionality to load AutoQUEST plug-ins from a pre-defined folder.
 * </p>
 * 
 * @author Steffen Herbold
 * @version 1.0
 */
public class PluginLoader implements AutoCloseable {

    /**
     * <p>
     * Handle of the plug-in directory.
     * </p>
     */
    private final File pluginDir;

    /**
     * <p>
     * Collection of the loaded plug-ins.
     * </p>
     */
    private final Collection<AutoQUESTPlugin> plugins;
    
    /**
     * <p>
     * the class loaders instantiated internally
     * </p>
     */
    private List<URLClassLoader> instantiatedClassLoaders = new LinkedList<>();

    /**
     * <p>
     * Constructor. Creates a new PluginLoader that can load plug-ins the defined directory.
     * </p>
     * 
     * @param pluginDir
     *            handle of the directory; in case the handle is <code>null</code> or does not
     *            describe a directory, an {@link IllegalArgumentException} is thrown
     */
    public PluginLoader(File pluginDir) {
        if (pluginDir == null) {
            throw new IllegalArgumentException("Parameter pluginDir must not be null!");
        }
        if (!pluginDir.isDirectory()) {
            throw new IllegalArgumentException("File " + pluginDir.getPath() +
                " is not a directory");
        }
        this.pluginDir = pluginDir;
        plugins = new LinkedList<AutoQUESTPlugin>();
    }

    /**
     * <p>
     * Loads plug-ins from {@link #pluginDir}.
     * </p>
     * 
     * @throws PluginLoaderException
     *             thrown if there is a problem loading a plug-in or updating the classpath
     */
    public void load() throws PluginLoaderException {
        File[] jarFiles = pluginDir.listFiles(new FilenameFilter() {
            @Override
            public boolean accept(File dir, String name) {
                return checkNameConformity(name);
            }
        });

        if (jarFiles != null) {
            for (File jarFile : jarFiles) {
                updateClassLoader(jarFile);

                String pluginName = jarFile.getName().split("-")[2];
                String pluginClassName = "de.ugoe.cs.autoquest.plugin." + pluginName + "." +
                    pluginName.toUpperCase() + "Plugin";

                Class<?> pluginClass = null;
                try {
                    pluginClass = Class.forName(pluginClassName);
                }
                catch (ClassNotFoundException e) {
                    throw new PluginLoaderException("No class '" + pluginClassName + "' found in " +
                        pluginDir + "/" + jarFile.getName());
                }
                try {
                    AutoQUESTPlugin pluginObject =
                        (AutoQUESTPlugin) pluginClass.getDeclaredConstructor().newInstance();
                    plugins.add(pluginObject);
                }
                catch (InstantiationException e) {
                    throw new PluginLoaderException("Could not instantiate " + pluginClassName);
                }
                catch (IllegalAccessException e) {
                    throw new PluginLoaderException("Could not access " + pluginClassName);
                }
                catch (ClassCastException e) {
                    throw new PluginLoaderException("Class " + pluginClassName +
                        " not instance of AutoQUESTPlugin");
                }
                catch (IllegalArgumentException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                catch (InvocationTargetException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                catch (NoSuchMethodException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                catch (SecurityException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * <p>
     * Retrieves the classpath from a Jar file's MANIFEST.
     * </p>
     */
    /*
     * protected String[] getClassPathFromJar(File jarFile) { String[] classPath;
     * 
     * JarInputStream jarInputStream = null; Manifest manifest = null; try { FileInputStream
     * fileStream = new FileInputStream(jarFile); try { jarInputStream = new
     * JarInputStream(fileStream); manifest = jarInputStream.getManifest(); } finally {
     * jarInputStream.close(); fileStream.close(); } } catch (FileNotFoundException e) { throw new
     * AssertionError( "FileNotFoundException should be impossible!"); } catch (IOException e) {
     * throw new PluginLoaderException(e); }
     * 
     * String jarClassPath = manifest.getMainAttributes().getValue( "Class-Path");
     * 
     * if (jarClassPath != null) { String[] jarClassPathElements = jarClassPath.split(" ");
     * classPath = new String[jarClassPathElements.length]; for (int i = 0; i <
     * jarClassPathElements.length; i++) { classPath[i] = "file:" +
     * jarFile.getParentFile().getAbsolutePath() + "/" + jarClassPathElements[i]; } try {
     * jarInputStream.close(); } catch (IOException e) { throw new PluginLoaderException(e); } }
     * else { classPath = new String[] {}; } return classPath; }
     */

    /**
     * <p>
     * Updates the classpath of the {@link ClassLoader} to include the plug-in jar as well as
     * further libraries required by the plug-in jar as defined in the <code>Class-Path</code>
     * section of its manifest.
     * </p>
     * 
     * @param jarFile
     *            handle of the plug-in jar file
     * @throws PluginLoaderException
     *             thrown if there is a problem updating the class loader or loading the plug-in jar
     */
    private void updateClassLoader(File jarFile) throws PluginLoaderException {
        // String[] classPath = getClassPathFromJar(jarFile);

        try {
            URLClassLoader loader = new URLClassLoader(new URL[]
                { new URL("file:" + jarFile.getAbsoluteFile()) },
                                                    ClassLoader.getSystemClassLoader());
            
            instantiatedClassLoaders.add(loader);
        }
        catch (MalformedURLException e) {
            throw new PluginLoaderException(e);
        }
    }

    /**
     * <p>
     * Checks if the name of a file indicates that it defines a AutoQUEST plug-in. The structure of
     * valid plug-in filenames is <code>autoquest-plugin-%PLUGIN_NAME%-version.jar</code>, where
     * <code>%PLUGIN_NAME%</code> is replaced by the name of the plug-in. Note that plug-in names
     * must not contain any dashes.
     * </p>
     * 
     * @param filename
     *            filename that is checked
     * @return true if filename matches pattern of AutoQUEST plug-in; false otherwise
     */
    protected boolean checkNameConformity(String filename) {
        if (filename == null) {
            return false;
        }
        return filename.startsWith("autoquest-plugin-") &&
            !filename.startsWith("autoquest-plugin-core") &&
            ((filename.split("-").length == 4 && filename.endsWith(".jar")) ||
                filename.split("-").length == 5 && filename.endsWith("SNAPSHOT.jar") ||
                filename.split("-").length == 6 && filename.endsWith(".jar"));
    }

    public Collection<AutoQUESTPlugin> getPlugins() {
        return Collections.unmodifiableCollection(plugins);
    }

    @Override
    public void close() throws Exception {
        for (URLClassLoader loader : instantiatedClassLoaders) {
            loader.close();
        }
    }

}
