/**
   * Merge regions by testing whether they would overlap after applying the deflection.
   *
   * @param layeredGraph the layered graph
   * @return true if any two regions have been merged
   */
  private boolean mergeRegions(final LGraph layeredGraph) {

    boolean changed = false;
    double threshold = OVERLAP_DETECT * spacings.nodeSpacing * spacings.inLayerSpacingFactor;
    for (Layer layer : layeredGraph) {
      Iterator<LNode> nodeIter = layer.getNodes().iterator();

      // Get the first node
      LNode node1 = nodeIter.next();
      LinearSegment region1 = linearSegments[node1.id].region();

      // While there are still nodes following the current node
      while (nodeIter.hasNext()) {
        // Test whether nodes have different regions
        LNode node2 = nodeIter.next();
        LinearSegment region2 = linearSegments[node2.id].region();

        if (region1 != region2) {
          // Calculate how much space is allowed between the nodes
          double spacing = spacings.getVerticalSpacing(node1, node2);

          double node1Extent =
              node1.getPosition().y
                  + node1.getSize().y
                  + node1.getMargin().bottom
                  + region1.deflection
                  + spacing;
          double node2Extent = node2.getPosition().y - node2.getMargin().top + region2.deflection;

          // Test if the nodes are overlapping
          if (node1Extent > node2Extent + threshold) {
            // Merge the first region under the second top level segment
            int weightSum = region1.weight + region2.weight;
            assert weightSum > 0;
            region2.deflection =
                (region2.weight * region2.deflection + region1.weight * region1.deflection)
                    / weightSum;
            region2.weight = weightSum;
            region1.refSegment = region2;
            changed = true;
          }
        }

        node1 = node2;
        region1 = region2;
      }
    }
    return changed;
  }
  /**
   * Creates an unbalanced placement for the sorted linear segments.
   *
   * @param layeredGraph the layered graph to create an unbalanced placement for.
   */
  private void createUnbalancedPlacement(final LGraph layeredGraph) {
    // How many nodes are currently placed in each layer
    int[] nodeCount = new int[layeredGraph.getLayers().size()];

    // The type of the node most recently placed in a given layer
    NodeType[] recentNodeType = new NodeType[layeredGraph.getLayers().size()];

    // Iterate through the linear segments (in proper order!) and place them
    for (LinearSegment segment : linearSegments) {
      // Determine the uppermost placement for the linear segment
      double uppermostPlace = 0.0f;
      for (LNode node : segment.nodes) {
        NodeType nodeType = node.getType();
        int layerIndex = node.getLayer().getIndex();
        nodeCount[layerIndex]++;

        // Calculate how much space to leave between the linear segment and the last
        // node of the given layer
        float spacing = spacings.edgeEdgeSpacing * spacings.inLayerSpacingFactor;
        if (nodeCount[layerIndex] > 0) {
          if (recentNodeType[layerIndex] != null) {
            spacing = spacings.getVerticalSpacing(recentNodeType[layerIndex], nodeType);
          }
        }

        uppermostPlace = Math.max(uppermostPlace, node.getLayer().getSize().y + spacing);
      }

      // Apply the uppermost placement to all elements
      for (LNode node : segment.nodes) {
        // Set the node position
        node.getPosition().y = uppermostPlace + node.getMargin().top;

        // Adjust layer size
        Layer layer = node.getLayer();
        layer.getSize().y =
            uppermostPlace + node.getMargin().top + node.getSize().y + node.getMargin().bottom;

        recentNodeType[layer.getIndex()] = node.getType();
      }
    }
  }
  /**
   * Post-process the balanced placement by moving linear segments where obvious improvements can be
   * made.
   *
   * @param layeredGraph the layered graph
   */
  private void postProcess(final LGraph layeredGraph) {

    // process each linear segment independently
    for (LinearSegment segment : linearSegments) {
      double minRoomAbove = Integer.MAX_VALUE, minRoomBelow = Integer.MAX_VALUE;

      for (LNode node : segment.nodes) {
        double roomAbove, roomBelow;
        int index = node.getIndex();

        // determine the amount by which the linear segment can be moved up without overlap
        if (index > 0) {
          LNode neighbor = node.getLayer().getNodes().get(index - 1);
          float spacing = spacings.getVerticalSpacing(node, neighbor);
          roomAbove =
              node.getPosition().y
                  - node.getMargin().top
                  - (neighbor.getPosition().y
                      + neighbor.getSize().y
                      + neighbor.getMargin().bottom
                      + spacing);
        } else {
          roomAbove = node.getPosition().y - node.getMargin().top;
        }
        minRoomAbove = Math.min(roomAbove, minRoomAbove);

        // determine the amount by which the linear segment can be moved down without
        // overlap
        if (index < node.getLayer().getNodes().size() - 1) {
          LNode neighbor = node.getLayer().getNodes().get(index + 1);
          float spacing = spacings.getVerticalSpacing(node, neighbor);
          roomBelow =
              neighbor.getPosition().y
                  - neighbor.getMargin().top
                  - (node.getPosition().y + node.getSize().y + node.getMargin().bottom + spacing);
        } else {
          roomBelow = 2 * node.getPosition().y;
        }
        minRoomBelow = Math.min(roomBelow, minRoomBelow);
      }

      double minDisplacement = Integer.MAX_VALUE;
      boolean foundPlace = false;

      // determine the minimal displacement that would make one incoming edge straight
      LNode firstNode = segment.nodes.get(0);
      for (LPort target : firstNode.getPorts()) {
        double pos = firstNode.getPosition().y + target.getPosition().y + target.getAnchor().y;
        for (LEdge edge : target.getIncomingEdges()) {
          LPort source = edge.getSource();
          double d =
              source.getNode().getPosition().y
                  + source.getPosition().y
                  + source.getAnchor().y
                  - pos;
          if (Math.abs(d) < Math.abs(minDisplacement)
              && Math.abs(d) < (d < 0 ? minRoomAbove : minRoomBelow)) {
            minDisplacement = d;
            foundPlace = true;
          }
        }
      }

      // determine the minimal displacement that would make one outgoing edge straight
      LNode lastNode = segment.nodes.get(segment.nodes.size() - 1);
      for (LPort source : lastNode.getPorts()) {
        double pos = lastNode.getPosition().y + source.getPosition().y + source.getAnchor().y;
        for (LEdge edge : source.getOutgoingEdges()) {
          LPort target = edge.getTarget();
          double d =
              target.getNode().getPosition().y
                  + target.getPosition().y
                  + target.getAnchor().y
                  - pos;
          if (Math.abs(d) < Math.abs(minDisplacement)
              && Math.abs(d) < (d < 0 ? minRoomAbove : minRoomBelow)) {
            minDisplacement = d;
            foundPlace = true;
          }
        }
      }

      // if such a displacement could be found, apply it to the whole linear segment
      if (foundPlace && minDisplacement != 0) {
        for (LNode node : segment.nodes) {
          node.getPosition().y += minDisplacement;
        }
      }
    }
  }