package de.renew.fa;

import java.awt.event.KeyEvent;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Vector;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.JMenuItem;

import CH.ifa.draw.DrawPlugin;
import CH.ifa.draw.application.DrawApplication;
import CH.ifa.draw.figures.TextFigure;
import CH.ifa.draw.framework.DrawingTypeManager;
import CH.ifa.draw.util.CommandMenu;
import de.renew.draw.storables.api.StorableApi;
import de.renew.draw.storables.ontology.Drawing;
import de.renew.draw.storables.ontology.Figure;
import de.renew.draw.storables.ontology.FigureEnumeration;
import de.renew.draw.ui.api.CommandApi;
import de.renew.draw.ui.api.MenuApi;
import de.renew.draw.ui.ontology.AbstractCommand;
import de.renew.draw.ui.ontology.DrawingEditor;
import de.renew.draw.ui.ontology.MenuSeparatorFactory;
import de.renew.fa.commands.ChangeDecorationCommand;
import de.renew.fa.commands.ChangeFADrawModeCommand;
import de.renew.fa.commands.ShowComparatorCommand;
import de.renew.fa.commands.ShowPaletteCommand;
import de.renew.fa.commands.ShowSettingsCommand;
import de.renew.fa.figures.EndDecoration;
import de.renew.fa.figures.FAArcConnection;
import de.renew.fa.figures.FADrawMode;
import de.renew.fa.figures.FAStateFigure;
import de.renew.fa.figures.FATextFigure;
import de.renew.fa.figures.NullDecoration;
import de.renew.fa.figures.StartDecoration;
import de.renew.fa.figures.StartEndDecoration;
import de.renew.fa.gui.FACompareGui;
import de.renew.fa.gui.FASimulationSettingsDelegator;
import de.renew.fa.gui.FASimulationSettingsInterface;
import de.renew.fa.service.JflapExportFormat;
import de.renew.fa.service.XFAExportFormat;
import de.renew.fa.service.XFAImportFormat;
import de.renew.faformalism.FAFormalismPlugin;
import de.renew.faformalism.util.FAAutomatonModelEnum;
import de.renew.faformalism.util.SimulationSettingsManager;
import de.renew.formalism.FormalismChangeListener;
import de.renew.formalism.FormalismPlugin;
import de.renew.gui.CPNInstanceDrawing;
import de.renew.gui.CPNTextFigure;
import de.renew.gui.GuiPlugin;
import de.renew.gui.InscribableFigure;
import de.renew.gui.TextFigureCreator;
import de.renew.plugin.IPlugin;
import de.renew.plugin.PluginAdapter;
import de.renew.plugin.PluginException;
import de.renew.plugin.PluginManager;
import de.renew.plugin.PluginProperties;
import de.renew.plugin.PropertyHelper;
import de.renew.propertymanagement.prop.ConfigurablePropertyManager;
import de.renew.propertymanagement.prop.PropertyChangeEvent;
import de.renew.propertymanagement.prop.PropertyChangeListener;


/**
 * The wrapper for the FA plugin.
 * Provides the *.fa-drawing, UI elements to draw finite automata, a comparator to compare regular languages,
 * and a settings gui to alter the FAFormalisms simulation settings.
 *
 *
 * @author Lawrence Cabac
 * @author Möller
 * @author Karl Ihlenfeldt
 * @version 2.1
 *
 */
public class FAPlugin extends PluginAdapter {
    private static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(FAPlugin.class);
    private static final String KEY_USEINDICES = "de.renew.fa.useindices";
    private static final String KEY_STATE_PREFIX = "de.renew.fa.state-prefix";
    private static final String KEY_MODE = "de.renew.fa.mode";
    private static final String KEY_INIT = "de.renew.fa.init";
    private static final String ALTERNATIVE_EVENT = "alternative";

