/*
 * Copyright DataStax, Inc.
 *
 * Licensed 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 com.datastax.dse.driver.internal.core.data.geometry;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Fail.fail;

import com.datastax.dse.driver.api.core.data.geometry.LineString;
import com.datastax.dse.driver.api.core.data.geometry.Point;
import com.datastax.dse.driver.api.core.data.geometry.Polygon;
import com.esri.core.geometry.ogc.OGCPolygon;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import org.junit.Test;

public class DefaultPolygonTest {

  private Polygon polygon =
      Polygon.fromPoints(
          Point.fromCoordinates(30, 10),
          Point.fromCoordinates(10, 20),
          Point.fromCoordinates(20, 40),
          Point.fromCoordinates(40, 40));

  private String wkt = "POLYGON ((30 10, 40 40, 20 40, 10 20, 30 10))";

  private String json =
      "{\"type\":\"Polygon\",\"coordinates\":[[[30.0,10.0],[10.0,20.0],[20.0,40.0],[40.0,40.0],[30.0,10.0]]]}";

  @Test
  public void should_parse_valid_well_known_text() {
    assertThat(Polygon.fromWellKnownText(wkt)).isEqualTo(polygon);
  }

  @Test
  public void should_fail_to_parse_invalid_well_known_text() {
    assertInvalidWkt("polygon(())"); // malformed
    assertInvalidWkt("polygon((30 10 1, 40 40 1, 20 40 1, 10 20 1, 30 10 1))"); // 3d
    assertInvalidWkt("polygon((0 0, 1 1, 0 1, 1 0, 0 0))"); // crosses itself
    assertInvalidWkt("polygon123((30 10, 40 40, 20 40, 10 20, 30 10))"); // malformed
  }

  @Test
  public void should_convert_to_well_known_binary() {
    ByteBuffer actual = polygon.asWellKnownBinary();

    ByteBuffer expected = ByteBuffer.allocate(1024).order(ByteOrder.nativeOrder());
    expected.position(0);
    expected.put((byte) (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN ? 1 : 0)); // endianness
    expected.putInt(3); // type
    expected.putInt(1); // num rings
    expected.putInt(5); // num polygons (ring 1/1)
    expected.putDouble(30); // x1
    expected.putDouble(10); // y1
    expected.putDouble(40); // x2
    expected.putDouble(40); // y2
    expected.putDouble(20); // x3
    expected.putDouble(40); // y3
    expected.putDouble(10); // x4
    expected.putDouble(20); // y4
    expected.putDouble(30); // x5
    expected.putDouble(10); // y5
    expected.flip();

    assertThat(actual).isEqualTo(expected);
  }

  @Test
  public void should_load_from_well_known_binary() {
    ByteBuffer bb = ByteBuffer.allocate(1024).order(ByteOrder.nativeOrder());
    bb.position(0);
    bb.put((byte) (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN ? 1 : 0)); // endianness
    bb.putInt(3); // type
    bb.putInt(1); // num rings
    bb.putInt(5); // num polygons (ring 1/1)
    bb.putDouble(30); // x1
    bb.putDouble(10); // y1
    bb.putDouble(40); // x2
    bb.putDouble(40); // y2
    bb.putDouble(20); // x3
    bb.putDouble(40); // y3
    bb.putDouble(10); // x4
    bb.putDouble(20); // y4
    bb.putDouble(30); // x5
    bb.putDouble(10); // y5
    bb.flip();

    assertThat(Polygon.fromWellKnownBinary(bb)).isEqualTo(polygon);
  }

  @Test
  public void should_parse_valid_geo_json() {
    assertThat(Polygon.fromGeoJson(json)).isEqualTo(polygon);
  }

  @Test
  public void should_convert_to_geo_json() {
    assertThat(polygon.asGeoJson()).isEqualTo(json);
  }

  @Test
  public void should_convert_to_ogc_polygon() {
    assertThat(((DefaultPolygon) polygon).getOgcGeometry()).isInstanceOf(OGCPolygon.class);
  }

  @Test
  public void should_produce_same_hashCode_for_equal_objects() {
    Polygon polygon1 =
        Polygon.fromPoints(
            Point.fromCoordinates(30, 10),
            Point.fromCoordinates(10, 20),
            Point.fromCoordinates(20, 40),
            Point.fromCoordinates(40, 40));
    Polygon polygon2 = Polygon.fromWellKnownText(wkt);
    assertThat(polygon1).isEqualTo(polygon2);
    assertThat(polygon1.hashCode()).isEqualTo(polygon2.hashCode());
  }

  @Test
  public void should_build_with_constructor_without_checking_orientation() {
    // By default, OGC requires outer rings to be clockwise and inner rings to be counterclockwise.
    // We disable that in our constructors.
    // This polygon has a single outer ring that is counterclockwise.
    Polygon polygon =
        Polygon.fromPoints(
            Point.fromCoordinates(5, 0),
            Point.fromCoordinates(5, 3),
            Point.fromCoordinates(0, 3),
            Point.fromCoordinates(0, 0));
    assertThat(polygon.asWellKnownText()).isEqualTo("POLYGON ((0 0, 5 0, 5 3, 0 3, 0 0))");
  }

  @Test
  public void should_build_complex_polygon_with_builder() {
    Polygon polygon =
        Polygon.builder()
            .addRing(
                Point.fromCoordinates(0, 0),
                Point.fromCoordinates(0, 3),
                Point.fromCoordinates(5, 3),
                Point.fromCoordinates(5, 0))
            .addRing(
                Point.fromCoordinates(1, 1),
                Point.fromCoordinates(1, 2),
                Point.fromCoordinates(2, 2),
                Point.fromCoordinates(2, 1))
            .addRing(
                Point.fromCoordinates(3, 1),
                Point.fromCoordinates(3, 2),
                Point.fromCoordinates(4, 2),
                Point.fromCoordinates(4, 1))
            .build();
    assertThat(polygon.asWellKnownText())
        .isEqualTo(
            "POLYGON ((0 0, 5 0, 5 3, 0 3, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1), (3 1, 3 2, 4 2, 4 1, 3 1))");
  }

  @Test
  public void should_expose_rings() {
    assertThat(polygon.getExteriorRing())
        .containsOnly(
            Point.fromCoordinates(30, 10),
            Point.fromCoordinates(10, 20),
            Point.fromCoordinates(20, 40),
            Point.fromCoordinates(40, 40));
    assertThat(polygon.getInteriorRings().isEmpty()).isTrue();

    Polygon fromWkt = Polygon.fromWellKnownText(wkt);
    assertThat(fromWkt.getExteriorRing())
        .containsOnly(
            Point.fromCoordinates(30, 10),
            Point.fromCoordinates(10, 20),
            Point.fromCoordinates(20, 40),
            Point.fromCoordinates(40, 40));
    assertThat(fromWkt.getInteriorRings().isEmpty()).isTrue();

    Polygon complex =
        Polygon.builder()
            .addRing(
                Point.fromCoordinates(0, 0),
                Point.fromCoordinates(0, 3),
                Point.fromCoordinates(5, 3),
                Point.fromCoordinates(5, 0))
            .addRing(
                Point.fromCoordinates(1, 1),
                Point.fromCoordinates(1, 2),
                Point.fromCoordinates(2, 2),
                Point.fromCoordinates(2, 1))
            .addRing(
                Point.fromCoordinates(3, 1),
                Point.fromCoordinates(3, 2),
                Point.fromCoordinates(4, 2),
                Point.fromCoordinates(4, 1))
            .build();
    assertThat(complex.getExteriorRing())
        .containsOnly(
            Point.fromCoordinates(0, 0),
            Point.fromCoordinates(0, 3),
            Point.fromCoordinates(5, 3),
            Point.fromCoordinates(5, 0));
    assertThat(complex.getInteriorRings()).hasSize(2);
    assertThat(complex.getInteriorRings().get(0))
        .containsOnly(
            Point.fromCoordinates(1, 1),
            Point.fromCoordinates(1, 2),
            Point.fromCoordinates(2, 2),
            Point.fromCoordinates(2, 1));
    assertThat(complex.getInteriorRings().get(1))
        .containsOnly(
            Point.fromCoordinates(3, 1),
            Point.fromCoordinates(3, 2),
            Point.fromCoordinates(4, 2),
            Point.fromCoordinates(4, 1));

    Polygon complexFromWkt =
        Polygon.fromWellKnownText(
            "POLYGON ((0 0, 5 0, 5 3, 0 3, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1), (3 1, 3 2, 4 2, 4 1, 3 1))");
    assertThat(complexFromWkt.getExteriorRing())
        .containsOnly(
            Point.fromCoordinates(0, 0),
            Point.fromCoordinates(0, 3),
            Point.fromCoordinates(5, 3),
            Point.fromCoordinates(5, 0));
    assertThat(complexFromWkt.getInteriorRings()).hasSize(2);
    assertThat(complexFromWkt.getInteriorRings().get(0))
        .containsOnly(
            Point.fromCoordinates(1, 1),
            Point.fromCoordinates(1, 2),
            Point.fromCoordinates(2, 2),
            Point.fromCoordinates(2, 1));
    assertThat(complexFromWkt.getInteriorRings().get(1))
        .containsOnly(
            Point.fromCoordinates(3, 1),
            Point.fromCoordinates(3, 2),
            Point.fromCoordinates(4, 2),
            Point.fromCoordinates(4, 1));
  }

  @Test
  public void should_encode_and_decode() throws Exception {
    assertThat(SerializationUtils.serializeAndDeserialize(polygon)).isEqualTo(polygon);
  }

  @Test
  public void should_contain_self() {
    assertThat(polygon.contains(polygon)).isTrue();
  }

  @Test
  public void should_not_contain_point_or_linestring_on_exterior_ring() {
    assertThat(polygon.contains(Point.fromCoordinates(30, 10))).isFalse();
    assertThat(polygon.contains(Point.fromCoordinates(30, 40))).isFalse();
    assertThat(
            polygon.contains(
                LineString.fromPoints(
                    Point.fromCoordinates(35, 40), Point.fromCoordinates(25, 40))))
        .isFalse();
  }

  @Test
  public void should_contain_interior_shape() {
    assertThat(polygon.contains(Point.fromCoordinates(20, 20))).isTrue();
    assertThat(
            polygon.contains(
                LineString.fromPoints(
                    Point.fromCoordinates(20, 20), Point.fromCoordinates(30, 20))))
        .isTrue();
    assertThat(
            polygon.contains(
                Polygon.fromPoints(
                    Point.fromCoordinates(20, 20),
                    Point.fromCoordinates(30, 20),
                    Point.fromCoordinates(20, 30))))
        .isTrue();
  }

  @Test
  public void should_not_contain_exterior_shape() {
    assertThat(polygon.contains(Point.fromCoordinates(10, 10))).isFalse();
    assertThat(
            polygon.contains(
                LineString.fromPoints(
                    Point.fromCoordinates(10, 10), Point.fromCoordinates(20, 20))))
        .isFalse();
    assertThat(
            polygon.contains(
                Polygon.fromPoints(
                    Point.fromCoordinates(0, 0),
                    Point.fromCoordinates(0, 10),
                    Point.fromCoordinates(10, 10))))
        .isFalse();
  }

  @Test
  public void should_not_contain_shapes_in_interior_hole() {
    Polygon complex =
        Polygon.builder()
            .addRing(
                Point.fromCoordinates(0, 0),
                Point.fromCoordinates(30, 0),
                Point.fromCoordinates(30, 30),
                Point.fromCoordinates(0, 30))
            .addRing(
                Point.fromCoordinates(10, 10),
                Point.fromCoordinates(20, 10),
                Point.fromCoordinates(20, 20),
                Point.fromCoordinates(10, 20))
            .build();
    assertThat(complex.contains(Point.fromCoordinates(15, 15))).isFalse();
  }

  @Test
  public void should_accept_empty_shape() throws Exception {
    Polygon polygon = Polygon.fromWellKnownText("POLYGON EMPTY");
    assertThat(polygon.getExteriorRing()).isEmpty();
    assertThat(((DefaultPolygon) polygon).getOgcGeometry().isEmpty()).isTrue();
  }

  private void assertInvalidWkt(String s) {
    try {
      Polygon.fromWellKnownText(s);
      fail("Should have thrown InvalidTypeException");
    } catch (IllegalArgumentException e) {
      // expected
    }
  }
}
