/**
 * Created on 12-Feb-2004
 */
package pipe.views;

import pipe.actions.gui.PipeApplicationModel;
import pipe.constants.GUIConstants;
import pipe.controllers.ArcController;
import pipe.controllers.PetriNetController;
import pipe.handlers.ArcPathPointHandler;
import pipe.utilities.gui.GuiUtils;
import pipe.utilities.math.Cubic;
import uk.ac.imperial.pipe.exceptions.PetriNetComponentException;
import uk.ac.imperial.pipe.models.petrinet.*;
import uk.ac.imperial.pipe.visitor.component.PetriNetComponentVisitor;

import java.awt.*;
import java.awt.geom.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;

/**
 * Class that displays the path of an arc graphically on screen.
 * It's old code so needs tidying up at some point
 */
public class ArcPath implements Shape, Cloneable {

    private static final Stroke PROXIMITY_STROKE = new BasicStroke(GUIConstants.ARC_PATH_PROXIMITY_WIDTH);

    private static final Stroke STROKE = new BasicStroke(GUIConstants.ARC_PATH_SELECTION_WIDTH);

    /**
     * The midpoint along the arc, used to display the arc weights here if necessary
     */
    public final Point2D.Double midPoint = new Point2D.Double();

    /**
     * Points along the arc path
     */
    private final List<ArcPathPoint> pathPoints = new ArrayList<>();

    /**
     * Parent view who the points belong to
     */
    private final ArcView<? extends Connectable, ? extends Connectable> arcView;

    /**
     * Petri net controller for which the arc belongs to
     */
    private final PetriNetController petriNetController;

    /**
     * Main PIPE application model
     */
    private final PipeApplicationModel applicationModel;

    /**
     * Graphical representation of the path
     */
    private GeneralPath path = new GeneralPath();

    /**
     * When pointlock is on no points will be displayed when the cursor is hovered
     * over them. Nor will they be dragable
     */
    private boolean pointLock = false;

    private Shape shape = STROKE.createStrokedShape(this);

    private Shape proximityShape = PROXIMITY_STROKE.createStrokedShape(this);

    /**
     * Angle at which to meet a transition
     */
    private int transitionAngle;

    /**
     * Constructor
     *
     * @param arcView            arc who these points belong to
     * @param petriNetController Petri net controller for which the underlying arc belongs to
     * @param applicationModel   main PIPE application model
     */
    public ArcPath(ArcView<? extends Connectable, ? extends Connectable> arcView, PetriNetController petriNetController,
                   PipeApplicationModel applicationModel) {
        this.arcView = arcView;
        this.petriNetController = petriNetController;
        this.applicationModel = applicationModel;
        transitionAngle = 0;
    }

