//------------------------------------------------------------------------------------------------//
//                                                                                                //
//                                  P l u g i n s M a n a g e r                                   //
//                                                                                                //
//------------------------------------------------------------------------------------------------//
// <editor-fold defaultstate="collapsed" desc="hdr">
//
//  Copyright © Audiveris 2018. All rights reserved.
//
//  This program is free software: you can redistribute it and/or modify it under the terms of the
//  GNU Affero 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 Affero General Public License for more details.
//
//  You should have received a copy of the GNU Affero General Public License along with this
//  program.  If not, see <http://www.gnu.org/licenses/>.
//------------------------------------------------------------------------------------------------//
// </editor-fold>
package org.audiveris.omr.plugin;

import org.audiveris.omr.WellKnowns;
import org.audiveris.omr.constant.Constant;
import org.audiveris.omr.constant.ConstantSet;
import org.audiveris.omr.sheet.ui.StubsController;
import org.audiveris.omr.ui.util.AbstractMenuListener;
import org.audiveris.omr.ui.util.SeparableMenu;
import org.audiveris.omr.util.param.Param;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

import javax.swing.Action;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.event.MenuEvent;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElementRef;
import javax.xml.bind.annotation.XmlRootElement;

/**
 * Class {@code PluginsManager} handles the collection of registered plugins.
 * <p>
 * Each registered plugin is represented by a menu item.
 * A plugin can be manually selected as default and directly launched by a dedicated toolbar button.
 * <p>
 * The <code>config</code> folder is lookup for a potential plugins file.
 *
 * @author Hervé Bitteur
 */
public class PluginsManager
{

    private static final Constants constants = new Constants();

    private static final Logger logger = LoggerFactory.getLogger(PluginsManager.class);

    /** Persistent default plugin id. */
    public static final Param<String> defaultPluginId = new Default();

    /** File name for plugins definitions: {@value}. */
    private static final String PLUGINS_FILE_NAME = "plugins.xml";

    /** Un/marshalling context for use with JAXB. */
    private static volatile JAXBContext jaxbContext;

    /** The concrete UI menu. */
    private JMenu menu;

    /** The list of registered plugins. */
    private final List<Plugin> plugins;

    /** The current default plugin. */
    private Plugin defaultPlugin;

    /**
     * Generates the menu to be inserted in the plugin menu hierarchy,
     * based on the plugins file discovered in Audiveris user config folder.
     *
     * @param menu the hosting menu, or null
     */
    private PluginsManager ()
    {
        // Load all defined plugins
        plugins = loadPlugins();

        // Default plugin, if any is defined
        if (!constants.defaultPlugin.getValue().trim().isEmpty()) {
            setDefaultPlugin(constants.defaultPlugin.getValue().trim());
        }
    }

    //------------------//
    // setDefaultPlugin //
    //------------------//
    /**
     * Assign the default plugin via its id.
     *
     * @param pluginId id of new default plugin
     */
    public final void setDefaultPlugin (String pluginId)
    {
        Plugin plugin = findDefaultPlugin(pluginId);

        if (!pluginId.isEmpty() && (plugin == null)) {
            logger.warn("Could not find default plugin {}", pluginId);
        } else {
            setDefaultPlugin(plugin);
        }
    }

    //------------------//
    // getDefaultPlugin //
    //------------------//
    /**
     * Return the default plugin if any.
     *
     * @return the default plugin, or null if none is defined
     */
    public Plugin getDefaultPlugin ()
    {
        return defaultPlugin;
    }

    //------------------//
    // setDefaultPlugin //
    //------------------//
    /**
     * Assign the default plugin.
     *
     * @param defaultPlugin the new default plugin
     */
    public final void setDefaultPlugin (Plugin defaultPlugin)
    {
        this.defaultPlugin = defaultPlugin;
    }

    //---------//
    // getMenu //
    //---------//
    /**
     * Report the concrete UI menu of all plugins
     *
     * @param menu a preallocated menu instance, or null
     * @return the populated menu entity
     */
    public JMenu getMenu (JMenu menu)
    {
        if (menu == null) {
            menu = new SeparableMenu();
        }

        for (Plugin plugin : plugins) {
            menu.add(new JMenuItem(new PluginAction(plugin)));
        }

        // Listener to modify attributes on-the-fly
        menu.addMenuListener(new MyMenuListener());

        this.menu = menu;

        return menu;
    }

    //------------//
    // getPlugins //
    //------------//
    /**
     * Report the collection of plugins ids
     *
     * @return the various plugins ids
     */
    public Collection<String> getPluginIds ()
    {
        List<String> ids = new ArrayList<>();

        for (Plugin plugin : plugins) {
            ids.add(plugin.getId());
        }

        return ids;
    }

