package pipe.controllers;

import pipe.gui.PetriNetTab;
import pipe.historyActions.component.DeletePetriNetObject;
import uk.ac.imperial.pipe.exceptions.PetriNetComponentException;
import uk.ac.imperial.pipe.exceptions.PetriNetComponentNotFoundException;
import uk.ac.imperial.pipe.models.petrinet.*;
import uk.ac.imperial.pipe.naming.PlaceNamer;
import uk.ac.imperial.pipe.naming.TransitionNamer;
import uk.ac.imperial.pipe.naming.UniqueNamer;
import uk.ac.imperial.pipe.parsers.FunctionalResults;
import uk.ac.imperial.pipe.visitor.ClonePetriNet;
import uk.ac.imperial.pipe.visitor.TranslationVisitor;
import uk.ac.imperial.pipe.visitor.component.PetriNetComponentVisitor;

import javax.swing.event.UndoableEditListener;
import javax.swing.undo.UndoManager;
import javax.swing.undo.UndoableEdit;

import java.awt.Color;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.io.Serializable;
import java.util.*;

@SuppressWarnings("serial")
public class PetriNetController implements Serializable {

    /**
     * Responsible for zooming of the current Petri net
     */
    private final ZoomController zoomController;

    /**
     * Responsible for handling undo/redo
     */
    private final UndoManager undoManager = new UndoManager();

    /**
     * Petri net being displayed
     */
    private final PetriNet petriNet;

    /**
     * Listener for tool bar actions that create undoable actions
     */
    private final UndoableEditListener undoListener;

    /**
     * Tab that the Petri net is shown on
     */
    private final PetriNetTab petriNetTab;

    /**
     * Selected components in the Petri net
     */
    private final Set<PetriNetComponent> selectedComponents = new HashSet<>();

    /**
     * Responsible for copy and pasting of selected components
     */
    private final CopyPasteManager copyPasteManager;

    /**
     * Responsible for naming places
     */
    private final UniqueNamer placeNamer;

    /**
     * Responsible for creating unique transition names
     */
    private final UniqueNamer transitionNamer;

    /**
     * Token id that is currently selected in the drop down
     */
    private String selectedToken;

    /**
     * Animator class for animating tokens in the petri net
     */
    private GUIAnimator animator;


    /**
     * Drag manager for dragging selected objects
     */
    private DragManager dragManager = new DragManager(this);

    /**
     * Name of file the Petri net is saved to. Empty string if it has not yet been saved/loaded
     * from file
     */
    private String fileName = "";

    /**
     * Copy of the last saved version of the Petri net
     */
    private PetriNet lastSavedNet;

    /**
     * Set to true if the Petri net is in animation mode
     */
    private boolean animateMode = false;

    /**
     * Selection manager for selecting petri net components
     */
    private SelectionManager selectionManager;

    /**
     * Constructor
     * @param model underlying Petri net
     * @param undoListener undo listener for tool bar buttons undo actions
     * @param animator Petri net animator
     * @param copyPasteManager copy paste manager for the Petri net
     * @param zoomController zoom controller for the Petri net
     * @param petriNetTab tab this Petri net is displayed on
     */
    public PetriNetController(PetriNet model, UndoableEditListener undoListener, GUIAnimator animator,
                              CopyPasteManager copyPasteManager, ZoomController zoomController,
                              PetriNetTab petriNetTab) {
        petriNet = model;
        this.undoListener = undoListener;
        this.petriNetTab = petriNetTab;
        selectionManager = new SelectionManager(this);
        lastSavedNet = ClonePetriNet.clone(model);
        this.zoomController = zoomController;
        this.animator = animator;
        this.copyPasteManager = copyPasteManager;
        if (model.getTokens().size() > 0) {
            selectedToken = model.getTokens().iterator().next().getId();
        }
        placeNamer = new PlaceNamer(model);
        transitionNamer = new TransitionNamer(model);
    }

    /**
     * @return Tab this controller is associated with
     */
    public PetriNetTab getPetriNetTab() {
        return petriNetTab;
    }

    /**
     * @return A unique name for a place in the current petri net
     */
    public String getUniquePlaceName() {
        return placeNamer.getName();
    }

    /**
     * @return A unique name for a transition in the current petri net
     */
    public String getUniqueTransitionName() {
        return transitionNamer.getName();
    }

    /**
     *
     * @param component to check for selection 
     * @return true if this component is selected on the canvas
     */
    public boolean isSelected(PetriNetComponent component) {
        return selectedComponents.contains(component);
    }

    /**
     * unselect the component on the canvas
     * @param component to unselect 
     */
    public void deselect(PetriNetComponent component) {
        selectedComponents.remove(component);
    }

    /**
     * Deselect all canvas componentns
     */
    public void deselectAll() {
        selectedComponents.clear();
    }

    /**
     * Translates any components that are selected using a TranslationVisitor
     *
     * @param translation translation distance
     * @throws PetriNetComponentException if component is not found 
     */
    public void translateSelected(Point translation) throws PetriNetComponentException {
        PetriNetComponentVisitor translationVisitor = new TranslationVisitor(translation, selectedComponents);
        for (PetriNetComponent component : selectedComponents) {
            if (component.isDraggable()) {
                component.accept(translationVisitor);
            }
        }
    }

