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

import static org.apache.lucene.geo.GeoUtils.lineCrossesLine;
import static org.apache.lucene.geo.GeoUtils.lineCrossesLineWithBoundary;
import static org.apache.lucene.geo.GeoUtils.orient;

import java.util.Arrays;

/**
 * Internal tree node: represents geometry edge from [x1, y1] to [x2, y2]. The sort value is {@code
 * low}, which is the minimum y of the edge. {@code max} stores the maximum y of this edge or any
 * children.
 *
 * <p>Construction takes {@code O(n log n)} time for sorting and tree construction. Methods are
 * {@code O(n)}, but for most practical lines and polygons are much faster than brute force.
 */
final class EdgeTree {
  // X-Y pair (in original order) of the two vertices
  final double y1, y2;
  final double x1, x2;
  /** min Y of this edge */
  final double low;
  /** max Y of this edge or any children */
  double max;
  /** left child edge, or null */
  EdgeTree left;
  /** right child edge, or null */
  EdgeTree right;
  /** helper bytes to signal if a point is on an edge, it is within the edge tree or disjoint */
  private static final byte FALSE = 0x00;

  private static final byte TRUE = 0x01;
  private static final byte ON_EDGE = 0x02;

  private EdgeTree(double x1, double y1, double x2, double y2, double low, double max) {
    this.y1 = y1;
    this.x1 = x1;
    this.y2 = y2;
    this.x2 = x2;
    this.low = low;
    this.max = max;
  }

  /** Returns true if the point is on an edge or crosses the edge subtree an odd number of times. */
  protected boolean contains(double x, double y) {
    return containsPnPoly(x, y) > FALSE;
  }

  /**
   * Returns byte 0x00 if the point crosses this edge subtree an even number of times. Returns byte
   * 0x01 if the point crosses this edge subtree an odd number of times. Returns byte 0x02 if the
   * point is on one of the edges.
   *
   * <p>See <a href="https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html">
   * https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html</a> for more information.
   */
  // ported to java from https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html
  // original code under the BSD license
  // (https://www.ecse.rpi.edu/~wrf/Research/Short_Notes/pnpoly.html#License%20to%20Use)
  //
  // Copyright (c) 1970-2003, Wm. Randolph Franklin
  //
  // Permission is hereby granted, free of charge, to any person obtaining a copy of this software
  // and associated
  // documentation files (the "Software"), to deal in the Software without restriction, including
  // without limitation
  // the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
  // the Software, and
  // to permit persons to whom the Software is furnished to do so, subject to the following
  // conditions:
  //
  // 1. Redistributions of source code must retain the above copyright
  //    notice, this list of conditions and the following disclaimers.
  // 2. Redistributions in binary form must reproduce the above copyright
  //    notice in the documentation and/or other materials provided with
  //    the distribution.
  // 3. The name of W. Randolph Franklin may not be used to endorse or
  //    promote products derived from this Software without specific
  //    prior written permission.
  //
  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
  // BUT NOT LIMITED
  // TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
  // NO EVENT SHALL
  // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
  // IN AN ACTION OF
  // CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
  // OR OTHER DEALINGS
  // IN THE SOFTWARE.
  private byte containsPnPoly(double x, double y) {
    byte res = FALSE;
    if (y <= this.max) {
      if (y == this.y1 && y == this.y2
          || (y <= this.y1 && y >= this.y2) != (y >= this.y1 && y <= this.y2)) {
        if ((x == this.x1 && x == this.x2)
            || ((x <= this.x1 && x >= this.x2) != (x >= this.x1 && x <= this.x2)
                && GeoUtils.orient(this.x1, this.y1, this.x2, this.y2, x, y) == 0)) {
          return ON_EDGE;
        } else if (this.y1 > y != this.y2 > y) {
          res =
              x < (this.x2 - this.x1) * (y - this.y1) / (this.y2 - this.y1) + this.x1
                  ? TRUE
                  : FALSE;
        }
      }
      if (this.left != null) {
        res ^= left.containsPnPoly(x, y);
        if ((res & 0x02) == 0x02) {
          return ON_EDGE;
        }
      }

      if (this.right != null && y >= this.low) {
        res ^= right.containsPnPoly(x, y);
        if ((res & 0x02) == 0x02) {
          return ON_EDGE;
        }
      }
    }
    assert res >= FALSE && res <= ON_EDGE;
    return res;
  }

