/*
 * 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.spatial.query;

import org.locationtech.spatial4j.context.SpatialContext;
import org.locationtech.spatial4j.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.Shape;

/**
 * Principally holds the query {@link Shape} and the {@link SpatialOperation}. It's used as an
 * argument to some methods on {@link org.apache.lucene.spatial.SpatialStrategy}.
 *
 * @lucene.experimental
 */
public class SpatialArgs {

  public static final double DEFAULT_DISTERRPCT = 0.025d;

  private SpatialOperation operation;
  private Shape shape;
  private Double distErrPct;
  private Double distErr;

  public SpatialArgs(SpatialOperation operation, Shape shape) {
    if (operation == null || shape == null)
      throw new NullPointerException("operation and shape are required");
    this.operation = operation;
    this.shape = shape;
  }

  /**
   * Computes the distance given a shape and the {@code distErrPct}. The algorithm is the fraction
   * of the distance from the center of the query shape to its closest bounding box corner.
   *
   * @param shape Mandatory.
   * @param distErrPct 0 to 0.5
   * @param ctx Mandatory
   * @return A distance (in degrees).
   */
  public static double calcDistanceFromErrPct(Shape shape, double distErrPct, SpatialContext ctx) {
    if (distErrPct < 0 || distErrPct > 0.5) {
      throw new IllegalArgumentException(
          "distErrPct " + distErrPct + " must be between [0 to 0.5]");
    }
    if (distErrPct == 0 || shape instanceof Point) {
      return 0;
    }
    Rectangle bbox = shape.getBoundingBox();
    // Compute the distance from the center to a corner.  Because the distance
    // to a bottom corner vs a top corner can vary in a geospatial scenario,
    // take the closest one (greater precision).
    Point ctr = bbox.getCenter();
    double y = (ctr.getY() >= 0 ? bbox.getMaxY() : bbox.getMinY());
    double diagonalDist = ctx.getDistCalc().distance(ctr, bbox.getMaxX(), y);
    return diagonalDist * distErrPct;
  }

  /**
   * Gets the error distance that specifies how precise the query shape is. This looks at {@link
   * #getDistErr()}, {@link #getDistErrPct()}, and {@code defaultDistErrPct}.
   *
   * @param defaultDistErrPct 0 to 0.5
   * @return {@code >= 0}
   */
  public double resolveDistErr(SpatialContext ctx, double defaultDistErrPct) {
    if (distErr != null) return distErr;
    double distErrPct = (this.distErrPct != null ? this.distErrPct : defaultDistErrPct);
    return calcDistanceFromErrPct(shape, distErrPct, ctx);
  }

  /** Check if the arguments make sense -- throw an exception if not */
  public void validate() throws IllegalArgumentException {
    if (distErr != null && distErrPct != null)
      throw new IllegalArgumentException("Only distErr or distErrPct can be specified.");
  }

  @Override
  public String toString() {
    return SpatialArgsParser.writeSpatialArgs(this);
  }

  // ------------------------------------------------
  // Getters & Setters
  // ------------------------------------------------

  public SpatialOperation getOperation() {
    return operation;
  }

  public void setOperation(SpatialOperation operation) {
    this.operation = operation;
  }

  public Shape getShape() {
    return shape;
  }

  public void setShape(Shape shape) {
    this.shape = shape;
  }

  /**
   * A measure of acceptable error of the shape as a fraction. This effectively inflates the size of
   * the shape but should not shrink it.
   *
   * @return 0 to 0.5
   * @see #calcDistanceFromErrPct(org.locationtech.spatial4j.shape.Shape, double,
   *     org.locationtech.spatial4j.context.SpatialContext)
   */
  public Double getDistErrPct() {
    return distErrPct;
  }

  public void setDistErrPct(Double distErrPct) {
    if (distErrPct != null) this.distErrPct = distErrPct;
  }

  /**
   * The acceptable error of the shape. This effectively inflates the size of the shape but should
   * not shrink it.
   *
   * @return {@code >= 0}
   */
  public Double getDistErr() {
    return distErr;
  }

  public void setDistErr(Double distErr) {
    this.distErr = distErr;
  }
}
