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

import org.apache.lucene.index.PointValues;
import org.apache.lucene.index.PointValues.Relation;
import org.apache.lucene.util.SloppyMath;

/**
 * Utility methods for geo-spatial calculations and coordinate validation.
 *
 * <p>This class provides:
 *
 * <ul>
 *   <li>Coordinate validation methods
 *   <li>Earth model constants
 *   <li>Common geometric calculations
 *   <li>Coordinate normalization utilities
 * </ul>
 *
 * <p>Important Constants:
 *
 * <ul>
 *   <li>Earth's mean radius: 6,371,008.7714 meters
 *   <li>Latitude range: -90° to +90°
 *   <li>Longitude range: -180° to +180°
 * </ul>
 *
 * <p>Note: This class uses a spherical Earth model for simplicity and performance. For applications
 * requiring higher precision, consider using a geodetic library that implements the WGS84 ellipsoid
 * model.
 *
 * @lucene.experimental
 */
public final class GeoUtils {
  /** Minimum longitude value. */
  public static final double MIN_LON_INCL = -180.0D;

  /** Maximum longitude value. */
  public static final double MAX_LON_INCL = 180.0D;

  /** Minimum latitude value. */
  public static final double MIN_LAT_INCL = -90.0D;

  /** Maximum latitude value. */
  public static final double MAX_LAT_INCL = 90.0D;

  /** min longitude value in radians */
  public static final double MIN_LON_RADIANS = Math.toRadians(MIN_LON_INCL);

  /** min latitude value in radians */
  public static final double MIN_LAT_RADIANS = Math.toRadians(MIN_LAT_INCL);

  /** max longitude value in radians */
  public static final double MAX_LON_RADIANS = Math.toRadians(MAX_LON_INCL);

  /** max latitude value in radians */
  public static final double MAX_LAT_RADIANS = Math.toRadians(MAX_LAT_INCL);

  // WGS84 earth-ellipsoid parameters
  /** mean earth axis in meters */
  // see http://earth-info.nga.mil/GandG/publications/tr8350.2/wgs84fin.pdf
  public static final double EARTH_MEAN_RADIUS_METERS = 6_371_008.7714;

  // No instance:
  private GeoUtils() {}

  /** validates latitude value is within standard +/-90 coordinate bounds */
  public static void checkLatitude(double latitude) {
    if (Double.isNaN(latitude) || latitude < MIN_LAT_INCL || latitude > MAX_LAT_INCL) {
      throw new IllegalArgumentException(
          "invalid latitude "
              + latitude
              + "; must be between "
              + MIN_LAT_INCL
              + " and "
              + MAX_LAT_INCL);
    }
  }

  /** validates longitude value is within standard +/-180 coordinate bounds */
  public static void checkLongitude(double longitude) {
    if (Double.isNaN(longitude) || longitude < MIN_LON_INCL || longitude > MAX_LON_INCL) {
      throw new IllegalArgumentException(
          "invalid longitude "
              + longitude
              + "; must be between "
              + MIN_LON_INCL
              + " and "
              + MAX_LON_INCL);
    }
  }

  /**
   * binary search to find the exact sortKey needed to match the specified radius any sort key lte
   * this is a query match.
   */
  public static double distanceQuerySortKey(double radius) {
    // effectively infinite
    if (radius >= haversinMeters(Double.MAX_VALUE)) {
      return haversinMeters(Double.MAX_VALUE);
    }

    // this is a search through non-negative long space only
    long lo = 0;
    long hi = Double.doubleToRawLongBits(Double.MAX_VALUE);
    while (lo <= hi) {
      long mid = (lo + hi) >>> 1;
      double sortKey = Double.longBitsToDouble(mid);
      double midRadius = haversinMeters(sortKey);
      if (midRadius == radius) {
        return sortKey;
      } else if (midRadius > radius) {
        hi = mid - 1;
      } else {
        lo = mid + 1;
      }
    }

    // not found: this is because a user can supply an arbitrary radius, one that we will never
    // calculate exactly via our haversin method.
    double ceil = Double.longBitsToDouble(lo);
    assert haversinMeters(ceil) > radius;
    return ceil;
  }

