/*
 * 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.file.FileUtil;
import icy.gui.dialog.ConfirmDialog;
import icy.gui.frame.progress.CancelableProgressFrame;
import icy.gui.frame.progress.DownloadFrame;
import icy.gui.frame.progress.FailedAnnounceFrame;
import icy.gui.frame.progress.SuccessfullAnnounceFrame;
import icy.main.Icy;
import icy.network.NetworkUtil;
import icy.network.URLUtil;
import icy.plugin.PluginDescriptor.PluginIdent;
import icy.preferences.RepositoryPreferences.RepositoryInfo;
import icy.system.IcyExceptionHandler;
import icy.system.thread.ThreadUtil;
import icy.update.Updater;
import icy.util.StringUtil;
import icy.util.XMLUtil;
import icy.util.ZipUtil;

import java.net.URL;
import java.util.ArrayList;
import java.util.EventListener;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import javax.swing.event.EventListenerList;

/**
 * @author Stephane
 */
public class PluginInstaller implements Runnable
{
    public static interface PluginInstallerListener extends EventListener
    {
        public void pluginInstalled(PluginDescriptor plugin, boolean success);

        public void pluginRemoved(PluginDescriptor plugin, boolean success);
    }

    private static class PluginInstallInfo
    {
        // final PluginRepositoryLoader loader;
        final PluginDescriptor plugin;
        final boolean showProgress;

        public PluginInstallInfo(PluginDescriptor plugin, boolean showProgress)
        {
            super();

            this.plugin = plugin;
            this.showProgress = showProgress;
        }

        @Override
        public boolean equals(Object obj)
        {
            if (obj instanceof PluginInstallInfo)
                return ((PluginInstallInfo) obj).plugin.equals(plugin);

            return super.equals(obj);
        }

        @Override
        public int hashCode()
        {
            return plugin.hashCode();
        }
    }

    private static final String ERROR_DOWNLOAD = "Error while downloading ";
    private static final String ERROR_SAVE = "Error while saving";
    // private static final String INSTALL_CANCELED = "Plugin installation canceled by user.";

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

    /**
     * plugin(s) to install FIFO
     */
    private final List<PluginInstallInfo> installFIFO;
    /**
     * plugin(s) to delete FIFO
     */
    private final List<PluginInstallInfo> removeFIFO;

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

    /**
     * internals
     */
    private final List<PluginDescriptor> installingPlugins;
    private final List<PluginDescriptor> desinstallingPlugin;

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

        installFIFO = new ArrayList<PluginInstallInfo>();
        removeFIFO = new ArrayList<PluginInstallInfo>();

        listeners = new EventListenerList();

        installingPlugins = new ArrayList<PluginDescriptor>();
        desinstallingPlugin = new ArrayList<PluginDescriptor>();