    /*
     * A list of commands added by FAPluginExtenders.
     */
    private Hashtable<AbstractCommand, IPlugin> _commandList;
    /*
     * A list of all registered plugins to this.
     */
    private Vector<IPlugin> _pluginList;
    /*
     * The FA Drawing Tool menu entry
     */
    private CommandMenu _menu;
    /*
     * A SaparatorFactory for separating FA Drawing Tool menu
     */
    private JMenuItem _separator;
    /*
     * A FigureCreator responsible for creating FAStateFigures
     */
    private FAFigureCreator _faFigureCreator = null;
    private boolean _loaded;

    /**
     * If the formalism is changed to FANetCompiler or FAAutomatonCompiler, we need to know, so we can update our internal
     * automaton model. Ignoring this change would lead to inconsistent simulation representation and exceptions.
     */
    private FormalismChangeListener _formalismListener;
    /*
     *
     */
    private PaletteCreator _pc;

    private final XFAImportFormat _xfaImportFormat = new XFAImportFormat();
    private final XFAExportFormat _xfaExportFormat = new XFAExportFormat();
    private final JflapExportFormat _jflapExportFormat = new JflapExportFormat();

    private FASimulationSettingsInterface _settingsMenu;
    private FACompareGui _compareGui;

    public static final String FA_FILEFILTER = "de.renew.fa.FADrawing";

    private PropertyChangeListener _propertyChangeListener;


    /**
     * Creates FAPlugin with the given PluginProperties.
     *
     * @param props the plugin configuration
     */
    public FAPlugin(PluginProperties props) {
        super(props);
        _pluginList = new Vector<IPlugin>();
        _commandList = new Hashtable<>();
    }

    /**
     * Constructs a new instance of the FAPlugin class with specified location
     * @param location the URL location of the plugin
     * @throws PluginException if an error occurs while loading the plugin
     */
    public FAPlugin(URL location) throws PluginException {
        super(location);
        _pluginList = new Vector<IPlugin>();
        _commandList = new Hashtable<>();
    }

