/*
 * Copyright (c) 2022 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 java.util.HashMap;
import java.util.Map;

import org.locationtech.jts.algorithm.PolygonNodeTopology;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.Dimension;
import org.locationtech.jts.geom.Location;
import org.locationtech.jts.geom.Position;
import org.locationtech.jts.util.Assert;

class TopologyComputer {

  private static final String MSG_GEOMETRY_DIMENSION_UNEXPECTED = "Unexpected combination of geometry dimensions";

  private TopologyPredicate predicate;
  private RelateGeometry geomA;
  private RelateGeometry geomB;
  private Map<Coordinate, NodeSections> nodeMap = new HashMap<Coordinate, NodeSections>();

  public TopologyComputer(TopologyPredicate predicate, RelateGeometry geomA, RelateGeometry geomB) {
    this.predicate = predicate;
    this.geomA = geomA;
    this.geomB = geomB;
    
    initExteriorDims();
  }

  /**
   * Determine a priori partial EXTERIOR topology based on dimensions.
   */
  private void initExteriorDims() {
    int dimRealA = geomA.getDimensionReal();
    int dimRealB = geomB.getDimensionReal();
    
    /**
     * For P/L case, P exterior intersects L interior
     */
    if (dimRealA == Dimension.P && dimRealB == Dimension.L) {
      updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.L);
    }
    else if (dimRealA == Dimension.L && dimRealB == Dimension.P) {
      updateDim(Location.INTERIOR, Location.EXTERIOR, Dimension.L);
    }
    /**
     * For P/A case, the Area Int and Bdy intersect the Point exterior.
     */
    else if (dimRealA == Dimension.P && dimRealB == Dimension.A) {
      updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.A);
      updateDim(Location.EXTERIOR, Location.BOUNDARY, Dimension.L);
    }
    else if (dimRealA == Dimension.A && dimRealB == Dimension.P) {
      updateDim(Location.INTERIOR, Location.EXTERIOR, Dimension.A);
      updateDim(Location.BOUNDARY, Location.EXTERIOR, Dimension.L);
    }
    else if (dimRealA == Dimension.L && dimRealB == Dimension.A) {
      updateDim(Location.EXTERIOR, Location.INTERIOR, Dimension.A);
     }
    else if (dimRealA == Dimension.A && dimRealB == Dimension.L) {
      updateDim(Location.INTERIOR, Location.EXTERIOR, Dimension.A);
    }
    //-- cases where one geom is EMPTY
    else if (dimRealA == Dimension.FALSE || dimRealB == Dimension.FALSE) {
      if (dimRealA != Dimension.FALSE) {
        initExteriorEmpty(RelateGeometry.GEOM_A);
      }
      if (dimRealB != Dimension.FALSE) {
        initExteriorEmpty(RelateGeometry.GEOM_B);
      }
    }
  }

  private void initExteriorEmpty(boolean geomNonEmpty) {
    int dimNonEmpty = getDimension(geomNonEmpty);
    switch (dimNonEmpty) {
    case Dimension.P:
      updateDim(geomNonEmpty, Location.INTERIOR, Location.EXTERIOR, Dimension.P);
      break;
    case Dimension.L:
      if (getGeometry(geomNonEmpty).hasBoundary()) {
        updateDim(geomNonEmpty, Location.BOUNDARY, Location.EXTERIOR, Dimension.P);
      }
      updateDim(geomNonEmpty, Location.INTERIOR, Location.EXTERIOR, Dimension.L);
      break;
    case Dimension.A:
      updateDim(geomNonEmpty, Location.BOUNDARY, Location.EXTERIOR, Dimension.L);
      updateDim(geomNonEmpty, Location.INTERIOR, Location.EXTERIOR, Dimension.A);
      break;
    }
  }
  
  private RelateGeometry getGeometry(boolean isA) {
    return isA ? geomA : geomB;
  }

  public int getDimension(boolean isA) {
    return getGeometry(isA).getDimension();
  }
  
  public boolean isAreaArea() {
    return getDimension(RelateGeometry.GEOM_A) == Dimension.A 
        && getDimension(RelateGeometry.GEOM_B) == Dimension.A;
  }
  
  /**
   * Indicates whether the input geometries require self-noding 
   * for correct evaluation of specific spatial predicates. 
   * Self-noding is required for geometries which may 
   * have self-crossing linework.
   * This causes the coordinates of nodes created by 
   * crossing segments to be computed explicitly.
   * This ensures that node locations match in situations
   * where a self-crossing and mutual crossing occur at the same logical location.
   * The canonical example is a self-crossing line tested against a single segment 
   * identical to one of the crossed segments.
   * 
   * @return true if self-noding is required
   */
  public boolean isSelfNodingRequired() {
    if (predicate.requireSelfNoding()) {
      if (geomA.isSelfNodingRequired()
          || geomB.isSelfNodingRequired()) 
        return true;
    }
    return false;
  }
  
  public boolean isExteriorCheckRequired(boolean isA) {
    return predicate.requireExteriorCheck(isA);
  }
  
  private void updateDim(int locA, int locB, int dimension) {
    //System.out.println(Location.toLocationSymbol(locA) + "/" + Location.toLocationSymbol(locB) + ": " + dimension);
    predicate.updateDimension(locA, locB, dimension);
  }
  
  private void updateDim(boolean isAB, int loc1, int loc2, int dimension) {
    if (isAB) {
      updateDim(loc1, loc2, dimension);
    }
    else {
      // is ordered BA
      updateDim(loc2, loc1, dimension);
    }
  }
  
  public boolean isResultKnown() {
    return predicate.isKnown();
  }
  
  public boolean getResult() {
    return predicate.value();
  }
  
  /**
   * Finalize the evaluation.
   */
  public void finish() {
    predicate.finish();
   }

  private NodeSections getNodeSections(Coordinate nodePt) {
    NodeSections node = nodeMap.get(nodePt);
    if (node == null) {
      node = new NodeSections(nodePt);
      nodeMap.put(nodePt, node);
    }
    return node;
  }
  
  public void addIntersection(NodeSection a, NodeSection b) {
    if (! a.isSameGeometry(b)) {
      updateIntersectionAB(a, b);
    }
    //-- add edges to node to allow full topology evaluation later
    addNodeSections(a, b);
  }
  
  /**
   * Update topology for an intersection between A and B.
   * 
   * @param a the section for geometry A 
   * @param b the section for geometry B
   */
  private void updateIntersectionAB(NodeSection a, NodeSection b) {
    if (NodeSection.isAreaArea(a, b)) {
      updateAreaAreaCross(a, b);
    }
    updateNodeLocation(a, b);
  }

  /**
   * Updates topology for an AB Area-Area crossing node.
   * Sections cross at a node if (a) the intersection is proper 
   * (i.e. in the interior of two segments)
   * or (b) if non-proper then whether the linework crosses
   * is determined by the geometry of the segments on either side of the node.
   * In these situations the area geometry interiors intersect (in dimension 2).
   * 
   * @param a the section for geometry A 
   * @param b the section for geometry B
   */
  private void updateAreaAreaCross(NodeSection a, NodeSection b) {
    boolean isProper = NodeSection.isProper(a, b);
    if (isProper || PolygonNodeTopology.isCrossing(a.nodePt(), 
        a.getVertex(0), a.getVertex(1), 
        b.getVertex(0), b.getVertex(1))) {
      updateDim(Location.INTERIOR, Location.INTERIOR, Dimension.A);
    }
  }
  /**
   * Updates topology for a node at an AB edge intersection.
   * 
   * @param a the section for geometry A 
   * @param b the section for geometry B
   */
  private void updateNodeLocation(NodeSection a, NodeSection b) {
    Coordinate pt = a.nodePt();
    int locA = geomA.locateNode(pt, a.getPolygonal()); 
    int locB = geomB.locateNode(pt, b.getPolygonal()); 
    updateDim(locA, locB, Dimension.P);
  }

  private void addNodeSections(NodeSection ns0, NodeSection ns1) {
    NodeSections sections = getNodeSections(ns0.nodePt());
    sections.addNodeSection(ns0);
    sections.addNodeSection(ns1);
  }
  
  public void addPointOnPointInterior(Coordinate pt) {
    updateDim(Location.INTERIOR, Location.INTERIOR, Dimension.P); 
  }
  
  public void addPointOnPointExterior(boolean isGeomA, Coordinate pt) {
    updateDim(isGeomA, Location.INTERIOR, Location.EXTERIOR, Dimension.P); 
  }
  
  public void addPointOnGeometry(boolean isA, int locTarget, int dimTarget, Coordinate pt) {
    updateDim(isA, Location.INTERIOR, locTarget, Dimension.P); 
    switch (dimTarget) {
    case Dimension.P:
      return;
    case Dimension.L:
      /**
       * Because zero-length lines are handled, 
       * a point lying in the exterior of the line target 
       * may imply either P or L for the Exterior interaction
       */
      //TODO: determine if effective dimension of linear target is L?
      //updateDim(isGeomA, Location.EXTERIOR, locTarget, Dimension.P); 
      return;
    case Dimension.A:
      /**
       * If a point intersects an area target, then the area interior and boundary
       * must extend beyond the point and thus interact with its exterior.
       */
      updateDim(isA, Location.EXTERIOR, Location.INTERIOR, Dimension.A);      
      updateDim(isA, Location.EXTERIOR, Location.BOUNDARY, Dimension.L);      
      return;
    }
    throw new IllegalStateException("Unknown target dimension: " + dimTarget);
  }
  
  /**
   * Add topology for a line end.
   * The line end point must be "significant";
   * i.e. not contained in an area if the source is a mixed-dimension GC.
   * 
   * @param isLineA the input containing the line end
   * @param locLineEnd the location of the line end (Interior or Boundary)
   * @param locTarget the location on the target geometry
   * @param dimTarget the dimension of the interacting target geometry element,
   *    (if any), or the dimension of the target
   * @param pt the line end coordinate
   */
  public void addLineEndOnGeometry(boolean isLineA, int locLineEnd, int locTarget, int dimTarget, Coordinate pt) {
    //-- record topology at line end point
    updateDim(isLineA, locLineEnd, locTarget, Dimension.P);
    
    //-- Line and Area targets may have additional topology
    switch (dimTarget) {
    case Dimension.P:
      return;
    case Dimension.L:
      addLineEndOnLine(isLineA, locLineEnd, locTarget, pt);
      return;
    case Dimension.A:
      addLineEndOnArea(isLineA, locLineEnd, locTarget, pt);      
      return;
    }
    throw new IllegalStateException("Unknown target dimension: " + dimTarget);
  }

  private void addLineEndOnLine(boolean isLineA, int locLineEnd, int locLine, Coordinate pt) {
    /**
     * When a line end is in the EXTERIOR of a Line, 
     * some length of the source Line INTERIOR
     * is also in the target Line EXTERIOR. 
     * This works for zero-length lines as well. 
     */
    if (locLine == Location.EXTERIOR) {
      updateDim(isLineA, Location.INTERIOR, Location.EXTERIOR, Dimension.L);      
    }
  }  
  
  private void addLineEndOnArea(boolean isLineA, int locLineEnd, int locArea, Coordinate pt) {
    if (locArea != Location.BOUNDARY) {
      /**
       * When a line end is in an Area INTERIOR or EXTERIOR 
       * some length of the source Line Interior  
       * AND the Exterior of the line
       * is also in that location of the target.
       * NOTE: this assumes the line end is NOT also in an Area of a mixed-dim GC
       */
      //TODO: handle zero-length lines?
      updateDim(isLineA, Location.INTERIOR, locArea, Dimension.L);
      updateDim(isLineA, Location.EXTERIOR, locArea, Dimension.A);     
    }
  }

  /**
   * Adds topology for an area vertex interaction with a target geometry element.
   * Assumes the target geometry element has highest dimension
   * (i.e. if the point lies on two elements of different dimension, 
   * the location on the higher dimension element is provided.
   * This is the semantic provided by {@link RelatePointLocator}.
   * <p>
   * Note that in a GeometryCollection containing overlapping or adjacent polygons,
   * the area vertex location may be INTERIOR instead of BOUNDARY.
   * 
   * @param isAreaA the input that is the area
   * @param locArea the location on the area
   * @param locTarget the location on the target geometry element
   * @param dimTarget the dimension of the target geometry element
   * @param pt the point of interaction
   */
  public void addAreaVertex(boolean isAreaA, int locArea, int locTarget, int dimTarget, Coordinate pt) {
    if (locTarget == Location.EXTERIOR) {
      updateDim(isAreaA, Location.INTERIOR, Location.EXTERIOR, Dimension.A);
      /**
       * If area vertex is on Boundary further topology can be deduced
       * from the neighbourhood around the boundary vertex.
       * This is always the case for polygonal geometries.
       * For GCs, the vertex may be either on boundary or in interior
       * (i.e. of overlapping or adjacent polygons) 
       */
      if (locArea == Location.BOUNDARY) {
        updateDim(isAreaA, Location.BOUNDARY, Location.EXTERIOR, Dimension.L);
        updateDim(isAreaA, Location.EXTERIOR, Location.EXTERIOR, Dimension.A);
      }
      return;
    }
    switch (dimTarget) {
    case Dimension.P:
      addAreaVertexOnPoint(isAreaA, locArea, pt);
      return;
    case Dimension.L:
      addAreaVertexOnLine(isAreaA, locArea, locTarget, pt);
      return;
    case Dimension.A:
      addAreaVertexOnArea(isAreaA, locArea, locTarget, pt);
      return;
    }
    throw new IllegalStateException("Unknown target dimension: " + dimTarget);
  }
  
  /**
   * Updates topology for an area vertex (in Interior or on Boundary)
   * intersecting a point.
   * Note that because the largest dimension of intersecting target is determined,
   * the intersecting point is not part of any other target geometry, 
   * and hence its neighbourhood is in the Exterior of the target.
   * 
   * @param isAreaA whether the area is the A input
   * @param locArea the location of the vertex in the area
   * @param pt the point at which topology is being updated
   */
  private void addAreaVertexOnPoint(boolean isAreaA, int locArea, Coordinate pt) {
    //-- Assert: locArea != EXTERIOR
    //-- Assert: locTarget == INTERIOR
    /**
     * The vertex location intersects the Point.
     */
    updateDim(isAreaA, locArea, Location.INTERIOR, Dimension.P);
    /**
     * The area interior intersects the point's exterior neighbourhood.
     */
    updateDim(isAreaA, Location.INTERIOR, Location.EXTERIOR, Dimension.A);
    /**
     * If the area vertex is on the boundary, 
     * the area boundary and exterior intersect the point's exterior neighbourhood
     */
    if (locArea == Location.BOUNDARY) {
      updateDim(isAreaA, Location.BOUNDARY, Location.EXTERIOR, Dimension.L);
      updateDim(isAreaA, Location.EXTERIOR, Location.EXTERIOR, Dimension.A);
    }
  }

  private void addAreaVertexOnLine(boolean isAreaA, int locArea, int locTarget, Coordinate pt) {
    //-- Assert: locArea != EXTERIOR
    /**
     * If an area vertex intersects a line, all we know is the 
     * intersection at that point.  
     * e.g. the line may or may not be collinear with the area boundary,
     * and the line may or may not intersect the area interior.
     * Full topology is determined later by node analysis
     */
    updateDim(isAreaA, locArea, locTarget, Dimension.P);
    if (locArea == Location.INTERIOR) {
      /**
       * The area interior intersects the line's exterior neighbourhood.
       */
      updateDim(isAreaA, Location.INTERIOR, Location.EXTERIOR, Dimension.A);      
    }
  }

  public void addAreaVertexOnArea(boolean isAreaA, int locArea, int locTarget, Coordinate pt) {
    if (locTarget == Location.BOUNDARY) {
      if (locArea == Location.BOUNDARY) {
        //-- B/B topology is fully computed later by node analysis
        updateDim(isAreaA, Location.BOUNDARY, Location.BOUNDARY, Dimension.P);
      }
      else {
        // locArea == INTERIOR
        updateDim(isAreaA, Location.INTERIOR, Location.INTERIOR, Dimension.A);
        updateDim(isAreaA, Location.INTERIOR, Location.BOUNDARY, Dimension.L);
        updateDim(isAreaA, Location.INTERIOR, Location.EXTERIOR, Dimension.A);
      }
    }
    else {  
      //-- locTarget is INTERIOR or EXTERIOR` 
      updateDim(isAreaA, Location.INTERIOR, locTarget, Dimension.A);
      /**
       * If area vertex is on Boundary further topology can be deduced
       * from the neighbourhood around the boundary vertex.
       * This is always the case for polygonal geometries.
       * For GCs, the vertex may be either on boundary or in interior
       * (i.e. of overlapping or adjacent polygons) 
       */
      if (locArea == Location.BOUNDARY) {
        updateDim(isAreaA, Location.BOUNDARY, locTarget, Dimension.L);
        updateDim(isAreaA, Location.EXTERIOR, locTarget, Dimension.A);
      }
    }
  }
  
  public void evaluateNodes() {
    for (NodeSections nodeSections : nodeMap.values()) {
      if (nodeSections.hasInteractionAB()) {
        evaluateNode(nodeSections);
        if (isResultKnown())
          return;
      }
    }
  }

  private void evaluateNode(NodeSections nodeSections) {
    Coordinate p = nodeSections.getCoordinate();
    RelateNode node = nodeSections.createNode();
    //-- Node must have edges for geom, but may also be in interior of a overlapping GC
    boolean isAreaInteriorA = geomA.isNodeInArea(p, nodeSections.getPolygonal(RelateGeometry.GEOM_A));
    boolean isAreaInteriorB = geomB.isNodeInArea(p, nodeSections.getPolygonal(RelateGeometry.GEOM_B));
    node.finish(isAreaInteriorA, isAreaInteriorB);
    evaluateNodeEdges(node);
  }

  private void evaluateNodeEdges(RelateNode node) {
    //TODO: collect distinct dim settings by using temporary matrix?
    for (RelateEdge e : node.getEdges()) {
      //-- An optimization to avoid updates for cases with a linear geometry
      if (isAreaArea()) {
        updateDim(e.location(RelateGeometry.GEOM_A, Position.LEFT), 
                  e.location(RelateGeometry.GEOM_B, Position.LEFT), Dimension.A);
        updateDim(e.location(RelateGeometry.GEOM_A, Position.RIGHT), 
                  e.location(RelateGeometry.GEOM_B, Position.RIGHT), Dimension.A);
      }
      updateDim(e.location(RelateGeometry.GEOM_A, Position.ON), 
                e.location(RelateGeometry.GEOM_B, Position.ON), Dimension.L);
    }
  }

}