    //-------------------//
    // findDefaultPlugin //
    //-------------------//
    private Plugin findDefaultPlugin (String pluginId)
    {
        for (Plugin plugin : plugins) {
            if (plugin.getId().equalsIgnoreCase(pluginId)) {
                return plugin;
            }
        }

        return null;
    }

    //-------------//
    // loadPlugins //
    //-------------//
    private List<Plugin> loadPlugins ()
    {
        final Path folder = WellKnowns.CONFIG_FOLDER;
        final Path pluginsPath = folder.resolve(PLUGINS_FILE_NAME);

        if (Files.exists(pluginsPath)) {
            try {
                Unmarshaller um = getJaxbContext().createUnmarshaller();
                PluginsHolder pluginsHolder = (PluginsHolder) um.unmarshal(pluginsPath.toFile());

                for (Plugin plugin : pluginsHolder.list) {
                    plugin.check();
                }

                logger.info("Loaded plugins from {}", pluginsPath);

                return pluginsHolder.list; // Normal exit
            } catch (JAXBException ex) {
                logger.warn("Error loading {}", pluginsPath, ex);
            }
        } else {
            logger.info("No {} file found", pluginsPath);
        }

        return Collections.EMPTY_LIST;
    }

    //-------------//
    // getInstance //
    //-------------//
    /**
     * Report the single instance of PluginsManager in the application.
     *
     * @return the instance
     */
    public static PluginsManager getInstance ()
    {
        return LazySingleton.INSTANCE;
    }

    //----------------//
    // getJaxbContext //
    //----------------//
    private static JAXBContext getJaxbContext ()
            throws JAXBException
    {
        // Lazy creation
        if (jaxbContext == null) {
            jaxbContext = JAXBContext.newInstance(PluginsHolder.class);
        }

        return jaxbContext;
    }

    //---------------//
    // LazySingleton //
    //---------------//
    private static class LazySingleton
    {

        static final PluginsManager INSTANCE = new PluginsManager();
    }

    //----------------//
    // MyMenuListener //
    //----------------//
    /**
     * Class {@code MyMenuListener} is triggered when menu is entered.
     * <p>
     * This is meant to enable menu items only when a sheet is selected,
     * and to indicate the default plugin if any.
     */
    private class MyMenuListener
            extends AbstractMenuListener
    {

        @Override
        public void menuSelected (MenuEvent e)
        {
            boolean enabled = StubsController.getCurrentStub() != null;

            for (int i = 0; i < menu.getItemCount(); i++) {
                JMenuItem item = menu.getItem(i);

                // Beware of separators (for which returned menuItem is null)
                if (item != null) {
                    item.setEnabled(enabled);

                    // Indicate which plugin is the default (if any)
                    Action action = item.getAction();

                    if (action instanceof PluginAction) {
                        Plugin plugin = ((PluginAction) action).getPlugin();
                        item.setText(
                                plugin.getId() + ((plugin == defaultPlugin) ? " (default)" : ""));
                    }
                }
            }
        }
    }

    //-----------//
    // Constants //
    //-----------//
    private static class Constants
            extends ConstantSet
    {

        Constant.String defaultPlugin = new Constant.String("", "Name of default plugin");
    }

    //---------//
    // Default //
    //---------//
    private static class Default
            extends Param<String>
    {

        @Override
        public String getSpecific ()
        {
            if (isSpecific()) {
                return getValue();
            } else {
                return null;
            }
        }

        @Override
        public String getValue ()
        {
            return constants.defaultPlugin.getValue();
        }

        @Override
        public boolean isSpecific ()
        {
            return !constants.defaultPlugin.isSourceValue();
        }

        @Override
        public boolean setSpecific (String specific)
        {
            if (!getValue().equals(specific)) {
                constants.defaultPlugin.setStringValue(specific);
                getInstance().setDefaultPlugin(specific);
                logger.info("Default plugin is now: {}", specific);

                return true;
            }

            return false;
        }
    }

    //---------------//
    // PluginsHolder //
    //---------------//
    /**
     * Class {@code PluginsHolder} is used to unmarshal the plugins root element.
     */
    @XmlAccessorType(XmlAccessType.NONE)
    @XmlRootElement(name = "plugins")
    private static class PluginsHolder
    {

        /** List of plugins. */
        @XmlElementRef
        private List<Plugin> list = new ArrayList<>();

        /** No-arg constructor meant for JAXB. */
        private PluginsHolder ()
        {
        }
    }
}