/*******************************************************************************
 * Copyright (c) 2015 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.shape.Point;
import org.locationtech.spatial4j.shape.Rectangle;
import org.locationtech.spatial4j.shape.SpatialRelation;

import static org.locationtech.spatial4j.shape.SpatialRelation.*;

/**
 * INERNAL: A buffered line of infinite length.
 * Public for test access.
 */
public class InfBufLine {

  /** Error epsilon. */
  private static final double EPS = 10e-14;

  //TODO consider removing support for vertical line -- let caller
  // do something else.  BufferedLine could have a factory method
  // that returns a rectangle, for example.

  // line: y = slope * x + intercept

  private final double slope;//can be infinite for vertical line
  //if slope is infinite, this is x intercept, otherwise y intercept
  private final double intercept;

  private final double buf;

  private final double distDenomInv;//cached: 1 / Math.sqrt(slope * slope + 1)

  InfBufLine(double slope, Point point, double buf) {
    assert !Double.isNaN(slope);
    this.slope = slope;
    if (Double.isInfinite(slope)) {
      intercept = point.getX();
      distDenomInv = Double.NaN;
    } else {
      intercept = point.getY() - slope * point.getX();
      distDenomInv = 1 / Math.sqrt(slope * slope + 1);
    }
    this.buf = buf;
  }

  SpatialRelation relate(Rectangle r, Point prC, Point scratch) {
    assert r.getCenter().equals(prC);

    int cQuad = quadrant(prC);

    Point nearestP = scratch;
    cornerByQuadrant(r, oppositeQuad[cQuad], nearestP);
    boolean nearestContains = contains(nearestP);

    if (nearestContains) {
      Point farthestP = scratch;
      nearestP = null;//just to be safe (same scratch object)
      cornerByQuadrant(r, cQuad, farthestP);
      boolean farthestContains = contains(farthestP);
      if (farthestContains)
        return CONTAINS;
      return INTERSECTS;
    } else {// not nearestContains
      if (quadrant(nearestP) == cQuad)
        return DISJOINT;//out of buffer on same side as center
      return INTERSECTS;//nearest & farthest points straddle the line
    }
  }

  boolean contains(Point p) {
    return (distanceUnbuffered(p) <= buf + EPS);
  }

  /** INTERNAL AKA lineToPointDistance */
  public double distanceUnbuffered(Point c) {
    if (Double.isInfinite(slope))
      return Math.abs(c.getX() - intercept);
    // http://math.ucsd.edu/~wgarner/math4c/derivations/distance/distptline.htm
    double num = Math.abs(c.getY() - slope * c.getX() - intercept);
    return num * distDenomInv;
  }

//  /** Amount to add or subtract to intercept to indicate where the
//   * buffered line edges cross the y axis.
//   * @return
//   */
//  double interceptBuffOffset() {
//    if (Double.isInfinite(slope))
//      return slope;
//    if (buf == 0)
//      return 0;
//    double slopeDivBuf = slope / buf;
//    return Math.sqrt(buf*buf + slopeDivBuf*slopeDivBuf);
//  }

  /** INTERNAL: AKA lineToPointQuadrant */
  public int quadrant(Point c) {
    //check vertical line case 1st
    if (Double.isInfinite(slope)) {
      //when slope is infinite, intercept is x intercept instead of y
      return c.getX() > intercept ? 1 : 2; //4 : 3 would work too
    }
    //(below will work for slope==0 horizontal line too)
    //is c above or below the line
    double yAtCinLine = slope * c.getX() + intercept;
    boolean above = c.getY() >= yAtCinLine;
    if (slope > 0) {
      //if slope is a forward slash, then result is 2 | 4
      return above ? 2 : 4;
    } else {
      //if slope is a backward slash, then result is 1 | 3
      return above ? 1 : 3;
    }
  }

  //TODO ? Use an Enum for quadrant?

  /* quadrants 1-4: NE, NW, SW, SE. */
  private static final int[] oppositeQuad= {-1,3,4,1,2};

  public static void cornerByQuadrant(Rectangle r, int cornerQuad, Point out) {
    double x = (cornerQuad == 1 || cornerQuad == 4) ? r.getMaxX() : r.getMinX();
    double y = (cornerQuad == 1 || cornerQuad == 2) ? r.getMaxY() : r.getMinY();
    out.reset(x, y);
  }

  public double getSlope() {
    return slope;
  }

  public double getIntercept() {
    return intercept;
  }

  public double getBuf() {
    return buf;
  }

  /** 1 / Math.sqrt(slope * slope + 1) */
  public double getDistDenomInv() {
    return distDenomInv;
  }

  @Override
  public String toString() {
    return "InfBufLine{" +
        "buf=" + buf +
        ", intercept=" + intercept +
        ", slope=" + slope +
        '}';
  }
}
