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

/**
 * An object for accumulating XYZ bounds information.
 *
 * @lucene.experimental
 */
public class XYZBounds implements Bounds {

  /**
   * A 'fudge factor', which is added to maximums and subtracted from minimums, in order to
   * compensate for potential error deltas. This would not be necessary except that our 'bounds' is
   * defined as always equaling or exceeding the boundary of the shape, and we cannot guarantee that
   * without making MINIMUM_RESOLUTION unacceptably large. Also, see LUCENE-7290 for a description
   * of how geometry can magnify the bounds delta.
   */
  private static final double FUDGE_FACTOR = Vector.MINIMUM_RESOLUTION * 1e3;

  /** Minimum x */
  private Double minX = null;
  /** Maximum x */
  private Double maxX = null;
  /** Minimum y */
  private Double minY = null;
  /** Maximum y */
  private Double maxY = null;
  /** Minimum z */
  private Double minZ = null;
  /** Maximum z */
  private Double maxZ = null;

  /** Construct an empty bounds object */
  public XYZBounds() {}

  // Accessor methods

  /**
   * Return the minimum X value.
   *
   * @return minimum X value.
   */
  public Double getMinimumX() {
    return minX;
  }

  /**
   * Return the maximum X value.
   *
   * @return maximum X value.
   */
  public Double getMaximumX() {
    return maxX;
  }

  /**
   * Return the minimum Y value.
   *
   * @return minimum Y value.
   */
  public Double getMinimumY() {
    return minY;
  }

  /**
   * Return the maximum Y value.
   *
   * @return maximum Y value.
   */
  public Double getMaximumY() {
    return maxY;
  }

  /**
   * Return the minimum Z value.
   *
   * @return minimum Z value.
   */
  public Double getMinimumZ() {
    return minZ;
  }

  /**
   * Return the maximum Z value.
   *
   * @return maximum Z value.
   */
  public Double getMaximumZ() {
    return maxZ;
  }

  /**
   * Return true if minX is as small as the planet model allows.
   *
   * @return true if minX has reached its bound.
   */
  public boolean isSmallestMinX(final PlanetModel planetModel) {
    if (minX == null) {
      return false;
    }
    return minX - planetModel.getMinimumXValue() < Vector.MINIMUM_RESOLUTION;
  }

  /**
   * Return true if maxX is as large as the planet model allows.
   *
   * @return true if maxX has reached its bound.
   */
  public boolean isLargestMaxX(final PlanetModel planetModel) {
    if (maxX == null) {
      return false;
    }
    return planetModel.getMaximumXValue() - maxX < Vector.MINIMUM_RESOLUTION;
  }

  /**
   * Return true if minY is as small as the planet model allows.
   *
   * @return true if minY has reached its bound.
   */
  public boolean isSmallestMinY(final PlanetModel planetModel) {
    if (minY == null) {
      return false;
    }
    return minY - planetModel.getMinimumYValue() < Vector.MINIMUM_RESOLUTION;
  }

  /**
   * Return true if maxY is as large as the planet model allows.
   *
   * @return true if maxY has reached its bound.
   */
  public boolean isLargestMaxY(final PlanetModel planetModel) {
    if (maxY == null) {
      return false;
    }
    return planetModel.getMaximumYValue() - maxY < Vector.MINIMUM_RESOLUTION;
  }

  /**
   * Return true if minZ is as small as the planet model allows.
   *
   * @return true if minZ has reached its bound.
   */
  public boolean isSmallestMinZ(final PlanetModel planetModel) {
    if (minZ == null) {
      return false;
    }
    return minZ - planetModel.getMinimumZValue() < Vector.MINIMUM_RESOLUTION;
  }

  /**
   * Return true if maxZ is as large as the planet model allows.
   *
   * @return true if maxZ has reached its bound.
   */
  public boolean isLargestMaxZ(final PlanetModel planetModel) {
    if (maxZ == null) {
      return false;
    }
    return planetModel.getMaximumZValue() - maxZ < Vector.MINIMUM_RESOLUTION;
  }

