/*******************************************************************************
 * 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 com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
import com.carrotsearch.randomizedtesting.annotations.Repeat;
import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.context.SpatialContextFactory;
import org.locationtech.spatial4j.context.jts.JtsSpatialContextFactory;
import org.locationtech.spatial4j.distance.DistanceCalculator;
import org.locationtech.spatial4j.distance.DistanceUtils;
import org.locationtech.spatial4j.distance.GeodesicSphereDistCalc;
import org.locationtech.spatial4j.exception.InvalidShapeException;
import org.junit.Test;

import java.util.Arrays;

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


public class TestShapesGeo extends AbstractTestShapes {

  @ParametersFactory
  public static Iterable<Object[]> parameters() {
    final DistanceCalculator distCalcL = new GeodesicSphereDistCalc.LawOfCosines();
    final DistanceCalculator distCalcH = new GeodesicSphereDistCalc.Haversine();//default
    final DistanceCalculator distCalcV = new GeodesicSphereDistCalc.Vincenty();
    return Arrays.asList($$(
        $(new SpatialContextFactory(){{geo = true; distCalc = new RoundingDistCalc(distCalcL);}}.newSpatialContext()),
        $(new SpatialContextFactory(){{geo = true; distCalc = new RoundingDistCalc(distCalcH);}}.newSpatialContext()),
        $(new SpatialContextFactory(){{geo = true; distCalc = new RoundingDistCalc(distCalcV);}}.newSpatialContext()),
        $(new JtsSpatialContextFactory(){{geo = true; distCalc = new RoundingDistCalc(distCalcH);}}.newSpatialContext()))
    );
  }

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

  private static double degToKm(double deg) {
    return DistanceUtils.degrees2Dist(deg, DistanceUtils.EARTH_MEAN_RADIUS_KM);
  }

  private static double kmToDeg(double km) {
    return DistanceUtils.dist2Degrees(km, DistanceUtils.EARTH_MEAN_RADIUS_KM);
  }

  @Test @Repeat(iterations = 1)
  public void testGeoRectangle() {
    double v = 200 * (randomBoolean() ? -1 : 1);
    try { ctx.makeRectangle(v,0,0,0); fail(); } catch (InvalidShapeException e) {}
    try { ctx.makeRectangle(0,v,0,0); fail(); } catch (InvalidShapeException e) {}
    try { ctx.makeRectangle(0,0,v,0); fail(); } catch (InvalidShapeException e) {}
    try { ctx.makeRectangle(0,0,0,v); fail(); } catch (InvalidShapeException e) {}
    try { ctx.makeRectangle(0,0,10,-10); fail(); } catch (InvalidShapeException e) {}

    //test some relateXRange
    //    opposite +/- 180
    assertEquals(INTERSECTS,  ctx.makeRectangle(170, 180, 0, 0).relateXRange(-180, -170));
    assertEquals(INTERSECTS,  ctx.makeRectangle(-90, -45, 0, 0).relateXRange(-45, -135));
    assertEquals(CONTAINS, ctx.getWorldBounds().relateXRange(-90, -135));
    //point on edge at dateline using opposite +/- 180
    assertEquals(CONTAINS, ctx.makeRectangle(170, 180, 0, 0).relate(ctx.makePoint(-180, 0)));

    //test 180 becomes -180 for non-zero width rectangle
    assertEquals(ctx.makeRectangle(-180, -170, 0, 0),ctx.makeRectangle(180, -170, 0, 0));
    assertEquals(ctx.makeRectangle(170, 180, 0, 0),ctx.makeRectangle(170, -180, 0, 0));

    double[] lons = new double[]{0,45,160,180,-45,-175, -180};//minX
    for (double lon : lons) {
      double[] lonWs = new double[]{0,20,180,200,355, 360};//width
      for (double lonW : lonWs) {
        if (lonW == 360 && lon != -180)
          continue;
        testRectangle(lon, lonW, 0, 0);
        testRectangle(lon, lonW, -10, 10);
        testRectangle(lon, lonW, 80, 10);//polar cap
        testRectangle(lon, lonW, -90, 180);//full lat range
      }
    }

    //Test geo rectangle intersections
    testRectIntersect();

    //Ambiguous vertical line at dateline; -180 vs +180.  Bug #85
    assertRelation(WITHIN, ctx.makeRectangle(-180, -180, -10, 10), ctx.makeRectangle(180, 180, -30, 30));

    //Test buffer
    assertEquals(ctx.makeRectangle(-10, 10, -10, 10), ctx.makeRectangle(0, 0, 0, 0).getBuffered(10, ctx));
    int MAX_TRIES = scaledRandomIntBetween(100, 1000);
    for (int i = 0; i < MAX_TRIES; i++) {
      Rectangle r = randomRectangle(1);
      int buf = randomIntBetween(0, 90);
      Rectangle br = (Rectangle) r.getBuffered(buf, ctx);
      assertRelation(null, CONTAINS, br, r);
      if (r.getWidth() + 2 * buf >= 360) {
        assertEquals(360, br.getWidth(), 0.0);
      } else {
        assertGreaterOrEqual(br.getWidth() - r.getWidth(), 2 * buf, EPS);
        //TODO test more thoroughly; we don't check that we over-buf
      }
    }
    assertTrue(ctx.makeRectangle(0, 10, 0, 89).getBuffered(0.5, ctx).getBoundingBox().getWidth()
        > 11);
  }

  @Test
  public void testGeoCircle() {
    assertEquals("Circle(Pt(x=10.0,y=20.0), d=30.0° 3335.85km)", ctx.makeCircle(10,20,30).toString());

    double v = 200 * (randomBoolean() ? -1 : 1);
    try { ctx.makeCircle(v,0,5); fail(); } catch (InvalidShapeException e) {}
    try { ctx.makeCircle(0, v, 5); fail(); } catch (InvalidShapeException e) {}
//    try { ctx.makeCircle(randomIntBetween(-180,180), randomIntBetween(-90,90), v); fail(); }
//    catch (InvalidShapeException e) {}

    //--Start with some static tests that once failed:

    //Bug: numeric edge at pole, fails to init
    ctx.makeCircle(110, -12, 90 + 12);

    //Bug: horizXAxis not in enclosing rectangle, assertion
    ctx.makeCircle(-44,16,106);
    ctx.makeCircle(-36,-76,14);
    ctx.makeCircle(107,82,172);

//TODO need to update this test to be valid
//    {
//      //Bug in which distance was being confused as being in the same coordinate system as x,y.
//      double distDeltaToPole = 0.001;//1m
//      double distDeltaToPoleDEG = ctx.getDistCalc().distanceToDegrees(distDeltaToPole);
//      double dist = 1;//1km
//      double distDEG = ctx.getDistCalc().distanceToDegrees(dist);
//      Circle c = ctx.makeCircle(0,90-distDeltaToPoleDEG-distDEG,dist);
//      Rectangle cBBox = c.getBoundingBox();
//      Rectangle r = ctx.makeRect(cBBox.getMaxX()*0.99,cBBox.getMaxX()+1,c.getCenter().getY(),c.getCenter().getY());
//      assertEquals(INTERSECTS,c.getBoundingBox().relate(r));
//      assertEquals("dist != xy space",INTERSECTS,c.relate(r));//once failed here
//    }

    //These two are related to a circle being on-edge with another shape
    //assertEquals("?", INTERSECTS, ctx.makeCircle(156, -70, 20).relate(ctx.makeRectangle(-62, -52, -90, -90)));
    //Pt(x=-52.24150368914137,y=-90.0)
    //assertEquals("?", DISJOINT, ctx.makeCircle(156, -70, 20).relate(ctx.makePoint(-52, -90)));//pt.x != c.x

    //What is the "correct" result?  Add a DistUtils edge condition check to return a nibble
    // when dist 0 and points not the same?  No; we cancel the assertion failure
    // if the circle touches the rect edge in onAssertFail() instead.
    //assertEquals("0 radius at pole", DISJOINT, ctx.makeCircle(-98, 90, 0).relate(ctx.makePoint(-144,90)));


    assertEquals("bad proportion logic", INTERSECTS, ctx.makeCircle(64, -70, 18).relate(ctx.makeRectangle(46, 116, -86, -62)));

    assertEquals("Both touch pole", INTERSECTS, ctx.makeCircle(-90, 30, 60).relate(ctx.makeRectangle(-24, -16, 14, 90)));

    assertEquals("Spherical cap should contain enclosed band", CONTAINS,
        ctx.makeCircle(0, -90, 30).relate(ctx.makeRectangle(-180, 180, -90, -80)));

    assertEquals("touches pole", INTERSECTS, ctx.makeCircle(0, -88, 2).relate(ctx.makeRectangle(40,60,-90,-86)));

    assertEquals("wrong farthest opp corner", INTERSECTS, ctx.makeCircle(92, 36, 46).relate(ctx.makeRectangle(134,136,32,80)));

    assertEquals("edge rounding issue 2", INTERSECTS, ctx.makeCircle(84, -40, 136).relate(ctx.makeRectangle(-150, -80, 34, 84)));

    assertEquals("edge rounding issue", CONTAINS, ctx.makeCircle(0, 66, 156).relate(ctx.makePoint(0, -90)));

    assertEquals("nudge back circle", CONTAINS, ctx.makeCircle(-150, -90, 122).relate(ctx.makeRectangle(0, -132, 32, 32)));

    assertEquals("wrong estimate", DISJOINT,ctx.makeCircle(-166,59,kmToDeg(5226.2)).relate(ctx.makeRectangle(36, 66, 23, 23)));

    assertEquals("bad CONTAINS (dateline)",INTERSECTS,ctx.makeCircle(56,-50,kmToDeg(12231.5)).relate(ctx.makeRectangle(108, 26, 39, 48)));

    assertEquals("bad CONTAINS (backwrap2)",INTERSECTS,
        ctx.makeCircle(112,-3,91).relate(ctx.makeRectangle(-163, 29, -38, 10)));

    assertEquals("bad CONTAINS (r x-wrap)",INTERSECTS,
        ctx.makeCircle(-139,47,80).relate(ctx.makeRectangle(-180, 180, -3, 12)));

    assertEquals("bad CONTAINS (pwrap)",INTERSECTS,
        ctx.makeCircle(-139,47,80).relate(ctx.makeRectangle(-180, 179, -3, 12)));

    assertEquals("no-dist 1",WITHIN,
        ctx.makeCircle(135,21,0).relate(ctx.makeRectangle(-103, -154, -47, 52)));

    assertEquals("bbox <= >= -90 bug",CONTAINS,
        ctx.makeCircle(-64,-84,124).relate(ctx.makeRectangle(-96, 96, -10, -10)));

    //The horizontal axis line of a geo circle doesn't necessarily pass through c's ctr.
    assertEquals("c's horiz axis doesn't pass through ctr",INTERSECTS,
        ctx.makeCircle(71,-44,40).relate(ctx.makeRectangle(15, 27, -62, -34)));

    assertEquals("pole boundary",INTERSECTS,
        ctx.makeCircle(-100,-12,102).relate(ctx.makeRectangle(143, 175, 4, 32)));

    assertEquals("full circle assert",CONTAINS,
        ctx.makeCircle(-64,32,180).relate(ctx.makeRectangle(47, 47, -14, 90)));

    //--Now proceed with systematic testing:

    TestShapes2D.testCircleReset(ctx);

    assertEquals(ctx.getWorldBounds(), ctx.makeCircle(0,0,180).getBoundingBox());
    //assertEquals(ctx.makeCircle(0,0,180/2 - 500).getBoundingBox());

    double[] theXs = new double[]{-180,-45,90};
    for (double x : theXs) {
      double[] theYs = new double[]{-90,-45,0,45,90};
      for (double y : theYs) {
        testCircle(x, y, 0);
        testCircle(x, y, kmToDeg(500));
        testCircle(x, y, 90);
        testCircle(x, y, 180);
      }
    }

    testCircleIntersect();
  }


  @Test
  public void testEmptyLineString() {
    Shape shape = ctx.getShapeFactory().lineString().buffer(randomInt(3)).build();
    testEmptiness(shape);
  }

}
