/*
 *
 * This file is part of the iText (R) project.
    Copyright (c) 1998-2022 iText Group NV
 * Authors: Bruno Lowagie, Paulo Soares, et al.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation with the addition of the
 * following permission added to Section 15 as permitted in Section 7(a):
 * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
 * ITEXT GROUP. ITEXT GROUP DISCLAIMS THE WARRANTY OF NON INFRINGEMENT
 * OF THIRD PARTY RIGHTS
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA, 02110-1301 USA, or download the license from the following URL:
 * http://itextpdf.com/terms-of-use/
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License,
 * a covered work must retain the producer line in every PDF that is created
 * or manipulated using iText.
 *
 * You can be released from the requirements of the license by purchasing
 * a commercial license. Buying such a license is mandatory as soon as you
 * develop commercial activities involving the iText software without
 * disclosing the source code of your own applications.
 * These activities include: offering paid services to customers as an ASP,
 * serving PDFs on the fly in a web application, shipping iText with a closed
 * source product.
 *
 * For more information, please contact iText Software Corp. at this
 * address: sales@itextpdf.com
 */
package com.itextpdf.text.pdf.pdfcleanup;

import com.itextpdf.awt.geom.AffineTransform;
import com.itextpdf.awt.geom.NoninvertibleTransformException;
import com.itextpdf.awt.geom.Point;
import com.itextpdf.awt.geom.Point2D;
import com.itextpdf.text.Rectangle;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.parser.*;
import com.itextpdf.text.pdf.parser.Path;
import com.itextpdf.text.pdf.parser.clipper.*;
import com.itextpdf.text.pdf.parser.clipper.Clipper.*;
import com.itextpdf.text.pdf.parser.clipper.Point.LongPoint;

import java.util.*;

class PdfCleanUpRegionFilter extends RenderFilter {

    private List<Rectangle> rectangles;

    private static final double circleApproximationConst = 0.55191502449;

    public PdfCleanUpRegionFilter(List<Rectangle> rectangles) {
        this.rectangles = rectangles;
    }

    /**
     * Checks if the text is inside render filter region.
     */
    @Override
    public boolean allowText(TextRenderInfo renderInfo) {
        LineSegment ascent = renderInfo.getAscentLine();
        LineSegment descent = renderInfo.getDescentLine();

        Point2D[] glyphRect = new Point2D[] {
                new Point2D.Float(ascent.getStartPoint().get(0), ascent.getStartPoint().get(1)),
                new Point2D.Float(ascent.getEndPoint().get(0), ascent.getEndPoint().get(1)),
                new Point2D.Float(descent.getEndPoint().get(0), descent.getEndPoint().get(1)),
                new Point2D.Float(descent.getStartPoint().get(0), descent.getStartPoint().get(1)),
        };

        for (Rectangle rectangle : rectangles) {
            Point2D[] redactRect = getVertices(rectangle);

            if (intersect(glyphRect, redactRect)) {
                return false;
            }
        }

        return true;
    }

    @Override
    public boolean allowImage(ImageRenderInfo renderInfo) {
        throw new UnsupportedOperationException();
    }

    /**
     * Calculates intersection of the image and the render filter region in the coordinate system relative to the image.
     *
     * @return <code>null</code> if the image is not allowed, {@link java.util.List} of
     * {@link com.itextpdf.text.Rectangle} objects otherwise.
     */
    protected List<Rectangle> getCoveredAreas(ImageRenderInfo renderInfo) {
        Rectangle imageRect = calcImageRect(renderInfo);
        List<Rectangle> coveredAreas = new ArrayList<Rectangle>();

        if (imageRect == null) {
            return null;
        }

        for (Rectangle rectangle : rectangles) {
            Rectangle intersectionRect = intersection(imageRect, rectangle);

            if (intersectionRect != null) {
                // True if the image is completely covered
                if (imageRect.equals(intersectionRect)) {
                    return null;
                }

                coveredAreas.add(transformIntersection(renderInfo.getImageCTM(), intersectionRect));
            }
        }

        return coveredAreas;
    }

