/*
 * Copyright (c) 2024 Martin Davis.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * and Eclipse Distribution License v. 1.0 which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v20.html
 * and the Eclipse Distribution License is available at
 *
 * http://www.eclipse.org/org/documents/edl-v10.php.
 */
package org.locationtech.jts.operation.relateng;

import static org.locationtech.jts.operation.relateng.RelateGeometry.GEOM_A;
import static org.locationtech.jts.operation.relateng.RelateGeometry.GEOM_B;

import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.locationtech.jts.algorithm.BoundaryNodeRule;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Dimension;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.geom.GeometryCollectionIterator;
import org.locationtech.jts.geom.IntersectionMatrix;
import org.locationtech.jts.geom.LineString;
import org.locationtech.jts.geom.LinearRing;
import org.locationtech.jts.geom.Location;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Polygon;
import org.locationtech.jts.geom.prep.PreparedGeometry;
import org.locationtech.jts.noding.MCIndexSegmentSetMutualIntersector;
import org.locationtech.jts.operation.relate.RelateOp;

/**
 * Computes the value of topological predicates between two geometries based on the 
 * <a href="https://en.wikipedia.org/wiki/DE-9IM">Dimensionally-Extended 9-Intersection Model</a> (DE-9IM).
 * Standard and custom topological predicates are provided by {@link RelatePredicate}.
 * <p>
 * The RelateNG algorithm has the following capabilities:
 * <ol>
 * <li>Efficient short-circuited evaluation of topological predicates
 *     (including matching custom DE-9IM matrix patterns)
 * <li>Optimized repeated evaluation of predicates against a single geometry 
 *     via cached spatial indexes (AKA "prepared mode")
 * <li>Robust computation (only point-local topology is required,
 *     so invalid geometry topology does not cause failures)
 * <li>{@link GeometryCollection} inputs containing mixed types and overlapping polygons
 *     are supported, using <i>union semantics</i>.
 * <li>Zero-length LineStrings are treated as being topologically identical to Points.
 * <li>Support for {@link BoundaryNodeRule}s.
 * </ol>
 * 
 * See {@link IntersectionMatrixPattern} for a description of DE-9IM patterns.
 * 
 * If not specified, the standard {@link BoundaryNodeRule#MOD2_BOUNDARY_RULE} is used.
 * 
 * RelateNG operates in 2D only; it ignores any Z ordinates.
 * 
 * This implementation replaces {@link RelateOp} and {@link PreparedGeometry}.
 * 
 * <h3>FUTURE WORK</h3>
 * <ul>
 * <li>Support for a distance tolerance to provide "approximate" predicate evaluation
 * </ul>
 * 
 * 
 * @author Martin Davis
 *
 * @see RelateOp
 * @see PreparedGeometry
 */
public class RelateNG 
{
 
  /**
   * Tests whether the topological relationship between two geometries
   * satisfies a topological predicate.
   * 
   * @param a the A input geometry
   * @param b the A input geometry
   * @param pred the topological predicate
   * @return true if the topological relationship is satisfied
   */
  public static boolean relate(Geometry a, Geometry b, TopologyPredicate pred) {
    RelateNG rng = new RelateNG(a, false);
    return rng.evaluate(b, pred);
  } 
  
  /**
   * Tests whether the topological relationship between two geometries
   * satisfies a topological predicate,
   * using a given {@link BoundaryNodeRule}.
   * 
   * @param a the A input geometry
   * @param b the A input geometry
   * @param pred the topological predicate
   * @param bnRule the Boundary Node Rule to use
   * @return true if the topological relationship is satisfied
   */
  public static boolean relate(Geometry a, Geometry b, TopologyPredicate pred, BoundaryNodeRule bnRule) {
    RelateNG rng = new RelateNG(a, false, bnRule);
    return rng.evaluate(b, pred);
  } 
  
