package de.renew.gui;

import java.awt.EventQueue;
import java.awt.Toolkit;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import javax.swing.JMenu;
import javax.swing.JMenuItem;

import CH.ifa.draw.DrawPlugin;
import CH.ifa.draw.application.DrawApplication;
import CH.ifa.draw.framework.FigureWithID;
import CH.ifa.draw.util.CommandMenu;
import CH.ifa.draw.util.CommandMenuItem;
import de.renew.draw.storables.ontology.Figure;
import de.renew.draw.storables.ontology.FigureEnumeration;
import de.renew.draw.ui.api.ErrorApi;
import de.renew.draw.ui.api.MenuApi;
import de.renew.draw.ui.ontology.ErrorState;
import de.renew.engine.thread.SimulationThreadPool;
import de.renew.net.Net;
import de.renew.net.NetElementID;
import de.renew.net.NetInstance;
import de.renew.net.NetLookup;
import de.renew.net.Place;
import de.renew.net.PlaceInstance;
import de.renew.net.Transition;
import de.renew.net.TransitionInstance;
import de.renew.net.event.PlaceEventProducer;
import de.renew.net.event.TransitionEventProducer;
import de.renew.plugin.PluginProperties;
import de.renew.remote.NetInstanceAccessor;
import de.renew.remote.RemotePlugin;
import de.renew.simulatorontology.loading.NetNotFoundException;
import de.renew.simulatorontology.shadow.ShadowCompilationResult;
import de.renew.simulatorontology.simulation.SimulationEnvironment;
import de.renew.simulatorontology.simulation.SimulatorExtension;


/**
 * Manages Breakpoints and keeps all information together needed by {@link Breakpoint}
 * objects.
 *
 * <p>Breakpoints are objects attached to transitions or places. Breakpoints can be bound
 * to one specific net instance or can exist within all instances of a net, depending on which
 * element they observe.
 *
 * @author Michael Duvigneau
 */
public class BreakpointManager implements SimulatorExtension {
    private static final int MENU_SHORTCUT_KEY_MASK =
        Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
    private static final org.apache.log4j.Logger logger =
        org.apache.log4j.Logger.getLogger(BreakpointManager.class);
    private CPNSimulation simulation;

    private CommandMenu breakpointSimulationMenu = null;
    private CommandMenu breakpointNetMenu = null;
    private final Vector<Breakpoint> breakpoints = new Vector<>();
    private final Vector<BreakpointHitListener> breakpointHitListeners = new Vector<>();
    private final Map<Breakpoint, JMenuItem> breakpointMenuItems = new HashMap<>();

    //Hotkey property names
    public static final String BP_NONE_HOTKEY_PROP_NAME = "de.renew.hotkeymap.gui.breakpointnone";

    public static final String BP_CLEAR_HOTKEY_PROP_NAME = "de.renew.hotkeymap.gui.breakpointclear";

    public static final String BP_DEFAULT_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointdefault";

    public static final String BP_FIRING_STARTS_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointfiringstarts";

    public static final String BP_FIRING_COMPLETES_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointfiringcompletes";

    public static final String BP_MARKING_CHANGES_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointmarkingchanges";

    public static final String BP_MARKING_CHANGES_IRGNORE_TEST_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointmarkingchangesignoringtestarcs";

    public static final String BP_PLUS_ONE_TOKEN_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointplusonetoken";

    public static final String BP_MINUS_ONE_TOKEN_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointminusonetoken";

    public static final String BP_TEST_STATUS_CHANGES_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointteststatuschanges";

    public static final String BP_GLOBAL_CLEAR_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointclear";

    public static final String BP_GLOBAL_DEFAULT_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointsglobaldefault";

    public static final String BP_GLOBAL_FIRING_STARTS_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointsglobalfiringstarts";

    public static final String BP_GLOBAL_FIRING_COMPLETES_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointsglobalfiringcompletes";

    public static final String BP_GLOBAL_MARKING_CHANGES_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointsglobalmarkingchanges";

    public static final String BP_GLOBAL_MARKING_CHANGES_IRGNORE_TEST_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointsglobalmarkingchangesignoringtestarcs";

    public static final String BP_GLOBAL_PLUS_ONE_TOKEN_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointsglobalplusonetoken";

    public static final String BP_GLOBAL_MINUS_ONE_TOKEN_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointsglobalminusonetoken";

    public static final String BP_GLOBAL_TEST_STATUS_CHANGES_HOTKEY_PROP_NAME =
        "de.renew.hotkeymap.gui.breakpointsglobalteststatuschanges";