    protected Path filterStrokePath(Path sourcePath, Matrix ctm, float lineWidth, int lineCapStyle,
                                    int lineJoinStyle, float miterLimit, LineDashPattern lineDashPattern) {
        Path path = sourcePath;
        JoinType joinType = getJoinType(lineJoinStyle);
        EndType endType = getEndType(lineCapStyle);

        if (lineDashPattern != null) {
            if (!lineDashPattern.isSolid()) {
                path = applyDashPattern(path, lineDashPattern);
            }
        }

        ClipperOffset offset = new ClipperOffset(miterLimit, PdfCleanUpProcessor.arcTolerance * PdfCleanUpProcessor.floatMultiplier);
        List<Subpath> degenerateSubpaths = addPath(offset, path, joinType, endType);

        PolyTree resultTree = new PolyTree();
        offset.execute(resultTree, lineWidth * PdfCleanUpProcessor.floatMultiplier / 2);
        Path offsetedPath = convertToPath(resultTree);

        if (degenerateSubpaths.size() > 0) {
            if (endType == EndType.OPEN_ROUND) {
                List<Subpath> circles = convertToCircles(degenerateSubpaths, lineWidth / 2);
                offsetedPath.addSubpaths(circles);
            } else if (endType == EndType.OPEN_SQUARE && lineDashPattern != null) {
                List<Subpath> squares = convertToSquares(degenerateSubpaths, lineWidth, sourcePath);
                offsetedPath.addSubpaths(squares);
            }
        }

        return filterFillPath(offsetedPath, ctm, PathPaintingRenderInfo.NONZERO_WINDING_RULE);
    }

    /**
     * Note: this method will close all unclosed subpaths of the passed path.
     *
     * @param fillingRule If the subpath is contour, pass any value.
     */
    protected Path filterFillPath(Path path, Matrix ctm, int fillingRule) {
        path.closeAllSubpaths();

        Clipper clipper = new DefaultClipper();
        addPath(clipper, path);

        for (Rectangle rectangle : rectangles) {
            Point2D[] transfRectVertices = transformPoints(ctm, true, getVertices(rectangle));
            addRect(clipper, transfRectVertices, PolyType.CLIP);
        }

        PolyFillType fillType = PolyFillType.NON_ZERO;

        if (fillingRule == PathPaintingRenderInfo.EVEN_ODD_RULE) {
            fillType = PolyFillType.EVEN_ODD;
        }

        PolyTree resultTree = new PolyTree();
        clipper.execute(ClipType.DIFFERENCE, resultTree, fillType, PolyFillType.NON_ZERO);

        return convertToPath(resultTree);
    }

    private static JoinType getJoinType(int lineJoinStyle) {
        switch (lineJoinStyle) {
            case PdfContentByte.LINE_JOIN_BEVEL:
                return JoinType.BEVEL;

            case PdfContentByte.LINE_JOIN_MITER:
                return JoinType.MITER;
        }

        return JoinType.ROUND;
    }

    private static EndType getEndType(int lineCapStyle) {
        switch (lineCapStyle) {
            case PdfContentByte.LINE_CAP_BUTT:
                return EndType.OPEN_BUTT;

            case PdfContentByte.LINE_CAP_PROJECTING_SQUARE:
                return EndType.OPEN_SQUARE;
        }

        return EndType.OPEN_ROUND;
    }

    /**
     * Converts specified degenerate subpaths to circles.
     * Note: actually the resultant subpaths are not real circles but approximated.
     *
     * @param radius Radius of each constructed circle.
     * @return {@link java.util.List} consisting of circles constructed on given degenerated subpaths.
     */
    private static List<Subpath> convertToCircles(List<Subpath> degenerateSubpaths, double radius) {
        List<Subpath> circles = new ArrayList<Subpath>(degenerateSubpaths.size());

        for (Subpath subpath : degenerateSubpaths) {
            BezierCurve[] circleSectors = approximateCircle(subpath.getStartPoint(), radius);

            Subpath circle = new Subpath();
            circle.addSegment(circleSectors[0]);
            circle.addSegment(circleSectors[1]);
            circle.addSegment(circleSectors[2]);
            circle.addSegment(circleSectors[3]);

            circles.add(circle);
        }

        return circles;
    }

