package de.renew.gui;

import java.util.ArrayList;
import java.util.List;

import CH.ifa.draw.framework.UndoableCommand;
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.EditorApi;
import de.renew.draw.ui.ontology.DrawingView;
import de.renew.net.Net;
import de.renew.net.Place;
import de.renew.net.PlaceInstance;
import de.renew.net.Transition;
import de.renew.net.TransitionInstance;
import de.renew.remote.PlaceInstanceAccessor;
import de.renew.remote.PlaceInstanceAccessorImpl;
import de.renew.remote.TransitionInstanceAccessor;
import de.renew.remote.TransitionInstanceAccessorImpl;


/**
 * Base class for commands related to breakpoints (adding, removing, toggling) on
 * transition figures, place figures, or their instance figures.
 *
 * @author Michael Duvigneau
 */
public class ToggleBreakpointCommand extends UndoableCommand {

    /**
     * Operation mode:
     * Add breakpoints to the selected figures, regardless of whether
     * any breakpoints already exist.
     */
    public static final int ADD = 1;

    /**
     * Operation mode:
     * Remove all breakpoints from the selected figures that
     * already exist.
     */
    public static final int REMOVE = 2;

    /**
     * Operation context:
     * Force the creation/removal of global breakpoints.
     * E.g. try to find the static transition or place behind
     * any selected figure and add the breakpoint there.
     * This mode also allows to create preset breakpoints
     * in 'normal' CPNDrawings.
     */
    public static final int GLOBAL = 10;

    /**
     * Operation context:
     * Force the creation/removal of local breakpoints.
     * E.g. try to find the transition or place instance behind
     * any selected figure and add the breakpoint there.
     * This mode restricts the breakpoint manipulation to
     * CPNInstanceDrawings.
     */
    public static final int LOCAL = 11;

    /**
     * Operation context:
     * This mode allows to create preset breakpoints
     * in 'normal' CPNDrawings (marking node figures
     * with a breakpoint attribute).
     */
    public static final int PRESET = 12;
    private final BreakpointManager manager;

    private final int op;
    private final int context;
    private final int breakpointMode;

    /**
     * Constructs an add, remove or toggle breakpoint command
     * manipulating breakpoints with default mode.
     *
     * @param name    the command name
     * @param manager the manager to add the breakpoint to
     *                and to request necessary information from
     * @param op      the operation to be performed
     * @param context the context of the operation
     */
    public ToggleBreakpointCommand(String name, BreakpointManager manager, int op, int context) {
        this(name, manager, op, context, Breakpoint.DEFAULT);
    }

    /**
     * Creates a new instance with the specified values.
     *
     * <p>If Breakpoint removal is selected, the trigger will be ignored.
     *
     * @param name    the command name
     * @param manager the manager to add the breakpoint to
     *                and to request necessary information from.
     * @param op      the operation to perform
     * @param context the context of the operation
     * @param mode    the breakpoint trigger mode
     */
    public ToggleBreakpointCommand(
        String name, BreakpointManager manager, int op, int context, int mode)
    {
        super(name);
        this.manager = manager;
        this.op = op;
        this.context = context;
        this.breakpointMode = mode;
    }


    /**
     * Indicates if at least one figure is selected and the drawing type matches the context.
     *
     * <p>If exactly one figure is selected, this method also checks if the type of the figure
     * matches the chosen breakpoint mode.
     *
     * <p>Note that context {@link #PRESET} is only valid for CPNDrawings.
     *
     * @return {@code true} if the command is executable, {@code false} otherwise
     */
    @Override
    public boolean isExecutable() {
        if (!super.isExecutable()) {
            return false;
        }
        Drawing drawing = EditorApi.getCurrentDrawing();
        DrawingView view = getEditor().view();

        if (view.selectionCount() <= 0) {
            return false;
        } else if (view.selectionCount() > 1) {
            if (context == PRESET) {
                return (drawing instanceof CPNDrawing);
            } else {
                return (drawing instanceof CPNInstanceDrawing cpnInstanceDrawing)
                    && cpnInstanceDrawing.isLocal();
            }
        } else {
            Figure figure = view.selectionElements().nextElement();

            if (context == PRESET) {
                if (figure instanceof TransitionFigure) {
                    return BreakpointManager.isValidTransitionMode(breakpointMode);
                } else if (figure instanceof PlaceFigure) {
                    return BreakpointManager.isValidPlaceMode(breakpointMode);
                }
            } else if ((drawing instanceof CPNInstanceDrawing cpnInstanceDrawing)
                && cpnInstanceDrawing.isLocal()) {
                if (figure instanceof TransitionInstanceFigure) {
                    return BreakpointManager.isValidTransitionMode(breakpointMode);
                } else if ((figure instanceof PlaceInstanceFigure)
                    || (figure instanceof TokenBagFigure)) {
                    return BreakpointManager.isValidPlaceMode(breakpointMode);
                }
            }
            return false;
        }
    }

