import y.base.DataProvider;
import y.base.Edge;
import y.base.EdgeCursor;
import y.base.Node;
import y.geom.YPoint;
import y.layout.AbstractLayoutStage;
import y.layout.EdgeLayout;
import y.layout.IntersectionCalculator;
import y.layout.LayoutGraph;
import y.layout.NodeLayout;
import y.layout.PortConstraint;
import y.layout.PortConstraintKeys;

import java.util.ArrayList;
import java.util.List;

/**
 * This class implements a Layoutstage that can be used to adjust the final port
 * assignments after a layout has been calculated.
 * <p>
 * This stage tries to place ports onto the border of their associated node's
 * visual representation if the ports reside within the bounds of said
 * representation (i.e.
 * <code>realizer.contains(port.absoluteX, port.absoluteY)</code> returns
 * <code>true</code>) and their associated edges are not completely contained
 * in said bounds.
 * Additionally, for each port that is reassigned, this stage removes bends
 * from the associated edge if these reside in the above mentioned bounds.
 * </p>
 * <p>
 * This stage is designed to work similar to {@link y.layout.PortCalculator}
 * with respect to the way intersection points are calculated and strong port
 * constraints are handled.
 * Due to the fact that {@link y.layout.NodeLayout} does not provide any means
 * to handle a true inclusion test for non-rectangular node geometries,
 * this stage uses a {@link y.base.DataProvider} instance bound to the
 * graph using the key defined in the {@link demo.view.layout.ContainsTest}
 * interface to be able to access appropriate implementations of said test
 * for each node.
 * </p>
 *
 * @see demo.view.layout.ContainsTest
 * @see demo.view.layout.ContainsTest#CONTAINS_TEST_DPKEY
 * @see y.layout.PortCalculator
 * @see IntersectionCalculator
 * @see IntersectionCalculator#SOURCE_INTERSECTION_CALCULATOR_DPKEY
 * @see IntersectionCalculator#TARGET_INTERSECTION_CALCULATOR_DPKEY
 * @see PortConstraint
 * @author Thomas Behr
 */
public class PortCalculator2 extends AbstractLayoutStage {
  private static final double EPS = 0.0001;


  public boolean canLayout( final LayoutGraph graph ) {
    return getCoreLayouter() == null || getCoreLayouter().canLayout(graph);
  }

  public void doLayout( final LayoutGraph graph ) {
    if (getCoreLayouter() != null) {
      getCoreLayouter().doLayout(graph);
    }
    adjustPorts(graph);
  }

