/*******************************************************************************
 * Copyright (c) 2015 MITRE
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License, Version 2.0 which
 * accompanies this distribution and is available at
 *    http://www.apache.org/licenses/LICENSE-2.0.txt
 ******************************************************************************/

package org.locationtech.spatial4j.shape;

import org.locationtech.spatial4j.TestLog;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.distance.DistanceCalculator;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.shape.impl.PointImpl;
import org.locationtech.spatial4j.shape.impl.RectangleImpl;
import org.junit.Rule;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.locationtech.spatial4j.shape.SpatialRelation.CONTAINS;
import static org.locationtech.spatial4j.shape.SpatialRelation.DISJOINT;


/**
 * Some basic tests that should work with various {@link SpatialContext}
 * configurations.  Subclasses add more.
 */
public abstract class AbstractTestShapes extends RandomizedShapeTest {

  public AbstractTestShapes(SpatialContext ctx) {
    super(ctx);
  }

  @Rule
  public final TestLog testLog = TestLog.instance;

  protected void testRectangle(double minX, double width, double minY, double height) {
    double maxX = minX + width;
    double maxY = minY + height;
    minX = normX(minX);
    maxX = normX(maxX);

    Rectangle r = ctx.makeRectangle(minX, maxX, minY, maxY);
    //test equals & hashcode of duplicate
    Rectangle r2 = ctx.makeRectangle(minX, maxX, minY, maxY);
    assertEquals(r,r2);
    assertEquals(r.hashCode(),r2.hashCode());

    String msg = r.toString();

    assertEquals(msg, width != 0 && height != 0, r.hasArea());
    assertEquals(msg, width != 0 && height != 0, r.getArea(ctx) > 0);
    if (ctx.isGeo() && r.getWidth() == 360 && r.getHeight() == 180) {
      //whole globe
      double earthRadius = DistanceUtils.toDegrees(1);
      assertEquals(4*Math.PI * earthRadius * earthRadius, r.getArea(ctx), 1.0);//1km err
    }

    assertEqualsRatio(msg, height, r.getHeight());
    assertEqualsRatio(msg, width, r.getWidth());
    Point center = r.getCenter();
    msg += " ctr:"+center;
    //System.out.println(msg);
    assertRelation(msg, CONTAINS, r, center);

    DistanceCalculator dc = ctx.getDistCalc();
    double dUR = dc.distance(center, r.getMaxX(), r.getMaxY());
    double dLR = dc.distance(center, r.getMaxX(), r.getMinY());
    double dUL = dc.distance(center, r.getMinX(), r.getMaxY());
    double dLL = dc.distance(center, r.getMinX(), r.getMinY());

    assertEquals(msg,width != 0 || height != 0, dUR != 0);
    if (dUR != 0)
      assertTrue(dUR > 0 && dLL > 0);
    assertEqualsRatio(msg, dUR, dUL);
    assertEqualsRatio(msg, dLR, dLL);
    if (!ctx.isGeo() || center.getY() == 0)
      assertEqualsRatio(msg, dUR, dLL);
  }

  protected void testRectIntersect() {
    //This test loops past the dateline for some variables but the makeNormRect()
    // method ensures the rect is valid.
    final double INCR = 45;
    final double Y = 20;
    for(double left = -180; left <= 180; left += INCR) {
      for(double right = left; right - left <= 360; right += INCR) {
        Rectangle r = makeNormRect(left, right, -Y, Y);

        //test contains (which also tests within)
        for(double left2 = left; left2 <= right; left2 += INCR) {
          for(double right2 = left2; right2 <= right; right2 += INCR) {
            Rectangle r2 = makeNormRect(left2, right2, -Y, Y);
            assertRelation(null, SpatialRelation.CONTAINS, r, r2);

            //test point contains
            assertRelation(null, SpatialRelation.CONTAINS, r, r2.getCenter());
          }
        }

        //test disjoint
        for(double left2 = right+INCR; left2 - left < 360; left2 += INCR) {
          //test point disjoint
          assertRelation(null, SpatialRelation.DISJOINT, r, ctx.makePoint(
                  normX(left2), randomIntBetween(-90, 90)));

          for(double right2 = left2; right2 - left < 360; right2 += INCR) {
            Rectangle r2 = makeNormRect(left2, right2, -Y, Y);
            assertRelation(null, SpatialRelation.DISJOINT, r, r2);
          }
        }
        //test intersect
        for(double left2 = left+INCR; left2 <= right; left2 += INCR) {
          for(double right2 = right+INCR; right2 - left < 360; right2 += INCR) {
            Rectangle r2 = makeNormRect(left2, right2, -Y, Y);
            assertRelation(null, SpatialRelation.INTERSECTS, r, r2);
          }
        }

      }
    }
  }

