/*
 * Copyright (c) 2023 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 org.locationtech.jts.geom.Dimension;
import org.locationtech.jts.geom.Envelope;
import org.locationtech.jts.geom.Location;

/**
 * Creates predicate instances for evaluating OGC-standard named topological relationships.
 * Predicates can be evaluated for geometries using {@link RelateNG}.
 * 
 * @author Martin Davis
 *
 */
public interface RelatePredicate {

  /**
   * Creates a predicate to determine whether two geometries intersect.
   * <p>
   * The <code>intersects</code> predicate has the following equivalent definitions:
   * <ul>
   * <li>The two geometries have at least one point in common
   * <li>The DE-9IM Intersection Matrix for the two geometries matches
   * at least one of the patterns
   *  <ul>
   *   <li><code>[T********]</code>
   *   <li><code>[*T*******]</code>
   *   <li><code>[***T*****]</code>
   *   <li><code>[****T****]</code>
   *  </ul>
   * <li><code>disjoint() = false</code>
   * <br>(<code>intersects</code> is the inverse of <code>disjoint</code>)
   * </ul>
   *
   *@return the predicate instance
   *
   * @see #disjoint()
   */
  public static TopologyPredicate intersects() {
    return new BasicPredicate() {
      
      public String name() { return "intersects"; }
  
      @Override
      public boolean requireSelfNoding() {
        //-- self-noding is not required to check for a simple interaction
        return false;
      }
      
      @Override
      public boolean requireExteriorCheck(boolean isSourceA) {
        //-- intersects only requires testing interaction
        return false;
      }
      
      @Override
      public void init(Envelope envA, Envelope envB) {
        require(envA.intersects(envB));
      }
      
      @Override
      public void updateDimension(int locA, int locB, int dimension) {
        setValueIf(true, isIntersection(locA, locB));
      }
  
      @Override
      public void finish() {
        //-- if no intersecting locations were found
        setValue(false);
      }
  
    };
  }

  /**
   * Creates a predicate to determine whether two geometries are disjoint.
   * <p>
   * The <code>disjoint</code> predicate has the following equivalent definitions:
   * <ul>
   * <li>The two geometries have no point in common
   * <li>The DE-9IM Intersection Matrix for the two geometries matches
   * <code>[FF*FF****]</code>
   * <li><code>intersects() = false</code>
   * <br>(<code>disjoint</code> is the inverse of <code>intersects</code>)
   * </ul>
   *
   *@return the predicate instance
   *
   * @see #intersects()
   */
  public static TopologyPredicate disjoint() {
    return new BasicPredicate() {
       
      public String name() { return "disjoint"; }
      
      @Override
      public boolean requireSelfNoding() {
        //-- self-noding is not required to check for a simple interaction
        return false;
      }
      
      @Override
      public boolean requireInteraction() {
        //-- ensure entire matrix is computed
        return false;
      }
      
      @Override
      public boolean requireExteriorCheck(boolean isSourceA) {
        //-- disjoint only requires testing interaction
        return false;
      }

      @Override
      public void init(Envelope envA, Envelope envB) {
        setValueIf(true, envA.disjoint(envB));
      }

      @Override
      public void updateDimension(int locA, int locB, int dimension) {
        setValueIf(false, isIntersection(locA, locB));
      }
  
      @Override
      public void finish() {
        //-- if no intersecting locations were found
        setValue(true);
      }
  
    };
  }

  /**
   * Creates a predicate to determine whether a geometry contains another geometry.
   * <p>
   * The <code>contains</code> predicate has the following equivalent definitions:
   * <ul>
   * <li>Every point of the other geometry is a point of this geometry,
   * and the interiors of the two geometries have at least one point in common.
   * <li>The DE-9IM Intersection Matrix for the two geometries matches
   * the pattern
   * <code>[T*****FF*]</code>
   * <li><code>within(B, A) = true</code>
   * <br>(<code>contains</code> is the converse of {@link #within} )
   * </ul>
   * An implication of the definition is that "Geometries do not
   * contain their boundary".  In other words, if a geometry A is a subset of
   * the points in the boundary of a geometry B, <code>B.contains(A) = false</code>.
   * (As a concrete example, take A to be a LineString which lies in the boundary of a Polygon B.)
   * For a predicate with similar behavior but avoiding
   * this subtle limitation, see {@link #covers}.
   *
   *@return the predicate instance
   *
   * @see #within()
   */
  public static TopologyPredicate contains() {
    return new IMPredicate() {
  
    public String name() { return "contains"; }
      
    @Override
    public boolean requireCovers(boolean isSourceA) {
      return isSourceA == RelateGeometry.GEOM_A;
    }
    
    @Override
    public boolean requireExteriorCheck(boolean isSourceA) {
      //-- only need to check B against Exterior of A
      return isSourceA == RelateGeometry.GEOM_B;
    }
    
    @Override
    public void init(int dimA, int dimB) {
      super.init(dimA, dimB);
      require( isDimsCompatibleWithCovers(dimA, dimB) );
    }
    
    @Override
    public void init(Envelope envA, Envelope envB) {
      requireCovers(envA, envB);
    }
  
    @Override
    public boolean isDetermined() {
      return intersectsExteriorOf(RelateGeometry.GEOM_A);
    }
  
    @Override
    public boolean valueIM() {
      return intMatrix.isContains();
    }
  };
  }

