/*******************************************************************************
 * Copyright (c) 2015 Voyager Search and MITRE
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Apache License, Version 2.0 which
 * accompanies this distribution and is available at
 *    http://www.apache.org/licenses/LICENSE-2.0.txt
 ******************************************************************************/

package org.locationtech.spatial4j.shape.impl;

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

/**
 * A circle, also known as a point-radius, based on a {@link
 * org.locationtech.spatial4j.distance.DistanceCalculator} which does all the work. This
 * implementation should work for both cartesian 2D and geodetic sphere
 * surfaces.
 */
public class CircleImpl extends BaseShape<SpatialContext> implements Circle {

  protected final Point point;
  protected double radiusDEG;

  // calculated & cached
  protected Rectangle enclosingBox;

  public CircleImpl(Point p, double radiusDEG, SpatialContext ctx) {
    super(ctx);
    //We assume any validation of params already occurred (including bounding dist)
    this.point = p;
    this.radiusDEG = point.isEmpty() ? Double.NaN : radiusDEG;
    this.enclosingBox = point.isEmpty() ? ctx.makeRectangle(Double.NaN, Double.NaN, Double.NaN, Double.NaN) :
      ctx.getDistCalc().calcBoxByDistFromPt(point, this.radiusDEG, ctx, null);
  }

  @Override
  public void reset(double x, double y, double radiusDEG) {
    assert ! isEmpty();
    point.reset(x, y);
    this.radiusDEG = radiusDEG;
    this.enclosingBox = ctx.getDistCalc().calcBoxByDistFromPt(point, this.radiusDEG, ctx, enclosingBox);
  }

  @Override
  public boolean isEmpty() {
    return point.isEmpty();
  }

  @Override
  public Point getCenter() {
    return point;
  }

  @Override
  public double getRadius() {
    return radiusDEG;
  }

  @Override
  public double getArea(SpatialContext ctx) {
    if (ctx == null) {
      return Math.PI * radiusDEG * radiusDEG;
    } else {
      return ctx.getDistCalc().area(this);
    }
  }

  @Override
  public Circle getBuffered(double distance, SpatialContext ctx) {
    return ctx.makeCircle(point, distance + radiusDEG);
  }

  public boolean contains(double x, double y) {
    return ctx.getDistCalc().within(point, x, y, radiusDEG);
  }

  @Override
  public boolean hasArea() {
    return radiusDEG > 0;
  }

  /**
   * Note that the bounding box might contain a minX that is &gt; maxX, due to WGS84 anti-meridian.
   */
  @Override
  public Rectangle getBoundingBox() {
    return enclosingBox;
  }

  @Override
  public SpatialRelation relate(Shape other) {
//This shortcut was problematic in testing due to distinctions of CONTAINS/WITHIN for no-area shapes (lines, points).
//    if (distance == 0) {
//      return point.relate(other,ctx).intersects() ? SpatialRelation.WITHIN : SpatialRelation.DISJOINT;
//    }
    if (isEmpty() || other.isEmpty())
      return SpatialRelation.DISJOINT;
    if (other instanceof Point) {
      return relate((Point) other);
    }
    if (other instanceof Rectangle) {
      return relate((Rectangle) other);
    }
    if (other instanceof Circle) {
      return relate((Circle) other);
    }
    return other.relate(this).transpose();
  }

  public SpatialRelation relate(Point point) {
    return contains(point.getX(),point.getY()) ? SpatialRelation.CONTAINS : SpatialRelation.DISJOINT;
  }

  public SpatialRelation relate(Rectangle r) {
    //Note: Surprisingly complicated!

    //--We start by leveraging the fact we have a calculated bbox that is "cheaper" than use of DistanceCalculator.
    final SpatialRelation bboxSect = enclosingBox.relate(r);
    if (bboxSect == SpatialRelation.DISJOINT || bboxSect == SpatialRelation.WITHIN)
      return bboxSect;
    else if (bboxSect == SpatialRelation.CONTAINS && enclosingBox.equals(r))//nasty identity edge-case
      return SpatialRelation.WITHIN;
    //bboxSect is INTERSECTS or CONTAINS
    //The result can be DISJOINT, CONTAINS, or INTERSECTS (not WITHIN)

    return relateRectanglePhase2(r, bboxSect);
  }

