/*
 * 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.util;

import static org.apache.lucene.util.SloppyMath.asin;
import static org.apache.lucene.util.SloppyMath.cos;
import static org.apache.lucene.util.SloppyMath.haversinMeters;
import static org.apache.lucene.util.SloppyMath.haversinSortKey;

import java.util.Random;
import org.apache.lucene.tests.geo.GeoTestUtil;
import org.apache.lucene.tests.util.LuceneTestCase;

public class TestSloppyMath extends LuceneTestCase {
  // accuracy for cos()
  static double COS_DELTA = 1E-15;
  // accuracy for asin()
  static double ASIN_DELTA = 1E-7;
  // accuracy for haversinMeters()
  static double HAVERSIN_DELTA = 38E-2;
  // accuracy for haversinMeters() for "reasonable" distances (< 1000km)
  static double REASONABLE_HAVERSIN_DELTA = 1E-5;

  public void testCos() {
    assertTrue(Double.isNaN(cos(Double.NaN)));
    assertTrue(Double.isNaN(cos(Double.NEGATIVE_INFINITY)));
    assertTrue(Double.isNaN(cos(Double.POSITIVE_INFINITY)));
    assertEquals(StrictMath.cos(1), cos(1), COS_DELTA);
    assertEquals(StrictMath.cos(0), cos(0), COS_DELTA);
    assertEquals(StrictMath.cos(Math.PI / 2), cos(Math.PI / 2), COS_DELTA);
    assertEquals(StrictMath.cos(-Math.PI / 2), cos(-Math.PI / 2), COS_DELTA);
    assertEquals(StrictMath.cos(Math.PI / 4), cos(Math.PI / 4), COS_DELTA);
    assertEquals(StrictMath.cos(-Math.PI / 4), cos(-Math.PI / 4), COS_DELTA);
    assertEquals(StrictMath.cos(Math.PI * 2 / 3), cos(Math.PI * 2 / 3), COS_DELTA);
    assertEquals(StrictMath.cos(-Math.PI * 2 / 3), cos(-Math.PI * 2 / 3), COS_DELTA);
    assertEquals(StrictMath.cos(Math.PI / 6), cos(Math.PI / 6), COS_DELTA);
    assertEquals(StrictMath.cos(-Math.PI / 6), cos(-Math.PI / 6), COS_DELTA);

    // testing purely random longs is inefficent, as for stupid parameters we just
    // pass thru to Math.cos() instead of doing some huperduper arg reduction
    for (int i = 0; i < 10000; i++) {
      double d = random().nextDouble() * SloppyMath.SIN_COS_MAX_VALUE_FOR_INT_MODULO;
      if (random().nextBoolean()) {
        d = -d;
      }
      assertEquals(StrictMath.cos(d), cos(d), COS_DELTA);
    }
  }

  public void testAsin() {
    assertTrue(Double.isNaN(asin(Double.NaN)));
    assertTrue(Double.isNaN(asin(2)));
    assertTrue(Double.isNaN(asin(-2)));
    assertEquals(-Math.PI / 2, asin(-1), ASIN_DELTA);
    assertEquals(-Math.PI / 3, asin(-0.8660254), ASIN_DELTA);
    assertEquals(-Math.PI / 4, asin(-0.7071068), ASIN_DELTA);
    assertEquals(-Math.PI / 6, asin(-0.5), ASIN_DELTA);
    assertEquals(0, asin(0), ASIN_DELTA);
    assertEquals(Math.PI / 6, asin(0.5), ASIN_DELTA);
    assertEquals(Math.PI / 4, asin(0.7071068), ASIN_DELTA);
    assertEquals(Math.PI / 3, asin(0.8660254), ASIN_DELTA);
    assertEquals(Math.PI / 2, asin(1), ASIN_DELTA);
    // only values -1..1 are useful
    for (int i = 0; i < 10000; i++) {
      double d = random().nextDouble();
      if (random().nextBoolean()) {
        d = -d;
      }
      assertEquals(StrictMath.asin(d), asin(d), ASIN_DELTA);
      assertTrue(asin(d) >= -Math.PI / 2);
      assertTrue(asin(d) <= Math.PI / 2);
    }
  }

  public void testHaversin() {
    assertTrue(Double.isNaN(haversinMeters(1, 1, 1, Double.NaN)));
    assertTrue(Double.isNaN(haversinMeters(1, 1, Double.NaN, 1)));
    assertTrue(Double.isNaN(haversinMeters(1, Double.NaN, 1, 1)));
    assertTrue(Double.isNaN(haversinMeters(Double.NaN, 1, 1, 1)));

    assertEquals(0, haversinMeters(0, 0, 0, 0), 0D);
    assertEquals(0, haversinMeters(0, -180, 0, -180), 0D);
    assertEquals(0, haversinMeters(0, -180, 0, 180), 0D);
    assertEquals(0, haversinMeters(0, 180, 0, 180), 0D);
    assertEquals(0, haversinMeters(90, 0, 90, 0), 0D);
    assertEquals(0, haversinMeters(90, -180, 90, -180), 0D);
    assertEquals(0, haversinMeters(90, -180, 90, 180), 0D);
    assertEquals(0, haversinMeters(90, 180, 90, 180), 0D);

    // Test half a circle on the equator, using WGS84 mean earth radius in meters
    double earthRadiusMs = 6_371_008.7714;
    double halfCircle = earthRadiusMs * Math.PI;
    assertEquals(halfCircle, haversinMeters(0, 0, 0, 180), 0D);

    Random r = random();
    double randomLat1 = 40.7143528 + (r.nextInt(10) - 5) * 360;
    double randomLon1 = -74.0059731 + (r.nextInt(10) - 5) * 360;

    double randomLat2 = 40.65 + (r.nextInt(10) - 5) * 360;
    double randomLon2 = -73.95 + (r.nextInt(10) - 5) * 360;

    assertEquals(8_572.1137, haversinMeters(randomLat1, randomLon1, randomLat2, randomLon2), 0.01D);

    // from solr and ES tests (with their respective epsilons)
    assertEquals(0, haversinMeters(40.7143528, -74.0059731, 40.7143528, -74.0059731), 0D);
    assertEquals(5_285.89, haversinMeters(40.7143528, -74.0059731, 40.759011, -73.9844722), 0.01D);
    assertEquals(462.10, haversinMeters(40.7143528, -74.0059731, 40.718266, -74.007819), 0.01D);
    assertEquals(1_054.98, haversinMeters(40.7143528, -74.0059731, 40.7051157, -74.0088305), 0.01D);
    assertEquals(1_258.12, haversinMeters(40.7143528, -74.0059731, 40.7247222, -74), 0.01D);
    assertEquals(2_028.52, haversinMeters(40.7143528, -74.0059731, 40.731033, -73.9962255), 0.01D);
    assertEquals(8_572.11, haversinMeters(40.7143528, -74.0059731, 40.65, -73.95), 0.01D);
  }

  /** Test this method sorts the same way as real haversin */
  public void testHaversinSortKey() {
    int iters = atLeast(10000);
    for (int i = 0; i < iters; i++) {
      double centerLat = GeoTestUtil.nextLatitude();
      double centerLon = GeoTestUtil.nextLongitude();

      double lat1 = GeoTestUtil.nextLatitude();
      double lon1 = GeoTestUtil.nextLongitude();

      double lat2 = GeoTestUtil.nextLatitude();
      double lon2 = GeoTestUtil.nextLongitude();

      int expected =
          Integer.signum(
              Double.compare(
                  haversinMeters(centerLat, centerLon, lat1, lon1),
                  haversinMeters(centerLat, centerLon, lat2, lon2)));
      int actual =
          Integer.signum(
              Double.compare(
                  haversinSortKey(centerLat, centerLon, lat1, lon1),
                  haversinSortKey(centerLat, centerLon, lat2, lon2)));
      assertEquals(expected, actual);
      assertEquals(
          haversinMeters(centerLat, centerLon, lat1, lon1),
          haversinMeters(haversinSortKey(centerLat, centerLon, lat1, lon1)),
          0.0D);
      assertEquals(
          haversinMeters(centerLat, centerLon, lat2, lon2),
          haversinMeters(haversinSortKey(centerLat, centerLon, lat2, lon2)),
          0.0D);
    }
  }

  public void testHaversinFromSortKey() {
    assertEquals(0.0, haversinMeters(0), 0.0D);
  }

  public void testAgainstSlowVersion() {
    for (int i = 0; i < 100_000; i++) {
      double lat1 = GeoTestUtil.nextLatitude();
      double lon1 = GeoTestUtil.nextLongitude();
      double lat2 = GeoTestUtil.nextLatitude();
      double lon2 = GeoTestUtil.nextLongitude();

      double expected = slowHaversin(lat1, lon1, lat2, lon2);
      double actual = haversinMeters(lat1, lon1, lat2, lon2);
      assertEquals(expected, actual, HAVERSIN_DELTA);
    }
  }

  /**
   * Step across the whole world to find huge absolute errors. Don't rely on random number generator
   * to pick these massive distances.
   */
  public void testAcrossWholeWorldSteps() {
    for (int lat1 = -90; lat1 <= 90; lat1 += 10) {
      for (int lon1 = -180; lon1 <= 180; lon1 += 10) {
        for (int lat2 = -90; lat2 <= 90; lat2 += 10) {
          for (int lon2 = -180; lon2 <= 180; lon2 += 10) {
            double expected = slowHaversin(lat1, lon1, lat2, lon2);
            double actual = haversinMeters(lat1, lon1, lat2, lon2);
            assertEquals(expected, actual, HAVERSIN_DELTA);
          }
        }
      }
    }
  }

  public void testAgainstSlowVersionReasonable() {
    for (int i = 0; i < 100_000; i++) {
      double lat1 = GeoTestUtil.nextLatitude();
      double lon1 = GeoTestUtil.nextLongitude();
      double lat2 = GeoTestUtil.nextLatitude();
      double lon2 = GeoTestUtil.nextLongitude();

      double expected = haversinMeters(lat1, lon1, lat2, lon2);
      if (expected < 1_000_000) {
        double actual = slowHaversin(lat1, lon1, lat2, lon2);
        assertEquals(expected, actual, REASONABLE_HAVERSIN_DELTA);
      }
    }
  }

  // simple incorporation of the wikipedia formula
  private static double slowHaversin(double lat1, double lon1, double lat2, double lon2) {
    double h1 = (1 - StrictMath.cos(StrictMath.toRadians(lat2) - StrictMath.toRadians(lat1))) / 2;
    double h2 = (1 - StrictMath.cos(StrictMath.toRadians(lon2) - StrictMath.toRadians(lon1))) / 2;
    double h =
        h1
            + StrictMath.cos(StrictMath.toRadians(lat1))
                * StrictMath.cos(StrictMath.toRadians(lat2))
                * h2;
    return 2 * 6371008.7714 * StrictMath.asin(Math.min(1, Math.sqrt(h)));
  }
}