  /**
   * Creates a predicate to determine whether a geometry is within another geometry.
   * <p>
   * The <code>within</code> predicate has the following equivalent definitions:
   * <ul>
   * <li>Every point of this geometry is a point of the other geometry,
   * and the interiors of the two geometries have at least one point in common.
   * <li>The DE-9IM Intersection Matrix for the two geometries matches
   * <code>[T*F**F***]</code>
   * <li><code>contains(B, A) = true</code>
   * <br>(<code>within</code> is the converse of {@link #contains})
   * </ul>
   * An implication of the definition is that
   * "The boundary of a Geometry is not within the Geometry".
   * In other words, if a geometry A is a subset of
   * the points in the boundary of a geometry B, <code>within(B, A) = false</code>
   * (As a concrete example, take A to be a LineString which lies in the boundary of a Polygon B.)
   * For a predicate with similar behavior but avoiding
   * this subtle limitation, see {@link #coveredBy}.
   *
   *@return the predicate instance
   *
   * @see #contains()
   */
  public static TopologyPredicate within() {
    return new IMPredicate() {
  
      public String name() { return "within"; }
      
      @Override
      public boolean requireCovers(boolean isSourceA) {
        return isSourceA == RelateGeometry.GEOM_B;
      }
      
      @Override
      public boolean requireExteriorCheck(boolean isSourceA) {
        //-- only need to check A against Exterior of B
        return isSourceA == RelateGeometry.GEOM_A;
      }
       
      @Override
      public void init(int dimA, int dimB) {
        super.init(dimA, dimB);
        require( isDimsCompatibleWithCovers(dimB, dimA) );
      }
      
      @Override
      public void init(Envelope envA, Envelope envB) {
        requireCovers(envB, envA);
      }
    
      @Override
      public boolean isDetermined() {
        return intersectsExteriorOf(RelateGeometry.GEOM_B);
      }
      
      public boolean valueIM() {
          return intMatrix.isWithin();
      }
    };
  }

  /**
   * Creates a predicate to determine whether a geometry covers another geometry.
   * <p>
   * The <code>covers</code> predicate has the following equivalent definitions:
   * <ul>
   * <li>Every point of the other geometry is a point of this geometry.
   * <li>The DE-9IM Intersection Matrix for the two geometries matches
   * at least one of the following patterns:
   *  <ul>
   *   <li><code>[T*****FF*]</code>
   *   <li><code>[*T****FF*]</code>
   *   <li><code>[***T**FF*]</code>
   *   <li><code>[****T*FF*]</code>
   *  </ul>
   * <li><code>coveredBy(b, a) = true</code>
   * <br>(<code>covers</code> is the converse of {@link #coveredBy})
   * </ul>
   * If either geometry is empty, the value of this predicate is <code>false</code>.
   * <p>
   * This predicate is similar to {@link #contains()},
   * but is more inclusive (i.e. returns <code>true</code> for more cases).
   * In particular, unlike <code>contains</code> it does not distinguish between
   * points in the boundary and in the interior of geometries.
   * For most cases, <code>covers</code> should be used in preference to <code>contains</code>.
   * As an added benefit, <code>covers</code> is more amenable to optimization,
   * and hence should be more performant.
   *
   *@return the predicate instance
   *
   * @see #coveredBy()
   */
  public static TopologyPredicate covers() {
    return new IMPredicate() {
  
      public String name() { return "covers"; }
      
      @Override
      public boolean requireCovers(boolean isSourceA) {
        return isSourceA == RelateGeometry.GEOM_A;
      }
      
      @Override
      public boolean requireExteriorCheck(boolean isSourceA) {
        //-- only need to check B against Exterior of A
        return isSourceA == RelateGeometry.GEOM_B;
      }
      
      @Override
      public void init(int dimA, int dimB) {
        super.init(dimA, dimB);
        require( isDimsCompatibleWithCovers(dimA, dimB) );
      }
      
      @Override
      public void init(Envelope envA, Envelope envB) {
        requireCovers(envA, envB);
      }
  
      @Override
      public boolean isDetermined() {
        return intersectsExteriorOf(RelateGeometry.GEOM_A);
      }
      
      @Override
      public boolean valueIM() {
        return intMatrix.isCovers();
      }
    };
  }
  
