package de.ugoe.cs.quest.plugin;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FilenameFilter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.InvalidParameterException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;

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

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

	/**
	 * <p>
	 * Collection of the loaded plug-ins.
	 * </p>
	 */
	private final Collection<QuestPlugin> plugins;

	/**
	 * <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 InvalidParameterException} is thrown
	 */
	public PluginLoader(File pluginDir) {
		if (pluginDir == null) {
			throw new InvalidParameterException(
					"Parameter pluginDir must not be null!");
		}
		if (!pluginDir.isDirectory()) {
			throw new InvalidParameterException("File " + pluginDir.getPath()
					+ " is not a directory");
		}
		this.pluginDir = pluginDir;
		plugins = new LinkedList<QuestPlugin>();
	}

	/**
	 * <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);
			}
		});

		for (File jarFile : jarFiles) {
			updateClassLoader(jarFile);

			String pluginName = jarFile.getName().split("-")[2];
			String pluginClassName = "de.ugoe.cs.quest.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 {
				QuestPlugin pluginObject = (QuestPlugin) pluginClass
						.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 QuestPlugin");
			}
		}
	}

	/**
	 * <p>
	 * Retrieves the classpath from a Jar file's MANIFEST.
	 * </p>
	 * 
	 * @throws IOException
	 * @throws FileNotFoundException
	 */
	protected String[] getClassPathFromJar(File jarFile) {
		String[] classPath;

		JarInputStream jarInputStream = null;
		try {
			jarInputStream = new JarInputStream(new FileInputStream(jarFile));
		} catch (FileNotFoundException e) {
			throw new AssertionError(
					"FileNotFoundException should be impossible!");
		} catch (IOException e) {
			throw new PluginLoaderException(e);
		}

		Manifest manifest = jarInputStream.getManifest();

		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);
		URLClassLoader classLoader = (URLClassLoader) ClassLoader
				.getSystemClassLoader();
		Method method;

		try {
			method = URLClassLoader.class.getDeclaredMethod("addURL",
					new Class[] { URL.class });
		} catch (SecurityException e) {
			throw new PluginLoaderException(
					"addURL method of URLClassLoader not accessible via reflection.");
		} catch (NoSuchMethodException e) {
			throw new AssertionError(
					"URLClassLoader does not have addURL method. Should be impossible!!");
		}
		method.setAccessible(true);

		try {
			method.invoke(
					classLoader,
					new Object[] { new URL("file:" + jarFile.getAbsoluteFile()) });
			for (String element : classPath) {
				method.invoke(classLoader, new Object[] { new URL(element) });
			}
		} catch (IllegalArgumentException e) {
			throw new AssertionError(
					"Illegal arguments for addURL method. Should be impossible!!");
		} catch (MalformedURLException e) {
			throw new PluginLoaderException(e);
		} catch (IllegalAccessException e) {
			throw new PluginLoaderException(
					"addURL method of URLClassLoader not accessible via reflection.");
		} catch (InvocationTargetException e) {
			throw new PluginLoaderException(e);
		}
	}

	/**
	 * <p>
	 * Checks if the name of a file indicates that it defines a QUEST plug-in.
	 * The structure of valid plug-in filenames is
	 * <code>quest-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 QUEST plug-in; false
	 *         otherwise
	 */
	protected boolean checkNameConformity(String filename) {
		if (filename == null) {
			return false;
		}
		return filename.startsWith("quest-plugin-") && !filename.startsWith("quest-plugin-core")
				&&
				((filename.split("-").length == 4 && filename.endsWith(".jar")) ||
				  filename.split("-").length == 5 && filename.endsWith("SNAPSHOT.jar"));
	}
	
	public Collection<QuestPlugin> getPlugins() {
		return Collections.unmodifiableCollection(plugins);
	}
}