  protected SpatialRelation relateRectanglePhase2(final Rectangle r, SpatialRelation bboxSect) {
    // DOES NOT WORK WITH GEO CROSSING DATELINE OR WORLD-WRAP. Other methods handle such cases.

    //At this point, the only thing we are certain of is that circle is *NOT* WITHIN r, since the
    // bounding box of a circle MUST be within r for the circle to be within r.

    //Quickly determine if they are DISJOINT or not.
    // Find the closest & farthest point to the circle within the rectangle
    final double closestX, farthestX;
    final double xAxis = getXAxis();
    if (xAxis < r.getMinX()) {
      closestX = r.getMinX();
      farthestX = r.getMaxX();
    } else if (xAxis > r.getMaxX()) {
      closestX = r.getMaxX();
      farthestX = r.getMinX();
    } else {
      closestX = xAxis; //we don't really use this value but to check this condition
      farthestX = r.getMaxX() - xAxis > xAxis - r.getMinX() ? r.getMaxX() : r.getMinX();
    }

    final double closestY, farthestY;
    final double yAxis = getYAxis();
    if (yAxis < r.getMinY()) {
      closestY = r.getMinY();
      farthestY = r.getMaxY();
    } else if (yAxis > r.getMaxY()) {
      closestY = r.getMaxY();
      farthestY = r.getMinY();
    } else {
      closestY = yAxis; //we don't really use this value but to check this condition
      farthestY = r.getMaxY() - yAxis > yAxis - r.getMinY() ? r.getMaxY() : r.getMinY();
    }

    //If r doesn't overlap an axis, then could be disjoint. Test closestXY
    if (xAxis != closestX && yAxis != closestY) {
      if (!contains(closestX, closestY))
        return SpatialRelation.DISJOINT;
    } // else CAN'T be disjoint if spans axis because earlier bbox check ruled that out

    //Now, we know it's *NOT* DISJOINT and it's *NOT* WITHIN either.
    // Does circle CONTAINS r or simply intersect it?

    //If circle contains r, then its bbox MUST also CONTAIN r.
    if (bboxSect != SpatialRelation.CONTAINS)
      return SpatialRelation.INTERSECTS;

    //If the farthest point of r away from the center of the circle is contained, then all of r is
    // contained.
    if (!contains(farthestX, farthestY))
      return SpatialRelation.INTERSECTS;

    //geodetic detection of farthest Y when rect crosses x axis can't be reliably determined, so
    // check other corner too, which might actually be farthest
    if (point.getY() != getYAxis()) {//geodetic
      if (yAxis == closestY) {//r crosses north to south over x axis (confusing)
        double otherY = (farthestY == r.getMaxY() ? r.getMinY() : r.getMaxY());
        if (!contains(farthestX, otherY))
          return SpatialRelation.INTERSECTS;
      }
    }
   
    return SpatialRelation.CONTAINS;
  }

  /**
   * The <code>Y</code> coordinate of where the circle axis intersect.
   */
  protected double getYAxis() {
    return point.getY();
  }

  /**
   * The <code>X</code> coordinate of where the circle axis intersect.
   */
  protected double getXAxis() {
    return point.getX();
  }

  public SpatialRelation relate(Circle circle) {
    double crossDist = ctx.getDistCalc().distance(point, circle.getCenter());
    double aDist = radiusDEG, bDist = circle.getRadius();
    if (crossDist > aDist + bDist)
      return SpatialRelation.DISJOINT;
    if (crossDist < aDist && crossDist + bDist <= aDist)
      return SpatialRelation.CONTAINS;
    if (crossDist < bDist && crossDist + aDist <= bDist)
      return SpatialRelation.WITHIN;

    return SpatialRelation.INTERSECTS;
  }

  @Override
  public String toString() {
    return "Circle(" + point + ", d=" + radiusDEG + "°)";
  }

  @Override
  public boolean equals(Object obj) {
    return equals(this,obj);
  }

  /**
   * All {@link Circle} implementations should use this definition of {@link Object#equals(Object)}.
   */
  public static boolean equals(Circle thiz, Object o) {
    assert thiz != null;
    if (thiz == o) return true;
    if (!(o instanceof Circle)) return false;

    Circle circle = (Circle) o;

    if (!thiz.getCenter().equals(circle.getCenter())) return false;
    if (Double.compare(circle.getRadius(), thiz.getRadius()) != 0) return false;

    return true;
  }

  @Override
  public int hashCode() {
    return hashCode(this);
  }

  /**
   * All {@link Circle} implementations should use this definition of {@link Object#hashCode()}.
   */
  public static int hashCode(Circle thiz) {
    int result;
    long temp;
    result = thiz.getCenter().hashCode();
    temp = thiz.getRadius() != +0.0d ? Double.doubleToLongBits(thiz.getRadius()) : 0L;
    result = 31 * result + (int) (temp ^ (temp >>> 32));
    return result;
  }
}