  /** returns true if the provided x, y point lies on the line */
  boolean isPointOnLine(double x, double y) {
    if (y <= max) {
      double a1x = x1;
      double a1y = y1;
      double b1x = x2;
      double b1y = y2;
      boolean outside =
          (a1y < y && b1y < y)
              || (a1y > y && b1y > y)
              || (a1x < x && b1x < x)
              || (a1x > x && b1x > x);
      if (outside == false && orient(a1x, a1y, b1x, b1y, x, y) == 0) {
        return true;
      }
      if (left != null && left.isPointOnLine(x, y)) {
        return true;
      }
      if (right != null && y >= this.low && right.isPointOnLine(x, y)) {
        return true;
      }
    }
    return false;
  }

  /** Returns true if the triangle crosses any edge in this edge subtree */
  boolean crossesTriangle(
      double minX,
      double maxX,
      double minY,
      double maxY,
      double ax,
      double ay,
      double bx,
      double by,
      double cx,
      double cy,
      boolean includeBoundary) {
    if (minY <= max) {
      double dy = y1;
      double ey = y2;
      double dx = x1;
      double ex = x2;

      // optimization: see if the rectangle is outside of the "bounding box" of the polyline at all
      // if not, don't waste our time trying more complicated stuff
      boolean outside =
          (dy < minY && ey < minY)
              || (dy > maxY && ey > maxY)
              || (dx < minX && ex < minX)
              || (dx > maxX && ex > maxX);

      if (outside == false) {
        if (includeBoundary == true) {
          if (lineCrossesLineWithBoundary(dx, dy, ex, ey, ax, ay, bx, by)
              || lineCrossesLineWithBoundary(dx, dy, ex, ey, bx, by, cx, cy)
              || lineCrossesLineWithBoundary(dx, dy, ex, ey, cx, cy, ax, ay)) {
            return true;
          }
        } else {
          if (lineCrossesLine(dx, dy, ex, ey, ax, ay, bx, by)
              || lineCrossesLine(dx, dy, ex, ey, bx, by, cx, cy)
              || lineCrossesLine(dx, dy, ex, ey, cx, cy, ax, ay)) {
            return true;
          }
        }
      }

      if (left != null
          && left.crossesTriangle(
              minX, maxX, minY, maxY, ax, ay, bx, by, cx, cy, includeBoundary)) {
        return true;
      }

      if (right != null
          && maxY >= low
          && right.crossesTriangle(
              minX, maxX, minY, maxY, ax, ay, bx, by, cx, cy, includeBoundary)) {
        return true;
      }
    }
    return false;
  }

  /** Returns true if the box crosses any edge in this edge subtree */
  boolean crossesBox(double minX, double maxX, double minY, double maxY, boolean includeBoundary) {
    // we just have to cross one edge to answer the question, so we descend the tree and return when
    // we do.
    if (minY <= max) {
      // we compute line intersections of every polygon edge with every box line.
      // if we find one, return true.
      // for each box line (AB):
      //   for each poly line (CD):
      //     intersects = orient(C,D,A) * orient(C,D,B) <= 0 && orient(A,B,C) * orient(A,B,D) <= 0
      double cy = y1;
      double dy = y2;
      double cx = x1;
      double dx = x2;

      // optimization: see if either end of the line segment is contained by the rectangle
      if (Rectangle.containsPoint(cy, cx, minY, maxY, minX, maxX)
          || Rectangle.containsPoint(dy, dx, minY, maxY, minX, maxX)) {
        return true;
      }

      // optimization: see if the rectangle is outside of the "bounding box" of the polyline at all
      // if not, don't waste our time trying more complicated stuff
      boolean outside =
          (cy < minY && dy < minY)
              || (cy > maxY && dy > maxY)
              || (cx < minX && dx < minX)
              || (cx > maxX && dx > maxX);

      if (outside == false) {
        if (includeBoundary == true) {
          if (lineCrossesLineWithBoundary(cx, cy, dx, dy, minX, minY, maxX, minY)
              || lineCrossesLineWithBoundary(cx, cy, dx, dy, maxX, minY, maxX, maxY)
              || lineCrossesLineWithBoundary(cx, cy, dx, dy, maxX, maxY, minX, maxY)
              || lineCrossesLineWithBoundary(cx, cy, dx, dy, minX, maxY, minX, minY)) {
            // include boundaries: ensures box edges that terminate on the polygon are included
            return true;
          }
        } else {
          if (lineCrossesLine(cx, cy, dx, dy, minX, minY, maxX, minY)
              || lineCrossesLine(cx, cy, dx, dy, maxX, minY, maxX, maxY)
              || lineCrossesLine(cx, cy, dx, dy, maxX, maxY, minX, maxY)
              || lineCrossesLine(cx, cy, dx, dy, minX, maxY, minX, minY)) {
            return true;
          }
        }
      }

      if (left != null && left.crossesBox(minX, maxX, minY, maxY, includeBoundary)) {
        return true;
      }

      if (right != null
          && maxY >= low
          && right.crossesBox(minX, maxX, minY, maxY, includeBoundary)) {
        return true;
      }
    }
    return false;
  }