    /**
     * Initializes the FAPlugin by
     * <ul>
     *   <li> registering the instance drawing for simulation visualization
     *   <li> adding the menu entry
     *   <li> adding import and export formats
     * </ul>
     * {@link de.renew.plugin.IPlugin#init()}
     */
    @Override
    public void init() {

        String pluginName = this.getName();

        String displayNameInit = "Load FA Palette on start-up";
        String displayNameMode = "Change FA Style";
        String displayNameUseIndices = "Toggle Indices On/Off";
        String displayNameStatePrefix = "Standard Name of State";

        String[] booleanChoices = new String[] { "true", "false" };
        String[] selectionChoicesMode = new String[] { ALTERNATIVE_EVENT, "standard" };

        String defaultValueInit = getProperties().getProperty(KEY_INIT);
        String defaultValueMode = getProperties().getProperty(KEY_MODE);
        String defaultValueUseindices = getProperties().getProperty(KEY_USEINDICES);
        String defaultValueStatePrefix = getProperties().getProperty(KEY_STATE_PREFIX);

        ConfigurablePropertyManager propertyManager = ConfigurablePropertyManager.getInstance();
        propertyManager.addConfigurableProperty(
            KEY_INIT, defaultValueInit, pluginName, displayNameInit, booleanChoices);
        propertyManager.addConfigurableProperty(
            KEY_MODE, defaultValueMode, pluginName, displayNameMode, selectionChoicesMode);
        propertyManager.addConfigurableProperty(
            KEY_USEINDICES, defaultValueUseindices, pluginName, displayNameUseIndices,
            booleanChoices);
        propertyManager.addConfigurableProperty(
            KEY_STATE_PREFIX, defaultValueStatePrefix, pluginName, displayNameStatePrefix, null);


        DrawPlugin gs = DrawPlugin.getCurrent();

        _settingsMenu = new FASimulationSettingsDelegator();
        _compareGui = new FACompareGui();

        _menu = createMenu();

        _propertyChangeListener = new PropertyChangeListener() {
            @Override
            public void propertyChanged(PropertyChangeEvent event) {
                switch (event.getProperty()) {
                    case (KEY_MODE) -> {
                        FADrawMode mode = FADrawMode.getInstance();
                        if (ALTERNATIVE_EVENT.equals(event.getNewValue())) {
                            mode.setMode(FADrawMode.ALTERNTIVE);
                        } else {
                            mode.setMode(FADrawMode.STANDARD);
                        }
                    }
                    case (KEY_USEINDICES) -> {
                        getProperties().setProperty(KEY_USEINDICES, event.getNewValue());
                        DrawingEditor editor = DrawPlugin.getCurrent().getDrawingEditor();
                        FigureEnumeration en = editor.drawing().figures();
                        while (en.hasMoreElements()) {
                            Figure fig = en.nextFigure();
                            fig.changed();
                        }
                        editor.view().checkDamage();
                    }
                    case (KEY_STATE_PREFIX) -> {
                        getProperties().setProperty(KEY_STATE_PREFIX, event.getNewValue());
                    }
                    case (KEY_INIT) -> {
                        getProperties().setProperty(KEY_INIT, event.getNewValue());
                    }
                }
            }

            @Override
            public void propertyAdded(PropertyChangeEvent event) {

            }

            @Override
            public void propertyRemoved(PropertyChangeEvent event) {

            }

        };

        propertyManager.addPropertyChangeListener(_propertyChangeListener);

        // register own InstanceDrawing for simulation support
        CPNInstanceDrawing.registerInstanceDrawingFactory(FADrawing.class, CPNInstanceDrawing::new);

        if (gs != null) {
            MenuSeparatorFactory sepFac = MenuApi.createMenuSeparatorFactory("de.renew.fa");
            _separator = sepFac.createSeparator();

            // add FA Drawing Tool menu to Tools menu and separate the the upper part
            MenuApi.registerMenu(DrawPlugin.TOOLS_MENU, _separator);
            MenuApi.registerMenu(DrawPlugin.TOOLS_MENU, _menu);

            _faFigureCreator = new FAFigureCreator();
            GuiPlugin.getCurrent().getFigureCreatorHolder().registerCreator(_faFigureCreator);

            // register the Drawing at the DrawingTypeManager
            //NOTICEredundant
            if (gs != null) {
                DrawingTypeManager.getInstance().register(FA_FILEFILTER, new FAFileFilter());
            }

            // Activating palette
            try {
                boolean init = PropertyHelper.getBoolProperty(getProperties(), "de.renew.fa.init");
                LOGGER.debug("init protperty is : " + init);
                if (init) {
                    // add stuff for execution when init is set
                }
            } catch (RuntimeException e) {
                // do nothing
            }

            // Adding import/export support
            StorableApi.addImportFormat(_xfaImportFormat);
            StorableApi.addExportFormat(_xfaExportFormat);
            StorableApi.addExportFormat(_jflapExportFormat);
        }
        AtomicReference<FAAutomatonModelEnum> previous =
            new AtomicReference<>(SimulationSettingsManager.getAutomatonModel());
        _formalismListener = (compilerName, source, action) -> {
            if (SimulationSettingsManager.getAutomatonModel() != FAAutomatonModelEnum.NET) {
                previous.set(SimulationSettingsManager.getAutomatonModel());
            }
            boolean isFAAutomatonCompilerFormalism =
                compilerName.equals(FAFormalismPlugin.FA_AUTOMATON_COMPILER_NAME);
            boolean isFANetCompilerFormalism =
                compilerName.equals(FAFormalismPlugin.FA_NET_COMPILER_NAME);
            if (!isFAAutomatonCompilerFormalism) {
                SimulationSettingsManager.setAutomatonModel(FAAutomatonModelEnum.NET);
            } else {
                SimulationSettingsManager.setAutomatonModel(previous.get());
            }
            if (isFAAutomatonCompilerFormalism || isFANetCompilerFormalism) {
                if (!_loaded) {
                    loadPalette();
                }
            }
        };
        FormalismPlugin fp = FormalismPlugin.getCurrent();
        if (fp != null) {
            FormalismPlugin.getCurrent().addFormalismChangeListener(_formalismListener);
        }

        // load palette if KEY_INIT is true
        if (getProperties().getProperty(KEY_INIT).equals("true") && !_loaded) {
            loadPalette();
        }

    }