  /**
   * Tests whether the topological relationship to a geometry 
   * matches a DE-9IM matrix pattern.
   * 
   * @param a the A input geometry
   * @param b the A input geometry
   * @param imPattern the DE-9IM pattern to match
   * @return true if the geometries relationship matches the DE-9IM pattern
   * 
   * @see IntersectionMatrixPattern
   */
  public static boolean relate(Geometry a, Geometry b, String imPattern) {
    RelateNG rng = new RelateNG(a, false);
    return rng.evaluate(b, imPattern); 
  }

  /**
   * Computes the DE-9IM matrix 
   * for the topological relationship between two geometries.
   * 
   * @param a the A input geometry
   * @param b the A input geometry
   * @return the DE-9IM matrix for the topological relationship
   */
  public static IntersectionMatrix relate(Geometry a, Geometry b) {
    RelateNG rng = new RelateNG(a, false);
    return rng.evaluate(b);
  } 
  
  /**
   * Computes the DE-9IM matrix 
   * for the topological relationship between two geometries.
   * 
   * @param a the A input geometry
   * @param b the A input geometry
   * @param bnRule the Boundary Node Rule to use
   * @return the DE-9IM matrix for the relationship
   */
  public static IntersectionMatrix relate(Geometry a, Geometry b, BoundaryNodeRule bnRule) {
    RelateNG rng = new RelateNG(a, false, bnRule);
    return rng.evaluate(b); 
  }
  
  /**
   * Creates a prepared RelateNG instance to optimize the
   * evaluation of relationships against a single geometry.
   * 
   * @param a the A input geometry
   * @return a prepared instance
   */
  public static RelateNG prepare(Geometry a) {
    return new RelateNG(a, true);
  }
  
  /**
   * Creates a prepared RelateNG instance to optimize the
   * computation of predicates against a single geometry,
   * using a given {@link BoundaryNodeRule}.
   * 
   * @param a the A input geometry
   * @param bnRule the required BoundaryNodeRule
   * @return a prepared instance
   */
  public static RelateNG prepare(Geometry a, BoundaryNodeRule bnRule) {
    return new RelateNG(a, true, bnRule);
  }
  
  private BoundaryNodeRule boundaryNodeRule;
  private RelateGeometry geomA;
  private MCIndexSegmentSetMutualIntersector edgeMutualInt;
  
  private RelateNG(Geometry inputA, boolean isPrepared) {
    this(inputA, isPrepared, BoundaryNodeRule.OGC_SFS_BOUNDARY_RULE);
  }
  
  private RelateNG(Geometry inputA, boolean isPrepared, BoundaryNodeRule bnRule) {
    this.boundaryNodeRule = bnRule;
    geomA = new RelateGeometry(inputA, isPrepared, boundaryNodeRule);
  }
  
  /**
   * Computes the DE-9IM matrix for the topological relationship to a geometry.
   * 
   * @param b the B geometry to test against
   * @return the DE-9IM matrix
   */
  public IntersectionMatrix evaluate(Geometry b) {
    RelateMatrixPredicate rel = new RelateMatrixPredicate();
    evaluate(b, rel); 
    return rel.getIM();
  }   
  
  /**
   * Tests whether the topological relationship to a geometry 
   * matches a DE-9IM matrix pattern.
   * 
   * @param b the B geometry to test against
   * @param imPattern the DE-9IM pattern to match
   * @return true if the geometries' topological relationship matches the DE-9IM pattern
   * 
   * @see IntersectionMatrixPattern
   */
  public boolean evaluate(Geometry b, String imPattern) {
    return evaluate(b, RelatePredicate.matches(imPattern)); 
  }
  