    /**
     * Converts specified degenerate subpaths to squares.
     * Note: the list of degenerate subpaths should contain at least 2 elements. Otherwise
     * we can't determine the direction which the rotation of each square depends on.
     *
     * @param squareWidth Width of each constructed square.
     * @param sourcePath The path which dash pattern applied to. Needed to calc rotation angle of each square.
     * @return {@link java.util.List} consisting of squares constructed on given degenerated subpaths.
     */
    private static List<Subpath> convertToSquares(List<Subpath> degenerateSubpaths, double squareWidth, Path sourcePath) {
        List<Point2D> pathApprox = getPathApproximation(sourcePath);

        if (pathApprox.size() < 2) {
            return Collections.EMPTY_LIST;
        }

        Iterator<Point2D> approxIter = pathApprox.iterator();
        Point2D approxPt1 = approxIter.next();
        Point2D approxPt2 = approxIter.next();
        StandardLine line = new StandardLine(approxPt1, approxPt2);

        List<Subpath> squares = new ArrayList<Subpath>(degenerateSubpaths.size());
        float widthHalf = (float) squareWidth / 2;

        for (int i = 0; i < degenerateSubpaths.size(); ++i) {
            Point2D point = degenerateSubpaths.get(i).getStartPoint();

            while (!line.contains(point)) {
                approxPt1 = approxPt2;
                approxPt2 = approxIter.next();
                line = new StandardLine(approxPt1, approxPt2);
            }

            double slope = line.getSlope();
            double angle;

            if (slope != Float.POSITIVE_INFINITY) {
                angle = Math.atan(slope);
            } else {
                angle = Math.PI / 2;
            }

            squares.add(constructSquare(point, widthHalf, angle));
        }

        return squares;
    }

    private static List<Point2D> getPathApproximation(Path path) {
        List<Point2D> approx = new ArrayList<Point2D>() {
            @Override
            public boolean addAll(Collection<? extends Point2D> c) {
                Point2D prevPoint = (size() - 1 < 0 ? null : get(size() - 1));
                boolean ret = false;

                for (Point2D pt : c) {
                    if (!pt.equals(prevPoint)) {
                        add(pt);
                        prevPoint = pt;
                        ret = true;
                    }
                }

                return true;
            }
        };

        for (Subpath subpath : path.getSubpaths()) {
            approx.addAll(subpath.getPiecewiseLinearApproximation());
        }

        return approx;
    }

    private static Subpath constructSquare(Point2D squareCenter, double widthHalf, double rotationAngle) {
        // Orthogonal square is the square with sides parallel to one of the axes.
        Point2D[] ortogonalSquareVertices = {
                new Point2D.Double(-widthHalf, -widthHalf),
                new Point2D.Double(-widthHalf, widthHalf),
                new Point2D.Double(widthHalf, widthHalf),
                new Point2D.Double(widthHalf, -widthHalf)
        };

        Point2D[] rotatedSquareVertices = getRotatedSquareVertices(ortogonalSquareVertices, rotationAngle, squareCenter);

        Subpath square = new Subpath();
        square.addSegment(new Line(rotatedSquareVertices[0], rotatedSquareVertices[1]));
        square.addSegment(new Line(rotatedSquareVertices[1], rotatedSquareVertices[2]));
        square.addSegment(new Line(rotatedSquareVertices[2], rotatedSquareVertices[3]));
        square.addSegment(new Line(rotatedSquareVertices[3], rotatedSquareVertices[0]));

        return square;
    }

    private static Point2D[] getRotatedSquareVertices(Point2D[] orthogonalSquareVertices, double angle, Point2D squareCenter) {
        Point2D[] rotatedSquareVertices = new Point2D[orthogonalSquareVertices.length];

        AffineTransform.getRotateInstance(angle).
                transform(orthogonalSquareVertices, 0, rotatedSquareVertices, 0, rotatedSquareVertices.length);
        AffineTransform.getTranslateInstance(squareCenter.getX(), squareCenter.getY()).
                transform(rotatedSquareVertices, 0, rotatedSquareVertices, 0, orthogonalSquareVertices.length);

        return rotatedSquareVertices;
    }

    /**
     * Adds all subpaths of the path to the {@link ClipperOffset} object with one
     * note: it doesn't add degenerate subpaths.
     *
     * @return {@link java.util.List} consisting of all degenerate subpaths of the path.
     */
    private static List<Subpath> addPath(ClipperOffset offset, Path path, JoinType joinType, EndType endType) {
        List<Subpath> degenerateSubpaths = new ArrayList<Subpath>();

        for (Subpath subpath : path.getSubpaths()) {
            if (subpath.isDegenerate()) {
                degenerateSubpaths.add(subpath);
                continue;
            }

            if (!subpath.isSinglePointClosed() && !subpath.isSinglePointOpen()) {
                EndType et;

                if (subpath.isClosed()) {
                    // Offsetting is never used for path being filled
                    et = EndType.CLOSED_LINE;
                } else {
                    et = endType;
                }

                List<Point2D> linearApproxPoints = subpath.getPiecewiseLinearApproximation();
                offset.addPath(convertToIntPoints(linearApproxPoints), joinType, et);
            }
        }

        return degenerateSubpaths;
    }