  // Modification methods

  /**
   * Check if another XYZBounds object overlaps this one.
   *
   * @param bounds is the other bounds object.
   * @return true if there is overlap.
   */
  public boolean overlaps(final XYZBounds bounds) {
    // Overlap occurs when any one corner is inside the other bounds
    // object, and visa versa
    return isCornerInside(this, bounds) || isCornerInside(bounds, this);
  }

  private static boolean isCornerInside(final XYZBounds one, final XYZBounds other) {
    if (one.minX == null
        || one.maxX == null
        || one.minY == null
        || one.maxY == null
        || one.minZ == null
        || one.maxZ == null) {
      return false;
    }
    if (other.minX == null
        || other.maxX == null
        || other.minY == null
        || other.maxY == null
        || other.minZ == null
        || other.maxZ == null) {
      return false;
    }
    return isPointInside(other, one.minX, one.minY, one.minZ)
        || isPointInside(other, one.maxX, one.minY, one.minZ)
        || isPointInside(other, one.minX, one.maxY, one.minZ)
        || isPointInside(other, one.maxX, one.maxY, one.minZ)
        || isPointInside(other, one.minX, one.minY, one.maxZ)
        || isPointInside(other, one.maxX, one.minY, one.maxZ)
        || isPointInside(other, one.minX, one.maxY, one.maxZ)
        || isPointInside(other, one.maxX, one.maxY, one.maxZ);
  }

  private static boolean isPointInside(
      final XYZBounds other, final double x, final double y, final double z) {
    return other.minX <= x
        && other.maxX >= x
        && other.minY <= y
        && other.maxY >= y
        && other.minZ <= z
        && other.maxZ >= z;
  }

  /**
   * Add a fully-formed XYZBounds to the current one.
   *
   * @param bounds is the bounds object to modify
   */
  public void addBounds(final XYZBounds bounds) {
    if (bounds.maxX == null || maxX > bounds.maxX) {
      bounds.maxX = maxX;
    }
    if (bounds.minX == null || minX < bounds.minX) {
      bounds.minX = minX;
    }
    if (bounds.maxY == null || maxY > bounds.maxY) {
      bounds.maxY = maxY;
    }
    if (bounds.minY == null || minY < bounds.minY) {
      bounds.minY = minY;
    }
    if (bounds.maxZ == null || maxZ > bounds.maxZ) {
      bounds.maxZ = maxZ;
    }
    if (bounds.minZ == null || minZ < bounds.minZ) {
      bounds.minZ = minZ;
    }
  }

  @Override
  public Bounds addPlane(
      final PlanetModel planetModel, final Plane plane, final Membership... bounds) {
    plane.recordBounds(planetModel, this, bounds);
    return this;
  }

  /**
   * Add a horizontal plane to the bounds description. This method should EITHER use the supplied
   * latitude, OR use the supplied plane, depending on what is most efficient.
   *
   * @param planetModel is the planet model.
   * @param latitude is the latitude.
   * @param horizontalPlane is the plane.
   * @param bounds are the constraints on the plane.
   * @return updated Bounds object.
   */
  @Override
  public Bounds addHorizontalPlane(
      final PlanetModel planetModel,
      final double latitude,
      final Plane horizontalPlane,
      final Membership... bounds) {
    return addPlane(planetModel, horizontalPlane, bounds);
  }

  /**
   * Add a vertical plane to the bounds description. This method should EITHER use the supplied
   * longitude, OR use the supplied plane, depending on what is most efficient.
   *
   * @param planetModel is the planet model.
   * @param longitude is the longitude.
   * @param verticalPlane is the plane.
   * @param bounds are the constraints on the plane.
   * @return updated Bounds object.
   */
  @Override
  public Bounds addVerticalPlane(
      final PlanetModel planetModel,
      final double longitude,
      final Plane verticalPlane,
      final Membership... bounds) {
    return addPlane(planetModel, verticalPlane, bounds);
  }