  /** Returns true if the line crosses any edge in this edge subtree */
  boolean crossesLine(
      double minX,
      double maxX,
      double minY,
      double maxY,
      double a2x,
      double a2y,
      double b2x,
      double b2y,
      boolean includeBoundary) {
    if (minY <= max) {
      double a1x = x1;
      double a1y = y1;
      double b1x = x2;
      double b1y = y2;

      boolean outside =
          (a1y < minY && b1y < minY)
              || (a1y > maxY && b1y > maxY)
              || (a1x < minX && b1x < minX)
              || (a1x > maxX && b1x > maxX);
      if (outside == false) {
        if (includeBoundary) {
          if (lineCrossesLineWithBoundary(a1x, a1y, b1x, b1y, a2x, a2y, b2x, b2y)) {
            return true;
          }
        } else {
          if (lineCrossesLine(a1x, a1y, b1x, b1y, a2x, a2y, b2x, b2y)) {
            return true;
          }
        }
      }
      if (left != null
          && left.crossesLine(minX, maxX, minY, maxY, a2x, a2y, b2x, b2y, includeBoundary)) {
        return true;
      }
      if (right != null
          && maxY >= low
          && right.crossesLine(minX, maxX, minY, maxY, a2x, a2y, b2x, b2y, includeBoundary)) {
        return true;
      }
    }
    return false;
  }

  /**
   * Creates an edge interval tree from a set of geometry vertices.
   *
   * @return root node of the tree.
   */
  static EdgeTree createTree(double[] x, double[] y) {
    EdgeTree[] edges = new EdgeTree[x.length - 1];
    for (int i = 1; i < x.length; i++) {
      double x1 = x[i - 1];
      double y1 = y[i - 1];
      double x2 = x[i];
      double y2 = y[i];
      edges[i - 1] = new EdgeTree(x1, y1, x2, y2, Math.min(y1, y2), Math.max(y1, y2));
    }
    // sort the edges then build a balanced tree from them
    Arrays.sort(
        edges,
        (left, right) -> {
          int ret = Double.compare(left.low, right.low);
          if (ret == 0) {
            ret = Double.compare(left.max, right.max);
          }
          return ret;
        });
    return createTree(edges, 0, edges.length - 1);
  }

  /** Creates tree from sorted edges (with range low and high inclusive) */
  private static EdgeTree createTree(EdgeTree[] edges, int low, int high) {
    if (low > high) {
      return null;
    }
    // add midpoint
    int mid = (low + high) >>> 1;
    EdgeTree newNode = edges[mid];
    // add children
    newNode.left = createTree(edges, low, mid - 1);
    newNode.right = createTree(edges, mid + 1, high);
    // pull up max values to this node
    if (newNode.left != null) {
      newNode.max = Math.max(newNode.max, newNode.left.max);
    }
    if (newNode.right != null) {
      newNode.max = Math.max(newNode.max, newNode.right.max);
    }
    return newNode;
  }
}
