package org.sunflow.system;

import org.codehaus.janino.ClassBodyEvaluator;
import org.codehaus.commons.compiler.CompileException;
import org.sunflow.system.UI.Module;
import org.sunflow.util.FastHashMap;

/**
 * This class represents a list of plugins which implement a certain interface
 * or extend a certain class. Many plugins may be registered and created at a
 * later time by recalling their unique name only.
 *
 * @param <T> Default constructible type or interface all plugins will derive
 * from or implement
 */
public final class Plugins<T> {

    private final FastHashMap<String, Class<? extends T>> pluginClasses;
    private final Class<T> baseClass;

    /**
     * Create an empty plugin list. You must specify <code>T.class</code> as an
     * argument.
     *
     * @param baseClass
     */
    public Plugins(Class<T> baseClass) {
        pluginClasses = new FastHashMap<>();
        this.baseClass = baseClass;
    }

    /**
     * Create an object from the specified type name. If this type name is
     * unknown or invalid, <code>null</code> is returned.
     *
     * @param name plugin type name
     * @return an instance of the specified plugin type, or <code>null</code> if
     * not found or invalid
     */
    public T createObject(String name) {
        if (name == null || name.equals("none")) {
            return null;
        }
        Class<? extends T> c = pluginClasses.get(name);
        if (c == null) {
            // don't print an error, this will be handled by the caller
            return null;
        }
        try {
            return c.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            UI.printError(Module.API, "Cannot create object of type \"%s\" - %s", name, e.getLocalizedMessage());
            return null;
        }
    }

    /**
     * Check this plugin list for the presence of the specified type name
     *
     * @param name plugin type name
     * @return <code>true</code> if this name has been registered,
     * <code>false</code> otherwise
     */
    public boolean hasType(String name) {
        return pluginClasses.get(name) != null;
    }

    /**
     * Generate a unique plugin type name which has not yet been registered.
     * This is meant to be used when the actual type name is not crucial, but
     * succesfully registration is.
     *
     * @param prefix a prefix to be used in generating the unique name
     * @return a unique plugin type name not yet in use
     */
    public String generateUniqueName(String prefix) {
        String type;
        for (int i = 1; hasType(type = String.format("%s_%d", prefix, i)); i++) {
        }
        return type;
    }

    /**
     * Define a new plugin type from java source code. The code string contains
     * import declarations and a class body only. The implemented type is
     * implicitly the one of the plugin list being registered against.If the
     * plugin type name was previously associated with a different class, it
     * will be overriden. This allows the behavior core classes to be modified
     * at runtime.
     *
     * @param name plugin type name
     * @param sourceCode Java source code definition for the plugin
     * @return <code>true</code> if the code compiled and registered
     * successfully, <code>false</code> otherwise
     */
    //  @SuppressWarnings("unchecked")
    public boolean registerPlugin(String name, String sourceCode) {
        try {
            ClassBodyEvaluator cbe = new ClassBodyEvaluator();
            cbe.setClassName(name);
            cbe.setExtendedClass(baseClass);
            cbe.cook(sourceCode);            
            return registerPlugin(name, cbe.getClazz());
        } catch (CompileException e) {
            UI.printError(Module.API, "Plugin \"%s\" could not be declared - %s", name, e.getLocalizedMessage());
            return false;
        }
    }

    /**
     * Define a new plugin type from an existing class. This checks to make sure
     * the provided class is default constructible (ie: has a constructor with
     * no parameters). If the plugin type name was previously associated with a
     * different class, it will be overriden. This allows the behavior core
     * classes to be modified at runtime.
     *
     * @param name plugin type name
     * @param pluginClass class object for the plugin class
     * @return <code>true</code> if the plugin registered successfully,
     * <code>false</code> otherwise
     */
    public boolean registerPlugin(String name, Class<?> pluginClass) {
        // check that the given class is compatible with the base class
        try {
            if (pluginClass.getConstructor() == null) {
                UI.printError(Module.API, "Plugin \"%s\" could not be declared - default constructor was not found", name);
                return false;
            }
        } catch (SecurityException e) {
            UI.printError(Module.API, "Plugin \"%s\" could not be declared - default constructor is not visible (%s)", name, e.getLocalizedMessage());
            return false;
        } catch (NoSuchMethodException e) {
            UI.printError(Module.API, "Plugin \"%s\" could not be declared - default constructor was not found (%s)", name, e.getLocalizedMessage());
            return false;
        }
        if (pluginClasses.get(name) != null) {
            UI.printWarning(Module.API, "Plugin \"%s\" was already defined - overwriting previous definition", name);
        }
        pluginClasses.put(name, (Class<? extends T>) pluginClass);
        return true;
    }
}