  @Override
  public Bounds addXValue(final GeoPoint point) {
    return addXValue(point.x);
  }

  /**
   * Add a specific X value.
   *
   * @param x is the value to add.
   * @return the bounds object.
   */
  public Bounds addXValue(final double x) {
    final double small = x - FUDGE_FACTOR;
    if (minX == null || minX > small) {
      minX = small;
    }
    final double large = x + FUDGE_FACTOR;
    if (maxX == null || maxX < large) {
      maxX = large;
    }
    return this;
  }

  @Override
  public Bounds addYValue(final GeoPoint point) {
    return addYValue(point.y);
  }

  /**
   * Add a specific Y value.
   *
   * @param y is the value to add.
   * @return the bounds object.
   */
  public Bounds addYValue(final double y) {
    final double small = y - FUDGE_FACTOR;
    if (minY == null || minY > small) {
      minY = small;
    }
    final double large = y + FUDGE_FACTOR;
    if (maxY == null || maxY < large) {
      maxY = large;
    }
    return this;
  }

  @Override
  public Bounds addZValue(final GeoPoint point) {
    return addZValue(point.z);
  }

  /**
   * Add a specific Z value.
   *
   * @param z is the value to add.
   * @return the bounds object.
   */
  public Bounds addZValue(final double z) {
    final double small = z - FUDGE_FACTOR;
    if (minZ == null || minZ > small) {
      minZ = small;
    }
    final double large = z + FUDGE_FACTOR;
    if (maxZ == null || maxZ < large) {
      maxZ = large;
    }
    return this;
  }

  @Override
  public Bounds addIntersection(
      final PlanetModel planetModel,
      final Plane plane1,
      final Plane plane2,
      final Membership... bounds) {
    plane1.recordBounds(planetModel, this, plane2, bounds);
    return this;
  }

  @Override
  public Bounds addPoint(final GeoPoint point) {
    return addXValue(point).addYValue(point).addZValue(point);
  }

  @Override
  public Bounds isWide() {
    // No specific thing we need to do.
    return this;
  }

  @Override
  public Bounds noLongitudeBound() {
    // No specific thing we need to do.
    return this;
  }

  @Override
  public Bounds noTopLatitudeBound() {
    // No specific thing we need to do.
    return this;
  }

  @Override
  public Bounds noBottomLatitudeBound() {
    // No specific thing we need to do.
    return this;
  }

  @Override
  public Bounds noBound(final PlanetModel planetModel) {
    minX = planetModel.getMinimumXValue();
    maxX = planetModel.getMaximumXValue();
    minY = planetModel.getMinimumYValue();
    maxY = planetModel.getMaximumYValue();
    minZ = planetModel.getMinimumZValue();
    maxZ = planetModel.getMaximumZValue();
    return this;
  }

  /**
   * Courtesy method to see if a point is within the bounds.
   *
   * @param v is the point/vector we want to check
   * @return true if the bounds contains the vector
   */
  public boolean isWithin(final Vector v) {
    return isWithin(v.x, v.y, v.z);
  }

  /**
   * Courtesy method to see if a point is within the bounds.
   *
   * @param x is the x coordinate
   * @param y is the y coordinate
   * @param z is the z coordinate
   * @return true if the bounds contains the vector
   */
  public boolean isWithin(final double x, final double y, final double z) {
    return (minX != null
        && x >= minX
        && maxX != null
        && x <= maxX
        && minY != null
        && y >= minY
        && maxY != null
        && y <= maxY
        && minZ != null
        && z >= minZ
        && maxZ != null
        && z <= maxZ);
  }

  @Override
  public String toString() {
    return "XYZBounds: [xmin="
        + minX
        + " xmax="
        + maxX
        + " ymin="
        + minY
        + " ymax="
        + maxY
        + " zmin="
        + minZ
        + " zmax="
        + maxZ
        + "]";
  }
}
