/*
 * Syncany, www.syncany.org
 * Copyright (C) 2011-2016 Philipp C. Heckel <philipp.heckel@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.syncany.plugins;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.syncany.util.StringUtil;

import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.ClassPath;
import com.google.common.reflect.ClassPath.ClassInfo;

/**
 * This class loads and manages all the {@link Plugin}s loaded in the classpath.
 * It provides two public methods:
 *
 * <ul>
 *  <li>{@link #list()} returns a list of all loaded plugins (as per classpath)</li>
 *  <li>{@link #get(String) get()} returns a specific plugin, defined by a name</li>
 * </ul>
 *
 * @see Plugin
 * @author Philipp C. Heckel <philipp.heckel@gmail.com>
 */
public class Plugins {
	private static final Logger logger = Logger.getLogger(Plugins.class.getSimpleName());

	private static final String PLUGIN_PACKAGE_NAME = Plugin.class.getPackage().getName();
	private static final String PLUGIN_CLASS_SUFFIX = Plugin.class.getSimpleName();

	private static final Map<String, Plugin> plugins = new TreeMap<String, Plugin>();

	/**
	 * Loads and returns a list of all available
	 * {@link Plugin}s.
	 */
	public static List<Plugin> list() {
		loadPlugins();
		return new ArrayList<Plugin>(plugins.values());
	}

	/**
	 * Loads and returns a list of all {@link Plugin}s
	 * matching the given subclass.
	 */
	public static <T extends Plugin> List<T> list(Class<T> pluginClass) {
		loadPlugins();
		List<T> matchingPlugins = new ArrayList<T>();

		for (Plugin plugin : plugins.values()) {
			if (pluginClass.isInstance(plugin)) {
				matchingPlugins.add(pluginClass.cast(plugin));
			}
		}

		return matchingPlugins;
	}

	/**
	 * Loads the {@link Plugin} by a given identifier.
	 *
	 * <p>Note: Unlike the {@link #list()} method, this method is not expected
	 * to take long, because there is no need to read all JARs in the classpath.
	 *
	 * @param pluginId Identifier of the plugin, as defined by {@link Plugin#getId() the plugin ID)
	 * @return Returns an instance of a plugin, or <tt>null</tt> if no plugin with the given identifier can be found
	 */
	public static Plugin get(String pluginId) {
		if (pluginId == null) {
			return null;
		}

		loadPlugin(pluginId);

		if (plugins.containsKey(pluginId)) {
			return plugins.get(pluginId);
		}
		else {
			return null;
		}
	}

	public static <T extends Plugin> T get(String pluginId, Class<T> pluginClass) {
		Plugin plugin = get(pluginId);

		if (pluginId == null || !pluginClass.isInstance(plugin)) {
			return null;
		}
		else {
			return pluginClass.cast(plugin);
		}
	}

	private static void loadPlugin(String pluginId) {
		if (plugins.containsKey(pluginId)) {
			return;
		}

		loadPlugins();

		if (plugins.containsKey(pluginId)) {
			return;
		}
		else {
			logger.log(Level.WARNING, "Could not load plugin (1): " + pluginId + " (not found or issues with loading)");
		}
	}

	public static void refresh() {
		plugins.clear();
	}

	/**
	 * Loads all plugins in the classpath.
	 *
	 * <p>First loads all classes in the 'org.syncany.plugins' package.
	 * For all classes ending with the 'Plugin' suffix, it tries to load
	 * them, checks whether they inherit from {@link Plugin} and whether
	 * they can be instantiated.
	 */
	private static void loadPlugins() {
		try {
			ImmutableSet<ClassInfo> pluginPackageSubclasses = ClassPath
				.from(Thread.currentThread().getContextClassLoader())
				.getTopLevelClassesRecursive(PLUGIN_PACKAGE_NAME);

			for (ClassInfo classInfo : pluginPackageSubclasses) {
				boolean classNameEndWithPluginSuffix = classInfo.getName().endsWith(PLUGIN_CLASS_SUFFIX);

				if (classNameEndWithPluginSuffix) {
					Class<?> pluginClass = classInfo.load();

					String camelCasePluginId = pluginClass.getSimpleName().replace(Plugin.class.getSimpleName(), "");
					String pluginId = StringUtil.toSnakeCase(camelCasePluginId);

					boolean isSubclassOfPlugin = Plugin.class.isAssignableFrom(pluginClass);
					boolean canInstantiate = !Modifier.isAbstract(pluginClass.getModifiers());
					boolean pluginAlreadyLoaded = plugins.containsKey(pluginId);

					if (isSubclassOfPlugin && canInstantiate && !pluginAlreadyLoaded) {
						logger.log(Level.INFO, "- " + pluginClass.getName());

						try {
							Plugin plugin = (Plugin) pluginClass.newInstance();
							plugins.put(plugin.getId(), plugin);
						}
						catch (Exception e) {
							logger.log(Level.WARNING, "Could not load plugin (2): " + pluginClass.getName(), e);
						}
					}
				}
			}
		}
		catch (Exception e) {
			throw new RuntimeException("Unable to load plugins.", e);
		}
	}
}
