/*
 * 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;
  
  /** Set to true if no longitude bounds can be stated */
  private boolean noLongitudeBound = false;
  /** Set to true if no top latitude bound can be stated */
  private boolean noTopLatitudeBound = false;
  /** Set to true if no bottom latitude bound can be stated */
  private boolean noBottomLatitudeBound = false;

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

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