// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.pdfimport;

import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class ParallelSegmentsFinder {
    public double angle;
    public double angleSum;
    public int refCount;
    public List<PdfPath> paths = new ArrayList<>();

    public void addPath(PdfPath path, double angle2) {
        angleSum += angle2;
        paths.add(path);
        angle = angleSum /paths.size();
    }

    public List<ParallelSegmentsFinder> splitByDistance(double maxDistance) {
        //sort perpendicular to angle
        AffineTransform tr = new AffineTransform();
        tr.rotate(-angle);

        final Map<PdfPath, Point2D> positions = new HashMap<>();
        Point2D src = new Point2D.Double();

        for (PdfPath path: paths) {
            src.setLocation((path.firstPoint().getX() + path.lastPoint().getX()) / 2, (path.firstPoint().getY() + path.lastPoint().getY()) / 2);
            Point2D dest = new Point2D.Double();
            Point2D destA = new Point2D.Double();
            Point2D destB = new Point2D.Double();
            tr.transform(src, dest);
            tr.transform(path.firstPoint(), destA);
            tr.transform(path.lastPoint(), destB);
            positions.put(path, dest);
        }
        //point.y = Perpendicular lines, point.x = parallel lines

        Collections.sort(paths, new Comparator<PdfPath>() {
            @Override
            public int compare(PdfPath o1, PdfPath o2) {
                double y1 = positions.get(o1).getY();
                double y2 = positions.get(o2).getY();
                return y1 > y2 ? 1 : y1 < y2 ? -1 : 0;
            }
        });

        //process sweep
        List<ParallelSegmentsFinder> result = new ArrayList<>();

        Map<PdfPath, ParallelSegmentsFinder> sweepLine = new HashMap<>();

        Set<ParallelSegmentsFinder> adjacentClustersSet = new HashSet<>();
        List<ParallelSegmentsFinder> adjacentClusters = new ArrayList<>();
        List<PdfPath> pathsToRemove = new ArrayList<>();

        for (PdfPath path: paths) {
            adjacentClusters.clear();
            adjacentClustersSet.clear();
            pathsToRemove.clear();

            for (PdfPath p: sweepLine.keySet()) {
                Point2D pathPos = positions.get(path);
                Point2D pPos = positions.get(p);

                if (pathPos.getY() - pPos.getY() > maxDistance) {
                    //path too far from sweep line
                    pathsToRemove.add(p);
                } else {

                    double distance = distanceLineLine(path, p);

                    if (distance <= maxDistance) {
                        if (adjacentClustersSet.add(sweepLine.get(p))) {
                            adjacentClusters.add(sweepLine.get(p));
                        }
                    }
                }
            }

            //remove segments too far apart
            for (PdfPath p: pathsToRemove) {
                ParallelSegmentsFinder finder = sweepLine.remove(p);
                finder.refCount--;
                if (finder.refCount == 0) {
                    result.add(finder);
                }
            }

            //join together joinable parts
            if (adjacentClusters.size() > 0) {
                ParallelSegmentsFinder finder = adjacentClusters.remove(0);
                finder.paths.add(path);
                sweepLine.put(path, finder);
                finder.refCount++;

                for (ParallelSegmentsFinder finder1: adjacentClusters) {
                    for (PdfPath path1: finder1.paths) {
                        finder.paths.add(path1);
                        sweepLine.put(path1, finder);
                        finder.refCount++;
                    }
                }
            } else {
                ParallelSegmentsFinder finder = new ParallelSegmentsFinder();
                finder.addPath(path, angle);
                sweepLine.put(path, finder);
                finder.refCount = 1;
            }
        }

        //process remaining paths in sweep line
        for (PdfPath p: sweepLine.keySet()) {
            ParallelSegmentsFinder finder = sweepLine.get(p);
            finder.refCount--;
            if (finder.refCount == 0) {
                result.add(finder);
            }
        }

        return result;
    }

    private double distanceLineLine(PdfPath p1, PdfPath p2) {
        return distanceLineLine(p1.firstPoint(), p1.lastPoint(), p2.firstPoint(), p2.lastPoint());
    }

    private double distanceLineLine(Point2D p1, Point2D p2, Point2D p3, Point2D p4) {
        double dist1 = closestPointToSegment(p1, p2, p3).distance(p3);
        double dist2 = closestPointToSegment(p1, p2, p4).distance(p4);
        double dist3 = closestPointToSegment(p3, p4, p1).distance(p1);
        double dist4 = closestPointToSegment(p3, p4, p2).distance(p2);
        return Math.min(Math.min(dist1, dist2), Math.min(dist3, dist4));
    }

    /**
     * Calculates closest point to a line segment.
     * @return segmentP1 if it is the closest point, segmentP2 if it is the closest point,
     * a new point if closest point is between segmentP1 and segmentP2.
     */
    public static Point2D closestPointToSegment(Point2D segmentP1, Point2D segmentP2, Point2D point) {

        double ldx = segmentP2.getX() - segmentP1.getX();
        double ldy = segmentP2.getY() - segmentP1.getY();

        if (ldx == 0 && ldy == 0) //segment zero length
            return segmentP1;

        double pdx = point.getX() - segmentP1.getX();
        double pdy = point.getY() - segmentP1.getY();

        double offset = (pdx * ldx + pdy * ldy) / (ldx * ldx + ldy * ldy);

        if (offset <= 0)
            return segmentP1;
        else if (offset >= 1)
            return segmentP2;
        else
            return new Point2D.Double(segmentP1.getX() + ldx * offset, segmentP1.getY() + ldy * offset);
    }
}