  /**
   * Tests whether the topological relationship to a geometry
   * satisfies a topology predicate.
   * 
   * @param b the B geometry to test against
   * @param predicate the topological predicate
   * @return true if the predicate is satisfied
   */
  public boolean evaluate(Geometry b, TopologyPredicate predicate) {
    //-- fast envelope checks
    if (! hasRequiredEnvelopeInteraction(b, predicate)) {
      return false;
    }
        
    RelateGeometry geomB = new RelateGeometry(b, boundaryNodeRule);
    
    if (geomA.isEmpty() && geomB.isEmpty()) {
      //TODO: what if predicate is disjoint?  Perhaps use result on disjoint envs?
      return finishValue(predicate);
    }
    int dimA = geomA.getDimensionReal();
    int dimB = geomB.getDimensionReal();
    
    //-- check if predicate is determined by dimension or envelope
    predicate.init(dimA, dimB);
    if (predicate.isKnown())
      return finishValue(predicate);
    
    predicate.init(geomA.getEnvelope(), geomB.getEnvelope());
    if (predicate.isKnown())
      return finishValue(predicate);
    
    TopologyComputer topoComputer = new TopologyComputer(predicate, geomA, geomB);
    
    //-- optimized P/P evaluation
    if (dimA == Dimension.P && dimB == Dimension.P) {
      computePP(geomB, topoComputer);
      topoComputer.finish();
      return topoComputer.getResult();
    }

    //-- test points against (potentially) indexed geometry first
    computeAtPoints(geomB, GEOM_B, geomA, topoComputer);
    if (topoComputer.isResultKnown()) {
      return topoComputer.getResult();
    }   
    computeAtPoints(geomA, GEOM_A, geomB, topoComputer);
    if (topoComputer.isResultKnown()) {
      return topoComputer.getResult();
    }   
    
    if (geomA.hasEdges() && geomB.hasEdges()) {
      computeAtEdges(geomB, topoComputer);
    }

    //-- after all processing, set remaining unknown values in IM
    topoComputer.finish();
    return topoComputer.getResult();
  }

  private boolean hasRequiredEnvelopeInteraction(Geometry b, TopologyPredicate predicate) {
    Envelope envB = b.getEnvelopeInternal();
    boolean isInteracts = false;
    if (predicate.requireCovers(GEOM_A)) {
      if (! geomA.getEnvelope().covers(envB)) {
        return false;
      }
      isInteracts = true;
    }
    else if (predicate.requireCovers(GEOM_B)) {
      if (! envB.covers(geomA.getEnvelope())) {
        return false;
      }
      isInteracts = true;
    }
    if (! isInteracts 
        && predicate.requireInteraction()
        && ! geomA.getEnvelope().intersects(envB)) {
      return false;
    }
    return true;
  }

  private boolean finishValue(TopologyPredicate predicate) {
    predicate.finish();
    return predicate.value();
  }

  /**
   * An optimized algorithm for evaluating P/P cases.
   * It tests one point set against the other.
   * 
   * @param geomB
   * @param topoComputer
   */
  private void computePP(RelateGeometry geomB, TopologyComputer topoComputer) {
    Set<Coordinate> ptsA = geomA.getUniquePoints();
    //TODO: only query points in interaction extent? 
    Set<Coordinate> ptsB = geomB.getUniquePoints();
    
    int numBinA = 0;
    for (Coordinate ptB : ptsB) {
      if (ptsA.contains(ptB)) {
        numBinA++;
        topoComputer.addPointOnPointInterior(ptB);
      }
      else {
        topoComputer.addPointOnPointExterior(GEOM_B, ptB);
      }
      if (topoComputer.isResultKnown()) {
        return;
      }
    }
    /**
     * If number of matched B points is less than size of A, 
     * there must be at least one A point in the exterior of B
     */
    if (numBinA < ptsA.size()) {
      //TODO: determine actual exterior point?
      topoComputer.addPointOnPointExterior(GEOM_A, null);
    }
  }  

  private void computeAtPoints(RelateGeometry geom, boolean isA, 
      RelateGeometry geomTarget, TopologyComputer topoComputer) {
    
    boolean isResultKnown = false;
    isResultKnown = computePoints(geom, isA, geomTarget, topoComputer);
    if (isResultKnown) 
      return;
    
    /**
     * Performance optimization: only check points against target
     * if it has areas OR if the predicate requires checking for 
     * exterior interaction.
     * In particular, this avoids testing line ends against lines 
     * for the intersects predicate (since these are checked
     * during segment/segment intersection checking anyway). 
     * Checking points against areas is necessary, since the input
     * linework is disjoint if one input lies wholly inside an area,
     * so segment intersection checking is not sufficient.
     */
    boolean checkDisjointPoints = geomTarget.hasDimension(Dimension.A) 
        || topoComputer.isExteriorCheckRequired(isA);
    if (! checkDisjointPoints)
      return;
    
    isResultKnown = computeLineEnds(geom, isA, geomTarget, topoComputer);
    if (isResultKnown) 
      return;
    
    computeAreaVertex(geom, isA, geomTarget, topoComputer);
  }
  