  /**
   * Creates a predicate to determine whether a geometry is covered by another geometry.
   * <p>
   * The <code>coveredBy</code> predicate has the following equivalent definitions:
   * <ul>
   * <li>Every point of this geometry is a point of the other geometry.
   * <li>The DE-9IM Intersection Matrix for the two geometries matches
   * at least one of the following patterns:
   *  <ul>
   *   <li><code>[T*F**F***]</code>
   *   <li><code>[*TF**F***]</code>
   *   <li><code>[**FT*F***]</code>
   *   <li><code>[**F*TF***]</code>
   *  </ul>
   * <li><code>covers(B, A) = true</code>
   * <br>(<code>coveredBy</code> is the converse of {@link #covers})
   * </ul>
   * If either geometry is empty, the value of this predicate is <code>false</code>.
   * <p>
   * This predicate is similar to {@link #within},
   * but is more inclusive (i.e. returns <code>true</code> for more cases).
   *
   *@return the predicate instance
   *
   * @see #covers()
   */
  public static TopologyPredicate coveredBy() {
    return new IMPredicate() {
      public String name() { return "coveredBy"; }
      
      @Override
      public boolean requireCovers(boolean isSourceA) {
        return isSourceA == RelateGeometry.GEOM_B;
      }
      
      @Override
      public boolean requireExteriorCheck(boolean isSourceA) {
        //-- only need to check A against Exterior of B
        return isSourceA == RelateGeometry.GEOM_A;
      }
      
      @Override
      public void init(int dimA, int dimB) {
        super.init(dimA, dimB);
        require( isDimsCompatibleWithCovers(dimB, dimA) );
      }
      
      @Override
      public void init(Envelope envA, Envelope envB) {
        requireCovers(envB, envA);
      }
  
      @Override
      public boolean isDetermined() {
        return intersectsExteriorOf(RelateGeometry.GEOM_B);
      }

      @Override
      public boolean valueIM() {
        return intMatrix.isCoveredBy();
      }
    };
  }

  /**
   * Creates a predicate to determine whether a geometry crosses another geometry.
   * <p>
   * The <code>crosses</code> predicate has the following equivalent definitions:
   * <ul>
   * <li>The geometries have some but not all interior points in common.
   * <li>The DE-9IM Intersection Matrix for the two geometries matches
   * one of the following patterns:
   *   <ul>
   *    <li><code>[T*T******]</code> (for P/L, P/A, and L/A cases)
   *    <li><code>[T*****T**]</code> (for L/P, A/P, and A/L cases)
   *    <li><code>[0********]</code> (for L/L cases)
   *   </ul>
   * </ul>
   * For the A/A and P/P cases this predicate returns <code>false</code>.
   * <p>
   * The SFS defined this predicate only for P/L, P/A, L/L, and L/A cases.
   * To make the relation symmetric
   * JTS extends the definition to apply to L/P, A/P and A/L cases as well.
   *
   * @return the predicate instance
   */
  public static TopologyPredicate crosses() {
    return new IMPredicate() {
      public String name() { return "crosses"; }
      
      @Override
      public void init(int dimA, int dimB) {
        super.init(dimA, dimB);
        boolean isBothPointsOrAreas = (dimA == Dimension.P && dimB == Dimension.P)
            ||  (dimA == Dimension.A && dimB == Dimension.A);
        require(! isBothPointsOrAreas);
      }
  
      @Override
      public boolean isDetermined() {
        if (dimA == Dimension.L && dimB == Dimension.L) {
          //-- L/L interaction can only be dim = P
          if (getDimension(Location.INTERIOR, Location.INTERIOR) > Dimension.P)
            return true;
        }
        else if (dimA < dimB) {
          if (isIntersects(Location.INTERIOR, Location.INTERIOR)
              && isIntersects(Location.INTERIOR, Location.EXTERIOR)) {
            return true;
          }
        }
        else if (dimA > dimB) {
          if (isIntersects(Location.INTERIOR, Location.INTERIOR)
              && isIntersects(Location.EXTERIOR, Location.INTERIOR)) {
            return true;
          }
        }
        return false;
      }
  
      @Override
      public boolean valueIM() {
        return intMatrix.isCrosses(dimA, dimB);
      }
    };
  }

