/*
 * Copyright (C) 2005 - 2014 by TESIS DYNAware GmbH
 */
package de.tesis.dynaware.grapheditor.core.skins.defaults.connection;

import java.util.List;

import javafx.geometry.Point2D;
import javafx.scene.shape.ArcTo;
import javafx.scene.shape.HLineTo;
import javafx.scene.shape.MoveTo;
import javafx.scene.shape.Path;
import javafx.scene.shape.PathElement;
import javafx.scene.shape.VLineTo;
import de.tesis.dynaware.grapheditor.core.skins.defaults.connection.segment.ConnectionSegment;
import de.tesis.dynaware.grapheditor.core.skins.defaults.utils.RectangularConnectionUtils;
import de.tesis.dynaware.grapheditor.model.GConnection;
import de.tesis.dynaware.grapheditor.utils.GeometryUtils;

/**
 * Helper class for calculating the offset of the cursor to a default connection skin.
 */
public class CursorOffsetCalculator {

    private final GConnection connection;
    private final Path path;
    private final Path backgroundPath;
    private final List<ConnectionSegment> connectionSegments;

    // Temporary variables used during calculation.
    private double minOffsetX;
    private double minOffsetY;
    private double currentX;
    private double currentY;

    /**
     * Creates a new cursor offset calculator instance for a default connection skin.
     *
     * @param path the connection's path
     * @param backgroundPath the connection's background path
     * @param connectionSegments the connection's list of segments
     */
    public CursorOffsetCalculator(final GConnection connection, final Path path, final Path backgroundPath,
            final List<ConnectionSegment> connectionSegments) {

        this.connection = connection;
        this.path = path;
        this.backgroundPath = backgroundPath;
        this.connectionSegments = connectionSegments;
    }

    /**
     * Gets the horizontal or vertical offset to the connection for the given cursor position.
     *
     * @param cursorSceneX the cursor x-position in the scene
     * @param cursorSceneY the cursor y-position in the scene
     * @return an offset to the nearest connection, or {@code null} if the cursor is too far away
     */
    public Point2D getOffset(final double cursorSceneX, final double cursorSceneY) {

        // Scale factor only relevant if we are zoomed in.
        final double scaleFactor = backgroundPath.getLocalToSceneTransform().getMxx();

        // This will be used as the largest acceptable offset value.
        final double offsetBound = Math.ceil(backgroundPath.getStrokeWidth() / 2) * scaleFactor;

        minOffsetX = offsetBound + 1;
        minOffsetY = offsetBound + 1;

        currentX = ((MoveTo) path.getElements().get(0)).getX();
        currentY = ((MoveTo) path.getElements().get(0)).getY();

        for (int i = 1; i < path.getElements().size(); i++) {

            final PathElement pathElement = path.getElements().get(i);

            calculateOffset(pathElement, cursorSceneX, cursorSceneY, offsetBound);
        }

        if (minOffsetX > offsetBound && minOffsetY > offsetBound) {
            return null;
        } else if (Math.abs(minOffsetX) <= Math.abs(minOffsetY)) {
            return new Point2D(minOffsetX, 0);
        } else {
            return new Point2D(0, minOffsetY);
        }
    }

    /**
     * Gets the index i of the connection segment that is closest to the given cursor position.
     *
     * @param cursorX the cursor X position
     * @param cursorY the cursor Y position
     * @return the index of the nearest connection segment
     */
    public int getNearestSegment(final double cursorX, final double cursorY) {

        int nearestIndex = -1;
        double nearestDistance = -1;

        for (int i = 0; i < connectionSegments.size(); i++) {

            final Point2D start = path.localToScene(connectionSegments.get(i).getStart());
            final Point2D end = path.localToScene(connectionSegments.get(i).getEnd());

            if (RectangularConnectionUtils.isSegmentHorizontal(connection, i)) {

                final boolean inRangeX = GeometryUtils.checkInRange(start.getX(), end.getX(), cursorX);
                final double distanceY = Math.abs(start.getY() - cursorY);

                if (inRangeX && (nearestDistance < 0 || distanceY < nearestDistance)) {
                    nearestIndex = i;
                    nearestDistance = distanceY;
                }
            } else {

                final boolean inRangeY = GeometryUtils.checkInRange(start.getY(), end.getY(), cursorY);
                final double distanceX = Math.abs(start.getX() - cursorX);

                if (inRangeY && (nearestDistance < 0 || distanceX < nearestDistance)) {
                    nearestIndex = i;
                    nearestDistance = distanceX;
                }
            }
        }

        return nearestIndex;
    }

    /**
     * Calculates the offset of the cursor to the given path element.
     *
     * <p>
     * If the offset is smaller than the current minimum variable, its value will be updated.
     * </p>
     *
     * @param pathElement a {@link PathElement} inside the connection path
     * @param cursorSceneX the x position of the cursor
     * @param cursorSceneY the y position of the cursor
     * @param offsetBound the maximum allowed offset value
     */
    private void calculateOffset(final PathElement pathElement, final double cursorSceneX, final double cursorSceneY,
            final double offsetBound) {

        final double currentSceneX = path.localToScene(currentX, currentY).getX();
        final double currentSceneY = path.localToScene(currentX, currentY).getY();

        if (pathElement instanceof HLineTo) {

            final HLineTo hLineTo = (HLineTo) pathElement;

            final double nextSceneX = path.localToScene(hLineTo.getX(), currentY).getX();
            final double possibleMinOffsetY = currentSceneY - cursorSceneY;

            final boolean inRangeX = GeometryUtils.checkInRange(currentSceneX, nextSceneX, cursorSceneX);
            final boolean cursorInRangeY = Math.abs(possibleMinOffsetY) < offsetBound;
            final boolean foundCloser = Math.abs(possibleMinOffsetY) < Math.abs(minOffsetY);

            if (inRangeX && cursorInRangeY && foundCloser) {
                minOffsetY = possibleMinOffsetY;
            }

            currentX = hLineTo.getX();

        } else if (pathElement instanceof ArcTo) {

            final ArcTo arcTo = (ArcTo) pathElement;

            currentX = arcTo.getX();
            currentY = arcTo.getY();

        } else if (pathElement instanceof VLineTo) {

            final VLineTo vLineTo = (VLineTo) pathElement;

            final double nextSceneY = path.localToScene(currentX, vLineTo.getY()).getY();
            final double possibleMinOffsetX = currentSceneX - cursorSceneX;

            final boolean cursorInRangeY = GeometryUtils.checkInRange(currentSceneY, nextSceneY, cursorSceneY);
            final boolean cursorInRangeX = Math.abs(possibleMinOffsetX) < offsetBound;
            final boolean foundCloser = Math.abs(possibleMinOffsetX) < Math.abs(minOffsetX);

            if (cursorInRangeY && cursorInRangeX && foundCloser) {
                minOffsetX = possibleMinOffsetX;
            }
            currentY = vLineTo.getY();
        }
    }
}