  private boolean computePoints(RelateGeometry geom, boolean isA, RelateGeometry geomTarget,
      TopologyComputer topoComputer) { 
    if (! geom.hasDimension(Dimension.P)) {
      return false;
    }
    
    List<Point> points = geom.getEffectivePoints();
    for (Point point : points) {
      //TODO: exit when all possible target locations (E,I,B) have been found?
      if (point.isEmpty())
        continue;
      
      Coordinate pt = point.getCoordinate();
      computePoint(isA, pt, geomTarget, topoComputer);
      if (topoComputer.isResultKnown()) {
        return true;
      }
    }
    return false;
  }

  private void computePoint(boolean isA, Coordinate pt, RelateGeometry geomTarget, TopologyComputer topoComputer) {
      int locDimTarget = geomTarget.locateWithDim(pt);
      int locTarget = DimensionLocation.location(locDimTarget);
      int dimTarget = DimensionLocation.dimension(locDimTarget, topoComputer.getDimension(! isA));
      topoComputer.addPointOnGeometry(isA, locTarget, dimTarget, pt);
  }
  
  private boolean computeLineEnds(RelateGeometry geom, boolean isA, RelateGeometry geomTarget,
      TopologyComputer topoComputer) {
    if (! geom.hasDimension(Dimension.L)) {
      return false;
    }
    
    boolean hasExteriorIntersection = false;
    Iterator geomi = new GeometryCollectionIterator(geom.getGeometry());
    while (geomi.hasNext()) {
      Geometry elem = (Geometry) geomi.next();
      if (elem.isEmpty()) 
        continue;
      
      if (elem instanceof LineString) {
        //-- once an intersection with target exterior is recorded, skip further known-exterior points
        if (hasExteriorIntersection 
            && elem.getEnvelopeInternal().disjoint(geomTarget.getEnvelope()))
          continue;
       
        LineString line = (LineString) elem;
        Coordinate e0 = line.getCoordinateN(0);
        hasExteriorIntersection |= computeLineEnd(geom, isA, e0, geomTarget, topoComputer);
        if (topoComputer.isResultKnown()) {
          return true;
        }

        if (! line.isClosed()) {
          Coordinate e1 = line.getCoordinateN(line.getNumPoints() - 1);
          hasExteriorIntersection |= computeLineEnd(geom, isA, e1, geomTarget, topoComputer);          
          if (topoComputer.isResultKnown()) {
            return true;
          }
        }
        //TODO: break when all possible locations have been found?
      }
    }
    return false;
  }

  /**
   * Compute the topology of a line endpoint.
   * Also reports if the line end is in the exterior of the target geometry,
   * to optimize testing multiple exterior endpoints.
   * 
   * @param geom
   * @param isA
   * @param pt
   * @param geomTarget
   * @param topoComputer
   * @return true if the line endpoint is in the exterior of the target
   */
  private boolean computeLineEnd(RelateGeometry geom, boolean isA, Coordinate pt,
      RelateGeometry geomTarget, TopologyComputer topoComputer) {
    int locDimLineEnd = geom.locateLineEndWithDim(pt);
    int dimLineEnd = DimensionLocation.dimension(locDimLineEnd, topoComputer.getDimension(isA));
    //-- skip line ends which are in a GC area
    if (dimLineEnd != Dimension.L)
      return false;
    int locLineEnd = DimensionLocation.location(locDimLineEnd);
    
    int locDimTarget = geomTarget.locateWithDim(pt);
    int locTarget = DimensionLocation.location(locDimTarget);
    int dimTarget = DimensionLocation.dimension(locDimTarget, topoComputer.getDimension(! isA));
    topoComputer.addLineEndOnGeometry(isA, locLineEnd, locTarget, dimTarget, pt);
    return locTarget == Location.EXTERIOR;
  }