  /**
   * Creates a predicate to determine whether two geometries are topologically equal.
   * <p>
   * The <code>equals</code> predicate has the following equivalent definitions:
   * <ul>
   * <li>The two geometries have at least one point in common,
   * and no point of either geometry lies in the exterior of the other geometry.
   * <li>The DE-9IM Intersection Matrix for the two geometries matches
   * the pattern <code>T*F**FFF*</code>
   * </ul>
   *
   * @return the predicate instance
   */
  public static TopologyPredicate equalsTopo() {
    return new IMPredicate() {
      public String name() { return "equals"; }
      
      @Override
      public void init(int dimA, int dimB) {
        super.init(dimA, dimB);
        require(dimA == dimB);
      }
      
      @Override
      public void init(Envelope envA, Envelope envB) {
        require(envA.equals(envB));
      }   
      
      @Override
      public boolean isDetermined() {
        boolean isEitherExteriorIntersects = 
            isIntersects(Location.INTERIOR, Location.EXTERIOR)
        || isIntersects(Location.BOUNDARY, Location.EXTERIOR)
        || isIntersects(Location.EXTERIOR, Location.INTERIOR)
        || isIntersects(Location.EXTERIOR, Location.BOUNDARY);

        return isEitherExteriorIntersects;
      }
  
      @Override
      public boolean valueIM() {
        return intMatrix.isEquals(dimA, dimB);
      }
    };
  }

  /**
   * Creates a predicate to determine whether a geometry overlaps another geometry.
   * <p>
   * The <code>overlaps</code> predicate has the following equivalent definitions:
   * <ul>
   * <li>The geometries have at least one point each not shared by the other
   *     (or equivalently neither covers the other),
   *     they have the same dimension,
   *     and the intersection of the interiors of the two geometries has
   *     the same dimension as the geometries themselves.
   * <li>The DE-9IM Intersection Matrix for the two geometries matches
   *     <code>[T*T***T**]</code> (for P/P and A/A cases)
   *     or <code>[1*T***T**]</code> (for L/L cases)
   * </ul>
   * If the geometries are of different dimension this predicate returns <code>false</code>.
   * This predicate is symmetric.
   *
   * @return the predicate instance
   */
  public static TopologyPredicate overlaps() {
    return new IMPredicate() {
      public String name() { return "overlaps"; }
      
      @Override
      public void init(int dimA, int dimB) {
        super.init(dimA, dimB);
        require(dimA == dimB);
      }
  
      @Override
      public boolean isDetermined() {
        if (dimA == Dimension.A || dimA == Dimension.P) {
          if (isIntersects(Location.INTERIOR, Location.INTERIOR)
              && isIntersects(Location.INTERIOR, Location.EXTERIOR)
              && isIntersects(Location.EXTERIOR, Location.INTERIOR))
            return true;          
        }
        if (dimA == Dimension.L) {
          if (isDimension(Location.INTERIOR, Location.INTERIOR, Dimension.L)
              && isIntersects(Location.INTERIOR, Location.EXTERIOR)
              && isIntersects(Location.EXTERIOR, Location.INTERIOR))
            return true;          
        }
        return false;
      }
  
      @Override
      public boolean valueIM() {
        return intMatrix.isOverlaps(dimA, dimB);
      }
    };
  }

  /**
   * Creates a predicate to determine whether a geometry touches another geometry.
   * <p>
   * The <code>touches</code> predicate has the following equivalent definitions:
   * <ul>
   * <li>The geometries have at least one point in common,
   * but their interiors do not intersect.
   * <li>The DE-9IM Intersection Matrix for the two geometries matches
   * at least one of the following patterns
   *  <ul>
   *   <li><code>[FT*******]</code>
   *   <li><code>[F**T*****]</code>
   *   <li><code>[F***T****]</code>
   *  </ul>
   * </ul>
   * If both geometries have dimension 0, the predicate returns <code>false</code>,
   * since points have only interiors.
   * This predicate is symmetric.
   *
   * @return the predicate instance
   */
  public static TopologyPredicate touches() {
    return new IMPredicate() {
      public String name() { return "touches"; }
      
      @Override
      public void init(int dimA, int dimB) {
        super.init(dimA, dimB);
        //-- Points have only interiors, so cannot touch
        boolean isBothPoints = dimA == 0 && dimB == 0;
        require(! isBothPoints);
      }
  
      @Override
      public boolean isDetermined() {
        //-- for touches interiors cannot intersect
        boolean isInteriorsIntersects = isIntersects(Location.INTERIOR, Location.INTERIOR);
        return isInteriorsIntersects;
      }
  
      @Override
      public boolean valueIM() {
        return intMatrix.isTouches(dimA, dimB);
      }
    };
  }

  /**
   * Creates a predicate that matches a DE-9IM matrix pattern.
   * 
   * @param imPattern the pattern to match
   * @return a predicate that matches the pattern
   * 
   * @see IntersectionMatrixPattern
   */
  public static TopologyPredicate matches(String imPattern) {
    return new IMPatternMatcher(imPattern);
  }
}