    @Override
    public boolean cleanup() {
        ConfigurablePropertyManager propertyManager = ConfigurablePropertyManager.getInstance();
        propertyManager.removePropertyChangeListener(_propertyChangeListener);

        propertyManager.removeConfigurableProperty(KEY_INIT);
        propertyManager.removeConfigurableProperty(KEY_MODE);

        DrawPlugin drawPlugin = DrawPlugin.getCurrent();
        GuiPlugin guiPlugin = GuiPlugin.getCurrent();

        if (guiPlugin != null) {

            if (guiPlugin.getGui() != null) {

                Enumeration<Drawing> openDrawings = guiPlugin.getGui().drawings();
                while (openDrawings.hasMoreElements()) {

                    Drawing drawing = openDrawings.nextElement();
                    if (drawing instanceof FADrawing) {

                        guiPlugin.getGui().closeDrawing(drawing);

                    }

                }
                openDrawings = guiPlugin.getGui().drawings();
                while (openDrawings.hasMoreElements()) {

                    Drawing drawing = openDrawings.nextElement();
                    if (drawing instanceof FADrawing) {

                        return false;

                    }

                }

            }

            if (_faFigureCreator != null) {
                guiPlugin.getFigureCreatorHolder().unregisterCreator(_faFigureCreator);
                _faFigureCreator = null;
            }

        }

        _settingsMenu.cleanup();
        _compareGui.cleanup();

        if (drawPlugin != null) {
            MenuApi.unregisterMenu(_menu);

            StorableApi.removeImportFormat(_xfaImportFormat);
            StorableApi.removeExportFormat(_xfaExportFormat);
            StorableApi.removeExportFormat(_jflapExportFormat);


        }
        if (_loaded) {
            _pc.remove();
            _loaded = false;
        }

        FormalismPlugin fp = FormalismPlugin.getCurrent();
        if (fp != null) {
            fp.removeFormalismChangeListener(_formalismListener);
        }

        DrawingTypeManager.getInstance().unregister(FA_FILEFILTER);

        this._commandList.clear();

        CPNInstanceDrawing.unregisterInstanceDrawingFactory(FADrawing.class);

        return super.cleanup();
    }

    /**
    * Creates a new default palette and refreshes the menuFrame, if not already loaded.
    * If loaded the palette gets removed.
    */
    public void create() {
        // toggel on/off
        if (_loaded) {
            _pc.remove();
            _loaded = false;
        } else {
            loadPalette();
        }
    }

    /**
     * Loads either the standard or the alternative palette.
     */
    private void loadPalette() {
        if (FADrawMode.getInstance().getMode() == FADrawMode.STANDARD) {
            _pc = new PaletteCreator("FAPalette");
            LOGGER.debug("created standard palette");
        } else {
            _pc = new AltPaletteCreator("FAPalette");
            LOGGER.debug("created alt palette");
        }

        _loaded = true;
    }

    /**
     * When FADrawMode changed, this methode switches palettes.
     */
    public void switchPalette() {
        if (_loaded) {
            _pc.remove();
            loadPalette();
        }
    }

