/*
 * 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 java.lang.Math.PI;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static org.apache.lucene.geo.GeoUtils.EARTH_MEAN_RADIUS_METERS;
import static org.apache.lucene.geo.GeoUtils.MAX_LAT_INCL;
import static org.apache.lucene.geo.GeoUtils.MAX_LAT_RADIANS;
import static org.apache.lucene.geo.GeoUtils.MAX_LON_RADIANS;
import static org.apache.lucene.geo.GeoUtils.MIN_LAT_INCL;
import static org.apache.lucene.geo.GeoUtils.MIN_LAT_RADIANS;
import static org.apache.lucene.geo.GeoUtils.MIN_LON_RADIANS;
import static org.apache.lucene.geo.GeoUtils.checkLatitude;
import static org.apache.lucene.geo.GeoUtils.checkLongitude;
import static org.apache.lucene.geo.GeoUtils.sloppySin;
import static org.apache.lucene.util.SloppyMath.asin;
import static org.apache.lucene.util.SloppyMath.cos;

/** Represents a lat/lon rectangle. */
public class Rectangle extends LatLonGeometry {
  /** maximum longitude value (in degrees) */
  public final double minLat;
  /** minimum longitude value (in degrees) */
  public final double minLon;
  /** maximum latitude value (in degrees) */
  public final double maxLat;
  /** minimum latitude value (in degrees) */
  public final double maxLon;

  /**
   * Constructs a bounding box by first validating the provided latitude and longitude coordinates
   */
  public Rectangle(double minLat, double maxLat, double minLon, double maxLon) {
    GeoUtils.checkLatitude(minLat);
    GeoUtils.checkLatitude(maxLat);
    GeoUtils.checkLongitude(minLon);
    GeoUtils.checkLongitude(maxLon);
    this.minLon = minLon;
    this.maxLon = maxLon;
    this.minLat = minLat;
    this.maxLat = maxLat;
    assert maxLat >= minLat;

    // NOTE: cannot assert maxLon >= minLon since this rect could cross the dateline
  }

  @Override
  protected Component2D toComponent2D() {
    return Rectangle2D.create(this);
  }

  @Override
  public String toString() {
    StringBuilder b = new StringBuilder();
    b.append("Rectangle(lat=");
    b.append(minLat);
    b.append(" TO ");
    b.append(maxLat);
    b.append(" lon=");
    b.append(minLon);
    b.append(" TO ");
    b.append(maxLon);
    if (maxLon < minLon) {
      b.append(" [crosses dateline!]");
    }
    b.append(")");

    return b.toString();
  }

  /** Returns true if this bounding box crosses the dateline */
  public boolean crossesDateline() {
    return maxLon < minLon;
  }

  /**
   * returns true if rectangle (defined by minLat, maxLat, minLon, maxLon) contains the lat lon
   * point
   */
  public static boolean containsPoint(
      final double lat,
      final double lon,
      final double minLat,
      final double maxLat,
      final double minLon,
      final double maxLon) {
    return lat >= minLat && lat <= maxLat && lon >= minLon && lon <= maxLon;
  }

  /** Compute Bounding Box for a circle using WGS-84 parameters */
  public static Rectangle fromPointDistance(
      final double centerLat, final double centerLon, final double radiusMeters) {
    checkLatitude(centerLat);
    checkLongitude(centerLon);
    final double radLat = Math.toRadians(centerLat);
    final double radLon = Math.toRadians(centerLon);
    // LUCENE-7143
    double radDistance = (radiusMeters + 7E-2) / EARTH_MEAN_RADIUS_METERS;
    double minLat = radLat - radDistance;
    double maxLat = radLat + radDistance;
    double minLon;
    double maxLon;

    if (minLat > MIN_LAT_RADIANS && maxLat < MAX_LAT_RADIANS) {
      double deltaLon = asin(sloppySin(radDistance) / cos(radLat));
      minLon = radLon - deltaLon;
      if (minLon < MIN_LON_RADIANS) {
        minLon += 2d * PI;
      }
      maxLon = radLon + deltaLon;
      if (maxLon > MAX_LON_RADIANS) {
        maxLon -= 2d * PI;
      }
    } else {
      // a pole is within the distance
      minLat = max(minLat, MIN_LAT_RADIANS);
      maxLat = min(maxLat, MAX_LAT_RADIANS);
      minLon = MIN_LON_RADIANS;
      maxLon = MAX_LON_RADIANS;
    }

    return new Rectangle(
        Math.toDegrees(minLat),
        Math.toDegrees(maxLat),
        Math.toDegrees(minLon),
        Math.toDegrees(maxLon));
  }

