/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy 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.
 * 
 * Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.plugin;

import icy.main.Icy;
import icy.network.NetworkUtil;
import icy.network.URLUtil;
import icy.plugin.PluginDescriptor.PluginIdent;
import icy.plugin.PluginDescriptor.PluginNameSorter;
import icy.plugin.PluginDescriptor.PluginOnlineIdent;
import icy.preferences.PluginPreferences;
import icy.preferences.RepositoryPreferences;
import icy.preferences.RepositoryPreferences.RepositoryInfo;
import icy.system.IcyExceptionHandler;
import icy.system.thread.SingleProcessor;
import icy.system.thread.ThreadUtil;
import icy.util.XMLUtil;

import java.util.ArrayList;
import java.util.Collections;
import java.util.EventListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.event.EventListenerList;

import org.w3c.dom.Document;
import org.w3c.dom.Node;

/**
 * @author stephane
 */
public class PluginRepositoryLoader
{
    public static interface PluginRepositoryLoaderListener extends EventListener
    {
        public void pluginRepositeryLoaderChanged(PluginDescriptor plugin);
    }

    private class Loader implements Runnable
    {
        public Loader()
        {
            super();
        }

        @Override
        public void run()
        {
            final List<PluginDescriptor> newPlugins = new ArrayList<PluginDescriptor>();

            try
            {
                final List<RepositoryInfo> repositories = RepositoryPreferences.getRepositeries();

                // load online plugins from all active repositories
                for (RepositoryInfo repoInfo : repositories)
                {
                    // reload requested --> stop current loading
                    if (processor.hasWaitingTasks())
                        return;

                    if (repoInfo.isEnabled())
                    {
                        final List<PluginDescriptor> pluginsRepos = loadInternal(repoInfo);

                        if (pluginsRepos == null)
                        {
                            failed = true;
                            return;
                        }

                        newPlugins.addAll(pluginsRepos);
                    }
                }

                // sort list on plugin class name
                Collections.sort(newPlugins, PluginNameSorter.instance);

                plugins = newPlugins;
            }
            catch (Exception e)
            {
                IcyExceptionHandler.showErrorMessage(e, true);
                failed = true;
                return;
            }

            // notify basic data has been loaded
            loaded = true;
            changed(null);
        }
    }

    private static final String ID_ROOT = "plugins";
    private static final String ID_PLUGIN = "plugin";
    // private static final String ID_PATH = "path";

    /**
     * static class
     */
    private static final PluginRepositoryLoader instance = new PluginRepositoryLoader();

    /**
     * Online plugin list
     */
    List<PluginDescriptor> plugins;

    /**
     * listeners
     */
    private final EventListenerList listeners;

    /**
     * internals
     */
    boolean loaded;
    boolean failed;

    private final Loader loader;
    final SingleProcessor processor;

    /**
     * static class
     */
    private PluginRepositoryLoader()
    {
        super();

        plugins = new ArrayList<PluginDescriptor>();
        listeners = new EventListenerList();

        loader = new Loader();
        processor = new SingleProcessor(true, "Online Plugin Loader");

        loaded = false;
        // initial loading
        load();
    }

    /**
     * Return the plugins identifier list from a repository URL
     */
    public static List<PluginOnlineIdent> getPluginIdents(RepositoryInfo repos)
    {
        String address = repos.getLocation();
        final boolean networkAddr = URLUtil.isNetworkURL(address);
        final boolean betaAllowed = PluginPreferences.getAllowBeta();

        if (networkAddr && repos.getSupportParam())
        {
            // prepare parameters for plugin list request
            final Map<String, String> values = new HashMap<String, String>();

            // add kernel information parameter
            values.put(NetworkUtil.ID_KERNELVERSION, Icy.version.toString());
            // add beta allowed information parameter
            values.put(NetworkUtil.ID_BETAALLOWED, Boolean.toString(betaAllowed));
            // concat to address
            address += "?" + NetworkUtil.getContentString(values);
        }

        // load the XML file
        final Document document = XMLUtil.loadDocument(address, repos.getAuthenticationInfo(), false);

        // error
        if (document == null)
        {
            if (networkAddr && !NetworkUtil.hasInternetAccess())
                System.out.println("You are not connected to internet.");

            return null;
        }

        final List<PluginOnlineIdent> result = new ArrayList<PluginOnlineIdent>();
        // get plugins node
        final Node pluginsNode = XMLUtil.getElement(document.getDocumentElement(), ID_ROOT);

        // plugins node found
        if (pluginsNode != null)
        {
            // ident nodes
            final List<Node> nodes = XMLUtil.getChildren(pluginsNode, ID_PLUGIN);

            for (Node node : nodes)
            {
                final PluginOnlineIdent ident = new PluginOnlineIdent();

                ident.loadFromXML(node);

                // accept only if not empty
                if (!ident.isEmpty())
                {
                    // accept only if required kernel version is ok and beta accepted
                    if (ident.getRequiredKernelVersion().isLowerOrEqual(Icy.version)
                            && (betaAllowed || (!ident.getVersion().isBeta())))
                    {
                        // check if we have several version of the same plugin
                        final int ind = PluginIdent.getIndex(result, ident.getClassName());
                        // other version found ?
                        if (ind != -1)
                        {
                            // replace old version if needed
                            if (result.get(ind).isOlderOrEqual(ident))
                                result.set(ind, ident);
                        }
                        else
                            result.add(ident);
                    }
                }
            }
        }

        return result;
    }