    private static BezierCurve[] approximateCircle(Point2D center, double radius) {
        // The circle is split into 4 sectors. Arc of each sector
        // is approximated  with bezier curve separately.
        BezierCurve[] approximation = new BezierCurve[4];
        double x = center.getX();
        double y = center.getY();

        approximation[0] = new BezierCurve(Arrays.asList(
                (Point2D) new Point2D.Double(x, y + radius),
                new Point2D.Double(x + radius * circleApproximationConst, y + radius),
                new Point2D.Double(x + radius, y + radius * circleApproximationConst),
                new Point2D.Double(x + radius, y)));

        approximation[1] = new BezierCurve(Arrays.asList(
                (Point2D) new Point2D.Double(x + radius, y),
                new Point2D.Double(x + radius, y - radius * circleApproximationConst),
                new Point2D.Double(x + radius * circleApproximationConst, y - radius),
                new Point2D.Double(x, y - radius)));

        approximation[2] = new BezierCurve(Arrays.asList(
                (Point2D) new Point2D.Double(x, y - radius),
                new Point2D.Double(x - radius * circleApproximationConst, y - radius),
                new Point2D.Double(x - radius, y - radius * circleApproximationConst),
                new Point2D.Double(x - radius, y)));

        approximation[3] = new BezierCurve(Arrays.asList(
                (Point2D) new Point2D.Double(x - radius, y),
                new Point2D.Double(x - radius, y + radius * circleApproximationConst),
                new Point2D.Double(x - radius * circleApproximationConst, y + radius),
                new Point2D.Double(x, y + radius)));

        return approximation;
    }


    private static void addPath(Clipper clipper, Path path) {
        for (Subpath subpath : path.getSubpaths()) {
            if (!subpath.isSinglePointClosed() && !subpath.isSinglePointOpen()) {
                List<Point2D> linearApproxPoints = subpath.getPiecewiseLinearApproximation();
                clipper.addPath(convertToIntPoints(linearApproxPoints), PolyType.SUBJECT, subpath.isClosed());
            }
        }
    }

    private static void addRect(Clipper clipper, Point2D[] rectVertices, PolyType polyType) {
        clipper.addPath(convertToIntPoints(new ArrayList<Point2D>(Arrays.asList(rectVertices))), polyType, true);
    }

    private static com.itextpdf.text.pdf.parser.clipper.Path convertToIntPoints(List<Point2D> points) {
        List<LongPoint> convertedPoints = new ArrayList<LongPoint>(points.size());

        for (Point2D point : points) {
            convertedPoints.add(new LongPoint(PdfCleanUpProcessor.floatMultiplier * point.getX(),
                                              PdfCleanUpProcessor.floatMultiplier * point.getY()));
        }

        return new com.itextpdf.text.pdf.parser.clipper.Path(convertedPoints);
    }

    private static List<Point2D> convertToFloatPoints(List<LongPoint> points) {
        List<Point2D> convertedPoints = new ArrayList<Point2D>(points.size());

        for (LongPoint point : points) {
            convertedPoints.add(new Point2D.Float((float) (point.getX() / PdfCleanUpProcessor.floatMultiplier),
                                                  (float) (point.getY() / PdfCleanUpProcessor.floatMultiplier)));
        }

        return convertedPoints;
    }

    private static Path convertToPath(PolyTree result) {
        Path path = new Path();
        PolyNode node = result.getFirst();

        while (node != null) {
            addContour(path, node.getContour(), !node.isOpen());
            node = node.getNext();
        }

        return path;
    }

    private static void addContour(Path path, List<LongPoint> contour, Boolean close) {
        List<Point2D> floatContour = convertToFloatPoints(contour);
        Iterator<Point2D> iter = floatContour.iterator();

        Point2D point = iter.next();
        path.moveTo((float) point.getX(), (float) point.getY());

        while (iter.hasNext()) {
            point = iter.next();
            path.lineTo((float) point.getX(), (float) point.getY());
        }

        if (close) {
            path.closeSubpath();
        }
    }

    private Point2D[] getVertices(Rectangle rect) {
        Point2D[] points = {
                new Point2D.Double(rect.getLeft(), rect.getBottom()),
                new Point2D.Double(rect.getRight(), rect.getBottom()),
                new Point2D.Double(rect.getRight(), rect.getTop()),
                new Point2D.Double(rect.getLeft(), rect.getTop())
        };

        return points;
    }