    /**
     * Selects all components within this rectangle
     *
     * @param selectionRectangle bounds for selection
     */
    public void select(Rectangle selectionRectangle) {
        for (Place place : petriNet.getPlaces()) {
            selectPlaceable(place, selectionRectangle);
        }
        for (Transition transition : petriNet.getTransitions()) {
            selectPlaceable(transition, selectionRectangle);
        }
        for (Arc<? extends Connectable, ? extends Connectable> arc : petriNet.getArcs()) {
            if (isArcSelected(arc, selectionRectangle)) {
                select(arc);
                for (ArcPoint arcPoint : arc.getArcPoints()) {
                    select(arcPoint);
                }
            } else if (selectedComponents.contains(arc.getSource()) || selectedComponents.contains(arc.getTarget())) {
                select(arc);
            }
        }
        for (Annotation annotation : petriNet.getAnnotations()) {
            selectPlaceable(annotation, selectionRectangle);
        }
    }

    /**
     * A crude method for selecting arcs, does not take into account bezier curves
     *
     * @param arc                arc to test to see if it is selected
     * @param selectionRectangle bounds of selection on screen
     * @return if selectionRectangle intersects the path
     */
    private boolean isArcSelected(Arc<? extends Connectable, ? extends Connectable> arc, Rectangle selectionRectangle) {
        GeneralPath path = createStraightPath(arc);
        return path.intersects(selectionRectangle);
    }

    /**
     * Creates an arc with a straight path arc
     *
     * @param arc
     * @return Straight path for arc, ignoring Bezier curves
     */
    private GeneralPath createStraightPath(Arc<? extends Connectable, ? extends Connectable> arc) {
        GeneralPath path = new GeneralPath();

        Collection<ArcPoint> arcPoints = arc.getArcPoints();
        int index = 0;
        for (ArcPoint arcPoint : arcPoints) {
            if (index == 0) {
                path.moveTo(arcPoint.getX(), arcPoint.getY());
            } else {
                Point2D point = arcPoint.getPoint();
                path.lineTo(point.getX(), point.getY());

            }
            index++;
        }
        return path;
    }

    /**
     * Select the Petri net component on the canvas
     * @param component to select 
     */
    public void select(PetriNetComponent component) {
        selectedComponents.add(component);
    }

    /**
     * Tests to see if the object is in the selection rectangle
     * If it is it selects in
     *
     * @param placeable          object to see if it is selectable
     * @param selectionRectangle bounds for selection
     */
    private void selectPlaceable(PlaceablePetriNetComponent placeable, Rectangle selectionRectangle) {
        int x = placeable.getX();
        int y = placeable.getY();
        Rectangle rectangle = new Rectangle(x, y, placeable.getHeight(), placeable.getWidth());
        if (selectionRectangle.intersects(rectangle)) {
            select(placeable);
        }
    }

    /**
     * Deletes selection and adds to history manager
     * @return list of edits performed 
     * @throws PetriNetComponentException if component not found 
     */
    public List<UndoableEdit> deleteSelection() throws PetriNetComponentException {
        List<UndoableEdit> edits = new LinkedList<>();
        for (PetriNetComponent component : selectedComponents) {
            edits.add(deleteComponent(component));
        }
        selectedComponents.clear();
        return edits;
    }

    /**
     * Deletes a component and returns the AbstractUndoableEdit in order
     * to redo the action
     *
     * @param component
     * @return AbstractUndoableEdit created for deleting the component
     * @throws PetriNetComponentException if component not found 
     */
    private UndoableEdit deleteComponent(PetriNetComponent component) throws PetriNetComponentException {
        petriNet.remove(component);
        return new DeletePetriNetObject(component, petriNet);
    }

    /**
     * Deletes single component, starts a newEdit for history manager
     *
     * @param component to delete
     * @return AbstractUndableEdit created
     * @throws PetriNetComponentException if component not found 
     */
    public UndoableEdit delete(PetriNetComponent component) throws PetriNetComponentException {
        return deleteComponent(component);
    }

    /**
     * Adds a new token to the petrinet
     *
     * @param name of the token
     * @param color of the token 
     */
    public void createNewToken(String name, Color color) {
        Token token = new ColoredToken(name, color);
        petriNet.addToken(token);
    }

    /**
     *
     * @return all tokens in the Petri net
     */
    public Collection<Token> getNetTokens() {
        return petriNet.getTokens();
    }

    /**
     * Update the token with the specified name and color
     * @param currentTokenName current name
     * @param name new token name
     * @param color of the token
     * @throws PetriNetComponentNotFoundException if token not found 
     */
    public void updateToken(String currentTokenName, String name, Color color)
            throws PetriNetComponentNotFoundException {
        Token token = petriNet.getComponent(currentTokenName, Token.class);
        if (!token.getId().equals(name)) {
            token.setId(name);
        }
        if (!token.getColor().equals(color)) {
            token.setColor(color);
        }
    }