  /** maximum error from {@link #axisLat(double, double)}. logic must be prepared to handle this */
  public static final double AXISLAT_ERROR = Math.toDegrees(0.1D / EARTH_MEAN_RADIUS_METERS);

  /**
   * Calculate the latitude of a circle's intersections with its bbox meridians.
   *
   * <p><b>NOTE:</b> the returned value will be +/- {@link #AXISLAT_ERROR} of the actual value.
   *
   * @param centerLat The latitude of the circle center
   * @param radiusMeters The radius of the circle in meters
   * @return A latitude
   */
  public static double axisLat(double centerLat, double radiusMeters) {
    // A spherical triangle with:
    // r is the radius of the circle in radians
    // l1 is the latitude of the circle center
    // l2 is the latitude of the point at which the circle intersect's its bbox longitudes
    // We know r is tangent to the bbox meridians at l2, therefore it is a right angle.
    // So from the law of cosines, with the angle of l1 being 90, we have:
    // cos(l1) = cos(r) * cos(l2) + sin(r) * sin(l2) * cos(90)
    // The second part cancels out because cos(90) == 0, so we have:
    // cos(l1) = cos(r) * cos(l2)
    // Solving for l2, we get:
    // l2 = acos( cos(l1) / cos(r) )
    // We ensure r is in the range (0, PI/2) and l1 in the range (0, PI/2]. This means we
    // cannot divide by 0, and we will always get a positive value in the range [0, 1) as
    // the argument to arc cosine, resulting in a range (0, PI/2].
    final double PIO2 = Math.PI / 2D;
    double l1 = Math.toRadians(centerLat);
    double r = (radiusMeters + 7E-2) / EARTH_MEAN_RADIUS_METERS;

    // if we are within radius range of a pole, the lat is the pole itself
    if (Math.abs(l1) + r >= MAX_LAT_RADIANS) {
      return centerLat >= 0 ? MAX_LAT_INCL : MIN_LAT_INCL;
    }

    // adjust l1 as distance from closest pole, to form a right triangle with bbox meridians
    // and ensure it is in the range (0, PI/2]
    l1 = centerLat >= 0 ? PIO2 - l1 : l1 + PIO2;

    double l2 = Math.acos(Math.cos(l1) / Math.cos(r));
    assert !Double.isNaN(l2);

    // now adjust back to range [-pi/2, pi/2], ie latitude in radians
    l2 = centerLat >= 0 ? PIO2 - l2 : l2 - PIO2;

    return Math.toDegrees(l2);
  }

  /** Returns the bounding box over an array of polygons */
  public static Rectangle fromPolygon(Polygon[] polygons) {
    // compute bounding box
    double minLat = Double.POSITIVE_INFINITY;
    double maxLat = Double.NEGATIVE_INFINITY;
    double minLon = Double.POSITIVE_INFINITY;
    double maxLon = Double.NEGATIVE_INFINITY;

    for (int i = 0; i < polygons.length; i++) {
      minLat = Math.min(polygons[i].minLat, minLat);
      maxLat = Math.max(polygons[i].maxLat, maxLat);
      minLon = Math.min(polygons[i].minLon, minLon);
      maxLon = Math.max(polygons[i].maxLon, maxLon);
    }

    return new Rectangle(minLat, maxLat, minLon, maxLon);
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    Rectangle rectangle = (Rectangle) o;

    if (Double.compare(rectangle.minLat, minLat) != 0) return false;
    if (Double.compare(rectangle.minLon, minLon) != 0) return false;
    if (Double.compare(rectangle.maxLat, maxLat) != 0) return false;
    return Double.compare(rectangle.maxLon, maxLon) == 0;
  }

  @Override
  public int hashCode() {
    int result;
    long temp;
    temp = Double.doubleToLongBits(minLat);
    result = (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(minLon);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(maxLat);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    temp = Double.doubleToLongBits(maxLon);
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    return result;
  }
}