  protected void testCircle(double x, double y, double dist) {
    Circle c = ctx.makeCircle(x, y, dist);
    String msg = c.toString();
    final Circle c2 = ctx.makeCircle(ctx.makePoint(x, y), dist);
    assertEquals(c, c2);
    assertEquals(c.hashCode(),c2.hashCode());

    assertEquals(msg, dist > 0, c.hasArea());
    double area = c.getArea(ctx);
    assertTrue(msg, c.hasArea() == (area > 0.0));
    final Rectangle bbox = c.getBoundingBox();
    assertEquals(msg, dist > 0, bbox.getArea(ctx) > 0);
    assertTrue(msg, area <= bbox.getArea(ctx));
    if (!ctx.isGeo()) {
      //if not geo then units of dist == units of x,y
      assertEqualsRatio(msg, bbox.getHeight(), dist * 2);
      assertEqualsRatio(msg, bbox.getWidth(), dist * 2);
    }

    assertRelation(msg, CONTAINS, c, c.getCenter());
    assertRelation(msg, CONTAINS, bbox, c);
  }

  protected void testCircleIntersect() {
    new RectIntersectionTestHelper<Circle>(ctx) {
      @Override
      protected Circle generateRandomShape(Point nearP) {
        double cX = randomIntBetweenDivisible(-180, 179);
        double cY = randomIntBetweenDivisible(-90, 90);
        double cR_dist = randomIntBetweenDivisible(0, 180);
        return ctx.makeCircle(cX, cY, cR_dist);
      }

      @Override
      protected Point randomPointInEmptyShape(Circle shape) {
        return shape.getCenter();
      }

      @Override
      protected void onAssertFail(AssertionError e, Circle s, Rectangle r, SpatialRelation ic) {
        //Check if the circle's edge appears to coincide with the shape.
        final double radius = s.getRadius();
        if (radius == 180)
          throw e;//if this happens, then probably a bug
        if (radius == 0) {
          Point p = s.getCenter();
          //if touches a side then don't throw
          if (p.getX() == r.getMinX() || p.getX() == r.getMaxX()
            || p.getY() == r.getMinY() || p.getY() == r.getMaxY())
            return;
          throw e;
        }
        final double eps = 0.0000001;
        s.reset(s.getCenter().getX(), s.getCenter().getY(), radius - eps);
        SpatialRelation rel1 = s.relate(r);
        s.reset(s.getCenter().getX(), s.getCenter().getY(), radius + eps);
        SpatialRelation rel2 = s.relate(r);
        if (rel1 == rel2)
          throw e;
        s.reset(s.getCenter().getX(), s.getCenter().getY(), radius);//reset
        System.out.println("Seed "+getContext().getRunnerSeedAsString()+": Hid assertion due to ambiguous edge touch: "+s+" "+r);
      }
    }.testRelateWithRectangle();
  }

  @Test
  public void testMakeRect() {
    //test rectangle constructor
    assertEquals(new RectangleImpl(1,3,2,4, ctx),
        new RectangleImpl(new PointImpl(1,2, ctx),new PointImpl(3,4, ctx), ctx));

    //test ctx.makeRect
    assertEquals(ctx.makeRectangle(1, 3, 2, 4),
        ctx.makeRectangle(ctx.makePoint(1, 2), ctx.makePoint(3, 4)));
  }

  protected void testEmptiness(Shape emptyShape) {
    assertTrue(emptyShape.isEmpty());
    Point emptyPt = emptyShape.getCenter();
    assertTrue(emptyPt.isEmpty());
    Rectangle emptyRect = emptyShape.getBoundingBox();
    assertTrue(emptyRect.isEmpty());
    assertEquals(emptyRect, emptyShape.getBoundingBox());
    assertEquals(emptyPt, emptyShape.getCenter());
    assertRelation("EMPTY", DISJOINT, emptyShape, emptyPt);
    assertRelation("EMPTY", DISJOINT, emptyShape, randomPoint());
    assertRelation("EMPTY", DISJOINT, emptyShape, emptyRect);
    assertRelation("EMPTY", DISJOINT, emptyShape, randomRectangle(10));
    assertTrue(emptyShape.getBuffered(randomInt(4), ctx).isEmpty());
  }
}
