/*
 * Created on 28.01.2004
 *
 */

package de.renew.gui;

import java.awt.EventQueue;
import java.awt.Toolkit;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.NotSerializableException;
import java.io.ObjectInput;
import java.io.ObjectStreamException;
import java.io.StreamCorruptedException;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EmptyStackException;
import java.util.Enumeration;
import java.util.List;
import java.util.Objects;
import java.util.Stack;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.swing.JMenuItem;
import javax.swing.JOptionPane;

import CH.ifa.draw.application.MenuManager.SeparatorFactory;
import CH.ifa.draw.framework.FigureWithID;
import CH.ifa.draw.util.CommandMenuItem;
import de.renew.application.IllegalCompilerException;
import de.renew.application.SimulatorExtensionAdapter;
import de.renew.application.SimulatorPlugin;
import de.renew.draw.storables.ontology.Drawing;
import de.renew.draw.storables.ontology.Figure;
import de.renew.draw.ui.api.DialogApi;
import de.renew.draw.ui.api.EditorApi;
import de.renew.draw.ui.api.ErrorApi;
import de.renew.draw.ui.api.MenuApi;
import de.renew.draw.ui.ontology.AbstractCommand;
import de.renew.draw.ui.ontology.DrawingView;
import de.renew.draw.ui.ontology.StatusDisplayer;
import de.renew.engine.searchqueue.SearchQueue;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.formalism.FormalismPlugin;
import de.renew.gui.nin.ConstructionGUI;
import de.renew.gui.nin.SystemNetBuilder;
import de.renew.io.FileFilterCreator;
import de.renew.net.IDRegistry;
import de.renew.net.Net;
import de.renew.net.NetElementID;
import de.renew.net.NetInstance;
import de.renew.net.Place;
import de.renew.net.PlaceInstance;
import de.renew.net.Transition;
import de.renew.net.TransitionInstance;
import de.renew.remote.MarkingAccessor;
import de.renew.remote.NetAccessor;
import de.renew.remote.NetInstanceAccessor;
import de.renew.remote.PlaceInstanceAccessor;
import de.renew.remote.RemotePlugin;
import de.renew.remote.TransitionAccessor;
import de.renew.remote.TransitionInstanceAccessor;
import de.renew.remote.TransitionInstanceAccessorImpl;
import de.renew.simulator.api.SimulationLockExecutor;
import de.renew.simulator.api.SimulatorExtensions;
import de.renew.simulatorontology.loading.NetNotFoundException;
import de.renew.simulatorontology.shadow.SyntaxException;
import de.renew.simulatorontology.simulation.NoSimulationException;
import de.renew.simulatorontology.simulation.SimulationEnvironment;
import de.renew.simulatorontology.simulation.SimulationRunningException;
import de.renew.simulatorontology.simulation.Simulator;
import de.renew.util.RenewObjectInputStream;
import de.renew.util.RenewObjectOutputStream;

/**
 * This class creates the Simulation menu for the Gui Plugin.<br>
 * It contains entries for running and controlling a simulation, saving/loading
 * a simulation to a database, connect to a remotely running simulation and lets
 * you choose the formalism for the present simulation (since 2.0).
 *
 * @author J&ouml;rn Schumacher
 */
public class SimulationMenuCreator {
    private static final org.apache.log4j.Logger logger =
        org.apache.log4j.Logger.getLogger(SimulationMenuCreator.class);

    /**
     * Used to synchronise access to some method calls that start threads. Dont
     * use synchronized for these methods use lock instead.
     */
    private static final Lock lock = new ReentrantLock();

