package tc.oc.commons.bukkit.geometry;

import org.bukkit.util.Vector;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * Line segment defined by two points
 */
public class LineSegment {

    private final ImVector start, finish, delta;

    private LineSegment(ImVector start, ImVector finish, ImVector delta) {
        this.start = checkNotNull(start);
        this.finish = checkNotNull(finish);
        this.delta = delta;
    }

    public static LineSegment of(double x1, double y1, double z1, double x2, double y2, double z2) {
        return new LineSegment(ImVector.of(x1, y1, z1),
                               ImVector.of(x2, y2, z2),
                               ImVector.of(x2 - x1, y2 - y1, z2 - z1));
    }

    public static LineSegment between(Vector start, Vector finish) {
        return new LineSegment(ImVector.copyOf(start),
                               ImVector.copyOf(finish),
                               ImVector.copyOf(finish.minus(start)));
    }

    public static LineSegment from(Vector start, Vector delta) {
        return new LineSegment(ImVector.copyOf(start),
                               ImVector.copyOf(start.plus(delta)),
                               ImVector.copyOf(delta));
    }

    /**
     * The "start" point of the line segment
     */
    public ImVector start() {
        return start;
    }

    /**
     * The "finish" point of the line segment
     */
    public ImVector finish() {
        return finish;
    }

    /**
     * The vector from {@link #start()} to {@link #finish()}
     */
    public ImVector delta() {
        return delta;
    }

    /**
     * Return an interpolated point on the line (but not necessarily on the segment)
     * based on the given parameter, such that:
     *
     *      t < 0       before start
     *      t == 0      start
     *      0 < t < 1   between start and finish
     *      t == 1      finish
     *      t > 1       after finish
     */
    public ImVector parametricPoint(double t) {
        return ImVector.interpolate(start, finish, t);
    }

    /**
     * Find the perpendicular projection of the given point onto this line,
     * as a parameter that can be passed to {@link #parametricPoint(double)}.
     *
     * The projection may be outside the segment.
     */
    public double perpendicularProjectionParameter(Vector point) {
        return point.minus(start).dot(delta) / delta.lengthSquared();
    }

    /**
     * Return the minimal distance of the given point from this line segment.
     *
     * This will either be the perpendicular distance to the line, or the
     * cartesian distance from one of the endpoints.
     */
    public double distance(Vector point) {
        return Math.sqrt(distanceSquared(point));
    }

    public double distanceSquared(Vector point) {
        if(delta.isZero()) {
            return point.distanceSquared(start);
        } else {
            final double t = perpendicularProjectionParameter(point);
            if(t <= 0) {
                return point.distanceSquared(start);
            } else if(t >= 1) {
                return point.distanceSquared(finish);
            } else {
                return point.distanceSquared(parametricPoint(t));
            }
        }
    }
}