  private void adjustPorts( final LayoutGraph graph ) {
    final DataProvider _ic = graph.getDataProvider(
            ContainsTest.CONTAINS_TEST_DPKEY);

    if (_ic == null) {
      return;
    }

    final DataProvider sic = graph.getDataProvider(
            IntersectionCalculator.SOURCE_INTERSECTION_CALCULATOR_DPKEY);
    final DataProvider tic = graph.getDataProvider(
            IntersectionCalculator.TARGET_INTERSECTION_CALCULATOR_DPKEY);

    final DataProvider spc = graph.getDataProvider(
            PortConstraintKeys.SOURCE_PORT_CONSTRAINT_KEY);
    final DataProvider tpc = graph.getDataProvider(
            PortConstraintKeys.TARGET_PORT_CONSTRAINT_KEY);

    for (EdgeCursor ec = graph.edges(); ec.ok(); ec.next()) {
      final Edge edge = ec.edge();
      final EdgeLayout el = graph.getEdgeLayout(edge);

      final Node src = edge.source();
      final NodeLayout srcNl = graph.getNodeLayout(src);

      final Node tgt = edge.target();
      final NodeLayout tgtNl = graph.getNodeLayout(tgt);

      // determine new source port location
      if (sic != null && !isStrongPortConstraint(spc, edge)) {

        // find last point of edge path within node bounds
        // find first point of edge path outside of node bounds
        YPoint lastIn = graph.getSourcePointAbs(edge);
        YPoint firstOut = null;

        int lastInIdx = -1;
        final ContainsTest incCalc = (ContainsTest) _ic.get(src);
        if (incCalc.contains(srcNl, lastIn.x, lastIn.y)) {
          for (int i = 0; i < el.pointCount(); ++i) {
            final YPoint p = el.getPoint(i);
            if (incCalc.contains(srcNl, p.x, p.y)) {
              lastIn = p;
              lastInIdx = i;
            } else {
              firstOut = p;
              break;
            }
          }
          // final point to check is target port
          if (firstOut == null) {
            firstOut = graph.getTargetPointAbs(edge);
            if (incCalc.contains(srcNl, firstOut.x, firstOut.y)) {
              // selfloop or overlapping nodes
              firstOut = null;
            }
          }
        }

        if (firstOut != null) {
          double dx = lastIn.x - firstOut.x;
          double dy = lastIn.y - firstOut.y;
          double len = Math.sqrt(dx*dx + dy*dy);
          if (len > EPS) {
            dx /= len;
            dy /= len;

            // determine new source port
            final IntersectionCalculator intCalc =
                    (IntersectionCalculator) sic.get(edge);
            final YPoint cip = intCalc.calculateIntersectionPoint(
                    srcNl,
                    firstOut.x - graph.getCenterX(src),
                    firstOut.y - graph.getCenterY(src),
                    dx, dy);
            if (cip != null) {
              el.setSourcePoint(new YPoint(cip.x, cip.y));

              // delete all points of edge path within node bounds
              if (lastInIdx > -1) {
                final int pc = el.pointCount();
                final List points = new ArrayList(pc - lastInIdx - 1);
                for (int i = lastInIdx + 1; i < pc; ++i) {
                  points.add(el.getPoint(i));
                }
                el.clearPoints();
                for (int i = 0, n = points.size(); i < n; ++i) {
                  final YPoint p = (YPoint) points.get(i);
                  el.addPoint(p.x, p.y);
                }
              }
            }
          }
        }
      }

      // determine new target port location
      if (tic != null && !isStrongPortConstraint(tpc, edge)) {
        // find last point of edge path within node bounds
        // find first point of edge path outside of node bounds
        YPoint lastIn = graph.getTargetPointAbs(edge);
        YPoint firstOut = null;

        int lastInIdx = -1;
        final ContainsTest incCalc = (ContainsTest) _ic.get(tgt);
        if (incCalc.contains(tgtNl, lastIn.x, lastIn.y)) {
          for (int i = el.pointCount(); i --> 0;) {
            final YPoint p = el.getPoint(i);
            if (incCalc.contains(tgtNl, p.x, p.y)) {
              lastIn = p;
              lastInIdx = i;
            } else {
              firstOut = p;
              break;
            }
          }
          // final point to check is source port
          if (firstOut == null) {
            firstOut = graph.getSourcePointAbs(edge);
            if (incCalc.contains(tgtNl, firstOut.x, firstOut.y)) {
              // selfloop or overlapping nodes
              firstOut = null;
            }
          }
        }

        if (firstOut != null) {
          double dx = lastIn.x - firstOut.x;
          double dy = lastIn.y - firstOut.y;
          double len = Math.sqrt(dx*dx + dy*dy);
          if (len > EPS) {
            dx /= len;
            dy /= len;

            // determine new target port
            final IntersectionCalculator intCalc =
                    (IntersectionCalculator) tic.get(edge);
            final YPoint cip = intCalc.calculateIntersectionPoint(
                    tgtNl,
                    firstOut.x - graph.getCenterX(tgt),
                    firstOut.y - graph.getCenterY(tgt),
                    dx, dy);
            if (cip != null) {
              el.setTargetPoint(new YPoint(cip.x, cip.y));

              // delete all points of edge path within node bounds
              if (lastInIdx > -1) {
                final List points = new ArrayList(lastInIdx);
                for (int i = 0; i < lastInIdx; ++i) {
                  points.add(el.getPoint(i));
                }
                el.clearPoints();
                for (int i = 0, n = points.size(); i < n; ++i) {
                  final YPoint p = (YPoint) points.get(i);
                  el.addPoint(p.x, p.y);
                }
              }
            }
          }
        }
      }
    }
  }


  private static boolean isStrongPortConstraint(DataProvider dp, Edge e) {
    return dp != null &&
           dp.get(e) instanceof PortConstraint &&
           ((PortConstraint) dp.get(e)).isStrong();
  }
}