    /**
     * Do loading process.
     */
    private void load()
    {
        loaded = false;
        failed = false;

        processor.submit(loader);
    }

    /**
     * Reload all plugins from all active repositories (old list is cleared).<br>
     * Asynchronous process, use {@link #waitLoaded()} method to wait for basic data to be loaded.
     */
    public static synchronized void reload()
    {
        instance.load();
    }

    /**
     * Load the list of online plugins located at specified repository
     */
    // public static void load(final RepositoryInfo repos, boolean asynch, final boolean
    // loadDescriptor,
    // final boolean loadImages)
    // {
    // instance.loadSingleRunner.setParameters(repos, loadDescriptor, loadImages);
    //
    // if (asynch)
    // ThreadUtil.bgRunSingle(instance.loadAllRunner);
    // else
    // instance.loadAllRunner.run();
    // }

    /**
     * Load and return the list of online plugins located at specified repository
     */
    static List<PluginDescriptor> loadInternal(RepositoryInfo repos)
    {
        // we start by loading only identifier part
        final List<PluginOnlineIdent> idents = getPluginIdents(repos);

        // error while retrieving identifiers ?
        if (idents == null)
        {
            System.out.println("Can't access repository '" + repos.getName() + "'");
            return null;
        }

        final List<PluginDescriptor> result = new ArrayList<PluginDescriptor>();

        for (PluginOnlineIdent ident : idents)
        {
            try
            {
                result.add(new PluginDescriptor(ident, repos));
            }
            catch (Exception e)
            {
                System.out.println("PluginRepositoryLoader.load('" + repos.getLocation() + "') error :");
                IcyExceptionHandler.showErrorMessage(e, false);
            }
        }

        return result;
    }

    /**
     * @return the pluginList
     */
    public static ArrayList<PluginDescriptor> getPlugins()
    {
        synchronized (instance.plugins)
        {
            return new ArrayList<PluginDescriptor>(instance.plugins);
        }
    }

    public static PluginDescriptor getPlugin(String className)
    {
        synchronized (instance.plugins)
        {
            return PluginDescriptor.getPlugin(instance.plugins, className);
        }
    }

    public static List<PluginDescriptor> getPlugins(String className)
    {
        synchronized (instance.plugins)
        {
            return PluginDescriptor.getPlugins(instance.plugins, className);
        }
    }

    /**
     * Return the plugins list from the specified repository
     */
    public static List<PluginDescriptor> getPlugins(RepositoryInfo repos)
    {
        final List<PluginDescriptor> result = new ArrayList<PluginDescriptor>();

        synchronized (instance.plugins)
        {
            for (PluginDescriptor plugin : instance.plugins)
                if (plugin.getRepository().equals(repos))
                    result.add(plugin);
        }

        return result;
    }

    /**
     * @return true if loader is loading the basic informations
     */
    public static boolean isLoading()
    {
        return instance.processor.isProcessing();
    }

    /**
     * @return true if basic informations (class names, versions...) are loaded.
     */
    public static boolean isLoaded()
    {
        return instance.failed || instance.loaded;
    }

    /**
     * Wait until basic informations are loaded.
     */
    public static void waitLoaded()
    {
        while (!isLoaded())
            ThreadUtil.sleep(10);
    }

    /**
     * @deprecated use {@link #isLoaded()} instead.
     */
    @Deprecated
    public static boolean isBasicLoaded()
    {
        return isLoaded();
    }

    /**
     * @deprecated descriptor loading is now done per descriptor when needed
     */
    @Deprecated
    public static boolean isDescriptorsLoaded()
    {
        return true;
    }

    /**
     * @deprecated image loading is now done per descriptor when needed
     */
    @Deprecated
    public static boolean isImagesLoaded()
    {
        return true;
    }

    /**
     * @deprecated use {@link #waitLoaded()} instead.
     */
    @Deprecated
    public static void waitBasicLoaded()
    {
        waitLoaded();
    }

    /**
     * @deprecated descriptor loading is now done per descriptor when needed
     */
    @Deprecated
    public static void waitDescriptorsLoaded()
    {
        // do nothing
    }

    /**
     * Returns true if an error occurred during the plugin loading process.
     */
    public static boolean failed()
    {
        return instance.failed;
    }

    /**
     * Plugin list has changed
     */
    void changed(PluginDescriptor plugin)
    {
        fireEvent(plugin);
    }

    /**
     * Add a listener
     * 
     * @param listener
     */
    public static void addListener(PluginRepositoryLoaderListener listener)
    {
        synchronized (instance.listeners)
        {
            instance.listeners.add(PluginRepositoryLoaderListener.class, listener);
        }
    }

    /**
     * Remove a listener
     * 
     * @param listener
     */
    public static void removeListener(PluginRepositoryLoaderListener listener)
    {
        synchronized (instance.listeners)
        {
            instance.listeners.remove(PluginRepositoryLoaderListener.class, listener);
        }
    }

    /**
     * fire event
     * 
     * @param plugin
     */
    private void fireEvent(PluginDescriptor plugin)
    {
        for (PluginRepositoryLoaderListener listener : listeners.getListeners(PluginRepositoryLoaderListener.class))
            listener.pluginRepositeryLoaderChanged(plugin);
    }
}