  private boolean computeAreaVertex(RelateGeometry geom, boolean isA, RelateGeometry geomTarget, TopologyComputer topoComputer) {
    if (! geom.hasDimension(Dimension.A)) {
      return false;
    }
    //-- evaluate for line and area targets only, since points are handled in the reverse direction
    if (geomTarget.getDimension() < Dimension.L)
      return false;

    boolean hasExteriorIntersection = false;
    Iterator geomi = new GeometryCollectionIterator(geom.getGeometry());
    while (geomi.hasNext()) {
      Geometry elem = (Geometry) geomi.next();
      if (elem.isEmpty()) 
        continue;
      
      if (elem instanceof Polygon) {
        //-- once an intersection with target exterior is recorded, skip further known-exterior points
        if (hasExteriorIntersection 
            && elem.getEnvelopeInternal().disjoint(geomTarget.getEnvelope()))
          continue;
        
        Polygon poly = (Polygon) elem;
        hasExteriorIntersection |= computeAreaVertex(geom, isA, poly.getExteriorRing(), geomTarget, topoComputer);
        if (topoComputer.isResultKnown()) {
          return true;
        }
        for (int j = 0; j < poly.getNumInteriorRing(); j++) {
          hasExteriorIntersection |= computeAreaVertex(geom, isA, poly.getInteriorRingN(j), geomTarget, topoComputer);        
          if (topoComputer.isResultKnown()) {
            return true;
          }
        }
      }
    }
    return false;
  }

  private boolean computeAreaVertex(RelateGeometry geom, boolean isA, LinearRing ring, RelateGeometry geomTarget, TopologyComputer topoComputer) {
    //TODO: use extremal (highest) point to ensure one is on boundary of polygon cluster
    Coordinate pt = ring.getCoordinate();
    
    int locArea = geom.locateAreaVertex(pt);
    int locDimTarget = geomTarget.locateWithDim(pt);
    int locTarget = DimensionLocation.location(locDimTarget);
    int dimTarget = DimensionLocation.dimension(locDimTarget, topoComputer.getDimension(! isA));
    topoComputer.addAreaVertex(isA, locArea, locTarget, dimTarget, pt);
    return locTarget == Location.EXTERIOR;
  }

  private void computeAtEdges(RelateGeometry geomB, TopologyComputer topoComputer) {
    Envelope envInt = geomA.getEnvelope().intersection(geomB.getEnvelope());
    if (envInt.isNull())
      return;
    
    List<RelateSegmentString> edgesB = geomB.extractSegmentStrings(GEOM_B, envInt);
    EdgeSegmentIntersector intersector = new EdgeSegmentIntersector(topoComputer);
    
    if (topoComputer.isSelfNodingRequired()) {
      computeEdgesAll(edgesB, envInt, intersector);      
    }
    else {
      computeEdgesMutual(edgesB, envInt, intersector);
    }
    if (topoComputer.isResultKnown()) {
      return;
    }
    
    topoComputer.evaluateNodes();
  }
  
  private void computeEdgesAll(List<RelateSegmentString> edgesB, Envelope envInt, EdgeSegmentIntersector intersector) {
    //TODO: find a way to reuse prepared index?
    List<RelateSegmentString> edgesA = geomA.extractSegmentStrings(GEOM_A, envInt);
    
    EdgeSetIntersector edgeInt = new EdgeSetIntersector(edgesA, edgesB, envInt);
    edgeInt.process(intersector);
  }
  
  private void computeEdgesMutual(List<RelateSegmentString> edgesB, Envelope envInt, EdgeSegmentIntersector intersector) {
    //-- in prepared mode the A edge index is reused
    if (edgeMutualInt == null) {  
      Envelope envExtract = geomA.isPrepared() ? null : envInt;
      List<RelateSegmentString> edgesA = geomA.extractSegmentStrings(GEOM_A, envExtract);
      edgeMutualInt = new MCIndexSegmentSetMutualIntersector(edgesA, envExtract);
    }
    
    edgeMutualInt.process(edgesB, intersector);
  }


}