    @Override
    public boolean executeUndoable() {
        DrawingView view = getEditor().view();
        FigureEnumeration figures;
        Figure figure;
        int count = 0;
        Breakpoint bp;
        List<PlaceInstanceAccessor> memory = new ArrayList<>();

        if (isExecutable()) {
            if (!super.isExecutable()) {
                return false;
            }
            figures = view.selectionElements();
            while (figures.hasMoreElements()) {
                figure = figures.nextFigure();
                if (context == PRESET) {
                    if (figure instanceof TransitionFigure) {
                        switch (op) {
                            case ADD:
                                if (BreakpointManager.isValidTransitionMode(breakpointMode)) {
                                    figure.setAttribute(Breakpoint.ATTRIBUTENAME, breakpointMode);
                                    count++;
                                }
                                break;
                            case REMOVE:
                                if (figure.getAttribute(Breakpoint.ATTRIBUTENAME) != null) {
                                    figure.setAttribute(Breakpoint.ATTRIBUTENAME, null);
                                    count++;
                                }
                                break;
                        }
                    } else if (figure instanceof PlaceFigure) {
                        switch (op) {
                            case ADD:
                                if (BreakpointManager.isValidPlaceMode(breakpointMode)) {
                                    figure.setAttribute(Breakpoint.ATTRIBUTENAME, breakpointMode);
                                    count++;
                                }
                                break;
                            case REMOVE:
                                if (figure.getAttribute(Breakpoint.ATTRIBUTENAME) != null) {
                                    figure.setAttribute(Breakpoint.ATTRIBUTENAME, null);
                                    count++;
                                }
                                break;
                        }
                    }
                } else {
                    if (figure instanceof TransitionInstanceFigure) {
                        TransitionInstanceAccessor transitionInstAccessor =
                            ((TransitionInstanceFigure) figure).getInstance();
                        if (!(transitionInstAccessor instanceof TransitionInstanceAccessorImpl)) {
                            throw new IllegalStateException(
                                "Only the local simulation may set breakpoints");
                        }
                        TransitionInstance transitionInst =
                            ((TransitionInstanceAccessorImpl) transitionInstAccessor)
                                .getTransitionInstance();
                        Transition transition = transitionInst.getTransition();
                        Net net = transitionInst.getNetInstance().getNet();

                        switch (op) {
                            case ADD:
                                if (context == GLOBAL) {
                                    bp = manager.createTransitionBreakpoint(
                                        transition, breakpointMode, net);
                                } else {
                                    bp = manager.createTransitionInstanceBreakpoint(
                                        transitionInst, breakpointMode);
                                }
                                if (bp != null) {
                                    count++;
                                }
                                break;
                            case REMOVE:
                                if (context == GLOBAL) {
                                    count += manager.deleteBreakpointsAt(transition);
                                } else {
                                    count += manager.deleteBreakpointsAt(transitionInst);
                                }
                                break;
                        }
                    } else {
                        PlaceInstanceAccessor placeInstAccessor = null;

                        if (figure instanceof PlaceInstanceFigure) {
                            placeInstAccessor = ((PlaceInstanceFigure) figure).getInstance();
                        } else if (figure instanceof TokenBagFigure) {
                            placeInstAccessor = ((TokenBagFigure) figure).getPlaceInstance();


                            // Check if we already met this place instance
                            // while executing this command.
                            // It is possible to meet a place instance twice
                            // because its token bag figure may be selected
                            // in addition to its place instance figure.
                        }
                        if (memory.contains(placeInstAccessor)) {
                            placeInstAccessor = null;

                        }
                        if (placeInstAccessor != null) {
                            if (!(placeInstAccessor instanceof PlaceInstanceAccessorImpl)) {
                                throw new IllegalStateException(
                                    "Only the local simulation may set breakpoints");
                            }
                            PlaceInstance placeInst =
                                ((PlaceInstanceAccessorImpl) placeInstAccessor).getPlaceInstance();


                            // augment our memory
                            memory.add(placeInstAccessor);

                            Place place = placeInst.getPlace();
                            Net net = placeInst.getNetInstance().getNet();

                            switch (op) {
                                case ADD:
                                    if (context == GLOBAL) {
                                        bp = manager
                                            .createPlaceBreakpoint(place, breakpointMode, net);
                                    } else {
                                        bp = manager.createPlaceInstanceBreakpoint(
                                            placeInst, breakpointMode);
                                    }
                                    if (bp != null) {
                                        count++;
                                    }
                                    break;
                                case REMOVE:
                                    if (context == GLOBAL) {
                                        count += manager.deleteBreakpointsAt(place);
                                    } else {
                                        count += manager.deleteBreakpointsAt(placeInst);
                                    }
                                    break;
                            }
                        }
                    }
                }
            }
            view.checkDamage();
            StringBuilder message = new StringBuilder();

            if (count == 1) {
                message.append("1 breakpoint");
            } else {
                message.append(count).append(" breakpoints");
            }
            switch (op) {
                case ADD:
                    message.append(" set.");
                    break;
                case REMOVE:
                    message.append(" cleared.");
                    break;
            }
            getEditor().showStatus(message.toString());
            return (context == PRESET) && (count > 0);
        }
        return false;
    }
}