    private boolean intersect(Point2D[] rect1, Point2D[] rect2) {
        Clipper clipper = new DefaultClipper();
        addRect(clipper, rect1, PolyType.SUBJECT);
        addRect(clipper, rect2, PolyType.CLIP);

        Paths paths = new Paths();
        clipper.execute(ClipType.INTERSECTION, paths, PolyFillType.NON_ZERO, PolyFillType.NON_ZERO);

        return !paths.isEmpty();
    }

    /**
     * @return Image boundary rectangle in device space.
     */
    private Rectangle calcImageRect(ImageRenderInfo renderInfo) {
        Matrix ctm = renderInfo.getImageCTM();

        if (ctm == null) {
            return null;
        }

        Point2D[] points = transformPoints(ctm, false, new Point(0, 0), new Point(0, 1),
                                                       new Point(1, 0), new Point(1, 1));
        return getRectangle(points[0], points[1], points[2], points[3]);
    }

    /**
     * @return null if the intersection is empty, {@link com.itextpdf.text.Rectangle} representing intersection otherwise
     */
    private Rectangle intersection(Rectangle rect1, Rectangle rect2) {
        com.itextpdf.awt.geom.Rectangle awtRect1 = new com.itextpdf.awt.geom.Rectangle(rect1);
        com.itextpdf.awt.geom.Rectangle awtRect2 = new com.itextpdf.awt.geom.Rectangle(rect2);
        com.itextpdf.awt.geom.Rectangle awtIntersection = awtRect1.intersection(awtRect2);

        return awtIntersection.isEmpty() ? null : new Rectangle(awtIntersection);
    }

    /**
     * Transforms the given Rectangle into the image coordinate system which is [0,1]x[0,1] by default
     */
    private Rectangle transformIntersection(Matrix imageCTM, Rectangle rect) {
        Point2D[] points = transformPoints(imageCTM, true, new Point(rect.getLeft(), rect.getBottom()),
                                                           new Point(rect.getLeft(), rect.getTop()),
                                                           new Point(rect.getRight(), rect.getBottom()),
                                                           new Point(rect.getRight(), rect.getTop()));
        return getRectangle(points[0], points[1], points[2], points[3]);
    }

    /**
     * Constructs Rectangle object on the given points
     */
    private Rectangle getRectangle(Point2D p1, Point2D p2, Point2D p3, Point2D p4) {
        List<Double> xs = Arrays.asList(p1.getX(), p2.getX(), p3.getX(), p4.getX());
        List<Double> ys = Arrays.asList(p1.getY(), p2.getY(), p3.getY(), p4.getY());

        double left = Collections.min(xs);
        double bottom = Collections.min(ys);
        double right = Collections.max(xs);
        double top = Collections.max(ys);

        return new Rectangle((float) left, (float) bottom, (float) right, (float) top);
    }

    private static Path applyDashPattern(Path path, LineDashPattern lineDashPattern) {
        Set<Integer> modifiedSubpaths = new HashSet<Integer>(path.replaceCloseWithLine());
        Path dashedPath = new Path();
        int currentSubpath = 0;

        for (Subpath subpath : path.getSubpaths()) {
            List<Point2D> subpathApprox = subpath.getPiecewiseLinearApproximation();

            if (subpathApprox.size() > 1) {
                dashedPath.moveTo((float) subpathApprox.get(0).getX(), (float) subpathApprox.get(0).getY());
                float remainingDist = 0;
                boolean remainingIsGap = false;

                for (int i = 1; i < subpathApprox.size(); ++i) {
                    Point2D nextPoint = null;

                    if (remainingDist != 0) {
                        nextPoint = getNextPoint(subpathApprox.get(i - 1), subpathApprox.get(i), remainingDist);
                        remainingDist = applyDash(dashedPath, subpathApprox.get(i - 1), subpathApprox.get(i), nextPoint, remainingIsGap);
                    }

                    while (Float.compare(remainingDist, 0) == 0 && !dashedPath.getCurrentPoint().equals(subpathApprox.get(i))) {
                        LineDashPattern.DashArrayElem currentElem = lineDashPattern.next();
                        nextPoint = getNextPoint(nextPoint != null ? nextPoint : subpathApprox.get(i - 1), subpathApprox.get(i), currentElem.getVal());
                        remainingDist = applyDash(dashedPath, subpathApprox.get(i - 1), subpathApprox.get(i), nextPoint, currentElem.isGap());
                        remainingIsGap = currentElem.isGap();
                    }
                }
                
                // If true, then the line closing the subpath was explicitly added (see Path.ReplaceCloseWithLine).
                // This causes a loss of a visual effect of line join style parameter, so in this clause
                // we simply add overlapping dash (or gap, no matter), which continues the last dash and equals to
                // the first dash (or gap) of the path.
                if (modifiedSubpaths.contains(currentSubpath)) {
                    lineDashPattern.reset();
                    LineDashPattern.DashArrayElem currentElem = lineDashPattern.next();
                    Point2D nextPoint = getNextPoint(subpathApprox.get(0), subpathApprox.get(1), currentElem.getVal());
                    applyDash(dashedPath, subpathApprox.get(0), subpathApprox.get(1), nextPoint, currentElem.isGap());
                }
            }

            // According to PDF spec. line dash pattern should be restarted for each new subpath.
            lineDashPattern.reset();
            ++currentSubpath;
        }

        return dashedPath;
    }