        // launch installer thread
        new Thread(this, "Plugin installer").start();
    }

    /**
     * Return true if install or desinstall is possible
     */
    private static boolean isEnabled()
    {
        return !PluginLoader.isJCLDisabled();
    }

    /**
     * Install a plugin (asynchronous)
     * 
     * @param plugin
     *        the plugin to install
     * @param showProgress
     *        show a progress frame during process
     */
    public static void install(PluginDescriptor plugin, boolean showProgress)
    {
        if ((plugin != null) && isEnabled())
        {
            if (!NetworkUtil.hasInternetAccess())
            {
                final String text = "Cannot install '" + plugin.getName()
                        + "' plugin : you are not connected to Internet.";

                if (Icy.getMainInterface().isHeadLess())
                    System.err.println(text);
                else
                    new FailedAnnounceFrame(text, 10);

                return;
            }

            synchronized (instance.installFIFO)
            {
                instance.installFIFO.add(new PluginInstallInfo(plugin, showProgress));
            }
        }
    }

    /**
     * return true if PluginInstaller is processing
     */
    public static boolean isProcessing()
    {
        return isInstalling() || isDesinstalling();
    }

    /**
     * return a copy of the install FIFO
     */
    public static ArrayList<PluginInstallInfo> getInstallFIFO()
    {
        synchronized (instance.installFIFO)
        {
            return new ArrayList<PluginInstaller.PluginInstallInfo>(instance.installFIFO);
        }
    }

    /**
     * Wait while installer is installing plugin.
     */
    public static void waitInstall()
    {
        while (isInstalling())
            ThreadUtil.sleep(100);
    }

    /**
     * return true if PluginInstaller is installing plugin(s)
     */
    public static boolean isInstalling()
    {
        return !instance.installFIFO.isEmpty() || !instance.installingPlugins.isEmpty();
    }

    /**
     * return true if 'plugin' is in the install FIFO
     */
    public static boolean isWaitingForInstall(PluginDescriptor plugin)
    {
        synchronized (instance.installFIFO)
        {
            for (PluginInstallInfo info : instance.installFIFO)
                if (plugin == info.plugin)
                    return true;
        }

        return false;
    }

    /**
     * return true if specified plugin is currently being installed or will be installed
     */
    public static boolean isInstallingPlugin(PluginDescriptor plugin)
    {
        return (instance.installingPlugins.indexOf(plugin) != -1) || isWaitingForInstall(plugin);
    }

    /**
     * Uninstall a plugin (asynchronous)
     * 
     * @param plugin
     *        the plugin to uninstall
     * @param showConfirm
     *        show a confirmation dialog
     * @param showProgress
     *        show a progress frame during process
     */
    public static void desinstall(PluginDescriptor plugin, boolean showConfirm, boolean showProgress)
    {
        if ((plugin != null) && isEnabled())
        {
            if (showConfirm)
            {
                // get local plugins which depend from the plugin we want to delete
                final List<PluginDescriptor> dependants = getLocalDependenciesFrom(plugin);

                String message = "<html>";

                if (!dependants.isEmpty())
                {
                    message = message + "The following plugin(s) won't work anymore :<br>";

                    for (PluginDescriptor depPlug : dependants)
                        message = message + depPlug.getName() + " " + depPlug.getVersion() + "<br>";

                    message = message + "<br>";
                }

                message = message + "Are you sure you want to remove '" + plugin.getName() + " " + plugin.getVersion()
                        + "' ?</html>";

                if (ConfirmDialog.confirm(message))
                {
                    synchronized (instance.removeFIFO)
                    {
                        instance.removeFIFO.add(new PluginInstallInfo(plugin, showConfirm));
                    }
                }
            }
            else
            {
                synchronized (instance.removeFIFO)
                {
                    instance.removeFIFO.add(new PluginInstallInfo(plugin, showProgress));
                }
            }
        }
    }

    /**
     * @deprecated Use {@link #desinstall(PluginDescriptor, boolean, boolean)} instead.
     */
    @Deprecated
    public static void desinstall(PluginDescriptor plugin, boolean showConfirm)
    {
        desinstall(plugin, showConfirm, showConfirm);
    }

    /**
     * return a copy of the remove FIFO
     */
    public static ArrayList<PluginInstallInfo> getRemoveFIFO()
    {
        synchronized (instance.removeFIFO)
        {
            return new ArrayList<PluginInstaller.PluginInstallInfo>(instance.removeFIFO);
        }
    }

    /**
     * Wait while installer is removing plugin.
     */
    public static void waitDesinstall()
    {
        while (isDesinstalling())
            ThreadUtil.sleep(100);
    }

    /**
     * return true if PluginInstaller is desinstalling plugin(s)
     */
    public static boolean isDesinstalling()
    {
        return !instance.removeFIFO.isEmpty() || !instance.desinstallingPlugin.isEmpty();
    }

    /**
     * return true if 'plugin' is in the remove FIFO
     */
    public static boolean isWaitingForDesinstall(PluginDescriptor plugin)
    {
        synchronized (instance.removeFIFO)
        {
            for (PluginInstallInfo info : instance.removeFIFO)
                if (plugin == info.plugin)
                    return true;
        }

        return false;
    }

    /**
     * return true if specified plugin is currently being desinstalled or will be desinstalled
     */
    public static boolean isDesinstallingPlugin(PluginDescriptor plugin)
    {
        return (instance.desinstallingPlugin.indexOf(plugin) != -1) || isWaitingForDesinstall(plugin);
    }

    @Override
    public void run()
    {
        while (!Thread.interrupted())
        {
            // process installations
            if (!installFIFO.isEmpty())
            {
                // so list has sometime to fill-up
                ThreadUtil.sleep(200);

                do
                    installInternal();
                while (!installFIFO.isEmpty());
            }

            // process deletions
            while (!removeFIFO.isEmpty())
            {
                // so list has sometime to fill-up
                ThreadUtil.sleep(200);

                do
                    desinstallInternal();
                while (!removeFIFO.isEmpty());
            }

            ThreadUtil.sleep(100);
        }
    }

    /**
     * Backup specified plugin if it already exists.<br>
     * Return an empty string if no error else return error message
     */
    private static String backup(PluginDescriptor plugin)
    {
        boolean ok;

        // backup JAR, XML and image files
        ok = Updater.backup(plugin.getJarFilename()) && Updater.backup(plugin.getXMLFilename())
                && Updater.backup(plugin.getIconFilename()) && Updater.backup(plugin.getImageFilename());

        if (!ok)
            return "Can't backup plugin '" + plugin.getName() + "'";

        return "";
    }

    /**
     * Return an empty string if no error else return error message
     */
    private static String downloadAndSavePlugin(PluginDescriptor plugin, DownloadFrame taskFrame)
    {
        String result;

        if (taskFrame != null)
            taskFrame.setMessage("Downloading " + plugin);

        // ensure descriptor is loaded
        plugin.loadDescriptor();

        final RepositoryInfo repos = plugin.getRepository();
        final String login;
        final String pass;

        // use authentication (repos should not be null at this point)
        if (repos.isAuthenticationEnabled())
        {
            login = repos.getLogin();
            pass = repos.getPassword();
        }
        else
        {
            login = null;
            pass = null;
        }

        // try to build the final path using base repository address and plugin relative address
        // (useful for local repository)
        URL url;
        final String basePath = FileUtil.getDirectory(repos.getLocation());
        // download and save JAR file
        url = URLUtil.buildURL(basePath, plugin.getJarUrl());
        result = downloadAndSave(url, plugin.getJarFilename(), login, pass, true, taskFrame);
        if (!StringUtil.isEmpty(result))
            return result;

        // verify JAR file is not corrupted
        if (!ZipUtil.isValid(plugin.getJarFilename(), false))
            return "Downloaded JAR file '" + plugin.getJarFilename() + "' is corrupted !";

        // download and save XML file
        url = URLUtil.buildURL(basePath, plugin.getUrl());
        result = downloadAndSave(url, plugin.getXMLFilename(), login, pass, true, taskFrame);
        if (!StringUtil.isEmpty(result))
            return result;

        // verify XML file is not corrupted
        if (XMLUtil.loadDocument(plugin.getXMLFilename()) == null)
            return "Downloaded XML file '" + plugin.getXMLFilename() + "' is corrupted !";

        // download and save icon & image files
        if (!StringUtil.isEmpty(plugin.getIconUrl()))
        {
            url = URLUtil.buildURL(basePath, plugin.getIconUrl());
            downloadAndSave(url, plugin.getIconFilename(), login, pass, false, taskFrame);
        }
        if (!StringUtil.isEmpty(plugin.getImageUrl()))
        {
            url = URLUtil.buildURL(basePath, plugin.getImageUrl());
            downloadAndSave(url, plugin.getImageFilename(), login, pass, false, taskFrame);
        }

        return "";
    }

    /**
     * Return an empty string if no error else return error message
     */
    private static String downloadAndSave(URL downloadPath, String savePath, String login, String pass,
            boolean displayError, DownloadFrame downloadFrame)
    {
        if (downloadFrame != null)
            downloadFrame.setPath(FileUtil.getFileName(savePath));

        // load data
        final byte[] data = NetworkUtil.download(downloadPath, login, pass, downloadFrame, displayError);
        if (data == null)
            return ERROR_DOWNLOAD + downloadPath.toString();

        // save data
        if (!FileUtil.save(savePath, data, displayError))
        {
            System.err.println("Can't write '" + savePath + "' !");
            System.err.println("File may be locked or you don't own the rights to write files here.");
            return ERROR_SAVE + savePath;
        }

        return null;
    }

    private static boolean deletePlugin(PluginDescriptor plugin)
    {
        if (!FileUtil.delete(plugin.getJarFilename(), false))
        {
            System.err.println("Can't delete '" + plugin.getJarFilename() + "' file !");
            // fatal error
            return false;
        }

        if (FileUtil.exists(plugin.getXMLFilename()))
            if (!FileUtil.delete(plugin.getXMLFilename(), false))
                System.err.println("Can't delete '" + plugin.getXMLFilename() + "' file !");

        FileUtil.delete(plugin.getImageFilename(), false);
        FileUtil.delete(plugin.getIconFilename(), false);

        return true;
    }

    /**
     * Fill list with local dependencies (plugins) of specified plugin
     */
    public static void getLocalDependenciesOf(List<PluginDescriptor> result, PluginDescriptor plugin)
    {
        // load plugin descriptor informations if not yet done
        plugin.loadDescriptor();

        for (PluginIdent ident : plugin.getRequired())
        {
            // already in our dependences ? --> pass to the next one
            if (PluginDescriptor.getPlugin(result, ident, true) != null)
                continue;

            // find local dependent plugin
            final PluginDescriptor dep = PluginLoader.getPlugin(ident, true);

            // dependence found ?
            if (dep != null)
            {
                // and add it to list
                PluginDescriptor.addToList(result, dep);
                // search its dependencies too
                getLocalDependenciesOf(result, dep);
            }
        }
    }

    /**
     * Return local plugins list which depend from the specified list of plugins.
     */
    public static List<PluginDescriptor> getLocalDependenciesFrom(List<PluginDescriptor> plugins)
    {
        final List<PluginDescriptor> result = new ArrayList<PluginDescriptor>();

        for (PluginDescriptor plugin : plugins)
            getLocalDependenciesFrom(plugin, result);

        return result;
    }

    /**
     * Return local plugins list which depend from the specified plugin.
     */
    public static List<PluginDescriptor> getLocalDependenciesFrom(PluginDescriptor plugin)
    {
        final List<PluginDescriptor> result = new ArrayList<PluginDescriptor>();

        getLocalDependenciesFrom(plugin, result);

        return result;
    }

    /**
     * Return local plugins list which depend from the specified plugin.
     */
    private static void getLocalDependenciesFrom(PluginDescriptor plugin, List<PluginDescriptor> result)
    {
        for (PluginDescriptor curPlug : PluginLoader.getPlugins(false))
            // require specified plugin ?
            if (curPlug.requires(plugin))
                PluginDescriptor.addToList(result, curPlug);
    }

    /**
     * Fill list with 'sources' dependencies of specified plugin
     */
    private static void getLocalDependenciesOf(List<PluginDescriptor> result, List<PluginDescriptor> sources,
            PluginDescriptor plugin)
    {
        // load plugin descriptor informations if not yet done
        plugin.loadDescriptor();

        for (PluginIdent ident : plugin.getRequired())
        {
            // already in our dependences ? --> pass to the next one
            if ((ident == null) || (PluginDescriptor.getPlugin(result, ident, true) != null))
                continue;

            // find sources dependent plugin
            final PluginDescriptor dep = PluginDescriptor.getPlugin(sources, ident, true);

            // dependence found ?
            if (dep != null)
            {
                // and add it to list
                PluginDescriptor.addToList(result, dep);
                // search its dependencies too
                getLocalDependenciesOf(result, sources, dep);
            }
        }
    }

    /**
     * Reorder the list so needed dependencies comes first in list
     */
    public static List<PluginDescriptor> orderDependencies(List<PluginDescriptor> plugins)
    {
        final List<PluginDescriptor> sources = new ArrayList<PluginDescriptor>(plugins);
        final List<PluginDescriptor> result = new ArrayList<PluginDescriptor>();

        while (sources.size() > 0)
        {
            final List<PluginDescriptor> deps = new ArrayList<PluginDescriptor>();

            getLocalDependenciesOf(result, sources, sources.get(0));

            // add last to first dep
            for (int i = deps.size() - 1; i >= 0; i--)
                PluginDescriptor.addToList(result, deps.get(i));

            // then add plugin
            PluginDescriptor.addToList(result, sources.get(0));

            // remove tested plugin and its dependencies from source
            sources.removeAll(result);
        }

        return result;
    }

    /**
     * Resolve dependencies for specified plugin
     * 
     * @param taskFrame
     */
    public static boolean getDependencies(PluginDescriptor plugin, List<PluginDescriptor> pluginsToInstall,
            CancelableProgressFrame taskFrame, boolean showError)
    {
        // load plugin descriptor informations if not yet done
        plugin.loadDescriptor();

        // check dependencies
        for (PluginIdent ident : plugin.getRequired())
        {
            if ((taskFrame != null) && taskFrame.isCancelRequested())
                return false;

            // should not happen but...
            if (ident == null)
                continue;

            // already in our dependencies ? --> pass to the next one
            if (PluginDescriptor.getPlugin(pluginsToInstall, ident, true) != null)
                continue;

            final String className = ident.getClassName();

            // get local & online plugin
            final PluginDescriptor localPlugin = PluginLoader.getPlugin(className);
            final PluginDescriptor onlinePlugin = PluginRepositoryLoader.getPlugin(className);

            // plugin not yet installed or outdated ?
            if ((localPlugin == null) || ident.getVersion().isGreater(localPlugin.getVersion()))
            {
                // online plugin not found ?
                if (onlinePlugin == null)
                {
                    // error
                    if (showError)
                    {
                        System.err.println("Can't resolve dependencies for plugin '" + plugin.getName() + "' :");

                        if (localPlugin == null)
                            System.err.println("Plugin class '" + ident.getClassName() + " not found !");
                        else
                        {
                            System.err.println(localPlugin.getName() + " " + localPlugin.getVersion() + " installed");
                            System.err.println("but version " + ident.getVersion() + " or greater needed.");
                        }
                    }

                    return false;
                }
                // online plugin version incorrect
                else if (ident.getVersion().isGreater(onlinePlugin.getVersion()))
                {
                    // error
                    if (showError)
                    {
                        System.err.println("Can't resolve dependencies for plugin '" + plugin.getName() + "' :");
                        System.err.println(onlinePlugin.getName() + " " + onlinePlugin.getVersion()
                                + " found in repository");
                        System.err.println("but version " + ident.getVersion() + " or greater needed.");
                    }

                    return false;
                }

                // add to the install list
                PluginDescriptor.addToList(pluginsToInstall, onlinePlugin);
                // and check dependencies for this plugin
                if (!getDependencies(onlinePlugin, pluginsToInstall, taskFrame, showError))
                    return false;
            }
            else
            {
                // just check if we have update for dependency
                if ((onlinePlugin != null) && (localPlugin.getVersion().isLower(onlinePlugin.getVersion())))
                {
                    // as web site doesn't handle version dependency, we force the update

                    // add to the install list
                    PluginDescriptor.addToList(pluginsToInstall, onlinePlugin);
                    // and check dependencies for this plugin
                    if (!getDependencies(onlinePlugin, pluginsToInstall, taskFrame, showError))
                        return false;
                }
            }
        }

        return true;
    }

    private void installInternal()
    {
        DownloadFrame taskFrame = null;

        try
        {
            final List<PluginInstallInfo> infos;
            boolean showProgress;

            synchronized (installFIFO)
            {
                infos = new ArrayList<PluginInstaller.PluginInstallInfo>(installFIFO);

                showProgress = false;
                for (int i = infos.size() - 1; i >= 0; i--)
                {
                    final PluginInstallInfo info = infos.get(i);

                    PluginDescriptor.addToList(installingPlugins, info.plugin);
                    showProgress |= info.showProgress;
                }

                installFIFO.clear();
            }

            if (showProgress && !Icy.getMainInterface().isHeadLess())
            {
                taskFrame = new DownloadFrame();
                taskFrame.setMessage("Initializing...");
            }

            List<PluginDescriptor> dependencies = new ArrayList<PluginDescriptor>();
            final Set<PluginDescriptor> pluginsOk = new HashSet<PluginDescriptor>();
            final Set<PluginDescriptor> pluginsNOk = new HashSet<PluginDescriptor>();

            // get dependencies
            for (int i = installingPlugins.size() - 1; i >= 0; i--)
            {
                final PluginDescriptor plugin = installingPlugins.get(i);
                final String plugDesc = plugin.getName() + " " + plugin.getVersion();

                if (taskFrame != null)
                {
                    // cancel requested ?
                    if (taskFrame.isCancelRequested())
                        return;

                    taskFrame.setMessage("Checking dependencies for '" + plugDesc + "' ...");
                }

                // check dependencies
                if (!getDependencies(plugin, dependencies, taskFrame, true))
                {
                    // can't resolve dependencies for this plugin
                    pluginsNOk.add(plugin);
                    installingPlugins.remove(i);
                }
            }

            // nothing to install
            if (installingPlugins.isEmpty())
                return;

            // order dependencies
            dependencies = orderDependencies(dependencies);
            // add dependencies at the beginning of the installing list
            for (PluginDescriptor plugin : dependencies)
                PluginDescriptor.addToList(installingPlugins, plugin, 0);

            String error = "";

            // clear backup folder
            FileUtil.delete(Updater.BACKUP_DIRECTORY, true);

            // now we can proceed the installation itself
            for (PluginDescriptor plugin : installingPlugins)
            {
                for (PluginIdent ident : plugin.getRequired())
                {
                    // one of the dependencies was not correctly installed ?
                    if (PluginDescriptor.existInList(pluginsNOk, ident))
                    {
                        // we can't install the plugin, continue with the next one
                        pluginsNOk.add(plugin);
                        continue;
                    }
                }

                final String plugDesc = plugin.getName() + " " + plugin.getVersion();

                if (taskFrame != null)
                {
                    // cancel requested ? --> interrupt installation
                    if (taskFrame.isCancelRequested())
                        break;

                    taskFrame.setMessage("Installing " + plugDesc + "...");
                }

                try
                {
                    // backup plugin
                    error = backup(plugin);

                    // backup ok --> install plugin
                    if (StringUtil.isEmpty(error))
                    {
                        error = downloadAndSavePlugin(plugin, taskFrame);

                        // an error occurred ? --> restore
                        if (!StringUtil.isEmpty(error))
                            Updater.restore();
                    }
                }
                finally
                {
                    // delete backup
                    FileUtil.delete(Updater.BACKUP_DIRECTORY, true);
                }

                if (StringUtil.isEmpty(error))
                    pluginsOk.add(plugin);
                else
                {
                    pluginsNOk.add(plugin);
                    // print error
                    System.err.println(error);
                }
            }

            // verify installed plugins
            if (taskFrame != null)
                taskFrame.setMessage("Verifying plugins...");

            // reload plugin list
            PluginLoader.reload();

            for (PluginDescriptor plugin : pluginsOk)
            {
                error = PluginLoader.verifyPlugin(plugin);

                // send report when we have verification error
                if (!StringUtil.isEmpty(error))
                {
                    IcyExceptionHandler.report(plugin, "An error occured while installing the plugin :\n" + error);
                    // print error
                    System.err.println(error);

                    pluginsNOk.add(plugin);
                }
            }

            // remove all plugins which failed from OK list
            pluginsOk.removeAll(pluginsNOk);

            if (!pluginsNOk.isEmpty())
            {
                System.err.println();
                System.err.println("Installation of the following plugin(s) failed:");
                for (PluginDescriptor plugin : pluginsNOk)
                {
                    System.err.println(plugin.getName() + " " + plugin.getVersion());
                    // notify about installation fails
                    fireInstalledEvent(plugin, false);
                }
                System.err.println();
            }

            if (!pluginsOk.isEmpty())
            {
                System.out.println();
                System.out.println("The following plugin(s) has been correctly installed:");
                for (PluginDescriptor plugin : pluginsOk)
                {
                    System.out.println(plugin.getName() + " " + plugin.getVersion());
                    // notify about installation successes
                    fireInstalledEvent(plugin, true);
                }
                System.out.println();
            }

            if (showProgress && !Icy.getMainInterface().isHeadLess())
            {
                if (pluginsNOk.isEmpty())
                    new SuccessfullAnnounceFrame("Plugin(s) installation was successful !");
                else if (pluginsOk.isEmpty())
                    new FailedAnnounceFrame("Plugin(s) installation failed !");
                else
                    new FailedAnnounceFrame(
                            "Some plugin(s) installation failed (looks at the output console for detail) !");
            }
        }
        finally
        {
            // installation end
            installingPlugins.clear();
            if (taskFrame != null)
                taskFrame.close();
        }
    }

    private void desinstallInternal()
    {
        CancelableProgressFrame taskFrame = null;

        try
        {
            final List<PluginInstallInfo> infos;
            boolean showProgress;

            synchronized (removeFIFO)
            {
                infos = new ArrayList<PluginInstaller.PluginInstallInfo>(removeFIFO);

                // determine if we should display the progress bar
                showProgress = false;
                for (int i = infos.size() - 1; i >= 0; i--)
                {
                    final PluginInstallInfo info = infos.get(i);

                    desinstallingPlugin.add(info.plugin);
                    showProgress |= info.showProgress;
                }

                removeFIFO.clear();
            }

            if (showProgress && !Icy.getMainInterface().isHeadLess())
                taskFrame = new CancelableProgressFrame("Initializing...");

            // now we can proceed remove
            for (PluginDescriptor plugin : desinstallingPlugin)
            {
                final String plugDesc = plugin.getName() + " " + plugin.getVersion();
                final boolean result;

                if (taskFrame != null)
                {
                    // cancel requested ?
                    if (taskFrame.isCancelRequested())
                        return;

                    taskFrame.setMessage("Removing plugin '" + plugDesc + "'...");
                }

                result = deletePlugin(plugin);

                // notify plugin deletion
                fireRemovedEvent(plugin, result);

                if (showProgress && !Icy.getMainInterface().isHeadLess())
                {
                    if (!result)
                        new FailedAnnounceFrame("Plugin '" + plugDesc + "' delete operation failed !");
                }

                if (result)
                    System.out.println("Plugin '" + plugDesc + "' correctly removed.");
                else
                    System.err.println("Plugin '" + plugDesc + "' delete operation failed !");
            }
        }
        finally
        {
            if (taskFrame != null)
                taskFrame.close();
            // removing end
            desinstallingPlugin.clear();
        }

        // reload plugin list
        PluginLoader.reload();
    }

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

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

    /**
     * fire plugin installed event
     */
    private void fireInstalledEvent(PluginDescriptor plugin, boolean success)
    {
        synchronized (listeners)
        {
            for (PluginInstallerListener listener : listeners.getListeners(PluginInstallerListener.class))
                listener.pluginInstalled(plugin, success);
        }
    }

    /**
     * fire plugin removed event
     */
    private void fireRemovedEvent(PluginDescriptor plugin, boolean success)
    {
        synchronized (listeners)
        {
            for (PluginInstallerListener listener : listeners.getListeners(PluginInstallerListener.class))
                listener.pluginRemoved(plugin, success);
        }
    }

}