/*
 * Copyright (c) 2016 Vivid Solutions.
 *
 * 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.geomgraph;

import java.io.PrintStream;

import org.locationtech.jts.geom.Location;
import org.locationtech.jts.geom.Position;
import org.locationtech.jts.geom.TopologyException;


/**
 * @version 1.7
 */
public class DirectedEdge
  extends EdgeEnd
{

  /**
   * Computes the factor for the change in depth when moving from one location to another.
   * E.g. if crossing from the {@link Location#INTERIOR} to the{@link Location#EXTERIOR}
   * the depth decreases, so the factor is -1.
   *
   * @param currLocation Current location
   * @param nextLocation Next location
   * @return change of depth moving from currLocation to nextLocation
   */
  public static int depthFactor(int currLocation, int nextLocation)
  {
    if (currLocation == Location.EXTERIOR && nextLocation == Location.INTERIOR)
       return 1;
    else if (currLocation == Location.INTERIOR && nextLocation == Location.EXTERIOR)
       return -1;
    return 0;
  }

  protected boolean isForward;
  private boolean isInResult = false;
  private boolean isVisited = false;

  private DirectedEdge sym; // the symmetric edge
  private DirectedEdge next;  // the next edge in the edge ring for the polygon containing this edge
  private DirectedEdge nextMin;  // the next edge in the MinimalEdgeRing that contains this edge
  private EdgeRing edgeRing;  // the EdgeRing that this edge is part of
  private EdgeRing minEdgeRing;  // the MinimalEdgeRing that this edge is part of
  /**
   * The depth of each side (position) of this edge.
   * The 0 element of the array is never used.
   */
  private int[] depth = { 0, -999, -999 };

  public DirectedEdge(Edge edge, boolean isForward)
  {
    super(edge);
    this.isForward = isForward;
    if (isForward) {
      init(edge.getCoordinate(0), edge.getCoordinate(1));
    }
    else {
      int n = edge.getNumPoints() - 1;
      init(edge.getCoordinate(n), edge.getCoordinate(n-1));
    }
    computeDirectedLabel();
  }
  public Edge getEdge() { return edge; }
  public void setInResult(boolean isInResult) { this.isInResult = isInResult; }
  public boolean isInResult() { return isInResult; }
  public boolean isVisited() { return isVisited; }
  public void setVisited(boolean isVisited) { this.isVisited = isVisited; }
  public void setEdgeRing(EdgeRing edgeRing) { this.edgeRing = edgeRing; }
  public EdgeRing getEdgeRing() { return edgeRing; }
  public void setMinEdgeRing(EdgeRing minEdgeRing) { this.minEdgeRing = minEdgeRing; }
  public EdgeRing getMinEdgeRing() { return minEdgeRing; }
  public int getDepth(int position) { return depth[position]; }

  /**
   * Set depth for a position.
   *
   * You may also use {@link #setEdgeDepths(int, int)} to
   * update depth and opposite depth together.
   *
   * @param position Position to update
   * @param depthVal Depth at the provided position
   */
  public void setDepth(int position, int depthVal)
  {
    if (depth[position] != -999) {
//      if (depth[position] != depthVal) {
//        Debug.print(this);
//      }
      if (depth[position] != depthVal)
        throw new TopologyException("assigned depths do not match", getCoordinate());
      //Assert.isTrue(depth[position] == depthVal, "assigned depths do not match at " + getCoordinate());
    }
    depth[position] = depthVal;
  }

  public int getDepthDelta()
  {
    int depthDelta = edge.getDepthDelta();
    if (! isForward) depthDelta = -depthDelta;
    return depthDelta;
  }

  /**
   * Marks both DirectedEdges attached to a given Edge.
   *
   * This is used for edges corresponding to lines, which will only
   * appear oriented in a single direction in the result.
   *
   * @param isVisited True to mark edge as visited
   */
  public void setVisitedEdge(boolean isVisited)
  {
    setVisited(isVisited);
    sym.setVisited(isVisited);
  }
  /**
   * Each Edge gives rise to a pair of symmetric DirectedEdges, in opposite
   * directions.
   * @return the DirectedEdge for the same Edge but in the opposite direction
   */
  public DirectedEdge getSym() { return sym; }
  public boolean isForward() { return isForward; }
  public void setSym(DirectedEdge de)
  {
    sym = de;
  }
  public DirectedEdge getNext() { return next; }
  public void setNext(DirectedEdge next) { this.next = next; }
  public DirectedEdge getNextMin() { return nextMin; }
  public void setNextMin(DirectedEdge nextMin) { this.nextMin = nextMin; }

  /**
   * This edge is a line edge if
   * <ul>
   * <li> at least one of the labels is a line label
   * <li> any labels which are not line labels have all Locations = EXTERIOR
   * </ul>
   *
   * @return If edge is a line edge
   */
  public boolean isLineEdge()
  {
    boolean isLine = label.isLine(0) || label.isLine(1);
    boolean isExteriorIfArea0 =
      ! label.isArea(0) || label.allPositionsEqual(0, Location.EXTERIOR);
    boolean isExteriorIfArea1 =
      ! label.isArea(1) || label.allPositionsEqual(1, Location.EXTERIOR);

    return isLine && isExteriorIfArea0 && isExteriorIfArea1;
  }
  /**
   * This is an interior Area edge if
   * <ul>
   * <li> its label is an Area label for both Geometries
   * <li> and for each Geometry both sides are in the interior.
   * </ul>
   *
   * @return true if this is an interior Area edge
   */
  public boolean isInteriorAreaEdge()
  {
    boolean isInteriorAreaEdge = true;
    for (int i = 0; i < 2; i++) {
      if (! ( label.isArea(i)
            && label.getLocation(i, Position.LEFT ) == Location.INTERIOR
            && label.getLocation(i, Position.RIGHT) == Location.INTERIOR) ) {
        isInteriorAreaEdge = false;
      }
    }
    return isInteriorAreaEdge;
  }

  /**
   * Compute the label in the appropriate orientation for this DirEdge
   */
  private void computeDirectedLabel()
  {
    label = new Label(edge.getLabel());
    if (! isForward)
      label.flip();
  }

  /**
   * Set both edge depths.  One depth for a given side is provided.  The other is
   * computed depending on the Location transition and the depthDelta of the edge.
   *
   * @param position Position to update
   * @param depth Depth at the provided position
   */
  public void setEdgeDepths(int position, int depth)
  {
    // get the depth transition delta from R to L for this directed Edge
    int depthDelta = getEdge().getDepthDelta();
    if (! isForward) depthDelta = -depthDelta;

    // if moving from L to R instead of R to L must change sign of delta
    int directionFactor = 1;
    if (position == Position.LEFT)
      directionFactor = -1;

    int oppositePos = Position.opposite(position);
    int delta = depthDelta * directionFactor;
    //TESTINGint delta = depthDelta * DirectedEdge.depthFactor(loc, oppositeLoc);
    int oppositeDepth = depth + delta;
    setDepth(position, depth);
    setDepth(oppositePos, oppositeDepth);
  }

  public void print(PrintStream out)
  {
    super.print(out);
    out.print(" " + depth[Position.LEFT] + "/" + depth[Position.RIGHT]);
    out.print(" (" + getDepthDelta() + ")");
    //out.print(" " + this.hashCode());
    //if (next != null) out.print(" next:" + next.hashCode());
    if (isInResult) out.print(" inResult");
  }
  public void printEdge(PrintStream out)
  {
    print(out);
    out.print(" ");
    if (isForward)
      edge.print(out);
    else
      edge.printReverse(out);
  }

}