    /**
     * Note: We cannot do this in O(1) time using a HashMap because ArcPoints are
     * mutable objects, meaning that they could change whilst keys in the HashMap
     *
     * @param point to be evaluated 
     * @return true if the path contains the point
     */
    public boolean contains(ArcPoint point) {
        for (ArcPathPoint p : pathPoints) {
            if (p.getModel().equals(point)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Remove the point from the graphical representation of the path
     *
     * @param point to be deleted 
     */
    public void deletePoint(ArcPoint point) {
        ArcPathPoint pointView = null;
        for (ArcPathPoint p : pathPoints) {
            if (p.getModel().equals(point)) {
                pointView = p;
            }
        }
        if (pointView != null) {
            pointView.delete();
            pathPoints.remove(pointView);
        }
    }

    /**
     * @return number of arc points in the path
     */
    public int getNumPoints() {
        return pathPoints.size();
    }

    /**
     * @param index of the point 
     * @return the location of the point at this index, or null if it does not exist
     */
    public Point2D getPoint(int index) {
        return pathPoints.get(index).getPoint();
    }

    /**
     * @param index of the point 
     * @return the point at this index or null if it does not exist
     */
    public ArcPathPoint getPathPoint(int index) {
        return pathPoints.get(index);
    }

    /**
     * Graphically show the points along the path
     */
    public void showPoints() {
        if (!pointLock) {
            for (ArcPathPoint pathPoint : pathPoints) {
                pathPoint.setVisible(true);
            }
        }
    }

    /**
     * @param lock true if points should always be shown on the canvas
     */
    public void setPointVisibilityLock(boolean lock) {
        pointLock = lock;
    }

    /**
     * Hide all points on the canvas
     */
    public void hidePoints() {
        if (!pointLock) {
            for (ArcPathPoint pathPoint : pathPoints) {
                if (!pathPoint.isSelected()) {
                    pathPoint.setVisible(false);
                }
            }
        }
    }

    /**
     * @return the angle at which the arc leaves its source
     */
    public double getStartAngle() {

        if (getEndIndex() > 0) {
            return pathPoints.get(0).getAngle(pathPoints.get(1).getControl());
        }
        return 0;
    }

    /**
     * @return the index of the last item in the path points
     */
    public int getEndIndex() {
        return pathPoints.size() - 1;
    }

    /**
     * Used because there is no layout manager for the canvas
     *
     * @return the bounds of the arc path
     */
    @Override
    public Rectangle getBounds() {
        return path.getBounds();
    }

    @Override
    public Rectangle2D getBounds2D() {
        return null;
    }


    /**
     * @param x coordinate
     * @param y coordinate
     * @return false, no point is contained within this arc
     */
    @Override
    public boolean contains(double x, double y) {
        return false;
    }

    /**
     * @param point to be evaluated 
     * @return true if the point intersects the shape
     */
    @Override
    public boolean contains(Point2D point) {
        return shape.contains(point);
    }

    /**
     * @param arg0 coordinate
     * @param arg1 coordinate
     * @param arg2 coordinate
     * @param arg3 coordinate
     * @return false always false 
     */
    @Override
    public boolean intersects(double arg0, double arg1, double arg2, double arg3) {
        return false;
    }

    /**
     * @param rect rectangle
     * @return true if the rectangle intersects the shape
     */
    @Override
    public boolean intersects(Rectangle2D rect) {
        return shape.intersects(rect);
    }

    /**
     * @param arg0 coordinate
     * @param arg1 coordinate
     * @param arg2 coordinate
     * @param arg3 coordinate
     * @return false always false 
     */
    @Override
    public boolean contains(double arg0, double arg1, double arg2, double arg3) {
        return false;
    }

    /**
     * @param rect rectangle 
     * @return false always false 
     */
    @Override
    public boolean contains(Rectangle2D rect) {
        return false;
    }

    /**
     * @param arg0 affine transform
     * @return an iterator for the path
     */
    @Override
    public PathIterator getPathIterator(AffineTransform arg0) {
        return path.getPathIterator(arg0);
    }

    @Override
    public PathIterator getPathIterator(AffineTransform arg0, double arg1) {
        return path.getPathIterator(arg0, arg1);
    }


    public boolean proximityContains(Point2D p) {
        return proximityShape.contains(p);
    }

    /**
     * Tells the arc points to remove themselves
     */
    public void delete() {
        while (!pathPoints.isEmpty()) {
            // force delete of ALL points
//            pathPoints.get(0).kill();
            ArcPathPoint remove = pathPoints.remove(0);
            remove.kill();
        }
    }

    /**
     * @param point point to add to path
     * @param index position in the path
     */
    public void insertIntermediatePoint(ArcPoint point, int index) {
        insertPoint(index, createPoint(point));
    }

    /**
     * insertPoint()
     * Inserts a new point into the Array List of path points
     * at the specified index and shifts all the following points along
     *
     * @param index of point in list 
     * @param newpoint to be added 
     * @author Nadeem
     */
    public void insertPoint(int index, ArcPathPoint newpoint) {
        pathPoints.add(index, newpoint);
        addPointsToGui(arcView.getParent());
    }

    /**
     * @param point to be created 
     * @return a graphical point at the underlying point models location
     */
    private ArcPathPoint createPoint(ArcPoint point) {
        PropertyChangeListener listener = new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent propertyChangeEvent) {

                createPath();
                arcView.updateBounds();
                arcView.repaint();
            }
        };
        point.addPropertyChangeListener(listener);
        return new ArcPathPoint(point, this, petriNetController, arcView.getParent());

    }

    /**
     * Add all graphical arc points to the Petri net tab
     *
     * @param petriNetTab container to add points to 
     */
    public void addPointsToGui(Container petriNetTab) {
        if (petriNetTab == null) {
            //Parent has not yet been added
            return;
        }


        for (ArcPathPoint point : pathPoints) {
            point.setVisible(false);

            // Check whether the point has already been added to the gui
            // as addPointsToGui() may have been called after the user
            // split an existing point. If this is the case, we don't want
            // to add all the points again along with new action listeners,
            // we just want to add the new point.
            // Nadeem 21/06/2005
            //            if (petriNetTab.getIndexOf(point) < 0) {
            petriNetTab.add(point, new Integer(300));

            //TODO SEPERATE HANDLERS INTO THOSE THAT NEED THE CONTROLLER!
            ArcController<? extends Connectable, ? extends Connectable> arcController =
                    petriNetController.getArcController(arcView.getModel());
            ArcPathPointHandler pointHandler =
                    new ArcPathPointHandler(petriNetTab, point, petriNetController, arcController, applicationModel);

            if (point.getMouseListeners().length == 0) {
                point.addMouseListener(pointHandler);
            }

            if (point.getMouseMotionListeners().length == 0) {
                point.addMouseMotionListener(pointHandler);
            }

            if (point.getMouseWheelListeners().length == 0) {
                point.addMouseWheelListener(pointHandler);
            }
        }
    }

    /**
     * Creates the path layout using the path points set
     */
    public void createPath() {
        setControlPoints();

        path.reset();

        ArcPathPoint currentPoint = pathPoints.get(0);
        setStartingPoint(currentPoint);

        double length = 0;
        for (int point = 1; point <= getEndIndex(); point++) {
            ArcPathPoint previousPoint = currentPoint;
            currentPoint = pathPoints.get(point);

            if (!currentPoint.isCurved()) {
                createStraightPoint(currentPoint);
            } else if (currentPoint.isCurved()) {
                createCurvedPoint(currentPoint);
            }
            length += getLength(currentPoint.getPoint(), previousPoint.getPoint());
        }
        setMidPoint(length);
        shape = STROKE.createStrokedShape(this);
        proximityShape = PROXIMITY_STROKE.createStrokedShape(this);
    }

    /**
     * Set the control points for the Bezier curves
     */
    private void setControlPoints() {
        //must be in this order
        setCurveControlPoints();
        setStraightControlPoints();
        setEndControlPoints();
    }

    /**
     * Moves the path to the first point specified on the arc
     * @param point where path starts 
     */
    private void setStartingPoint(ArcPathPoint point) {
        path.moveTo(point.getPoint().getX(), point.getPoint().getY());
    }

    /**
     * Creates a straight line
     *
     * @param point point to create line to
     */
    private void createStraightPoint(ArcPathPoint point) {
        path.lineTo(point.getPoint().getX(), point.getPoint().getY());
    }

    /**
     * Creates a curved line
     *
     * @param point point to create curve to
     */
    private void createCurvedPoint(ArcPathPoint point) {
        path.curveTo(point.getControl1().x, point.getControl1().y, point.getControl().x, point.getControl().y,
                point.getPoint().getX(), point.getPoint().getY());
    }

    /**
     * @param A first point
     * @param B second point 
     * @return modulus of vector A -> B
     */
    private double getLength(Point2D A, Point2D B) {
        double ABx = A.getX() - B.getX();
        double ABy = A.getY() - B.getY();

        return Math.sqrt(ABx * ABx + ABy * ABy);
    }

    /**
     * Sets the midpoint which is used to relocate the arcs label
     *
     * @param length path length
     */
    private void setMidPoint(double length) {
        ArcPathPoint currentPoint = pathPoints.get(0);
        if (getEndIndex() < 2) {
            midPoint.x = (pathPoints.get(0).getPoint().getX() + pathPoints.get(1).getPoint().getX()) * 0.5;
            midPoint.y = (pathPoints.get(0).getPoint().getY() + pathPoints.get(1).getPoint().getY()) * 0.5;
        } else {
            double acc = 0;
            double percent = 0;
            ArcPathPoint previousPoint = currentPoint;
            for (int point = 1; point < pathPoints.size(); point++) {
                previousPoint = currentPoint;
                currentPoint = pathPoints.get(point);

                double inc = getLength(currentPoint.getPoint(), previousPoint.getPoint());
                double halfLength = length / 2.0;
                if (acc + inc > halfLength) {
                    percent = (halfLength - acc) / inc;
                    break;
                }
                acc += inc;
            }

            midPoint.x = previousPoint.getPoint().getX() + (float) (
                    (currentPoint.getPoint().getX() - previousPoint.getPoint().getX()) * percent);
            midPoint.y = previousPoint.getPoint().getY() + (float) (
                    (currentPoint.getPoint().getY() - previousPoint.getPoint().getY()) * percent);
        }
    }

    /* function sets control points for any curved sections of the path */
    private void setCurveControlPoints() {
        if (pathPoints.isEmpty()) {
            return;
        }

        Cubic[] X;
        Cubic[] Y;
        int c = 1;
        while (c < pathPoints.size()) {
            int curveStartIndex;
            int curveEndIndex = 0;
            ArcPathPoint currentPoint = pathPoints.get(c);

            if (currentPoint.isCurved()) {
                curveStartIndex = c - 1;

                for (; c < pathPoints.size() && currentPoint.isCurved(); c++) {
                    currentPoint = pathPoints.get(c);
                    curveEndIndex = c;
                }

            /* calculate a cubic for each section of the curve */
                int lengthOfCurve = curveEndIndex - curveStartIndex;
                int k1;
                int[] x = new int[lengthOfCurve + 2];
                int[] y = new int[lengthOfCurve + 2];

                for (k1 = 0; k1 <= (curveEndIndex - curveStartIndex); k1++) {
                    x[k1] = (int) (pathPoints.get(curveStartIndex + k1)).getPoint().getX();
                    y[k1] = (int) (pathPoints.get(curveStartIndex + k1)).getPoint().getY();
                }
                x[k1] = x[k1 - 1];
                y[k1] = y[k1 - 1];

                X = calcNaturalCubic(k1, x);
                Y = calcNaturalCubic(k1, y);

                for (int k2 = 1; k2 <= lengthOfCurve; k2++) {
                    currentPoint = pathPoints.get(k2 + curveStartIndex);
                    currentPoint.setControl1(X[k2 - 1].getX1(), Y[k2 - 1].getX1());
                    currentPoint.setControl2(X[k2 - 1].getX2(), Y[k2 - 1].getX2());
                }
            } else {
                c++;
            }
        }
    }

    /* fuction sets the control points for any straight sections and for smooth
     * intersection between straight and curved sections */
    private void setStraightControlPoints() {

        ArcPathPoint myPreviousButOnePoint;

        for (int c = 1; c <= getEndIndex(); c++) {
            ArcPathPoint previousPoint = pathPoints.get(c - 1);
            ArcPathPoint currentPoint = pathPoints.get(c);

            if (!currentPoint.isCurved()) {
                currentPoint.setControl1(
                        getControlPoint(previousPoint.getPoint(), currentPoint.getPoint(), previousPoint.getPoint(),
                                currentPoint.getPoint())
                );
                currentPoint.setControl(
                        getControlPoint(currentPoint.getPoint(), previousPoint.getPoint(), currentPoint.getPoint(),
                                previousPoint.getPoint())
                );
            } else {
                if (c > 1 && !previousPoint.isCurved()) {
                    myPreviousButOnePoint = pathPoints.get(c - 2);
                    currentPoint.setControl1(getControlPoint(myPreviousButOnePoint.getPoint(), previousPoint.getPoint(),
                            previousPoint.getPoint(), currentPoint.getPoint()));
                }
                if (c < getEndIndex()) {
                    ArcPathPoint nextPoint = pathPoints.get(c + 1);
                    if (!nextPoint.isCurved()) {
                        currentPoint.setControl(
                                getControlPoint(nextPoint.getPoint(), currentPoint.getPoint(), currentPoint.getPoint(),
                                        previousPoint.getPoint())
                        );
                    }
                }
            }
        }
    }

    /**
     * Set the control points for the end of the arc
     */
    private void setEndControlPoints() {
        PetriNetComponentVisitor endPointVisitor = new ArcConnectableVisitor();
        Connectable source = getArc().getModel().getSource();
        try {
            source.accept(endPointVisitor);
        } catch (PetriNetComponentException e) {
            GuiUtils.displayErrorMessage(null, e.getMessage());
        }
    }

    /**
     * We solve the equation
     * [2 1       ] [D[0]]   [3(x[1] - x[0])  ]
     * |1 4 1     | |D[1]|   |3(x[2] - x[0])  |
     * |  1 4 1   | | .  | = |      .         |
     * |    ..... | | .  |   |      .         |
     * |     1 4 1| | .  |   |3(x[n] - x[n-2])|
     * [       1 2] [D[n]]   [3(x[n] - x[n-1])]
     * <p>
     * by using row operations to convert the matrix to upper triangular
     * and then back substitution.  The D[i] are the derivatives at the knots.
     * </p>
     * @param n number of rows 
     * @param x offset 
     * @return a natural cubic for the Bezier curve
     */
    private Cubic[] calcNaturalCubic(int n, int[] x) {
        float[] gamma = new float[n + 1];
        float[] delta = new float[n + 1];
        float[] D = new float[n + 1];

        gamma[0] = 1.0f / 2.0f;
        for (int i = 1; i < n; i++) {
            gamma[i] = 1 / (4 - gamma[i - 1]);
        }
        gamma[n] = 1 / (2 - gamma[n - 1]);

        delta[0] = 3 * (x[1] - x[0]) * gamma[0];
        for (int i = 1; i < n; i++) {
            delta[i] = (3 * (x[i + 1] - x[i - 1]) - delta[i - 1]) * gamma[i];
        }
        delta[n] = (3 * (x[n] - x[n - 1]) - delta[n - 1]) * gamma[n];

        D[n] = delta[n];
        for (int i = n - 1; i >= 0; i--) {
            D[i] = delta[i] - gamma[i] * D[i + 1];
        }

      /* now compute the coefficients of the cubics */
        Cubic[] C = new Cubic[n];
        for (int i = 0; i < n; i++) {
            C[i] = new Cubic(x[i], D[i], 3 * (x[i + 1] - x[i]) - 2 * D[i] - D[i + 1],
                    2 * (x[i] - x[i + 1]) + D[i] + D[i + 1]);
        }
        return C;
    }

    /* returns a control point for curve CD with incoming vector AB*/
    private Point2D.Double getControlPoint(Point2D A, Point2D B, Point2D C, Point2D D) {
        Point2D.Double p = new Point2D.Double(0, 0);

        double modAB = getLength(A, B);
        double modCD = getLength(C, D);

        double ABx = (B.getX() - A.getX()) / modAB;
        double ABy = (B.getY() - A.getY()) / modAB;

        if (modAB < 7) {
            // hack, stops division by zero, modAB can only be this low if the
            // points are virtually superimposed anyway
            p = (Point2D.Double) C.clone();
        } else {
            p.x = C.getX() + (ABx * modCD / GUIConstants.ARC_CONTROL_POINT_CONSTANT);
            p.y = C.getY() + (ABy * modCD / GUIConstants.ARC_CONTROL_POINT_CONSTANT);
        }
        return p;
    }

    /**
     * @return arc view associated with this point
     */
    public ArcView<? extends Connectable, ? extends Connectable> getArc() {
        return arcView;
    }

    /**
     * Removes all path points
     */
    public void clear() {
        for (ArcPathPoint pathPoint : pathPoints) {
            pathPoint.kill();
        }
        pathPoints.clear();
    }

    /**
     * Visitor interface that visits Places and Transitions
     */
    private interface ConnectableVisitor extends PlaceVisitor, TransitionVisitor {
    }


    private class ArcConnectableVisitor implements ConnectableVisitor {
        @Override
        public void visit(Place place) {
            if (pathPoints.get(getEndIndex()).isCurved()) {
                double angle = Math.toRadians(transitionAngle);
                ArcPathPoint myPoint = pathPoints.get(getEndIndex());
                ArcPathPoint myLastPoint = pathPoints.get(getEndIndex() - 1);
                float distance = (float) getLength(myPoint.getPoint(), myLastPoint.getPoint())
                        / GUIConstants.ARC_CONTROL_POINT_CONSTANT;
                myPoint.setControl2((float) (myPoint.getPoint().getX() + Math.cos(angle) * distance),
                        (float) (myPoint.getPoint().getY() + Math.sin(angle) * distance));

                myPoint = pathPoints.get(1);
                myPoint.setControl(getControlPoint(pathPoints.get(0).getPoint(), myPoint.getControl(),
                        pathPoints.get(0).getPoint(), myPoint.getControl()));
            }
        }

        @Override
        public void visit(Transition transition) {
            if (pathPoints.get(1).isCurved()) {
                double angle = Math.toRadians(transitionAngle);
                ArcPathPoint myPoint = pathPoints.get(1);
                ArcPathPoint myLastPoint = pathPoints.get(0);
                float distance = (float) getLength(myPoint.getPoint(), myLastPoint.getPoint())
                        / GUIConstants.ARC_CONTROL_POINT_CONSTANT;
                myPoint.setControl1((float) (myLastPoint.getPoint().getX() + Math.cos(angle) * distance),
                        (float) (myLastPoint.getPoint().getY() + Math.sin(angle) * distance));

                myPoint = pathPoints.get(getEndIndex());
                myPoint.setControl(getControlPoint(myPoint.getPoint(), myPoint.getControl1(), myPoint.getPoint(),
                        myPoint.getControl1()));
            }
        }
    }
}