    /**
     * Adds FA Drawing Tool menu entry with
     * <ul>
     *         <li>command for showing palette including shortcut</li>
     *         <li>submenu for changing decoration of selected element</li>
     *         <li>command for changing draw mode</li>
     *         <li>command for showing the simulation settings menu</li>
     *         <li>command for displaying version info</li>
     * </ul>
     * @return FA Drawing Tool CommandMenu
     */
    private CommandMenu createMenu() {
        _menu = new CommandMenu("FA Drawing Tool");

        // Shortcut part
        AbstractCommand command = new ShowPaletteCommand(this);
        _menu.add(command, KeyEvent.VK_6);
        _commandList.put(command, this);

        // Submenu part
        CommandMenu submenu = DrawApplication.createCommandMenu("Decoration");
        command = new ChangeDecorationCommand("Start", new StartDecoration());
        submenu.add(command);
        _commandList.put(command, this);
        command = new ChangeDecorationCommand("End", new EndDecoration());
        submenu.add(command);
        _commandList.put(command, this);
        command = new ChangeDecorationCommand("Start/End", new StartEndDecoration());
        submenu.add(command);
        _commandList.put(command, this);
        command = new ChangeDecorationCommand("none", new NullDecoration());
        submenu.add(command);
        _commandList.put(command, this);
        _menu.add(submenu);

        // Change drawmode part
        command = new ChangeFADrawModeCommand();
        _menu.add(command);
        _commandList.put(command, this);

        // Display settings menu
        command = new ShowSettingsCommand(_settingsMenu);
        _menu.add(command);
        _commandList.put(command, this);

        // Display compare menu
        command = new ShowComparatorCommand(_compareGui);
        _menu.add(command);
        _commandList.put(command, this);

        _menu.addSeparator();

        // Version info part
        command = CommandApi.createVersionInfoCommand(this);
        _menu.add(command);
        _menu.putClientProperty(MenuApi.ID_PROPERTY, "de.renew.fa");
        _commandList.put(command, this);

        //_menu.add(new ExportJflapCommand("jflap export"));
        //        _menu.addSeparator();
        //        submenu = DrawApplication.createCommandMenu("Export");
        //        submenu.add(new ExportAsXFACommand("as XFA"));
        //        _menu.add(submenu);
        //
        //        _menu.addSeparator();
        //        submenu = DrawApplication.createCommandMenu("Import");
        //        submenu.add(new ImportFromXFACommand("from XFA"), KeyEvent.VK_7);
        //        _menu.add(submenu);
        //
        //        _menu.addSeparator();
        //        submenu = DrawApplication.createCommandMenu("Analysis");
        //        submenu.add(new ConnectedPropertyTestCommand("FA is connected"),
        //                    KeyEvent.VK_5);
        //        _menu.add(submenu);
        return _menu;
    }

    /**
     * Deregisters the specified plugin and removes any associated menu entries.
     * @param plugin the plugin to deregister
     */
    public void deregisterPlugin(IPlugin plugin) {
        Enumeration<AbstractCommand> it = _commandList.keys();

        // Removing all menu entries for the deregistering plugin.
        while (it.hasMoreElements()) {
            AbstractCommand command = it.nextElement();
            if (_commandList.get(command) instanceof FAPluginExtender) {
                _menu.remove(command);
                _commandList.remove(command);
            }
        }
        _pluginList.remove(plugin);

    }

    /**
    * Adds plugins, but is only functional for FAPluginExtenders by adopting
    * their commands
     * @param plugin the plugin to register
    */
    public void registerPlugin(IPlugin plugin) {
        _pluginList.add(plugin);
        if (plugin instanceof FAPluginExtender) {
            Vector<AbstractCommand> v = ((FAPluginExtender) plugin).getMenuCommands();
            Iterator<AbstractCommand> it = v.iterator();
            while (it.hasNext()) {
                AbstractCommand command = it.next();
                _menu.add(command);
                _commandList.put(command, plugin);

                LOGGER.debug("Added command " + command.toString() + "\n" + " of Plugin " + plugin);
            }
        }
    }

    /**
     * This method is used to access the plugin instance from other parts of the application
     * @return the current FAPlugin instance, or null
     */
    public static FAPlugin getCurrent() {
        PluginManager pm = PluginManager.getInstance();
        if (pm == null) {
            return null;
        }
        return (FAPlugin) pm.getPluginByName("Renew Finite Automata Base");
    }

    /**
     * Returns whether the plugin is configured to use indices or not
     * @return true if the plugin is configured to use indices
     */
    public boolean getUseIndices() {
        return getProperties().getBoolProperty(KEY_USEINDICES);
    }

