package pipe.controllers;

import pipe.historyActions.MultipleEdit;
import pipe.historyActions.arc.AddArcPathPoint;
import pipe.historyActions.arc.ArcPathPointType;
import pipe.historyActions.arc.DeleteArcPathPoint;
import pipe.historyActions.arc.SetArcWeightAction;
import uk.ac.imperial.pipe.models.petrinet.Connectable;
import uk.ac.imperial.pipe.models.petrinet.Arc;
import uk.ac.imperial.pipe.models.petrinet.ArcPoint;
import uk.ac.imperial.pipe.parsers.FunctionalResults;
import uk.ac.imperial.pipe.parsers.UnparsableException;

import javax.swing.event.UndoableEditListener;
import javax.swing.undo.UndoableEdit;
import java.awt.geom.Point2D;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Controller for the underlying arc model
 * @param <S> source model
 * @param <T> target model
 */
public class ArcController<S extends Connectable, T extends Connectable> extends AbstractPetriNetComponentController<Arc<S, T>>
{
    /**
     * Underlying model
     */
    private final Arc<S, T> arc;

    /**
     * PetriNetController in order to determine if arc expressions are valid
     */
    //TODO: I cant at the moment think of a better way to do this since the arc model
    //      does not know anything about the petri net in which it resides
    private final PetriNetController petriNetController;

    /**
     * Constructor
     * @param arc underlying model
     * @param petriNetController Petri net controller for the Petri net the arc is housed in
     * @param listener undo event listener
     */
    ArcController(Arc<S, T> arc, PetriNetController petriNetController, UndoableEditListener listener) {
        super(arc, listener);
        this.arc = arc;
        this.petriNetController = petriNetController;
    }

    /**
     * Sets the weight for the current arc
     * @param token token id
     * @param expr weight for the associated token on the underlying arc
     * @throws UnparsableException if the weight could not be parsed or is not an integer
     */
    public void setWeight(String token, String expr) throws UnparsableException {
        throwExceptionIfWeightNotValid(expr);
        registerUndoableEdit(updateWeightForArc(token, expr));
    }

    /**
     *
     * @param expr weight expression
     * @throws UnparsableException if the weight could not be parsed or is not an integer
     */
    private void throwExceptionIfWeightNotValid(String expr) throws UnparsableException {
        FunctionalResults<Double> result = petriNetController.parseFunctionalExpression(expr);
        if (result.hasErrors()) {
            StringBuilder errorMessage = new StringBuilder();
            for (String error : result.getErrors()) {
                errorMessage.append(error).append("\n");
            }
            throw new UnparsableException(errorMessage.toString());
        } else if (valueIsNotInteger(result.getResult())) {
            throw new UnparsableException("Value is not an integer, please surround expression with floor or ceil");
        }
    }

    /**
     *
     * @param value to evaluate 
     * @return true if the value was an integer
     */
    private boolean valueIsNotInteger(double value) {
        return value % 1 != 0;
    }

    /**
     * Loop through the weights and throw an exception if its not valid
     * @param weights
     * @throws UnparsableException if the weight could not be parsed or is not an integer
     */
    private void throwExceptionIfWeightsNotValid(Map<String, String> weights) throws UnparsableException {
        for (Map.Entry<String, String> entry : weights.entrySet()) {
            throwExceptionIfWeightNotValid(entry.getValue());
        }
    }

    /**
     * Creates a historyItem for updating weight and applies it
     * @param token token id to associate the expression with
     * @param expr new weight expression for the arc
     * @return the UndoableEdit associated with this action
     */
    private UndoableEdit updateWeightForArc(String token,
                                    String expr) {
        String oldWeight = arc.getWeightForToken(token);
        arc.setWeight(token, expr);

        return new SetArcWeightAction<>(arc, token, oldWeight, expr);
    }

    /**
     * Set the new token weights
     * @param newWeights map token id -&gt;  functional expression
     * @throws UnparsableException if the weight could not be parsed or is not an integer
     */
    public void setWeights(Map<String, String> newWeights) throws UnparsableException {
        throwExceptionIfWeightsNotValid(newWeights);
        List<UndoableEdit> edits = new LinkedList<>();
        for (Map.Entry<String, String> entry : newWeights.entrySet()) {
            edits.add(updateWeightForArc(entry.getKey(), entry.getValue()));
        }
        registerUndoableEdit(new MultipleEdit(edits));
    }

    /**
     *
     * @param token to evaluate 
     * @return functional expression for the token
     */
    public String getWeightForToken(String token) {
        return arc.getWeightForToken(token);
    }

    /**
     *
     * @return target model
     */
    public Connectable getTarget() {
        return arc.getTarget();
    }

    /**
     * Toggle arc point between straight and curved
     * @param arcPoint to be toggled 
     */
    public void toggleArcPointType(ArcPoint arcPoint) {
        arcPoint.setCurved(!arcPoint.isCurved());
        registerUndoableEdit(new ArcPathPointType(arcPoint));
    }

    /**
     * Add another arc point between this point and the next
     * @param arcPoint at which to split 
     */
    public void splitArcPoint(ArcPoint arcPoint) {
        ArcPoint nextPoint = arc.getNextPoint(arcPoint);

        double x = (arcPoint.getPoint().getX() + nextPoint.getPoint().getX())/2;
        double y = (arcPoint.getPoint().getY() + nextPoint.getPoint().getY())/2;

        Point2D point = new Point2D.Double(x,y);
        ArcPoint newPoint = new ArcPoint(point, arcPoint.isCurved());
        arc.addIntermediatePoint(newPoint);
        UndoableEdit splitEdit = new AddArcPathPoint<>(arc, newPoint);
        registerUndoableEdit(splitEdit);
    }

    /**
     * Add an arc point at this location
     * @param point to add
     */
    public void addPoint(Point2D point) {
        ArcPoint newPoint = new ArcPoint(point, false);
        arc.addIntermediatePoint(newPoint);
        registerUndoableEdit(new AddArcPathPoint<>(arc, newPoint));
    }

    /**
     * Delete this arc point
     * @param point to delete
     */
    public void deletePoint(ArcPoint point) {
        arc.removeIntermediatePoint(point);
        registerUndoableEdit(new DeleteArcPathPoint<>(arc, point));
    }
}
