/*
 * 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.parquet.column.statistics.geospatial;

import org.apache.parquet.ShouldNeverHappenException;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;

public class BoundingBox {

  private double xMin = Double.POSITIVE_INFINITY;
  private double xMax = Double.NEGATIVE_INFINITY;
  private double yMin = Double.POSITIVE_INFINITY;
  private double yMax = Double.NEGATIVE_INFINITY;
  private double zMin = Double.POSITIVE_INFINITY;
  private double zMax = Double.NEGATIVE_INFINITY;
  private double mMin = Double.POSITIVE_INFINITY;
  private double mMax = Double.NEGATIVE_INFINITY;
  private boolean valid = true;

  public BoundingBox() {}

  public BoundingBox(
      double xMin, double xMax, double yMin, double yMax, double zMin, double zMax, double mMin, double mMax) {
    this.xMin = xMin;
    this.xMax = xMax;
    this.yMin = yMin;
    this.yMax = yMax;
    this.zMin = zMin;
    this.zMax = zMax;
    this.mMin = mMin;
    this.mMax = mMax;

    // Update the validity
    valid = isXYValid();
  }

  private void resetBBox() {
    xMin = Double.POSITIVE_INFINITY;
    xMax = Double.NEGATIVE_INFINITY;
    yMin = Double.POSITIVE_INFINITY;
    yMax = Double.NEGATIVE_INFINITY;
    zMin = Double.POSITIVE_INFINITY;
    zMax = Double.NEGATIVE_INFINITY;
    mMin = Double.POSITIVE_INFINITY;
    mMax = Double.NEGATIVE_INFINITY;
  }

  public double getXMin() {
    return xMin;
  }

  public double getXMax() {
    return xMax;
  }

  public double getYMin() {
    return yMin;
  }

  public double getYMax() {
    return yMax;
  }

  public double getZMin() {
    return zMin;
  }

  public double getZMax() {
    return zMax;
  }

  public double getMMin() {
    return mMin;
  }

  public double getMMax() {
    return mMax;
  }

  /**
   * Checks if the bounding box is valid.
   * A bounding box is considered valid if none of the X / Y dimensions contain NaN.
   *
   * @return true if the bounding box is valid, false otherwise.
   */
  public boolean isValid() {
    return valid;
  }

  /**
   * Checks if the X and Y dimensions of the bounding box are valid.
   * The X and Y dimensions are considered valid if none of the bounds contain NaN.
   *
   * @return true if the X and Y dimensions are valid, false otherwise.
   */
  public boolean isXYValid() {
    return isXValid() && isYValid();
  }

  /**
   * Checks if the X dimension of the bounding box is valid.
   * The X dimension is considered valid if neither bound contains NaN.
   *
   * @return true if the X dimension is valid, false otherwise.
   */
  public boolean isXValid() {
    return !(Double.isNaN(xMin) || Double.isNaN(xMax));
  }

  /**
   * Checks if the Y dimension of the bounding box is valid.
   * The Y dimension is considered valid if neither bound contains NaN.
   *
   * @return true if the Y dimension is valid, false otherwise.
   */
  public boolean isYValid() {
    return !(Double.isNaN(yMin) || Double.isNaN(yMax));
  }

  /**
   * Checks if the Z dimension of the bounding box is valid.
   * The Z dimension is considered valid if none of the bounds contain NaN.
   *
   * @return true if the Z dimension is valid, false otherwise.
   */
  public boolean isZValid() {
    return !(Double.isNaN(zMin) || Double.isNaN(zMax));
  }

  /**
   * Checks if the M dimension of the bounding box is valid.
   * The M dimension is considered valid if none of the bounds contain NaN.
   *
   * @return true if the M dimension is valid, false otherwise.
   */
  public boolean isMValid() {
    return !(Double.isNaN(mMin) || Double.isNaN(mMax));
  }

  /**
   * Checks if the bounding box is empty in the X / Y dimension.
   *
   * @return true if the bounding box is empty, false otherwise.
   */
  public boolean isXYEmpty() {
    return isXEmpty() || isYEmpty();
  }

  /**
   * Checks if the bounding box is empty in the X dimension.
   *
   * @return true if the X dimension is empty, false otherwise.
   */
  public boolean isXEmpty() {
    return Double.isInfinite(xMin - xMax);
  }

  /**
   * Checks if the bounding box is empty in the Y dimension.
   *
   * @return true if the Y dimension is empty, false otherwise.
   */
  public boolean isYEmpty() {
    return Double.isInfinite(yMin - yMax);
  }

  /**
   * Checks if the bounding box is empty in the Z dimension.
   *
   * @return true if the Z dimension is empty, false otherwise.
   */
  public boolean isZEmpty() {
    return Double.isInfinite(zMin - zMax);
  }

  /**
   * Checks if the bounding box is empty in the M dimension.
   *
   * @return true if the M dimension is empty, false otherwise.
   */
  public boolean isMEmpty() {
    return Double.isInfinite(mMin - mMax);
  }

  /**
   * Checks if the X dimension of this bounding box wraps around the antimeridian.
   * This occurs when the minimum X value is greater than the maximum X value,
   * which is allowed by the Parquet specification for geometries that cross the antimeridian.
   *
   * @return true if the X dimension wraps around, false otherwise.
   */
  public boolean isXWraparound() {
    return isWraparound(xMin, xMax);
  }

  /**
   * Expands this bounding box to include the bounds of another box.
   * After merging, this bounding box will contain both its original extent
   * and the extent of the other bounding box.
   *
   * If either this bounding box or the other has wraparound X coordinates,
   * the X dimension will be marked as invalid (set to NaN) in the result.
   *
   * @param other the other BoundingBox whose bounds will be merged into this one
   */
  public void merge(BoundingBox other) {
    if (!valid) {
      return;
    }

    // If other is null or invalid, mark this as invalid
    if (other == null || !other.valid) {
      valid = false;
      resetBBox();
      return;
    }

    // We don't yet support merging wraparound bounds.
    // Rather than throw, we mark the X bounds as invalid.
    if (isXWraparound() || other.isXWraparound()) {
      // Mark X dimension as invalid by setting to NaN
      xMin = Double.NaN;
      xMax = Double.NaN;
    } else {
      // Normal case - merge X bounds
      this.xMin = Math.min(this.xMin, other.xMin);
      this.xMax = Math.max(this.xMax, other.xMax);
    }

    // Always merge Y, Z, and M bounds
    this.yMin = Math.min(this.yMin, other.yMin);
    this.yMax = Math.max(this.yMax, other.yMax);
    this.zMin = Math.min(this.zMin, other.zMin);
    this.zMax = Math.max(this.zMax, other.zMax);
    this.mMin = Math.min(this.mMin, other.mMin);
    this.mMax = Math.max(this.mMax, other.mMax);

    // Update the validity of this bounding box
    valid = isXYValid();
  }

  /**
   * Extends this bounding box to include the spatial extent of the provided geometry.
   * The bounding box coordinates (min/max values for x, y, z, m) will be adjusted
   * to encompass both the current bounds and the geometry's bounds.
   *
   * @param geometry The geometry whose coordinates will be used to update this bounding box.
   *                If null or empty, the method returns without making any changes.
   */
  public void update(Geometry geometry) {
    if (!valid) {
      return;
    }

    if (geometry == null || geometry.isEmpty()) {
      return;
    }

    Envelope envelope = geometry.getEnvelopeInternal();
    updateBounds(envelope.getMinX(), envelope.getMaxX(), envelope.getMinY(), envelope.getMaxY());

    for (Coordinate coord : geometry.getCoordinates()) {
      if (!Double.isNaN(coord.getZ())) {
        zMin = Math.min(zMin, coord.getZ());
        zMax = Math.max(zMax, coord.getZ());
      }
      if (!Double.isNaN(coord.getM())) {
        mMin = Math.min(mMin, coord.getM());
        mMax = Math.max(mMax, coord.getM());
      }
    }

    // Update the validity of this bounding box based on the other bounding box
    valid = isXYValid();
  }

  /**
   * Updates the X and Y bounds of this bounding box with the given coordinates.
   * Updates are conditional:
   * - X bounds are only updated if both minX and maxX are not NaN
   * - Y bounds are only updated if both minY and maxY are not NaN
   *
   * Note: JTS (Java Topology Suite) does not natively support wraparound envelopes
   * or geometries that cross the antimeridian (±180° longitude). It operates strictly
   * in a 2D Cartesian coordinate space and doesn't account for the Earth's spherical
   * nature or longitudinal wrapping.
   *
   * When JTS encounters a geometry that crosses the antimeridian, it will represent
   * it with an envelope spanning from the westernmost to easternmost points, often
   * covering most of the Earth's longitude range (e.g., minX=-180, maxX=180).
   *
   * The wraparound check below is defensive but should never be triggered with standard
   * JTS geometry operations, as JTS will never produce an envelope with minX > maxX.
   *
   * @throws ShouldNeverHappenException if the update creates an X wraparound condition
   */
  private void updateBounds(double minX, double maxX, double minY, double maxY) {
    if (!Double.isNaN(minX) && !Double.isNaN(maxX)) {
      // Check if the update would create a wraparound condition
      // This should never happen with standard JTS geometry operations
      if (isWraparound(minX, maxX) || isWraparound(xMin, xMax)) {
        throw new ShouldNeverHappenException("Wraparound bounding boxes are not yet supported");
      }

      xMin = Math.min(xMin, minX);
      xMax = Math.max(xMax, maxX);
    }

    if (!Double.isNaN(minY) && !Double.isNaN(maxY)) {
      yMin = Math.min(yMin, minY);
      yMax = Math.max(yMax, maxY);
    }
  }

  /**
   * Aborts the bounding box by resetting it to its initial state.
   */
  public void abort() {
    valid = false;
    resetBBox();
  }

  /**
   * Resets the bounding box to its initial state.
   */
  public void reset() {
    resetBBox();
    valid = true;
  }

  /**
   * The Parquet specification allows X bounds to be "wraparound" to allow for
   * more compact bounding boxes when a geometry happens to include components
   * on both sides of the antimeridian (e.g., the nation of Fiji). This function
   * checks for that case.
   */
  public static boolean isWraparound(double xmin, double xmax) {
    return !Double.isInfinite(xmin - xmax) && xmin > xmax;
  }

  /**
   * Creates a copy of the current bounding box.
   *
   * @return a new BoundingBox instance with the same values as this one.
   */
  public BoundingBox copy() {
    return new BoundingBox(
        this.xMin, this.xMax,
        this.yMin, this.yMax,
        this.zMin, this.zMax,
        this.mMin, this.mMax);
  }

  @Override
  public String toString() {
    StringBuilder sb = new StringBuilder("BoundingBox{xMin=")
        .append(xMin)
        .append(", xMax=")
        .append(xMax)
        .append(", yMin=")
        .append(yMin)
        .append(", yMax=")
        .append(yMax)
        .append(", zMin=")
        .append(zMin)
        .append(", zMax=")
        .append(zMax)
        .append(", mMin=")
        .append(mMin)
        .append(", mMax=")
        .append(mMax);

    // Only include the valid flag when it's false
    if (!valid) {
      sb.append(", valid=false");
    }

    sb.append('}');
    return sb.toString();
  }
}
