package pipe.views;

import pipe.actions.gui.PipeApplicationModel;
import pipe.controllers.PetriNetController;
import uk.ac.imperial.pipe.models.petrinet.Arc;
import uk.ac.imperial.pipe.models.petrinet.ArcPoint;
import uk.ac.imperial.pipe.models.petrinet.Connectable;

import javax.swing.event.MouseInputAdapter;

import java.awt.Container;
import java.awt.geom.Point2D;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

/**
 * This class contains the common methods for different arc types.
 * <p>
 * At present when arc points are modified the whole arc is redrawn. At some point
 * in the future it would be good if they're more dynamic than this.
 * </p>
 * @param <S> Model source type
 * @param <T> Model target type
 */
@SuppressWarnings("serial")
public abstract class ArcView<S extends Connectable, T extends Connectable>
        extends AbstractPetriNetViewComponent<Arc<S, T>> {
    /**
     * Bounds of arc need to be grown in order to avoid clipping problems.
     * This value achieves it.
     */
    protected static final int ZOOM_GROW = 10;

    /**
     * Actual visible path
     */
    protected final ArcPath arcPath;

    public ArcView(Arc<S, T> model, PetriNetController controller, Container parent,
                   MouseInputAdapter arcHandler,
                   PipeApplicationModel applicationModel) {
        super(model.getId(), model, controller, parent);
        arcPath = new ArcPath(this, controller, applicationModel);

        updatePath();
        updateBounds();
        registerModelListeners();
        setMouseListener(arcHandler);
    }

    /**
     * Repopulates the path with the models points
     */
    protected final void updatePath() {
        addIntermediatePoints();
        arcPath.createPath();
        arcPath.addPointsToGui(getParent());
        repaint();
    }

    /**
     * Updates the bounding box of the arc component based on the arcs bounds
     */
    public final void updateBounds() {
        bounds = arcPath.getBounds();
        bounds.grow(getComponentDrawOffset() + ZOOM_GROW, getComponentDrawOffset() + ZOOM_GROW);
        setBounds(bounds);
    }

    /**
     * Registers listeners for the arc model and it's source and target models
     */
    private void registerModelListeners() {
        addArcChangeListener();
    }

    /**
     * Register the mouse handler to this view
     * @param arcHandler handler to determine what the arc does on mouse events
     */
    public final void setMouseListener(MouseInputAdapter arcHandler) {
        addMouseListener(arcHandler);
        addMouseWheelListener(arcHandler);
        addMouseMotionListener(arcHandler);
    }

    /**
     * Loops through points adding them to the path if they don't already
     * exist
     */
    private void addIntermediatePoints() {
        int index = 0;
        for (ArcPoint arcPoint : model.getArcPoints()) {
            if (!arcPath.contains(arcPoint)) {
                arcPath.insertIntermediatePoint(arcPoint, index);
            }
            index++;
        }
    }

    /**
     * Listens for intermediate points being added/deleted
     * Will call a redraw of the existing points
     */
    private void addArcChangeListener() {
        PropertyChangeListener listener = new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent propertyChangeEvent) {
                String name = propertyChangeEvent.getPropertyName();
                if (name.equals(Arc.NEW_INTERMEDIATE_POINT_CHANGE_MESSAGE)) {
                    updateAllPoints();
                } else if (name.equals(Arc.DELETE_INTERMEDIATE_POINT_CHANGE_MESSAGE)) {
                    ArcPoint point = (ArcPoint) propertyChangeEvent.getOldValue();
                    arcPath.deletePoint(point);
                    updateAllPoints();
                }
            }
        };
        model.addPropertyChangeListener(listener);
    }

    /**
     * Updates all arc points displayed based on their positions
     * in the model
     */
    private void updateAllPoints() {
        updatePath();
        updateBounds();
    }


    /**
     *
     * @param x coordinate
     * @param y coordinate 
     * @return true if (x,y) intersect the arc path
     */
    @Override
    public final boolean contains(int x, int y) {
        Point2D.Double point = new Point2D.Double(x + arcPath.getBounds().getX() - getComponentDrawOffset() -
                ZOOM_GROW, y + arcPath.getBounds().getY() - getComponentDrawOffset() -
                ZOOM_GROW);
        if (!petriNetController.isInAnimationMode()) {
            if (arcPath.proximityContains(point) || isSelected()) {
                // show also if Arc itself selected
                arcPath.showPoints();
            } else {
                arcPath.hidePoints();
            }
        }

        return arcPath.contains(point);
    }


    @Override
    public void componentSpecificDelete() {
        arcPath.delete();
    }

    /**
     *
     * @return the graphical arc path which displays the arc and its points
     */
    public final ArcPath getArcPath() {
        return arcPath;
    }


    // Accessor function to check whether or not the Arc is tagged

    /**
     *
     * This should return if the arc is tagged for the tagging module but
     * the functionality has not been implemented
     *
     * @return false
     */
    public final boolean isTagged() {
        return false;
    }
}