    public Collection<JMenuItem> createMenus(BreakpointManager bpm) {
        SeparatorFactory sepFac = new SeparatorFactory("de.renew.gui.simulation");
        Collection<JMenuItem> result = new ArrayList<>();

        result.add(MenuApi.createMenuItem("Run simulation", KeyEvent.VK_R, this::runSimulation));

        result.add(MenuApi.createMenuItem("Simulation Step", KeyEvent.VK_I, this::simulationStep));

        //result.add(
        //MenuApi.createMenuItem("Simulation X Steps", KeyEvent.VK_X, this::simulationXSteps));

        result.add(
            new CommandMenuItem(
                new SimulationNetStepCommand("Simulation Net Step"), KeyEvent.VK_I,
                Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()
                    + InputEvent.SHIFT_DOWN_MASK));

        // workaround for the Ctrl-H Problem in Mac OSX
        int modifier = Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
        if ("Mac OS X".equals(System.getProperty("os.name"))) {
            modifier += InputEvent.SHIFT_DOWN_MASK;
        }

        result.add(new CommandMenuItem(new SimulationHaltCommand(), KeyEvent.VK_H, modifier));
        result.add(new CommandMenuItem(new SimulationTerminateCommand(), KeyEvent.VK_T));

        result.add(sepFac.createSeparator());

        result.add(new CommandMenuItem(new ConfigureSimulationCommand("Configure Simulation...")));
        result.add(bpm.getSimulationMenu());

        result.add(sepFac.createSeparator());

        result.add(new CommandMenuItem(new SaveSimulationStateCommand()));
        result.add(new CommandMenuItem(new LoadSimulationStateCommand()));

        result.add(sepFac.createSeparator());

        result.add(new CommandMenuItem(new OpenServerFrameCommand("Remote Server...")));

        return result;
    }

    private void simulationStep() {
        SimulationThreadPool.getCurrent().execute(() -> {
            GuiPlugin plugin = GuiPlugin.getCurrent();
            CPNSimulation simulation = getSimulation();

            if (!simulation.isSimulationActive()) {
                initSimulation(0);
            } else {
                plugin.showStatus("Searching for a binding...");
                int status = simulation.simulationStep();

                switch (status) {
                    case Simulator.STATUS_STOPPED:
                        if (simulation.isSimulationActive()) {
                            plugin.showStatus("Simulation halted.");
                        } else {
                            plugin.showStatus("Simulation terminated.");
                        }
                        break;
                    case Simulator.STATUS_STEP_COMPLETE:
                        plugin.showStatus("Simulation step completed.");
                        break;
                    case Simulator.STATUS_LAST_COMPLETE:
                        plugin.showStatus(
                            "Simulation step completed. " + "No more enabled bindings.");
                        break;
                    case Simulator.STATUS_CURRENTLY_DISABLED:
                        plugin.showStatus("No enabled bindings found.");
                        break;
                    case Simulator.STATUS_DISABLED:
                        plugin.showStatus("No more enabled bindings.");
                        break;
                }
            }
        });
    }

    /**
     * Prompts the user to enter the number of simulation steps to execute.
     * Validates the input and runs the specified number of steps in the simulation.
     *
     * @throws NumberFormatException if the entered value is not a valid integer
     */
    private void simulationXSteps() {
        String input = JOptionPane.showInputDialog(
            null, "Enter the number of simulation steps:", "Simulation Steps",
            JOptionPane.PLAIN_MESSAGE);

        if (input != null && !input.trim().isEmpty()) {
            try {
                int steps = Integer.parseInt(input.trim());
                if (steps > 0) {
                    if (!getSimulation().isSimulationActive()) {
                        initSimulation(steps);
                    } else {
                        for (int i = 0; i < steps; i++) {
                            simulationStep();
                        }
                    }
                    GuiPlugin.getCurrent().showStatus(steps + " simulation steps completed.");
                } else {
                    JOptionPane.showMessageDialog(
                        null, "The number of steps must be greater than 0.", "Invalid Input",
                        JOptionPane.ERROR_MESSAGE);
                }
            } catch (NumberFormatException e) {
                JOptionPane.showMessageDialog(
                    null, "Invalid input. Please enter a valid number.", "Error",
                    JOptionPane.ERROR_MESSAGE);
            }
        }
    }

