/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.lucene.geo;

import static org.apache.lucene.tests.geo.GeoTestUtil.createRegularPolygon;
import static org.apache.lucene.tests.geo.GeoTestUtil.nextLatitude;
import static org.apache.lucene.tests.geo.GeoTestUtil.nextLongitude;
import static org.apache.lucene.tests.geo.GeoTestUtil.nextPointNear;
import static org.apache.lucene.tests.geo.GeoTestUtil.nextPolygon;

import com.carrotsearch.randomizedtesting.generators.RandomNumbers;
import org.apache.lucene.index.PointValues.Relation;
import org.apache.lucene.tests.geo.GeoTestUtil;
import org.apache.lucene.tests.util.LuceneTestCase;
import org.apache.lucene.tests.util.TestUtil;

/** Test Polygon2D impl */
public class TestPolygon2D extends LuceneTestCase {

  /** Three boxes, an island inside a hole inside a shape */
  public void testMultiPolygon() {
    Polygon hole =
        new Polygon(new double[] {-10, -10, 10, 10, -10}, new double[] {-10, 10, 10, -10, -10});
    Polygon outer =
        new Polygon(
            new double[] {-50, -50, 50, 50, -50}, new double[] {-50, 50, 50, -50, -50}, hole);
    Polygon island = new Polygon(new double[] {-5, -5, 5, 5, -5}, new double[] {-5, 5, 5, -5, -5});
    Component2D polygon = LatLonGeometry.create(outer, island);

    // contains(point)
    assertTrue(polygon.contains(-2, 2)); // on the island
    assertFalse(polygon.contains(-6, 6)); // in the hole
    assertTrue(polygon.contains(-25, 25)); // on the mainland
    assertFalse(polygon.contains(-51, 51)); // in the ocean

    // relate(box): this can conservatively return CELL_CROSSES_QUERY
    assertEquals(Relation.CELL_INSIDE_QUERY, polygon.relate(-2, 2, -2, 2)); // on the island
    assertEquals(Relation.CELL_OUTSIDE_QUERY, polygon.relate(6, 7, 6, 7)); // in the hole
    assertEquals(Relation.CELL_INSIDE_QUERY, polygon.relate(24, 25, 24, 25)); // on the mainland
    assertEquals(Relation.CELL_OUTSIDE_QUERY, polygon.relate(51, 52, 51, 52)); // in the ocean
    assertEquals(
        Relation.CELL_CROSSES_QUERY, polygon.relate(-60, 60, -60, 60)); // enclosing us completely
    assertEquals(
        Relation.CELL_CROSSES_QUERY, polygon.relate(49, 51, 49, 51)); // overlapping the mainland
    assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(9, 11, 9, 11)); // overlapping the hole
    assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(5, 6, 5, 6)); // overlapping the island
  }

  public void testPacMan() throws Exception {
    // pacman
    double[] px = {0, 10, 10, 0, -8, -10, -8, 0, 10, 10, 0};
    double[] py = {0, 5, 9, 10, 9, 0, -9, -10, -9, -5, 0};

    // candidate crosses cell
    double xMin = 2; // -5;
    double xMax = 11; // 0.000001;
    double yMin = -1; // 0;
    double yMax = 1; // 5;

    // test cell crossing poly
    Component2D polygon = Polygon2D.create(new Polygon(py, px));
    assertEquals(Relation.CELL_CROSSES_QUERY, polygon.relate(yMin, yMax, xMin, xMax));
  }

  public void testBoundingBox() throws Exception {
    for (int i = 0; i < 100; i++) {
      Component2D polygon = Polygon2D.create(nextPolygon());

      for (int j = 0; j < 100; j++) {
        double latitude = nextLatitude();
        double longitude = nextLongitude();
        // if the point is within poly, then it should be in our bounding box
        if (polygon.contains(longitude, latitude)) {
          assertTrue(latitude >= polygon.getMinY() && latitude <= polygon.getMaxY());
          assertTrue(longitude >= polygon.getMinX() && longitude <= polygon.getMaxX());
        }
      }
    }
  }

  // targets the bounding box directly
  public void testBoundingBoxEdgeCases() throws Exception {
    for (int i = 0; i < 100; i++) {
      Polygon polygon = nextPolygon();
      Component2D impl = Polygon2D.create(polygon);

      for (int j = 0; j < 100; j++) {
        double[] point = GeoTestUtil.nextPointNear(polygon);
        double latitude = point[0];
        double longitude = point[1];
        // if the point is within poly, then it should be in our bounding box
        if (impl.contains(longitude, latitude)) {
          assertTrue(latitude >= polygon.minLat && latitude <= polygon.maxLat);
          assertTrue(longitude >= polygon.minLon && longitude <= polygon.maxLon);
        }
      }
    }
  }

  /**
   * If polygon.contains(box) returns true, then any point in that box should return true as well
   */
  public void testContainsRandom() throws Exception {
    int iters = atLeast(50);
    for (int i = 0; i < iters; i++) {
      Polygon polygon = nextPolygon();
      Component2D impl = Polygon2D.create(polygon);

      for (int j = 0; j < 100; j++) {
        Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon);
        // allowed to conservatively return false
        if (impl.relate(rectangle.minLat, rectangle.maxLat, rectangle.minLon, rectangle.maxLon)
            == Relation.CELL_INSIDE_QUERY) {
          for (int k = 0; k < 500; k++) {
            // this tests in our range but sometimes outside! so we have to double-check its really
            // in other box
            double[] point = GeoTestUtil.nextPointNear(rectangle);
            double latitude = point[0];
            double longitude = point[1];
            // check for sure its in our box
            if (latitude >= rectangle.minLat
                && latitude <= rectangle.maxLat
                && longitude >= rectangle.minLon
                && longitude <= rectangle.maxLon) {
              assertTrue(impl.contains(latitude, longitude));
            }
          }
          for (int k = 0; k < 100; k++) {
            // this tests in our range but sometimes outside! so we have to double-check its really
            // in other box
            double[] point = GeoTestUtil.nextPointNear(polygon);
            double latitude = point[0];
            double longitude = point[1];
            // check for sure its in our box
            if (latitude >= rectangle.minLat
                && latitude <= rectangle.maxLat
                && longitude >= rectangle.minLon
                && longitude <= rectangle.maxLon) {
              assertTrue(impl.contains(latitude, longitude));
            }
          }
        }
      }
    }
  }

  /**
   * If polygon.contains(box) returns true, then any point in that box should return true as well
   */
  // different from testContainsRandom in that its not a purely random test. we iterate the vertices
  // of the polygon
  // and generate boxes near each one of those to try to be more efficient.
  public void testContainsEdgeCases() throws Exception {
    for (int i = 0; i < 1000; i++) {
      Polygon polygon = nextPolygon();
      Component2D impl = Polygon2D.create(polygon);

      for (int j = 0; j < 10; j++) {
        Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon);
        // allowed to conservatively return false
        if (impl.relate(rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat)
            == Relation.CELL_INSIDE_QUERY) {
          for (int k = 0; k < 100; k++) {
            // this tests in our range but sometimes outside! so we have to double-check its really
            // in other box
            double[] point = GeoTestUtil.nextPointNear(rectangle);
            double latitude = point[0];
            double longitude = point[1];
            // check for sure its in our box
            if (latitude >= rectangle.minLat
                && latitude <= rectangle.maxLat
                && longitude >= rectangle.minLon
                && longitude <= rectangle.maxLon) {
              assertTrue(impl.contains(longitude, latitude));
            }
          }
          for (int k = 0; k < 20; k++) {
            // this tests in our range but sometimes outside! so we have to double-check its really
            // in other box
            double[] point = GeoTestUtil.nextPointNear(polygon);
            double latitude = point[0];
            double longitude = point[1];
            // check for sure its in our box
            if (latitude >= rectangle.minLat
                && latitude <= rectangle.maxLat
                && longitude >= rectangle.minLon
                && longitude <= rectangle.maxLon) {
              assertTrue(impl.contains(longitude, latitude));
            }
          }
        }
      }
    }
  }

  /**
   * If polygon.intersects(box) returns false, then any point in that box should return false as
   * well
   */
  public void testIntersectRandom() {
    int iters = atLeast(10);
    for (int i = 0; i < iters; i++) {
      Polygon polygon = nextPolygon();
      Component2D impl = Polygon2D.create(polygon);

      int innerIters = atLeast(10);
      for (int j = 0; j < innerIters; j++) {
        Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon);
        // allowed to conservatively return true.
        if (impl.relate(rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat)
            == Relation.CELL_OUTSIDE_QUERY) {
          for (int k = 0; k < 1000; k++) {
            double[] point = GeoTestUtil.nextPointNear(rectangle);
            // this tests in our range but sometimes outside! so we have to double-check its really
            // in other box
            double latitude = point[0];
            double longitude = point[1];
            // check for sure its in our box
            if (latitude >= rectangle.minLat
                && latitude <= rectangle.maxLat
                && longitude >= rectangle.minLon
                && longitude <= rectangle.maxLon) {
              assertFalse(impl.contains(longitude, latitude));
            }
          }
          for (int k = 0; k < 100; k++) {
            double[] point = GeoTestUtil.nextPointNear(polygon);
            // this tests in our range but sometimes outside! so we have to double-check its really
            // in other box
            double latitude = point[0];
            double longitude = point[1];
            // check for sure its in our box
            if (latitude >= rectangle.minLat
                && latitude <= rectangle.maxLat
                && longitude >= rectangle.minLon
                && longitude <= rectangle.maxLon) {
              assertFalse(impl.contains(longitude, latitude));
            }
          }
        }
      }
    }
  }

  /**
   * If polygon.intersects(box) returns false, then any point in that box should return false as
   * well
   */
  // different from testIntersectsRandom in that its not a purely random test. we iterate the
  // vertices of the polygon
  // and generate boxes near each one of those to try to be more efficient.
  public void testIntersectEdgeCases() {
    for (int i = 0; i < 100; i++) {
      Polygon polygon = nextPolygon();
      Component2D impl = Polygon2D.create(polygon);

      for (int j = 0; j < 10; j++) {
        Rectangle rectangle = GeoTestUtil.nextBoxNear(polygon);
        // allowed to conservatively return false.
        if (impl.relate(rectangle.minLon, rectangle.maxLon, rectangle.minLat, rectangle.maxLat)
            == Relation.CELL_OUTSIDE_QUERY) {
          for (int k = 0; k < 100; k++) {
            // this tests in our range but sometimes outside! so we have to double-check its really
            // in other box
            double[] point = GeoTestUtil.nextPointNear(rectangle);
            double latitude = point[0];
            double longitude = point[1];
            // check for sure its in our box
            if (latitude >= rectangle.minLat
                && latitude <= rectangle.maxLat
                && longitude >= rectangle.minLon
                && longitude <= rectangle.maxLon) {
              assertFalse(impl.contains(longitude, latitude));
            }
          }
          for (int k = 0; k < 50; k++) {
            // this tests in our range but sometimes outside! so we have to double-check its really
            // in other box
            double[] point = GeoTestUtil.nextPointNear(polygon);
            double latitude = point[0];
            double longitude = point[1];
            // check for sure its in our box
            if (latitude >= rectangle.minLat
                && latitude <= rectangle.maxLat
                && longitude >= rectangle.minLon
                && longitude <= rectangle.maxLon) {
              assertFalse(impl.contains(longitude, latitude));
            }
          }
        }
      }
    }
  }

  /** Tests edge case behavior with respect to insideness */
  public void testEdgeInsideness() {
    Component2D poly =
        Polygon2D.create(
            new Polygon(new double[] {-2, -2, 2, 2, -2}, new double[] {-2, 2, 2, -2, -2}));
    assertTrue(poly.contains(-2, -2)); // bottom left corner: true
    assertTrue(poly.contains(2, -2)); // bottom right corner: true
    assertTrue(poly.contains(-2, 2)); // top left corner: true
    assertTrue(poly.contains(2, 2)); // top right corner: true
    assertTrue(poly.contains(-1, -2)); // bottom side: true
    assertTrue(poly.contains(0, -2)); // bottom side: true
    assertTrue(poly.contains(1, -2)); // bottom side: true
    assertTrue(poly.contains(-1, 2)); // top side: true
    assertTrue(poly.contains(0, 2)); // top side: true
    assertTrue(poly.contains(1, 2)); // top side: true
    assertTrue(poly.contains(2, -1)); // right side: true
    assertTrue(poly.contains(2, 0)); // right side: true
    assertTrue(poly.contains(2, 1)); // right side: true
    assertTrue(poly.contains(-2, -1)); // left side: true
    assertTrue(poly.contains(-2, 0)); // left side: true
    assertTrue(poly.contains(-2, 1)); // left side: true
  }

  /** Tests edge case behavior with respect to insideness */
  public void testIntersectsSameEdge() {
    Component2D poly =
        Polygon2D.create(
            new Polygon(new double[] {-2, -2, 2, 2, -2}, new double[] {-2, 2, 2, -2, -2}));
    // line inside edge
    assertTrue(poly.containsTriangle(-1, -1, 1, 1, -1, -1));
    assertTrue(poly.containsTriangle(-2, -2, 2, 2, -2, -2));
    assertTrue(poly.intersectsTriangle(-1, -1, 1, 1, -1, -1));
    assertTrue(poly.intersectsTriangle(-2, -2, 2, 2, -2, -2));
    // line over edge
    assertFalse(poly.containsTriangle(-4, -4, 4, 4, -4, -4));
    assertFalse(poly.containsTriangle(-2, -2, 4, 4, 4, 4));
    assertTrue(poly.intersectsTriangle(-4, -4, 4, 4, -4, -4));
    assertTrue(poly.intersectsTriangle(-2, -2, 4, 4, 4, 4));
    // line inside edge
    assertFalse(poly.containsTriangle(-1, -1, 3, 3, 1, 1));
    assertFalse(poly.containsTriangle(-2, -2, 3, 3, 2, 2));
    assertTrue(poly.intersectsTriangle(-1, -1, 3, 3, 1, 1));
    assertTrue(poly.intersectsTriangle(-2, -2, 3, 3, 2, 2));
    // line over edge
    assertFalse(poly.containsTriangle(-4, -4, 7, 7, 4, 4));
    assertFalse(poly.containsTriangle(-2, -2, 7, 7, 4, 4));
    assertTrue(poly.intersectsTriangle(-4, -4, 7, 7, 4, 4));
    assertTrue(poly.intersectsTriangle(-2, -2, 7, 7, 4, 4));
  }

  /** Tests current impl against original algorithm */
  public void testContainsAgainstOriginal() {
    int iters = atLeast(100);
    for (int i = 0; i < iters; i++) {
      Polygon polygon = nextPolygon();
      // currently we don't generate these, but this test does not want holes.
      while (polygon.getHoles().length > 0) {
        polygon = nextPolygon();
      }
      Component2D impl = Polygon2D.create(polygon);

      // random lat/lons against polygon
      for (int j = 0; j < 1000; j++) {
        double[] point = GeoTestUtil.nextPointNear(polygon);
        double latitude = point[0];
        double longitude = point[1];
        boolean expected = GeoTestUtil.containsSlowly(polygon, longitude, latitude);
        assertEquals(expected, impl.contains(latitude, longitude));
      }
    }
  }

  // targets the polygon directly
  public void testRelateTriangle() {
    for (int i = 0; i < 100; ++i) {
      Polygon polygon = nextPolygon();
      Component2D impl = Polygon2D.create(polygon);

      for (int j = 0; j < 100; j++) {
        double[] a = nextPointNear(polygon);
        double[] b = nextPointNear(polygon);
        double[] c = nextPointNear(polygon);

        // if the point is within poly, then triangle should not intersect
        if (impl.contains(a[1], a[0]) || impl.contains(b[1], b[0]) || impl.contains(c[1], c[0])) {
          assertTrue(impl.intersectsTriangle(a[1], a[0], b[1], b[0], c[1], c[0]));
        }
      }
    }
  }

  public void testRelateTriangleContainsPolygon() {
    Polygon polygon = new Polygon(new double[] {0, 0, 1, 1, 0}, new double[] {0, 1, 1, 0, 0});
    Component2D impl = Polygon2D.create(polygon);
    assertTrue(impl.intersectsTriangle(-10, -1, 2, -1, 10, 10));
  }

  // test
  public void testRelateTriangleEdgeCases() {
    for (int i = 0; i < 100; ++i) {
      // random radius between 1Km and 100Km
      int randomRadius = RandomNumbers.randomIntBetween(random(), 1000, 100000);
      // random number of vertices
      int numVertices = RandomNumbers.randomIntBetween(random(), 100, 1000);
      Polygon polygon = createRegularPolygon(0, 0, randomRadius, numVertices);
      Component2D impl = Polygon2D.create(polygon);

      // create and test a simple tessellation
      for (int j = 1; j < numVertices; ++j) {
        double[] a = new double[] {0d, 0d}; // center of poly
        double[] b = new double[] {polygon.getPolyLat(j - 1), polygon.getPolyLon(j - 1)};
        // occassionally test pancake triangles
        double[] c =
            random().nextBoolean()
                ? new double[] {polygon.getPolyLat(j), polygon.getPolyLon(j)}
                : new double[] {a[0], a[1]};
        assertTrue(impl.intersectsTriangle(a[0], a[1], b[0], b[1], c[0], c[1]));
      }
    }
  }

  public void testLineCrossingPolygonPoints() {
    Polygon p = new Polygon(new double[] {0, -1, 0, 1, 0}, new double[] {-1, 0, 1, 0, -1});
    Component2D polygon2D = Polygon2D.create(p);
    boolean intersects =
        polygon2D.intersectsTriangle(
            GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(-1.5)),
            GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(0)),
            GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(1.5)),
            GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(0)),
            GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(-1.5)),
            GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(0)));
    assertTrue(intersects);
  }

  public void testRandomLineCrossingPolygon() {
    Polygon p =
        GeoTestUtil.createRegularPolygon(0, 0, 1000, TestUtil.nextInt(random(), 100, 10000));
    Component2D polygon2D = Polygon2D.create(p);
    for (int i = 0; i < 1000; i++) {
      double longitude = GeoTestUtil.nextLongitude();
      double latitude = GeoTestUtil.nextLatitude();
      boolean intersects =
          polygon2D.intersectsTriangle(
              GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(-longitude)),
              GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(-latitude)),
              GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(longitude)),
              GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(latitude)),
              GeoEncodingUtils.decodeLongitude(GeoEncodingUtils.encodeLongitude(-longitude)),
              GeoEncodingUtils.decodeLatitude(GeoEncodingUtils.encodeLatitude(-latitude)));
      assertTrue(intersects);
    }
  }
}