    private static Point2D getNextPoint(Point2D segStart, Point2D segEnd, float dist) {
        Point2D vector = componentwiseDiff(segEnd, segStart);
        Point2D unitVector = getUnitVector(vector);

        return new Point2D.Float((float) (segStart.getX() + dist * unitVector.getX()),
                                 (float) (segStart.getY() + dist * unitVector.getY()));
    }

    private static Point2D componentwiseDiff(Point2D minuend, Point2D subtrahend) {
        return new Point2D.Float((float) (minuend.getX() - subtrahend.getX()),
                                 (float) (minuend.getY() - subtrahend.getY()));
    }

    private static Point2D getUnitVector(Point2D vector) {
        double vectorLength = getVectorEuclideanNorm(vector);
        return new Point2D.Float((float) (vector.getX() / vectorLength),
                                 (float) (vector.getY() / vectorLength));
    }

    private static double getVectorEuclideanNorm(Point2D vector) {
        return vector.distance(0, 0);
    }

    private static float applyDash(Path dashedPath, Point2D segStart, Point2D segEnd, Point2D dashTo, boolean isGap) {
        float remainingDist = 0;

        if (!liesOnSegment(segStart, segEnd, dashTo)) {
            remainingDist = (float) dashTo.distance(segEnd);
            dashTo = segEnd;
        }

        if (isGap) {
            dashedPath.moveTo((float) dashTo.getX(), (float) dashTo.getY());
        } else {
            dashedPath.lineTo((float) dashTo.getX(), (float) dashTo.getY());
        }

        return remainingDist;
    }

    private static boolean liesOnSegment(Point2D segStart, Point2D segEnd, Point2D point) {
        return point.getX() >= Math.min(segStart.getX(), segEnd.getX()) &&
               point.getX() <= Math.max(segStart.getX(), segEnd.getX()) &&
               point.getY() >= Math.min(segStart.getY(), segEnd.getY()) &&
               point.getY() <= Math.max(segStart.getY(), segEnd.getY());
    }

    private Point2D[] transformPoints(Matrix transormationMatrix, boolean inverse, Point2D... points) {
        AffineTransform t = new AffineTransform(transormationMatrix.get(Matrix.I11), transormationMatrix.get(Matrix.I12),
                transormationMatrix.get(Matrix.I21), transormationMatrix.get(Matrix.I22),
                transormationMatrix.get(Matrix.I31), transormationMatrix.get(Matrix.I32));
        Point2D[] transformed = new Point2D[points.length];

        if (inverse) {
            try {
                t = t.createInverse();
            } catch (NoninvertibleTransformException e) {
                throw new RuntimeException(e);
            }
        }

        t.transform(points, 0, transformed, 0, points.length);

        return transformed;
    }

    // Constants from the standard line representation: Ax+By+C
    private static class StandardLine {

        float A;
        float B;
        float C;

        StandardLine(Point2D p1, Point2D p2) {
            A = (float) (p2.getY() - p1.getY());
            B = (float) (p1.getX() - p2.getX());
            C = (float) (p1.getY() * (-B) - p1.getX() * A);
        }

        float getSlope() {
            if (B == 0) {
                return Float.POSITIVE_INFINITY;
            }

            return -A / B;
        }

        boolean contains(Point2D point) {
            return Float.compare(Math.abs(A * (float) point.getX() + B * (float) point.getY() + C), 0.1f) < 0;
        }
    }
}
