package CH.ifa.draw.figures;

import java.awt.Point;
import java.util.Random;
import java.util.Vector;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

public class RamerDouglasPeuckerTest {

    /**
     * Test simplifying a multiple points with smoothness
     * This test verifies if the `simplifyBySmoothness()` method correctly simplifies a vector of points by removing points that are too close to their previous points, based on the given smoothness.
     **/
    @Test
    public void testSimplifyBySmoothness() {
        // given
        Vector<Point> points = getPointsWithCloseNeighbors();

        // when
        Vector<Point> simplifiedPoints = RamerDouglasPeucker.simplifyBySmoothness(points, 0.6);

        // then
        assertEquals(3, simplifiedPoints.size());
    }

    /**
     * Test simplifying a multiple points on the same line
     * When the points are on the same line, the Ramer-Douglas-Peucker Algorithm should remove redundant points and only keep the first and last point.
     */
    @Test
    public void testSimplifyBySmoothnessOnSameLine() {
        // given
        Vector<Point> points = getPointsOnLine();

        // when
        Vector<Point> simplifiedPoints = RamerDouglasPeucker.simplifyBySmoothness(points, 0.5);

        // then
        assertTrue(simplifiedPoints.size() < points.size());
    }

    /**
     * Test for simplifying multiple points not on the same line with minimum smoothness.
     * When the points are not on the same line and the smoothness is zero, the Ramer-Douglas-Peucker Algorithm should not remove any points and should return the original points.
     */
    @Test
    public void testSimplifyWithMinimumSmoothness() {
        // given
        Vector<Point> points = getPointsNotOnLine();

        // when
        Vector<Point> simplifiedPoints = RamerDouglasPeucker.simplifyBySmoothness(points, 0);

        // then
        assertEquals(points.size(), simplifiedPoints.size());
    }

    /**
     * Test simplifying a multiple points with smoothness
     * This test verifies if the `simplifyBySmoothness()` method correctly simplifies a vector of points by removing points that are too close to their previous points, based on the given smoothness.
     **/
    @Test
    public void testSimplifyByTolerance() {
        // given
        Vector<Point> points = getPointsWithCloseNeighbors();

        // when
        Vector<Point> simplifiedPoints = RamerDouglasPeucker.simplifyByTolerance(points, 2.0);

        // then
        assertEquals(3, simplifiedPoints.size());
    }

    /**
     * Test for simplifying multiple points not on the same line with minimum tolerance.
     * When the points are not on the same line and the tolerance is zero, the Ramer-Douglas-Peucker Algorithm should not remove any points and should return the original points.
     */
    @Test
    public void testSimplifyWithMinimumTolerance() {
        // given
        Vector<Point> points = getPointsNotOnLine();

        // when
        Vector<Point> simplifiedPoints = RamerDouglasPeucker.simplifyBySmoothness(points, 0);

        // then
        assertEquals(points.size(), simplifiedPoints.size());
    }

    /**
     * Test for simplifying the tail part of points by smoothness
     * By simplifying, the head should remain and only the tail should be simplified
     */
    @Test
    public void testSimplifyTailBySmoothness() {
        // given
        Vector<Point> headPoints = get50RandomPoints();
        Vector<Point> tailPoints = get50RandomPoints();
        Vector<Point> allPoints = new Vector<>(headPoints);
        allPoints.addAll(tailPoints);

        // when
        Vector<Point> simplifiedPoints =
            RamerDouglasPeucker.simplifyTailBySmoothness(allPoints, 0.6);

        // then
        assertTrue(isPrefix(headPoints, simplifiedPoints));
    }

    // HELPER METHODS:
    /**
     * Gets three points that are not in a straight line.
     *
     * @return A list of three points not on the same line.
     */
    private Vector<Point> getPointsNotOnLine() {
        Vector<Point> points = new Vector<>();
        points.add(new Point(100, 100));
        points.add(new Point(200, 130));
        points.add(new Point(300, 150));
        return points;
    }

    /**
     * Gets three points that are in a straight line.
     *
     * @return A list of three points on the same line.
     */
    private Vector<Point> getPointsOnLine() {
        Vector<Point> points = new Vector<>();
        points.add(new Point(100, 200));
        points.add(new Point(200, 200));
        points.add(new Point(300, 200));
        return points;
    }

    /**
     * Returns a vector of points with two points that are very close to each other.
     * @return A Vector of Point objects containing four points, including two points that are very near each other (the distance between 2 points is 1).
     */
    private Vector<Point> getPointsWithCloseNeighbors() {
        Vector<Point> points = new Vector<>();
        points.add(new Point(100, 100));
        points.add(new Point(200, 130));
        points.add(new Point(300, 165)); // This point is very near to the next point
        points.add(new Point(300, 166));
        return points;
    }

    /**
     * Returns a vector of 50 random points.
     * @return Returns a vector of random points. The range of x and y location is 0...1000
     */
    private Vector<Point> get50RandomPoints() {
        Vector<Point> points = new Vector<>();
        Random random = new Random();

        for (int i = 0; i < 50; i++) {
            int x = random.nextInt(1000); // Change the range as per your requirements
            int y = random.nextInt(1000); // Change the range as per your requirements
            points.add(new Point(x, y));
        }

        return points;
    }

    /**
    * Checks if a vector of Point objects is a prefix of another vector of Point objects.
    **/
    private boolean isPrefix(Vector<Point> prefix, Vector<Point> vector) {
        if (prefix.size() > vector.size()) {
            return false; // Prefix cannot be longer than the vector
        }

        for (int i = 0; i < prefix.size(); i++) {
            if (!prefix.get(i).equals(vector.get(i))) {
                return false; // Elements differ, not a prefix
            }
        }

        return true; // All elements in the prefix matched the vector
    }

}
