package com.mobify.astro;

import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.NonNull;

import com.mobify.astro.messaging.EventRegistrar;
import com.mobify.astro.messaging.MessageSender;
import com.mobify.astro.messaging.RpcMessageListener;
import com.mobify.astro.messaging.annotations.RpcMethod;

import org.json.JSONObject;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class PluginManager
        extends RpcMessageListener
        implements PluginResolver {

    private static final String TAG = PluginManager.class.getName();
    private static final String ADDRESS = "PluginManager:0";

    private static class PluginInstanceLookupException extends RuntimeException {
        private PluginInstanceLookupException(String pluginAddress) {
            super("Could not find plugin instance with address " + pluginAddress);
        }
    }

    private static class PluginConstructorNotFoundException extends Exception {
        private PluginConstructorNotFoundException(Class pluginClass) {
            super("Required constructor (Activity, PluginResolver, EventRegistrar, MessageSender) not found on plugin type: " + pluginClass.getName());
        }
    }

    public void onPause() {
        HashMap<String, AstroPlugin> plugins = getPluginInstances();
        for(AstroPlugin plugin: plugins.values()) {
            plugin.onPause();
        }
    }

    public void onSaveInstanceState(Bundle outBundle) {
        HashMap<String, AstroPlugin> plugins = getPluginInstances();

        for(AstroPlugin plugin: plugins.values()) {
            String instanceName = plugin.getInstanceName();
            if (instanceName != null) {
                outBundle.putBundle(instanceName, plugin.getSavedPluginState());
            }
        }
    }

    public void onDestroy() {
        HashMap<String, AstroPlugin> plugins = this.pluginInstances;
        this.pluginInstances = new HashMap<>();

        Iterator<Map.Entry<String, AstroPlugin>> it = plugins.entrySet().iterator();
        while (it.hasNext())
        {
            Map.Entry<String, AstroPlugin> item = it.next();
            item.getValue().destroy();
        }
    }

    private Activity activity;
    private HashMap<String, Class<? extends AstroPlugin>> pluginClasses = new HashMap<>();

    private HashMap<String, AstroPlugin> pluginInstances = new HashMap<>();

    public PluginManager(@NonNull Activity activity, @NonNull EventRegistrar eventRegistrar,
                         @NonNull MessageSender messageSender) {
        super(eventRegistrar, messageSender);
        this.activity = activity;
    }

    /**
     * Fetch the instance address for the plugin manager
     *
     * @return String representation of the plugin manager instance address
     */
    @Override
    public String getInstanceAddress() {
        return ADDRESS;
    }

    /**
     * Creates a plugin of the given name if it is in the list of registered plugins.
     *
     * @param pluginName the class name of the plugin to create
     * @return Instance address of the plugin as a string
     * @throws Exception
     */
    @RpcMethod(methodName = "createPlugin", parameterNames = {"pluginName", "options"})
    public String createPlugin(String pluginName, JSONObject options) throws Exception {
        AstroPlugin plugin = createPluginInstanceFromName(pluginName, options);
        addPluginToInstanceList(plugin);
        return plugin.getInstanceAddress();
    }

    /**
     * Registers an AstroPlugin class so that it can be created by the PluginManager.
     *
     * @param astroPlugin the Class to register with the PluginManager
     */
    public void register(Class<? extends AstroPlugin> astroPlugin) {
        pluginClasses.put(astroPlugin.getSimpleName(), astroPlugin);
    }

    /**
     * Creates an instance of an AstroPlugin with the given name that is registered
     * with the PluginManager
     *
     * @param name Name of the plugin to create
     * @param options Plugin options
     * @return A new AstroPlugin instance created from the provided name
     * @throws Exception
     */
    protected AstroPlugin createPluginInstanceFromName(String name, JSONObject options) throws Exception {
        Class<? extends AstroPlugin> PluginClass = pluginClasses.get(name);

        // Create the instance.
        Constructor<?>[] constructors = PluginClass.getDeclaredConstructors();

        Constructor constructorCandidate = null;
        boolean constructorHasOptions = false;

        // Looping through constructors to find a candidate. The plugin must implement one of the following
        // constructor signatures (with preference given to the first):
        // AstroPlugin(AstroActivity, PluginResolver, EventRegistrar, MessageSender, JSONObject)
        // AstroPlugin(AstroActivity, PluginResolver, EventRegistrar, MessageSender)
        for (Constructor c : constructors) {
            Class<?>[] parameters = c.getParameterTypes();
            if (parameters[0] == AstroActivity.class && parameters[1] == PluginResolver.class && parameters[2] == EventRegistrar.class && parameters[3] == MessageSender.class) {
                if (parameters.length == 5 && parameters[4] == JSONObject.class) {
                    constructorCandidate = c;
                    constructorHasOptions = true;
                    break;
                } else if (parameters.length == 4) {
                    constructorCandidate = c;
                    constructorHasOptions = false;
                }
            }
        }

        if(constructorCandidate == null) {
            throw new PluginConstructorNotFoundException(PluginClass);
        }

        AstroPlugin astroPlugin = null;

        try {
            if (constructorHasOptions) {
                astroPlugin = (AstroPlugin)constructorCandidate.newInstance(activity, this, eventRegistrar, messageSender, options);
            } else {
                astroPlugin = (AstroPlugin)constructorCandidate.newInstance(activity, this, eventRegistrar, messageSender);
            }
        } catch (InvocationTargetException invocationTargetException) {
            Throwable invocationTargetExceptionCause = invocationTargetException.getCause();
            if (invocationTargetExceptionCause instanceof Exception) {
                throw (Exception) invocationTargetExceptionCause;
            }
            throw invocationTargetException;
        }

        return astroPlugin;
    }

    /**
     * Add an AstroPlugin instance to the PluginManager's list of known instances.
     * @param plugin The plugin to add
     */
    public void addPluginToInstanceList(AstroPlugin plugin) {
        pluginInstances.put(plugin.getInstanceAddress(), plugin);
    }

    /**
     * Removes a plugin from the PluginManager's list of known instances.
     * @param plugin The plugin to remove
     */
    public void removePluginFromList(AstroPlugin plugin) {
        String pluginAddress = plugin.getInstanceAddress();
        if (pluginInstances.get(pluginAddress) != null) {
            pluginInstances.remove(pluginAddress);
        }
    }

    /**
     * Gets the AstroPlugin instance for a given `address`.
     *
     * @param address The address of the plugin instance we want
     */
    public AstroPlugin instanceForAddress(String address) {
        AstroPlugin plugin =  pluginInstances.get(address);
        if (plugin != null) {
            return plugin;
        }
        throw new PluginInstanceLookupException(address);
    }

    protected Activity getActivity() {
        return activity;
    }

    protected HashMap<String, Class<? extends AstroPlugin>> getPluginClasses() {
        return pluginClasses;
    }

    protected HashMap<String, AstroPlugin> getPluginInstances() {
        return pluginInstances;
    }
}