    /**
     *
     * @return underlying Petri net model
     */
    //TODO: Shouldnt expose this!
    public PetriNet getPetriNet() {
        return petriNet;
    }

    /**
     * @param arc arc
     * @param <S> source
     * @param <T> target 
     * @return controller for the model
     */
    public <S extends Connectable, T extends Connectable> ArcController<S, T> getArcController(Arc<S, T> arc) {
        return new ArcController<>(arc, this, undoListener);
    }

    /**
     *
     * @param place current place 
     * @return controller for the place
     */
    public PlaceController getPlaceController(Place place) {
        return new PlaceController(place, undoListener);
    }

    /**
     *
     * @param annotation current annotation
     * @return controller for the annotation
     */
    public AnnotationController getAnnotationController(Annotation annotation) {
       return new AnnotationController(annotation, undoListener);
    }

    /**
     *
     * @param transition current transition 
     * @return controller for the transition
     */
    public TransitionController getTransitionController(final Transition transition) {
        return new TransitionController(transition, undoListener);
    }

    /**
     *
     * @param rateParameter current rate parameter
     * @return controller for the rate parameter
     * @throws PetriNetComponentNotFoundException if not found 
     */
    public RateParameterController getRateParameterController(final String rateParameter)
            throws PetriNetComponentNotFoundException {
        RateParameter parameter = petriNet.getComponent(rateParameter, RateParameter.class);
        return new RateParameterController(parameter, petriNet, undoListener);
    }

    /**
     * Selected token on the drop down menu
     * @param tokenName selected
     * @throws PetriNetComponentNotFoundException if not found 
     */
    public void selectToken(String tokenName) throws PetriNetComponentNotFoundException {
        selectedToken = tokenName;
    }

    /**
     * @param name token name to find
     * @return Token from PetriNet
     * @throws PetriNetComponentNotFoundException if the token does not exist
     */
    public Token getToken(String name) throws PetriNetComponentNotFoundException {
        return petriNet.getComponent(name, Token.class);
    }

    /**
     * Copy all components that are selected
     */
    public void copySelection() {
        copyPasteManager.copy(selectedComponents);
    }

    /**
     *
     * @return true if a paste has been enabled
     */
    public boolean isCopyInProgress() {
        return copyPasteManager.pasteEnabled();
    }

    /**
     * Cancels the current paste
     */
    public void cancelPaste() {
        copyPasteManager.cancelPaste();
    }

    /**
     *
     * The selected token can then be used to add tokens to places
     *
     * @return the current token on the drop down menu
     */
    public String getSelectedToken() {
        return selectedToken;
    }

    /**
     *
     * @return the animator of the Petri net
     */
    public GUIAnimator getAnimator() {
        return animator;
    }

    /**
     *
     * @return the zoom controller of the Petri net
     */
    public ZoomController getZoomController() {
        return zoomController;
    }

    /**
     * Paste the copied items onto the Petri net
     */
    public void paste() {
        copyPasteManager.showPasteRectangle();
    }

    /**
     *
     * @return Petri net drag manager
     */
    public DragManager getDragManager() {
        return dragManager;
    }

    /**
     *
     * @return rate parameters in the Petri net
     */
    public Collection<RateParameter> getRateParameters() {
        return petriNet.getRateParameters();
    }

    /**
     *
     * @param id of the component 
     * @return true if this id does not exist in the Petri net
     */
    public boolean isUniqueName(String id) {
        return placeNamer.isUniqueName(id) && transitionNamer.isUniqueName(id);
    }

    /**
     *
     * @return true if the Petri net has changed since it was last saved/loaded
     */
    public boolean hasChanged() {
        return !petriNet.equals(lastSavedNet);
    }

    /**
     * Take a clone of the Petri net
     */
    public void save() {
        lastSavedNet = ClonePetriNet.clone(petriNet);
    }

    /**
     *
     * @param expr functional expression to parse 
     * @return parsed functional expression in relation to the Petri nets current state
     */
    public FunctionalResults<Double> parseFunctionalExpression(String expr) {
        return petriNet.parseExpression(expr);
    }

    /**
     *
     * @return Petri nets undo manager
     */
    public UndoManager getUndoManager() {
        return undoManager;
    }


    /**
     *
     * @return all selected components
     */
    public Set<PetriNetComponent> getSelectedComponents() {
        return selectedComponents;
    }

    /**
     *
     * @return Petri net undo listener
     */
    public UndoableEditListener getUndoListener() {
        return undoListener;
    }

    /**
     *
     * Toggles animation from false -&gt; true or true -&gt; false
     * @return new mode
     */
    public boolean toggleAnimation() {
        animateMode = !animateMode;
        return animateMode;
    }

    /**
     *
     * @return if the Petri net should be displayed in animation mode on the canvas
     */
    public boolean isInAnimationMode() {
        return animateMode;
    }

    /**
     *
     * @return Petri net selection manager
     */
    public SelectionManager getSelectionManager() {
        return selectionManager;
    }
}