    private void runSimulation() {
        try {
            lock.lock();
            GuiPlugin plugin = GuiPlugin.getCurrent();
            CPNSimulation simulation = getSimulation();
            if (!simulation.isSimulationActive()) {
                // Be notified if the simulation is setup and start
                // running
                SimulatorExtensions.addExtension(new SimulatorExtensionAdapter() {
                    @Override
                    public void simulationSetup(SimulationEnvironment env) {
                        // get the simulation again, as it may have changed
                        CPNSimulation simulation = getSimulation();
                        simulation.simulationRun();
                        SimulatorExtensions.removeExtension(this);
                    }
                });

                initSimulation(0);
            } else {
                plugin.showStatus("Simulation running.");
                simulation.simulationRun();
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * Initialize the simulation.
     *
     * <p>This method starts a new simulation for the active net. Depending on the
     * value of {@code performSteps}, it may also execute a number of steps
     * immediately after a successful initialization.</p>
     *
     * @param performSteps number of steps requested when starting from an
     * <em>inactive</em> state, with the first step performing
     * initialization. Values of {@code 0} or {@code 1} only
     * initialize the simulation; values {@code > 1} will perform
     * {@code performSteps - 1} additional calls to
     * {@link #simulationStep()} after initialization.
     * If there are no enabled bindings after initialization,
     * no steps are performed.
     */
    void initSimulation(int performSteps) {
        // TODO: refactor this method, too long, too complicated, untestable
        Runnable action = () -> {
            SimulationThreadPool.getNew().execute(() -> {
                final GuiPlugin plugin = GuiPlugin.getCurrent();
                final CPNApplication app = plugin.getGui();
                app.showStatus("Starting Simulation ...");
                if (!(app.drawing() instanceof CPNDrawing)) {
                    app.showStatus(
                        "Cannot start new simulation with this drawing! Please activate a net window.");
                    return;
                }

                String netName;
                SystemNetBuilder systemNetBuilder = null;

                if (Objects.requireNonNull(FormalismPlugin.getCurrent()).getCompiler()
                    .equals(SystemNetBuilder.formalismCompilerName)) {
                    systemNetBuilder = initSystemNet(app);
                    if (systemNetBuilder == null) {
                        return;
                    }
                    netName = systemNetBuilder.getSystemNet().getName();
                } else {
                    netName = GuiPlugin.getCurrent().getDrawingEditor().drawing().getName();
                }

                try {
                    final NetInstanceAccessor primaryInstance =
                        getSimulation().initSimulation(netName);

                    SystemNetBuilder finalSystemNetBuilder = systemNetBuilder;
                    EventQueue.invokeLater(() -> {
                        if (primaryInstance != null) {
                            app.openInstanceDrawing(primaryInstance);

                            if (Objects.requireNonNull(FormalismPlugin.getCurrent()).getCompiler()
                                .equals(SystemNetBuilder.formalismCompilerName)) {
                                fireCreateTransition(primaryInstance, finalSystemNetBuilder);
                                openAllSimulatedNetInstances(
                                    primaryInstance, finalSystemNetBuilder, app);
                            }

                            if (getSimulation().isSimulationActive()) {
                                if (performSteps > 1) {
                                    for (int i = 1; i < performSteps; i++) {
                                        simulationStep();
                                    }
                                }
                                app.showStatus("Simulation initialized.");
                            } else {
                                app.showStatus("Simulation initialized. " + "No enabled bindings.");
                            }
                        } else {
                            app.showStatus("The simulation could not be initialized.");
                        }
                    });
                } catch (final SyntaxException e) {
                    logger.debug(e.getMessage(), e);
                    EventQueue.invokeLater(
                        () -> plugin.processSyntaxException(
                            FigureExceptionFactory.createFigureException(e), true));
                } catch (NetNotFoundException e1) {
                    logger.warn(e1.getMessage(), e1);
                    plugin.showStatus(
                        "Net \"" + netName
                            + "\" not found although the drawing exists (should not happen).");
                } catch (IllegalCompilerException e1) {
                    logger.warn(e1.getMessage(), e1);
                    plugin.showStatus("No compiler selected. Please choose a formalism.");
                } catch (NoSimulationException e1) {
                    logger.debug(e1.getMessage(), e1);
                    plugin.showStatus("Simulation aborted during initialization.");
                } catch (RuntimeException e1) {
                    logger.warn("Simulation initialization failed: " + e1, e1);
                    plugin.showStatus("Simulation initialization failed: " + e1.getMessage());
                } catch (Error e1) {
                    logger.error("Simulation initialization failed: " + e1, e1);
                    plugin.showStatus("Simulation initialization failed: " + e1);
                }
            });
        };

        SimulationLockExecutor.runWithLock(action);
    }

    /**
     * Shows a load file dialog and replaces the current simulation state with
     * the loaded one.
     * <p>
     * added Apr 12 2000 Michael Duvigneau
     */
    void promptLoadSimulationState() {
        GuiPlugin plugin = GuiPlugin.getCurrent();
        CPNApplication gui = plugin.getGui();
        File path = DialogApi.showSelectFilesDialog(null, FileFilterCreator.createRSTFileFilter());

        if (path != null) {
            // Close all simulation related windows,
            // as the old simulation will be terminated.
            BindingSelectionFrame.close();
            gui.closeAllSimulationDrawings();

            String message = null;
            ObjectInput input = null;

            try {
                GuiPlugin.getCurrent().showStatus("Importing " + path + " ...");

                FileInputStream stream = new FileInputStream(path);

                // As the recursion depth seems to be too high to
                // fit in the Java VM stack space, we try to cut
                // it down by using de.renew.util.RenewObjectInputStream.
                // Additionally, some changes to SimulatablePlaceInstance are
                // required to save the tokens delayed.
                input = new RenewObjectInputStream(stream);

                // Below the original line instantiating the classic Stream.
                // input = new ObjectInputStream(stream);
                ModeReplacement.getInstance().getSimulation().loadState(input);

                plugin.showStatus("Imported " + path + ".");
            } catch (SimulationRunningException e) {
                message = "Cannot load state into running simulation";
            } catch (ObjectStreamException e) {
                message =
                    "File " + path + " is corrupted or does not " + "contain state information.";
                logger.error(e.getMessage(), e);
            } catch (ClassNotFoundException e) {
                message = "File " + path + " refers to java classes "
                    + "which are currently not available.";
                logger.error(e.getMessage(), e);
            } catch (IOException e) {
                message = "Problem occured while reading file " + path + ":" + e;
            } catch (StackOverflowError e) {
                message = "StackOverflowError: Probably the recursion in Java's "
                    + "serialization mechanism went too deep.\n"
                    + "If you are using JDK 1.1, try setting the java options "
                    + " -ss... and/or -oss... to increase native and java stack "
                    + "sizes. Setting -ss1m was appropriate for us.";
            }
            try {
                if (input != null) {
                    input.close();
                }
                if (message == null) {
                    message = "Simulation state restored from " + path + ".";
                }
            } catch (StreamCorruptedException e) {
                // A RenewObjectInputStream can throw this exception
                // if it is not able to read delayed fields on close().
                // If loadState() was not successfull, this is a natural
                // consequence, so ignore it.
                // The stream has then been closed anyway.
                if (message == null) {
                    message = "Problem occurred while reading file " + path
                        + ", but all relevant information was already read. "
                        + "Perhaps the simulation state is valid anyway.";
                    logger.error("Exception while closing: " + e);
                }
            } catch (Exception e) {
                if (message == null) {
                    message = "Problem occurred while reading file " + path
                        + ", but all relevant information was already read. "
                        + "Perhaps the simulation state is valid anyway.";
                }
                logger.error("Exception while closing: " + e);
            }
            plugin.showStatus(message);
            logger.error(message);
        }
    }

    /**
     * Shows a save file dialog and stores the current simulation state.
     * <p>
     * added Jan 20 2000 Michael Duvigneau
     */
    void promptSaveSimulationState() {
        GuiPlugin plugin = GuiPlugin.getCurrent();
        if (!ModeReplacement.getInstance().getSimulation().isSimulationActive()) {
            plugin.showStatus("Need running simulation to save state.");
        } else {
            File file = DialogApi.showSaveFileDialog(null, FileFilterCreator.createRSTFileFilter());

            if (file != null) {
                String path = file.getAbsolutePath();
                String message = null;
                Stack<Object> problemLocation = null;
                RenewObjectOutputStream output = null;

                try {
                    plugin.showStatus("Exporting " + path + " ...");

                    // As the recursion depth seems to be too high to
                    // fit in the Java VM stack space, we try to cut
                    // it down by using de.renew.util.RenewObjectOutputStream.
                    FileOutputStream stream = new FileOutputStream(path);

                    output = new RenewObjectOutputStream(stream);

                    ModeReplacement.getInstance().getSimulation().saveState(output);
                    plugin.showStatus("Exported " + path + ".");
                } catch (NotSerializableException e) {
                    message = "Some object or class is not serializable: " + e.getMessage()
                        + ".\nWriting of file " + path + " aborted.";
                    // NOTICEnull
                    problemLocation = output != null ? output.getDomainTrace() : null;
                    logger.debug(e.getMessage(), e);
                } catch (Exception e) {
                    message = "Problem encountered while writing file " + path + ": " + e;
                    logger.debug(e.getMessage(), e);
                } catch (StackOverflowError e) {
                    message = "StackOverflowError: Probably the recursion in Java's "
                        + "serialization mechanism went too deep.\n"
                        + "If you are using JDK 1.1, try setting the java options "
                        + " -ss... and/or -oss... to increase native and java stack "
                        + "sizes. Setting -ss1m was appropriate for us.";
                    logger.debug(e.getMessage(), e);
                }
                try {
                    if (output != null) {
                        output.close();
                    }
                    if (message == null) {
                        message = "Simulation state saved to " + path + ".";
                    }
                } catch (Exception e) {
                    if (message == null) {
                        message = "Problem encountered while " + "closing file " + path + ": " + e;
                    }
                }
                if (problemLocation != null) {
                    displaySaveSimError(problemLocation, message);
                } else {
                    plugin.showStatus(message);
                    logger.error(message);
                }
            }
        }
    }

    private void displaySaveSimError(Stack<Object> objectLocation, String message) {
        try {
            GuiPlugin plugin = GuiPlugin.getCurrent();

            NetAccessor net = null;
            NetInstanceAccessor netInstance = null;
            int id = FigureWithID.NOID;
            String thisShouldNotHappen = null;
            String specialReason = null;
            Object netElement = null;
            CPNDrawing drawing = null;
            String netName = null;
            boolean appendTrace;

            // Extract the element at which the problem occurred
            // from the top of the stack. Because the stack
            // contains simulation-local objects, but the GUI
            // needs remote accessors, wrap the information. If
            // some unexpected type of object is found, flag this
            // by setting the String thisShouldNotHappen to a
            // non-null value.
            try {
                netElement = objectLocation.peek();
                RemotePlugin remote = RemotePlugin.getInstance();
                if (netElement instanceof PlaceInstance placeInstance) {
                    PlaceInstanceAccessor placeInst = remote.wrapInstance(placeInstance);
                    id = placeInst.getPlace().getID().getFigureID();
                    netInstance = placeInst.getNetInstance();
                    net = netInstance.getNet();
                } else if (netElement instanceof TransitionInstance transitionInstance) {
                    TransitionInstanceAccessor transitionInst =
                        remote.wrapInstance(transitionInstance);
                    id = transitionInst.getTransition().getID().getFigureID();
                    netInstance = transitionInst.getNetInstance();
                    net = netInstance.getNet();
                } else if (netElement instanceof Place place) {
                    thisShouldNotHappen = "place " + netElement;
                    id = place.getID().getFigureID();
                } else if (netElement instanceof TransitionAccessor) {
                    thisShouldNotHappen = "transition " + netElement;
                    id = ((Transition) netElement).getID().getFigureID();
                } else if (netElement instanceof Net net1) {
                    thisShouldNotHappen = "net " + netElement;
                    net = remote.wrapNet(net1);
                } else if (netElement instanceof NetInstance netInstance1) {
                    thisShouldNotHappen = "net instance " + netElement;
                    netInstance = remote.wrapInstance(netInstance1);
                    net = netInstance.getNet();
                } else if (netElement instanceof IDRegistry) {
                    thisShouldNotHappen = "a token ID registry";
                } else if (netElement == SimulatorPlugin.class) {
                    thisShouldNotHappen = "instance drawings";
                } else if (netElement == Net.class) {
                    thisShouldNotHappen = "compiled nets";
                } else if (netElement == SearchQueue.class) {
                    thisShouldNotHappen = "search queue entries";
                    if (message.contains("de.renew.call.SynchronisationRequest")) {
                        specialReason = "You tried to save the simulation state "
                            + "while a Java call to a net method was still "
                            + "active. Wait until all methods have completed.";
                    }
                } else {
                    thisShouldNotHappen = "(DON'T KNOW - TRACE CONTAINS OBJECT OF UNEXPECTED TYPE: "
                        + netElement.getClass().getName() + ")";
                }
            } catch (EmptyStackException e) {
                thisShouldNotHappen = "(DON'T KNOW - TRACE IS EMPTY!?)";
            } catch (NoSimulationException e) {
                thisShouldNotHappen = "(DON'T KNOW - NO LOCAL SIMULATION RU)";
            }

            // Compose the message. This depends on whether an
            // expected element was found on top of the stack
            // trace.
            if (thisShouldNotHappen == null) {
                // NOTICEnull
                netName = net != null ? net.getName() : null;

                message = message + "\nThe problem lies around net element " + netElement + ".";
                drawing = ModeReplacement.getInstance().getDrawingLoader().getDrawing(netName);
                appendTrace = true;
            } else {
                if (specialReason == null) {
                    message = message + "\nDid you try to save a state while the "
                        + "simulation was running (concurrent transition "
                        + "firing or binding calculation is not allowed)?";
                } else {
                    message = message + "\n" + specialReason;
                }
                message = message + ("\nThe problem manifested itself while writing "
                    + thisShouldNotHappen + ".");
                appendTrace = true;
            }

            // If appropriate, append a stack trace to the message.
            if (appendTrace) {
                message = message + "\nSerialization trace:";
                try {
                    StringBuilder messageBuilder = new StringBuilder(message);
                    while (!objectLocation.isEmpty()) {
                        netElement = objectLocation.pop();
                        messageBuilder.append("\n    ").append(netElement);

                        // Take a guess, to which net the offending figure
                        // belongs.
                        // With chance, the net is listed in the trace.
                        if (net == null && netInstance == null) {
                            try {
                                RemotePlugin remote = RemotePlugin.getInstance();
                                if (netElement instanceof Net net1) {
                                    net = remote.wrapNet(net1);
                                } else if (netElement instanceof NetInstance netInstance1) {
                                    netInstance = remote.wrapInstance(netInstance1);
                                    net = netInstance.getNet();
                                }
                            } catch (NoSimulationException e) {
                                // FIXME: add a comment why this can be ignored
                                //  or add some handling/logging
                            }
                        }
                    }
                    message = messageBuilder.toString();
                } catch (EmptyStackException e) {
                    // FIXME: add a comment why this can be ignored
                    //  or add some handling/logging
                }
            }

            // Display the message and point the user to the
            // problem location. This again depends on whether an
            // expected element was found on top of the stack
            // trace.
            if (thisShouldNotHappen == null) {
                if (drawing == null) {
                    plugin.showStatus(message);
                    logger.error(message);
                    logger.error(
                        "Sorry, cannot show the location of the problem " + "because net " + netName
                            + " is not loaded.");
                } else {
                    GuiPlugin.getCurrent().getGui().openInstanceDrawing(netInstance);

                    InstanceDrawing instanceDrawing =
                        CPNInstanceDrawing.getInstanceDrawing(netInstance);
                    FigureWithID figure = drawing.getFigureWithID(id);
                    InstanceFigure instanceFigure = instanceDrawing.getInstanceFigure(figure);

                    plugin.showStatus(message);
                    logger.error(message);
                    GuiPlugin.getCurrent().processFigureException(
                        ErrorApi.createErrorState(
                            "Renew: Save simulation error", message, instanceDrawing,
                            instanceFigure),
                        false);
                }
            } else {
                plugin.showStatus(message);
                logger.error(message);
                if ((net != null) && (id != FigureWithID.NOID)) {
                    drawing =
                        ModeReplacement.getInstance().getDrawingLoader().getDrawing(net.getName());

                    if (drawing != null) {
                        FigureWithID figure = drawing.getFigureWithID(id);

                        if (figure != null) {
                            if (netInstance != null) {
                                try {
                                    InstanceDrawing instanceDrawing =
                                        CPNInstanceDrawing.getInstanceDrawing( // getMode(),
                                            netInstance);

                                    // NOTICEredundant
                                    plugin.processFigureException(
                                        ErrorApi.createErrorState(
                                            "Renew: Save simulation error", message,
                                            instanceDrawing, figure),
                                        false);
                                } catch (RemoteException e) {
                                    logger.error(e.getMessage(), e);
                                    JOptionPane.showMessageDialog(
                                        null,
                                        "A problem occurred: " + e
                                            + "\nSee the console for details.",
                                        "Renew", JOptionPane.ERROR_MESSAGE);
                                }
                            } else {
                                plugin.processFigureException(
                                    ErrorApi.createErrorState(
                                        "Renew: Save simulation error", message, drawing, figure),
                                    false);
                            }
                        }
                    }
                }
            }
        } catch (RemoteException e) {
            logger.error(e.getMessage(), e);
            JOptionPane.showMessageDialog(
                null, "A problem occurred: " + e + "\n" + "See the console for details.", "Renew",
                JOptionPane.ERROR_MESSAGE);
        }
    }

    /**
     * Initialize the construction of a system net in the Net-in-Net formalism
     *
     * @param app current {@link CPNApplication}
     * @return the {@link SystemNetBuilder}
     */
    private SystemNetBuilder initSystemNet(CPNApplication app) {
        if (!(app.syntaxCheck())) {
            app.showStatus("Invalid net syntax!");
            return null;
        }
        // Close system net if already opened
        Enumeration<Drawing> drawingEnumeration = app.drawings();
        while (drawingEnumeration.hasMoreElements()) {
            Drawing drawing = drawingEnumeration.nextElement();
            String drawingName = drawing.getName();
            if (drawingName.equals("SystemNet")) {
                app.closeDrawing(drawing);
            } else if (drawingName.matches(".*\\d.*")) {
                app.showStatus("Net name contains integer!");
                return null;
            }
        }
        ConstructionGUI constructionSettings = new ConstructionGUI(app);
        SystemNetBuilder systemNetBuilder = constructionSettings.getNetBuilder();
        if (systemNetBuilder != null) {
            CPNDrawing systemNet = systemNetBuilder.getSystemNet();
            app.openDrawing(systemNet);
        }
        return systemNetBuilder;
    }

    /**
     * Method to open all net instances which are currently simulated
     * and held in a place in a system net.
     *
     * @param primaryInstance this net instance hold references to other instances
     * @param systemNetBuilder get the reference place ID from this
     * @param app open the instances with this
     */
    private void openAllSimulatedNetInstances(
        NetInstanceAccessor primaryInstance, SystemNetBuilder systemNetBuilder, CPNApplication app)
    {
        try {
            NetAccessor net = primaryInstance.getNet();
            int referencePlaceID = systemNetBuilder.getReferencePlaceID();
            for (NetElementID netElementID : net.getPlaceIDs()) {
                if (netElementID.getFigureID() == referencePlaceID) {
                    MarkingAccessor markAccess =
                        primaryInstance.getPlaceInstance(netElementID).getMarking();
                    int numOfReferences = markAccess.getDistinctTokenCount();
                    for (int i = 0; i < numOfReferences; i++) {
                        app.openInstanceDrawing(markAccess.getToken(i).asNetInstance());
                    }
                }
            }
        } catch (RemoteException e) {
            logger.error("Could not open net instances.");
        }
    }

    /**
     * This method fires a transition in the given system net instance,
     * which is responsible for creating new net instances.
     *
     * @param primaryInstance fire a transition in this net instance
     * @param systemNetBuilder get the creation transition ID from this
     */
    private void fireCreateTransition(
        NetInstanceAccessor primaryInstance, SystemNetBuilder systemNetBuilder)
    {
        // Alternative: just run getSimulation().simulationStep()
        try {
            int createTransitionID = systemNetBuilder.getCreateTransitionID();
            NetAccessor net = primaryInstance.getNet();
            for (NetElementID netElementID : net.getTransitionIDs()) {
                if (netElementID.getFigureID() == createTransitionID) {
                    primaryInstance.getTransitionInstance(netElementID).fireOneBinding();
                    //Necessary? getSimulation().simulationRefresh();
                }
            }
        } catch (RemoteException e) {
            logger.error("Could not create new net instances.");
        }
    }

    protected CPNSimulation getSimulation() {
        return ModeReplacement.getInstance().getSimulation();
    }

    private class SimulationNetStepCommand extends AbstractCommand {
        public SimulationNetStepCommand(String name) {
            super(name);
        }

        @Override
        public void execute() {
            SimulationThreadPool.getCurrent().execute(() -> {
                CPNSimulation simulation = SimulationMenuCreator.this.getSimulation();

                if (!simulation.isSimulationActive()) {
                    initSimulation(0);
                    return;
                }

                GuiPlugin plugin = GuiPlugin.getCurrent();
                BreakpointManager bpm = simulation.getBreakpointManager();

                // add Breakpoint.FIRECOMPLETE to each
                // transition in the
                // current selected net instance
                DrawingView view = EditorApi.getCurrentEditor().view();
                Drawing drawing = view.drawing();

                if (!(drawing instanceof CPNInstanceDrawing)) {
                    plugin.showStatus("A net instance drawing must be selected.");
                    return;
                }

                List<Figure> figures = Collections.list(drawing.figures());
                List<Breakpoint> addedBPs = new ArrayList<>();

                for (Figure figure : figures) {
                    if (figure instanceof TransitionInstanceFigure tiFigure) {
                        TransitionInstanceAccessor accessor = tiFigure.getInstance();

                        if (!(accessor instanceof TransitionInstanceAccessorImpl)) {
                            plugin.showStatus(
                                "The net step feature is only enabled for local simulations.");
                            return;
                        }

                        TransitionInstance instance =
                            ((TransitionInstanceAccessorImpl) accessor).getTransitionInstance();
                        addedBPs.add(
                            bpm.createTransitionInstanceBreakpoint(
                                instance, Breakpoint.FIRECOMPLETE));
                    }
                }

                HitListener listener = new HitListener(addedBPs, plugin, simulation);
                bpm.addBreakpointHitListener(listener);

                // run simulation until a breakpoint
                // occurs
                plugin.showStatus("waiting for next transition firing in this net instance...");
                simulation.simulationRun();
            });

        }
    }

    private static class HitListener implements BreakpointHitListener {
        private final List<Breakpoint> bps;
        private final StatusDisplayer statusDisplayer;
        private final CPNSimulation simulation;

        private HitListener(
            List<Breakpoint> bps, StatusDisplayer statusDisplayer, CPNSimulation simulation)
        {
            this.bps = bps;
            this.statusDisplayer = statusDisplayer;
            this.simulation = simulation;
        }

        @Override
        public void hitBreakpoint(BreakpointHitEvent event) {
            if (bps.contains(event.getBp())) {
                event.consume();

                if (simulation.isSimulationActive()) {
                    statusDisplayer.showStatus("Simulation halted.");
                } else {
                    statusDisplayer.showStatus("Simulation terminated.");
                }

                BreakpointManager bpm = simulation.getBreakpointManager();

                // remove all added breakpoints
                for (Breakpoint bp : bps) {
                    bpm.deleteBreakpoint(bp);
                }

                // remove this listener from
                // BreakpointManager
                bpm.removeBreakpointHitListener(this);
            }
        }
    }

    private class SimulationHaltCommand extends AbstractCommand {
        public SimulationHaltCommand() {
            super("Halt simulation");
        }

        @Override
        public boolean isExecutable() {
            if (!super.isExecutable()) {
                return false;
            }
            return SimulationMenuCreator.this.getSimulation().isSimulationActive();
        }

        @Override
        public void execute() {
            SimulationMenuCreator.this.getSimulation().simulationStop();
            GuiPlugin.getCurrent().showStatus("Simulation halted.");
        }
    }

    private class SimulationTerminateCommand extends AbstractCommand {
        public SimulationTerminateCommand() {
            super("Terminate simulation");
        }

        @Override
        public boolean isExecutable() {
            if (!super.isExecutable()) {
                return false;
            }
            return SimulationMenuCreator.this.getSimulation().isSimulationActive();
        }

        @Override
        public void execute() {
            SimulationMenuCreator.this.getSimulation().simulationTerminate();
        }
    }

    private class SaveSimulationStateCommand extends AbstractCommand {
        public SaveSimulationStateCommand() {
            super("Save simulation state...");
        }

        @Override
        public boolean isExecutable() {
            if (!super.isExecutable()) {
                return false;
            }
            return SimulationMenuCreator.this.getSimulation().isSimulationActive();
        }

        @Override
        public void execute() {
            promptSaveSimulationState();
        }
    }

    private class LoadSimulationStateCommand extends AbstractCommand {
        public LoadSimulationStateCommand() {
            super("Load simulation state...");
        }

        @Override
        public void execute() {
            promptLoadSimulationState();
        }
    }
}