    /**
     * Keeps the hit messages generated by breakpoints to be shown in a single
     * window. This may be wanted if several breakpoints occur in a very short
     * time span. Instead of a single breakpoint hit message all messages will
     * be returned.
     */
    private StringBuffer messageMemory = new StringBuffer();

    /**
     * Creates a new {@link BreakpointManager} for the specified {@code simulation}.
     *
     * @param simulation The simulation monitored by the new {@link BreakpointManager} instance
     */
    public BreakpointManager(CPNSimulation simulation) {
        setSimulation(simulation);
    }

    public void setSimulation(CPNSimulation sim) {
        this.simulation = sim;
    }

    /**
     * Lazily creates and returns a menu for the breakpoint manager to be displayed
     * in the "Net" menu.
     *
     * @return A new menu instance on the first call, the same menu instance on subsequent calls
     */
    public JMenu getNetMenu(PluginProperties plugincfg) {
        if (breakpointNetMenu == null) {
            breakpointNetMenu =
                createSetMenu("Breakpoint", ToggleBreakpointCommand.PRESET, plugincfg);

            breakpointNetMenu.addSeparator();
            breakpointNetMenu.add(
                new ToggleBreakpointCommand(
                    "none", this, ToggleBreakpointCommand.REMOVE, ToggleBreakpointCommand.PRESET),
                PluginProperties.getUserProperties().getHotkeyProperty(
                    BP_NONE_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_NONE_HOTKEY_PROP_NAME)));

        }
        return breakpointNetMenu;
    }

    public CPNApplication getGui() {
        // this cant be done: the gui might be closed in between
        // if (application == null) {
        // application = (CPNApplication) DrawPlugin.getGui();
        // }
        // return application;
        return (CPNApplication) DrawPlugin.getGui();
    }

    /**
     * Lazily creates and returns the breakpoint menu of this manager to be displayed
     * in the "Simulation" menu.
     *
     * @return A new menu instance on the first call, the same menu instance on subsequent calls
     */
    public JMenu getSimulationMenu(PluginProperties plugincfg) {
        if (breakpointSimulationMenu == null) {
            CommandMenu menu = DrawApplication.createCommandMenu("Breakpoints");

            menu.add(createSetMenus("Set BP at selection", plugincfg));
            menu.add(createClearMenus("Clear BP at selection", plugincfg));

            menu.add(
                new ClearAllBreakpointsCommand(
                    "Clear all BPs in current simulation", this, simulation),
                PluginProperties.getUserProperties().getHotkeyProperty(
                    BP_NONE_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_NONE_HOTKEY_PROP_NAME)));
            menu.addSeparator();
            breakpointSimulationMenu = menu;
        }
        return breakpointSimulationMenu;
    }

    /**
     * Returns a menu consisting of two setMenus, one for global breakpoints,
     * one for local ones.
     */
    protected CommandMenu createSetMenus(String title, PluginProperties plugincfg) {
        CommandMenu menu = DrawApplication.createCommandMenu(title);

        menu.add(createSetMenu("local", ToggleBreakpointCommand.LOCAL, plugincfg));
        menu.add(createSetMenu("global", ToggleBreakpointCommand.GLOBAL, plugincfg));

        return menu;
    }

    /**
     * Creates a menu with commands to set breakpoints for any of the available breakpoint modes.
     *
     * @param title the title of the new menu
     * @param localGlobal either local or global context. Other values are not permitted
     * @return a new menu for setting breakpoints
     */
    protected CommandMenu createSetMenu(String title, int localGlobal, PluginProperties plugincfg) {
        CommandMenu menu = DrawApplication.createCommandMenu(title);
        PluginProperties prop = PluginProperties.getUserProperties();

        if (localGlobal == ToggleBreakpointCommand.GLOBAL) {
            menu.add(
                new ToggleBreakpointCommand(
                    "default (t,p)", this, ToggleBreakpointCommand.ADD, localGlobal),
                prop.getHotkeyProperty(
                    BP_GLOBAL_DEFAULT_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_GLOBAL_DEFAULT_HOTKEY_PROP_NAME)));
            menu.add(
                new ToggleBreakpointCommand(
                    "firing starts (t)", this, ToggleBreakpointCommand.ADD, localGlobal,
                    Breakpoint.FIRE),
                prop.getHotkeyProperty(
                    BP_GLOBAL_FIRING_STARTS_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_GLOBAL_FIRING_STARTS_HOTKEY_PROP_NAME)));
            menu.add(
                new ToggleBreakpointCommand(
                    "firing completes (t)", this, ToggleBreakpointCommand.ADD, localGlobal,
                    Breakpoint.FIRECOMPLETE),
                prop.getHotkeyProperty(
                    BP_GLOBAL_FIRING_COMPLETES_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_GLOBAL_FIRING_COMPLETES_HOTKEY_PROP_NAME)));
            menu.add(
                new ToggleBreakpointCommand(
                    "marking changes (p)", this, ToggleBreakpointCommand.ADD, localGlobal,
                    Breakpoint.MARKINGCHANGE),
                prop.getHotkeyProperty(
                    BP_GLOBAL_MARKING_CHANGES_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_GLOBAL_MARKING_CHANGES_HOTKEY_PROP_NAME)));
            menu.add(
                new ToggleBreakpointCommand(
                    "marking changes, ignoring test arcs (p)", this, ToggleBreakpointCommand.ADD,
                    localGlobal, Breakpoint.MARKINGCHANGENOTEST),
                prop.getHotkeyProperty(
                    BP_GLOBAL_MARKING_CHANGES_IRGNORE_TEST_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(
                        BP_GLOBAL_MARKING_CHANGES_IRGNORE_TEST_HOTKEY_PROP_NAME)));
            menu.add(
                new ToggleBreakpointCommand(
                    "+ 1 token (p)", this, ToggleBreakpointCommand.ADD, localGlobal,
                    Breakpoint.TOKENADDED),
                prop.getHotkeyProperty(
                    BP_GLOBAL_PLUS_ONE_TOKEN_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_GLOBAL_PLUS_ONE_TOKEN_HOTKEY_PROP_NAME)));
            menu.add(
                new ToggleBreakpointCommand(
                    "- 1 token (p)", this, ToggleBreakpointCommand.ADD, localGlobal,
                    Breakpoint.TOKENREMOVED),
                prop.getHotkeyProperty(
                    BP_GLOBAL_MINUS_ONE_TOKEN_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_GLOBAL_MINUS_ONE_TOKEN_HOTKEY_PROP_NAME)));
            menu.add(
                new ToggleBreakpointCommand(
                    "test status changes (p)", this, ToggleBreakpointCommand.ADD, localGlobal,
                    Breakpoint.TOKENTESTCHANGE),
                prop.getHotkeyProperty(
                    BP_GLOBAL_TEST_STATUS_CHANGES_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_GLOBAL_TEST_STATUS_CHANGES_HOTKEY_PROP_NAME)));
        } else {
            menu.add(
                new ToggleBreakpointCommand(
                    "default (t,p)", this, ToggleBreakpointCommand.ADD, localGlobal),
                prop.getHotkeyProperty(
                    BP_DEFAULT_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_DEFAULT_HOTKEY_PROP_NAME)));
            menu.add(
                new ToggleBreakpointCommand(
                    "firing starts (t)", this, ToggleBreakpointCommand.ADD, localGlobal,
                    Breakpoint.FIRE),
                prop.getHotkeyProperty(
                    BP_FIRING_STARTS_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_FIRING_STARTS_HOTKEY_PROP_NAME)));
            menu.add(
                new ToggleBreakpointCommand(
                    "firing completes (t)", this, ToggleBreakpointCommand.ADD, localGlobal,
                    Breakpoint.FIRECOMPLETE),
                prop.getHotkeyProperty(
                    BP_FIRING_COMPLETES_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_FIRING_COMPLETES_HOTKEY_PROP_NAME)));
            menu.add(
                new ToggleBreakpointCommand(
                    "marking changes (p)", this, ToggleBreakpointCommand.ADD, localGlobal,
                    Breakpoint.MARKINGCHANGE),
                prop.getHotkeyProperty(
                    BP_MARKING_CHANGES_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_MARKING_CHANGES_HOTKEY_PROP_NAME)));
            menu.add(
                new ToggleBreakpointCommand(
                    "marking changes, ignoring test arcs (p)", this, ToggleBreakpointCommand.ADD,
                    localGlobal, Breakpoint.MARKINGCHANGENOTEST),
                prop.getHotkeyProperty(
                    BP_MARKING_CHANGES_IRGNORE_TEST_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_MARKING_CHANGES_IRGNORE_TEST_HOTKEY_PROP_NAME)));
            menu.add(
                new ToggleBreakpointCommand(
                    "+ 1 token (p)", this, ToggleBreakpointCommand.ADD, localGlobal,
                    Breakpoint.TOKENADDED),
                prop.getHotkeyProperty(
                    BP_PLUS_ONE_TOKEN_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_PLUS_ONE_TOKEN_HOTKEY_PROP_NAME)));
            menu.add(
                new ToggleBreakpointCommand(
                    "- 1 token (p)", this, ToggleBreakpointCommand.ADD, localGlobal,
                    Breakpoint.TOKENREMOVED),
                prop.getHotkeyProperty(
                    BP_MINUS_ONE_TOKEN_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_MINUS_ONE_TOKEN_HOTKEY_PROP_NAME)));
            menu.add(
                new ToggleBreakpointCommand(
                    "test status changes (p)", this, ToggleBreakpointCommand.ADD, localGlobal,
                    Breakpoint.TOKENTESTCHANGE),
                prop.getHotkeyProperty(
                    BP_TEST_STATUS_CHANGES_HOTKEY_PROP_NAME,
                    plugincfg.getHotkeyProperty(BP_TEST_STATUS_CHANGES_HOTKEY_PROP_NAME)));
        }
        return menu;
    }

    /**
     * Returns a menu consisting of two clearMenus, one for global breakpoints,
     * one for local ones.
     */
    protected CommandMenu createClearMenus(String title, PluginProperties plugincfg) {
        CommandMenu menu = DrawApplication.createCommandMenu(title);

        menu.add(createClearMenu("local", ToggleBreakpointCommand.LOCAL, plugincfg));
        menu.add(createClearMenu("global", ToggleBreakpointCommand.GLOBAL, plugincfg));

        return menu;
    }

    /**
     * Creates a menu with commands to clear breakpoints in any available breakpoint trigger mode.
     *
     * @param title the title of the new menu
     * @param localGlobal either local or global context. Other values are not permitted
     * @return a new menu for setting breakpoints
     */
    protected CommandMenu createClearMenu(
        String title, int localGlobal, PluginProperties plugincfg)
    {
        CommandMenu menu = DrawApplication.createCommandMenu(title);

        PluginProperties prop = PluginProperties.getUserProperties();

        if (localGlobal == ToggleBreakpointCommand.GLOBAL) {
            menu.add(
                new CommandMenuItem(
                    new ToggleBreakpointCommand(
                        "any type", this, ToggleBreakpointCommand.REMOVE, localGlobal),
                    prop.getHotkeyProperty(
                        BP_GLOBAL_CLEAR_HOTKEY_PROP_NAME,
                        plugincfg.getHotkeyProperty(BP_GLOBAL_CLEAR_HOTKEY_PROP_NAME))));
        } else {
            menu.add(
                new CommandMenuItem(
                    new ToggleBreakpointCommand(
                        "any type", this, ToggleBreakpointCommand.REMOVE, localGlobal),
                    prop.getHotkeyProperty(
                        BP_CLEAR_HOTKEY_PROP_NAME,
                        plugincfg.getHotkeyProperty((BP_CLEAR_HOTKEY_PROP_NAME)))));
        }
        return menu;
    }

    /**
     * Returns a menu with show and clear breakpoint commands concerning the
     * specified breakpoint.
     */
    protected CommandMenu createBPMenu(final Breakpoint bp) {
        CommandMenu menu = DrawApplication.createCommandMenu(bp.toString());

        menu.add(MenuApi.createMenuItem("show", () -> showBreakpoint(bp)));
        menu.add(MenuApi.createMenuItem("clear", () -> deleteBreakpoint(bp)));
        return menu;
    }

    /**
     * Creates a breakpoint at the given transition instance.
     *
     * <p>The breakpoint is added to the internal list of known breakpoints automatically.
     *
     * @param instance the transition instance to observe
     * @param mode on which event to react on
     * @return the created breakpoint or {@code null} if the specified {@code trigger} is invalid
     */
    public Breakpoint createTransitionInstanceBreakpoint(TransitionInstance instance, int mode) {
        return createTransitionListenerBreakpoint(
            instance, mode, instance.getNetInstance().getNet());
    }

    /**
     * Creates one breakpoint at the given transition which will be hit at any
     * instance of that transition.
     *
     * <p>The created breakpoint is added to the internal list of known breakpoints automatically.
     *
     * @param transition the transition to observe
     * @param mode on which event to react on
     * @param net the net the transition belongs to
     * @return the created breakpoint or {@code null} if the specified {@code mode} is invalid
     */
    public Breakpoint createTransitionBreakpoint(Transition transition, int mode, Net net) {
        return createTransitionListenerBreakpoint(transition, mode, net);
    }

    /**
     * Creates a breakpoint at the given producer for the specified trigger and net.
     *
     * <p>The created breakpoint is added tot the internal list of known breakpoints automatically.
     *
     * @param producer the producer of the breakpoint that is being observed
     * @param mode the trigger under which the {@code producer} is observed
     * @param net the net in which the producer is observed
     * @return the created breakpoint or {@code null} if the specified {@code mode} is invalid
     */
    private Breakpoint createTransitionListenerBreakpoint(
        TransitionEventProducer producer, int mode, Net net)
    {
        if (!isValidTransitionMode(mode)) {
            return null;

        }
        TransitionInstanceBreakpoint newBreakpoint =
            new TransitionInstanceBreakpoint(this, producer, mode, net);

        addBreakpoint(newBreakpoint);
        return newBreakpoint;
    }

    /**
     * Creates a breakpoint at the given place instance.
     *
     * <p>The created breakpoint is added to the internal list of known breakpoints
     * automatically.
     *
     * @param instance the place instance to observe
     * @param mode the event to react on
     * @return the created breakpoint or {@code null} if the specified {@code mode} is invalid
     */
    public Breakpoint createPlaceInstanceBreakpoint(PlaceInstance instance, int mode) {
        return createPlaceListenerBreakpoint(instance, mode, instance.getNetInstance().getNet());
    }

    /**
     * Creates one breakpoint at the given place, which will be hit at any instance of that place.
     *
     * <p>The created breakpoint is added to the internal list of known breakpoints
     * automatically.
     *
     * @param place the place to observe
     * @param mode the event to react on
     * @param net the net the place belongs to
     * @return the created breakpoint or {@code null} if the specified {@code mode} is invalid
     */
    public Breakpoint createPlaceBreakpoint(Place place, int mode, Net net) {
        return createPlaceListenerBreakpoint(place, mode, net);
    }

    /**
     * Creates a breakpoint at the given producer for the specified trigger and net.
     *
     * <p>The created breakpoint is added tot the internal list of known breakpoints automatically.
     *
     * @param producer the producer of the breakpoint that is being observed
     * @param mode the trigger under which the {@code producer} is observed
     * @param net the net in which the producer is observed
     * @return the created breakpoint or {@code null} if the specified {@code mode} is invalid
     */
    private Breakpoint createPlaceListenerBreakpoint(
        PlaceEventProducer producer, int mode, Net net)
    {
        if (!isValidPlaceMode(mode)) {
            return null;

        }
        PlaceInstanceBreakpoint newBreakpoint =
            new PlaceInstanceBreakpoint(this, producer, mode, net);

        addBreakpoint(newBreakpoint);
        return newBreakpoint;
    }

    /**
     * Returns a list of all {@link Breakpoint}s set for the given net element.
     *
     * @param netElement the net element
     * @return the list of breakpoints
     */
    public List<Breakpoint> getBreakpointsAt(Object netElement) {
        List<Breakpoint> result = new ArrayList<>();
        for (Breakpoint bp : breakpoints) {
            if (bp.getTaggedElement() == netElement) {
                result.add(bp);
            }
        }
        return result;
    }

    /**
     * Checks whether a {@link Breakpoint} has been set for the given
     * net element.
     *
     * @param netElement the net element
     * @return {@code true} if there is at least one breakpoint set, {@code false} otherwise
     */
    public boolean isBreakpointSetAt(Object netElement) {
        // Instead of returning getBreakpointsAt(netElement).size() > 0, we
        // iterate the list to save the overhead of constructing a list.
        for (Breakpoint bp : breakpoints) {
            if (bp.getTaggedElement() == netElement) {
                return true;
            }
        }
        return false;
    }

    /**
     * Deletes all breakpoints attached to the given place, transition or place
     * or transition instance.
     *
     * <p>Note: If a transition is given, breakpoints attached to local instances of
     * that transition will not be deleted. Similarly, if a transition instance is given,
     * breakpoints attached to the transition will not be deleted.
     *
     * @param netElement the place, transition or place or transition instance which
     *                   should be freed of breakpoints.
     * @return the number of deleted breakpoints
     */
    public int deleteBreakpointsAt(Object netElement) {
        List<Breakpoint> points = getBreakpointsAt(netElement);
        for (Breakpoint bp : points) {
            deleteBreakpoint(bp);
        }
        return points.size();
    }

    /**
     * Clears all currently known breakpoints. This method should be called at
     * least when the current simulation is terminated.
     */
    public void deleteAllBreakpoints() {
        // As the deleteBreakpoint call will always call
        // breakpoints.remove(...) we would run into a
        // ConcurrentModificationException when using an
        // Iterator to find and remove all breakpoints.
        // Thus we just keep deleting the last breakpoint
        // until no breakpoints are left.
        int size;
        while ((size = breakpoints.size()) > 0) {
            deleteBreakpoint(breakpoints.get(size - 1));
        }
    }

    /**
     * Clears the specified breakpoint. Releases its resources and removes it
     * from the list of known breakpoints.
     */
    public void deleteBreakpoint(Breakpoint bp) {
        bp.release();
        removeBreakpoint(bp);
    }

    /**
     * Attaches global breakpoints to all places or transitions with breakpoint
     * attribute at the corresponding graphical figure. Traverses the drawings
     * of all nets included in the given shadow lookup, if they are opened.
     * Takes care not to request drawings from the drawing loader.
     *
     * <p>There is no check if any of these breakpoints already exist. So this
     * method should be called only once per simulation.
     *
     * @param result the compilation result of a shadow net to which breakpoints should be attached
     */
    protected void addPresetBreakpoints(ShadowCompilationResult result) {
        CPNDrawing currentDrawing;
        FigureEnumeration figures;
        Figure currentFigure;
        Integer currentMode;
        TransitionFigure currentTransFig;
        Transition currentTransition;
        PlaceFigure currentPlaceFig;
        Place currentPlace;
        Net currentNet;
        Breakpoint bp;
        CPNDrawingLoader drawingLoader = ModeReplacement.getInstance().getDrawingLoader();

        for (String currentNetName : result.allNewlyCompiledNetNames()) {
            try {
                currentNet = new NetLookup().findForName(currentNetName);
            } catch (NetNotFoundException e) {
                logger.error("Could not find net: " + currentNetName);
                return;
            }

            // To stay with the old behaviour, we deactivate the
            // drawing loading mechanism when looking for drawings.
            // This way only breakpoints for nets with open
            // drawings will be set, no new drawings will be opened.
            // TODO: This behaviour should be switchable.
            currentDrawing = drawingLoader.getDrawing(currentNetName, false);
            if (currentDrawing == null) {
                logger.debug(
                    "BreakpointManager: Could not add preset breakpoints for net " + currentNetName
                        + ": no drawing found.");
            } else {
                if (logger.isDebugEnabled()) {
                    logger.debug(
                        BreakpointManager.class.getName() + ": Looking for breakpoints in "
                            + currentNetName);
                }
                figures = currentDrawing.figures();
                while (figures.hasMoreElements()) {
                    currentFigure = figures.nextFigure();
                    currentMode = (Integer) currentFigure.getAttribute(Breakpoint.ATTRIBUTENAME);
                    if (currentMode != null) {
                        if (currentFigure instanceof TransitionFigure) {
                            currentTransFig = (TransitionFigure) currentFigure;
                            int id = currentTransFig.getID();
                            currentTransition =
                                currentNet.getTransitionWithID(new NetElementID(id));
                            // in most cases this simple NetElementID should
                            // suffice
                            if (currentTransition == null) {
                                logger.warn(
                                    BreakpointManager.class.getName()
                                        + ": Found no Transition for Breakpoint on Figure "
                                        + currentTransFig);
                            } else {
                                bp = createTransitionBreakpoint(
                                    currentTransition, currentMode, currentNet);
                                logger.debug("Created Breapoint: " + bp);
                            }
                        } else if (currentFigure instanceof PlaceFigure) {
                            currentPlaceFig = (PlaceFigure) currentFigure;
                            int id = currentPlaceFig.getID();
                            currentPlace = currentNet.getPlaceWithID(new NetElementID(id));
                            // in most cases this simple NetElementID should
                            // suffice
                            if (currentPlace == null) {
                                logger.warn(
                                    BreakpointManager.class.getName()
                                        + ": Found no Place for Breakpoint on Figure "
                                        + currentPlaceFig);
                            } else {
                                bp = createPlaceBreakpoint(currentPlace, currentMode, currentNet);
                                if (logger.isDebugEnabled()) {
                                    logger.debug("Created Breapoint: " + bp);
                                }
                            }
                        }
                    }
                }
            }
        }
    }

    /**
     * Appends the given breakpoint to the end of the list of known breakpoints.
     *
     * <p>This method also ensures that the menu returned by {@link #getSimulationMenu()}
     * stay consistent.
     */
    private void addBreakpoint(Breakpoint newBreakpoint) {
        breakpoints.add(newBreakpoint);
        JMenuItem mi = breakpointSimulationMenu.add(createBPMenu(newBreakpoint));
        breakpointMenuItems.put(newBreakpoint, mi);
    }

    /**
     * Removes the given breakpoint from the list of known breakpoints.
     *
     * <p>This method also ensures that the menu returned by {@link #getSimulationMenu()}
     * stays consistent.
     */
    private void removeBreakpoint(Breakpoint breakpoint) {
        breakpoints.remove(breakpoint);
        final JMenuItem mi = breakpointMenuItems.remove(breakpoint);

        // There was some problem with the net step command that caused a deadlock.
        // Removing the menuitems in the awt thread seems to fix it.
        EventQueue.invokeLater(() -> breakpointSimulationMenu.remove(mi));
    }

    /**
     * Signals to the simulator that the simulation should be stopped (which is what breakpoints do).
     *
     * <p>This method is intended for cases where the simulation needs to be stopped from within the
     * simulation. In most cases {@link de.renew.simulatorontology.simulation.Simulator#stopRun()}
     * should be used. After this method returns the simulation may still be running for an
     * undetermined amount of time.
     */
    protected void stopSimulation() {
        // The extra thread is needed to avoid deadlock situations within
        // {@link Simulator#stopRun()} implementations. The deadlock occurs because
        // the {@link Simulator#stopRun()} methods try to ensure that the simulation
        // has stopped, but the execution of this breakpoint is part of the running
        // simulation - and it would stop only after the {@link Simulator#stopRun()}
        // waiting condition is met.
        SimulationThreadPool.getCurrent().execute(simulation::simulationStop);
    }

    /**
     * Informs the BreakpointManager that the simulation has hit a breakpoint.
     *
     * @param breakpoint the breakpoint that was hit
     */
    protected void informHitBreakpoint(Breakpoint breakpoint) {
        // sent BreakpointHitEvent to all registered BreakpointHitListeners
        BreakpointHitEvent event = new BreakpointHitEvent(breakpoint);

        BreakpointHitListener[] listeners =
            this.breakpointHitListeners.toArray(new BreakpointHitListener[] { });
        for (BreakpointHitListener listener : listeners) {
            listener.hitBreakpoint(event);
        }

        if (!(event.isConsumed())) {
            showHitBreakpoint(breakpoint);
        }
    }

    /**
     * Shows a message dialog telling that the given breakpoint was hit and
     * selects its associated net element figure. Used by breakpoints when they
     * occur.
     *
     * <p>This method detaches from the current thread and synchronises its body
     * with the AWT event queue. This behaviour is needed because this method is
     * mostly called from simulation threads which should not wait on AWT-only
     * locks.
     *
     * @param breakpoint the breakpoint which was hit
     */
    protected void showHitBreakpoint(final Breakpoint breakpoint) {
        EventQueue.invokeLater(() -> {
            DrawPlugin.getGui().showStatus("A breakpoint was hit: " + breakpoint + ".");
            ErrorState e = locateBreakpoint(breakpoint, true);

            if (e != null) {
                GuiPlugin.getCurrent().processFigureException(e, true);
            }
        });
    }

    /**
     * Selects the associated net element figure of the given Breakpoint. Used
     * by breakpoint menu entries of kind "Show breakpoint".
     *
     * @param breakpoint the breakpoint to show.
     */
    protected void showBreakpoint(Breakpoint breakpoint) {
        assert EventQueue.isDispatchThread()
            : "showBreakpoint must be called within AWT event queue.";
        DrawPlugin.getGui().showStatus("Selecting " + breakpoint + ".");
        ErrorState e = locateBreakpoint(breakpoint, false);

        if (e != null) {
            getGui().selectOffendingElements(e);
        }
    }

    /**
     * Adds the specified listener to the set of listeners managed by the breakpoint manager.
     *
     * <p>If the {@code listener} has been added previously it will be added a second time.
     *
     * @param listener the listener to be added
     */
    public void addBreakpointHitListener(BreakpointHitListener listener) {
        this.breakpointHitListeners.add(listener);
    }

    /**
     * Removes a registered {@link BreakpointHitListener}.
     *
     * <p>If {@code listener} has not been registered previously this is a noop.
     *
     * @param listener the listener to remove
     */
    public void removeBreakpointHitListener(BreakpointHitListener listener) {
        this.breakpointHitListeners.remove(listener);
    }

    /**
     * Empties the memory of generated breakpoint hit messages so they won't be
     * shown again with the next hit breakpoint.
     */
    public void clearLog() {
        messageMemory = new StringBuffer();
    }

    /**
     * Looks up the associated net element figure of the given breakpoint and
     * returns it as an {@link ErrorState} object. The hit element of the
     * breakpoint will be used, if {@code hit} is {@code true}, otherwise the
     * tagged element will be looked up.
     *
     * <p>The {@link ErrorState} will also include a generated hit message, if
     * requested. If the mapping to a Figure was not possible, the messages are
     * printed to System.err.
     *
     * @param breakpoint the breakpoint to look up
     * @param hit if {@code true}, a hit message is generated
     * @return the error state containing the message, the element figure
     *         and the drawing; {@code null} if no figure was found
     *
     * @see #showHitBreakpoint
     * @see #showBreakpoint
     */
    private ErrorState locateBreakpoint(Breakpoint breakpoint, boolean hit) {
        String message = "";
        String allMessages;
        Net net;
        NetInstance netInstance = null;
        int id;
        Object netElement = hit ? breakpoint.getHitElement() : breakpoint.getTaggedElement();

        if (netElement instanceof PlaceInstance placeInst) {
            id = placeInst.getPlace().getID().getFigureID();
            netInstance = placeInst.getNetInstance();
            net = netInstance.getNet();
            if (hit) {
                message = "Hit " + breakpoint + ".";
            }
        } else if (netElement instanceof TransitionInstance transitionInst) {
            id = transitionInst.getTransition().getID().getFigureID();
            netInstance = transitionInst.getNetInstance();
            net = netInstance.getNet();
            if (hit) {
                message = "Hit " + breakpoint + ".";
            }
        } else if (netElement instanceof Transition transition) {
            id = transition.getID().getFigureID();
            net = breakpoint.getTaggedNet();

            // No hit message as static transitions cannot fire.
        } else if (netElement instanceof Place place) {
            id = place.getID().getFigureID();
            net = breakpoint.getTaggedNet();

            // No hit message as static places cannot change anything.
        } else {
            logger.error("Cannot determine the location of breakpoint at " + netElement + "!?");
            return null;
        }

        if (hit) {
            allMessages = messageMemory.toString() + "\n" + message;
            messageMemory.append("(");
            messageMemory.append(message);
            messageMemory.append(")\n");
        } else {
            allMessages = message;
        }

        String netName = net.getName();
        CPNDrawing drawing = ModeReplacement.getInstance().getDrawingLoader().getDrawing(netName);

        if (drawing == null) {
            message = message + "\nSorry, cannot show the location of breakpoint at " + netElement
                + " because net " + netName + " is not loaded.";
            getGui().showStatus(message);
            logger.error(message);
            return null;
        } else if (netInstance == null) {
            FigureWithID figure = drawing.getFigureWithID(id);

            return ErrorApi.createErrorState("Renew: Breakpoint", allMessages, drawing, figure);
        } else {
            try {
                if (RemotePlugin.getInstance() != null) {
                    NetInstanceAccessor instanceAcc =
                        RemotePlugin.getInstance().wrapInstance(netInstance);
                    getGui().openInstanceDrawing(instanceAcc);
                    InstanceDrawing instanceDrawing =
                        CPNInstanceDrawing.getInstanceDrawing(instanceAcc);
                    FigureWithID figure = drawing.getFigureWithID(id);
                    InstanceFigure instanceFigure = instanceDrawing.getInstanceFigure(figure);

                    return ErrorApi.createErrorState(
                        "Renew: Breakpoint", allMessages, instanceDrawing, instanceFigure);
                } else {
                    return null;
                }
            } catch (RemoteException e) {
                logger.error(e.getMessage(), e);
                return null;
            }
        }
    }

    @Override
    public void simulationSetup(SimulationEnvironment env) {}

    @Override
    public void netsCompiled(ShadowCompilationResult result) {
        if (result.containsNewlyCompiledNets()) {
            addPresetBreakpoints(result);
        }
        clearLog();
    }

    @Override
    public void simulationTerminated() {
        deleteAllBreakpoints();
        clearLog();
    }

    @Override
    public void simulationTerminating() {}

    /**
     * Tells wether the given mode is valid for transition breakpoints.
     * <p>
     * Valid values are: DEFAULT, FIRE, FIRECOMPLETE.
     * </p>
     *
     * @return <code>true</code>, if the given breakpoint mode is applicable to
     *         transitions or transition instances. <br>
     *         <code>false</code>, otherwise.
     *
     * @see Breakpoint#DEFAULT
     * @see Breakpoint#FIRE
     * @see Breakpoint#FIRECOMPLETE
     */
    public static boolean isValidTransitionMode(int mode) {
        return (mode == Breakpoint.FIRE) || (mode == Breakpoint.FIRECOMPLETE)
            || (mode == Breakpoint.DEFAULT);
    }

    /**
     * Tells wether the given mode is valid for place breakpoints.
     * <p>
     * Valid values are: DEFAULT, MARKINGCHANGE, MARKINGCHANGENOTEST,
     * TOKENADDED, TOKENREMOVED, TOKENTESTCHANGE.
     * </p>
     *
     * @return <code>true</code>, if the given breakpoint mode is applicable to
     *         places or place instances. <br>
     *         <code>false</code>, otherwise.
     *
     * @see Breakpoint#DEFAULT
     * @see Breakpoint#MARKINGCHANGE
     * @see Breakpoint#MARKINGCHANGENOTEST
     * @see Breakpoint#TOKENADDED
     * @see Breakpoint#TOKENREMOVED
     * @see Breakpoint#TOKENTESTCHANGE
     */
    public static boolean isValidPlaceMode(int mode) {
        return (mode == Breakpoint.MARKINGCHANGE) || (mode == Breakpoint.MARKINGCHANGENOTEST)
            || (mode == Breakpoint.TOKENADDED) || (mode == Breakpoint.TOKENREMOVED)
            || (mode == Breakpoint.TOKENTESTCHANGE) || (mode == Breakpoint.DEFAULT);
    }
}