  /**
   * Compute the relation between the provided box and distance query. This only works for boxes
   * that do not cross the dateline.
   */
  public static PointValues.Relation relate(
      double minLat,
      double maxLat,
      double minLon,
      double maxLon,
      double lat,
      double lon,
      double distanceSortKey,
      double axisLat) {

    if (minLon > maxLon) {
      throw new IllegalArgumentException("Box crosses the dateline");
    }

    if ((lon < minLon || lon > maxLon)
        && (axisLat + Rectangle.AXISLAT_ERROR < minLat
            || axisLat - Rectangle.AXISLAT_ERROR > maxLat)) {
      // circle not fully inside / crossing axis
      if (SloppyMath.haversinSortKey(lat, lon, minLat, minLon) > distanceSortKey
          && SloppyMath.haversinSortKey(lat, lon, minLat, maxLon) > distanceSortKey
          && SloppyMath.haversinSortKey(lat, lon, maxLat, minLon) > distanceSortKey
          && SloppyMath.haversinSortKey(lat, lon, maxLat, maxLon) > distanceSortKey) {
        // no points inside
        return Relation.CELL_OUTSIDE_QUERY;
      }
    }

    if (within90LonDegrees(lon, minLon, maxLon)
        && SloppyMath.haversinSortKey(lat, lon, minLat, minLon) <= distanceSortKey
        && SloppyMath.haversinSortKey(lat, lon, minLat, maxLon) <= distanceSortKey
        && SloppyMath.haversinSortKey(lat, lon, maxLat, minLon) <= distanceSortKey
        && SloppyMath.haversinSortKey(lat, lon, maxLat, maxLon) <= distanceSortKey) {
      // we are fully enclosed, collect everything within this subtree
      return Relation.CELL_INSIDE_QUERY;
    }

    return Relation.CELL_CROSSES_QUERY;
  }

  /** Return whether all points of {@code [minLon,maxLon]} are within 90 degrees of {@code lon}. */
  static boolean within90LonDegrees(double lon, double minLon, double maxLon) {
    if (maxLon <= lon - 180) {
      lon -= 360;
    } else if (minLon >= lon + 180) {
      lon += 360;
    }
    return maxLon - lon < 90 && lon - minLon < 90;
  }

  /**
   * Returns a positive value if points a, b, and c are arranged in counter-clockwise order,
   * negative value if clockwise, zero if collinear.
   */
  // see the "Orient2D" method described here:
  // http://www.cs.berkeley.edu/~jrs/meshpapers/robnotes.pdf
  // https://www.cs.cmu.edu/~quake/robust.html
  // Note that this one does not yet have the floating point tricks to be exact!
  public static int orient(double ax, double ay, double bx, double by, double cx, double cy) {
    double v1 = (bx - ax) * (cy - ay);
    double v2 = (cx - ax) * (by - ay);
    if (v1 > v2) {
      return 1;
    } else if (v1 < v2) {
      return -1;
    } else {
      return 0;
    }
  }

  /** uses orient method to compute whether two line segments cross */
  public static boolean lineCrossesLine(
      double a1x,
      double a1y,
      double b1x,
      double b1y,
      double a2x,
      double a2y,
      double b2x,
      double b2y) {
    return orient(a2x, a2y, b2x, b2y, a1x, a1y) * orient(a2x, a2y, b2x, b2y, b1x, b1y) < 0
        && orient(a1x, a1y, b1x, b1y, a2x, a2y) * orient(a1x, a1y, b1x, b1y, b2x, b2y) < 0;
  }

  /** uses orient method to compute whether two line overlap each other */
  public static boolean lineOverlapLine(
      double a1x,
      double a1y,
      double b1x,
      double b1y,
      double a2x,
      double a2y,
      double b2x,
      double b2y) {
    return orient(a2x, a2y, b2x, b2y, a1x, a1y) == 0
        && orient(a2x, a2y, b2x, b2y, b1x, b1y) == 0
        && orient(a1x, a1y, b1x, b1y, a2x, a2y) == 0
        && orient(a1x, a1y, b1x, b1y, b2x, b2y) == 0;
  }

  /**
   * uses orient method to compute whether two line segments cross; boundaries included - returning
   * true for lines that terminate on each other.
   *
   * <p>e.g., (plus sign) + == true, and (capital 't') T == true
   *
   * <p>Use {@link #lineCrossesLine} to exclude lines that terminate on each other from the truth
   * table
   */
  public static boolean lineCrossesLineWithBoundary(
      double a1x,
      double a1y,
      double b1x,
      double b1y,
      double a2x,
      double a2y,
      double b2x,
      double b2y) {
    if (orient(a2x, a2y, b2x, b2y, a1x, a1y) * orient(a2x, a2y, b2x, b2y, b1x, b1y) <= 0
        && orient(a1x, a1y, b1x, b1y, a2x, a2y) * orient(a1x, a1y, b1x, b1y, b2x, b2y) <= 0) {
      return true;
    }
    return false;
  }

  /**
   * used to define the orientation of 3 points -1 = Clockwise 0 = Colinear 1 = Counter-clockwise
   */
  public enum WindingOrder {
    CW(-1),
    COLINEAR(0),
    CCW(1);
    private final int sign;

    WindingOrder(int sign) {
      this.sign = sign;
    }

    public int sign() {
      return sign;
    }

    public static WindingOrder fromSign(final int sign) {
      if (sign == CW.sign) return CW;
      if (sign == COLINEAR.sign) return COLINEAR;
      if (sign == CCW.sign) return CCW;
      throw new IllegalArgumentException("Invalid WindingOrder sign: " + sign);
    }
  }
}