    /**
     * Provides the FAStateFigure figure with its standard inscription.
     */
    private class FAFigureCreator implements TextFigureCreator {
        @Override
        public boolean canCreateDefaultInscription(InscribableFigure figure) {
            if (figure instanceof FAStateFigure) {
                return true;
            }
            if (figure instanceof FAArcConnection) {
                return true;
            }
            return false;
        }

        //        @Override
        //        public boolean canCreateFigure(InscribableFigure figure) {
        //            if (figure instanceof FAStateFigure) {
        //                return true;
        //            }
        //            return false;
        //        }

        //        @Override
        //        public TextFigure createTextFigure(InscribableFigure figure) {
        //            if (figure instanceof FAStateFigure) {
        //                logger.debug("LABEL created!");
        //                return new FATextFigure(CPNTextFigure.NAME);
        //            }
        //            if (figure instanceof FAArcConnection) {
        //                return new FATextFigure(CPNTextFigure.INSCRIPTION);
        //            }
        //            return null;
        //        }

        //        /**
        //         * Gets standard inscription for FAStateFigures and FAArcConnections.
        //         *
        //         * @return inscription of form <code>Z[i]</code> for FAStateFigures
        //         *         of form <code>[lowercase letter]</code> (starting from a)
        //         *                                                                                         for FAArcConnections
        //         */
        //        @Override
        //        public String getDefaultInscription(InscribableFigure figure) {
        //            logger.debug("getDefaultInscription called");
        //            if (figure instanceof FAStateFigure) {
        //                DrawApplication da = GuiPlugin.getCurrent().getGui();
        //                int i = 0;
        //                if (da != null) {
        //                    HashSet<String> labels = new HashSet<String>();
        //                    Drawing d = da.drawing();
        //                    FigureEnumeration figEnum = d.figures();
        //                    while (figEnum.hasMoreElements()) {
        //                        Figure fig = figEnum.nextFigure();
        //                        if (fig instanceof CPNTextFigure) {
        //                            String text = ((CPNTextFigure) fig).getText();
        //                            if (text.startsWith("Z")) {
        //                                labels.add(text);
        //                            }
        //                        }
        //                    }
        //                    while (labels.contains("Z" + i)) {
        //                        i++;
        //                    }
        //                }
        //                return "Z" + i;
        //            }
        //            if (figure instanceof FAArcConnection) {
        //                return "a";
        //            }
        //            return null;
        //        }
        @Override
        public boolean canCreateFigure(InscribableFigure figure) {
            return canCreateDefaultInscription(figure);
        }

        @Override
        public TextFigure createTextFigure(InscribableFigure figure) {
            if (figure instanceof FAStateFigure) {
                return new FATextFigure(CPNTextFigure.NAME);
            } else if (figure instanceof FAArcConnection) {
                return new FATextFigure(CPNTextFigure.INSCRIPTION);
            }
            return null;
        }

        @Override
        public String getDefaultInscription(InscribableFigure figure) {
            boolean useindices = getProperties().getBoolProperty(KEY_USEINDICES);
            String sp = getProperties().getProperty(KEY_STATE_PREFIX);
            sp = (sp == null ? "z" : sp); // default if not set 
            if (figure instanceof FAStateFigure) {
                DrawApplication da = GuiPlugin.getCurrent().getGui();
                int i = 0;
                if (da != null) {
                    HashSet<String> labels = new HashSet<String>();
                    Drawing d = da.drawing();
                    FigureEnumeration figEnum = d.figures();
                    while (figEnum.hasMoreElements()) {
                        Figure fig = figEnum.nextFigure();
                        if (fig instanceof FAStateFigure) {
                            FigureEnumeration stateChildren = ((FAStateFigure) fig).children();
                            if (stateChildren.hasMoreElements()) {
                                String label = ((TextFigure) stateChildren.nextFigure()).getText();
                                labels.add(label);
                            }
                        }
                    }
                    while (labels.contains(sp + i) || labels.contains(sp + "_" + i)
                        || labels.contains(sp + "_{" + i + "}")) {
                        i++;
                    }
                }
                if (useindices) {
                    return sp + "_{" + i + "}";
                }
                return sp + i;
            }
            return null;
        }
    }
}