/**
   * Perform a DFS starting on the given node and collect all nodes that are found in the
   * corresponding connected component.
   *
   * @param node a node.
   * @param graph a graph representing a connected component, or {@code null}.
   * @param visited boolean indicating for each node whether it was already visited ({@code true})
   *     or not.
   * @param incidence list of incident edges for each node.
   * @return the connected component, or {@code null} if the node was already visited.
   */
  private FGraph dfs(
      final FNode node,
      final FGraph graph,
      final boolean[] visited,
      final List<FEdge>[] incidence) {

    if (!visited[node.id]) {
      visited[node.id] = true;
      FGraph component = graph;
      if (component == null) {
        component = new FGraph();
      }
      component.getNodes().add(node);
      for (FEdge edge : incidence[node.id]) {
        if (edge.getSource() != node) {
          dfs(edge.getSource(), component, visited, incidence);
        }
        if (edge.getTarget() != node) {
          dfs(edge.getTarget(), component, visited, incidence);
        }
        component.getEdges().add(edge);
        component.getLabels().addAll(edge.getLabels());
      }
      return component;
    }
    return null;
  }
  /**
   * Split the given graph into its connected components.
   *
   * @param graph an input graph.
   * @return a list of components that can be processed one by one.
   */
  public List<FGraph> split(final FGraph graph) {
    Boolean separate = graph.getProperty(LayoutOptions.SEPARATE_CC);
    if (separate == null || separate.booleanValue()) {
      boolean[] visited = new boolean[graph.getNodes().size()];
      List<FEdge>[] incidence = buildIncidenceLists(graph);

      // perform DFS starting on each node, collecting connected components
      List<FGraph> components = new LinkedList<FGraph>();
      for (FNode node : graph.getNodes()) {
        FGraph comp = dfs(node, null, visited, incidence);
        if (comp != null) {
          comp.copyProperties(graph);
          components.add(comp);
        }
      }

      // redistribute identifier numbers to each component
      if (components.size() > 1) {
        for (FGraph comp : components) {
          int id = 0;
          for (FNode node : comp.getNodes()) {
            node.id = id++;
          }
        }
      }
      return components;
    }
    return Lists.newArrayList(graph);
  }
  /**
   * Move the source graph into the destination graph using a specified offset.
   *
   * @param destGraph the destination graph.
   * @param sourceGraph the source graph.
   * @param offsetx x coordinate offset.
   * @param offsety y coordinate offset.
   */
  private void moveGraph(
      final FGraph destGraph,
      final FGraph sourceGraph,
      final double offsetx,
      final double offsety) {

    KVector graphOffset = new KVector(offsetx, offsety);
    graphOffset.sub(sourceGraph.getProperty(Properties.BB_UPLEFT));

    for (FNode node : sourceGraph.getNodes()) {
      node.getPosition().add(graphOffset);
      destGraph.getNodes().add(node);
    }

    for (FEdge edge : sourceGraph.getEdges()) {
      for (FBendpoint bendpoint : edge.getBendpoints()) {
        bendpoint.getPosition().add(graphOffset);
      }
      destGraph.getEdges().add(edge);
    }

    for (FLabel label : sourceGraph.getLabels()) {
      label.getPosition().add(graphOffset);
      destGraph.getLabels().add(label);
    }
  }
  /**
   * Creates and returns the incidence list that for each node lists the incident edges.
   *
   * @param graph a force graph.
   */
  @SuppressWarnings("unchecked")
  private List<FEdge>[] buildIncidenceLists(final FGraph graph) {
    int n = graph.getNodes().size();
    List<FEdge>[] incidence = new List[n];

    // create incidence lists
    for (FNode node : graph.getNodes()) {
      incidence[node.id] = new LinkedList<FEdge>();
    }

    // add edges to incidence lists
    for (FEdge edge : graph.getEdges()) {
      incidence[edge.getSource().id].add(edge);
      incidence[edge.getTarget().id].add(edge);
    }

    return incidence;
  }
  /**
   * Pack the given components into a single graph.
   *
   * @param components a list of components.
   * @return a single graph that contains all components.
   */
  public FGraph recombine(final List<FGraph> components) {
    if (components.size() == 1) {
      return components.get(0);
    } else if (components.size() <= 0) {
      return new FGraph();
    }

    // assign priorities and sizes
    for (FGraph graph : components) {
      int priority = 0;
      double minx = Integer.MAX_VALUE,
          miny = Integer.MAX_VALUE,
          maxx = Integer.MIN_VALUE,
          maxy = Integer.MIN_VALUE;
      for (FNode node : graph.getNodes()) {
        Integer p = node.getProperty(LayoutOptions.PRIORITY);
        if (p != null) {
          priority += p;
        }
        minx = Math.min(minx, node.getPosition().x);
        miny = Math.min(miny, node.getPosition().y);
        maxx = Math.max(maxx, node.getPosition().x + node.getSize().x);
        maxy = Math.max(maxy, node.getPosition().y + node.getSize().y);
      }
      graph.setProperty(Properties.PRIORITY, priority);
      graph.setProperty(Properties.BB_UPLEFT, new KVector(minx, miny));
      graph.setProperty(Properties.BB_LOWRIGHT, new KVector(maxx, maxy));
    }

    // sort the components by their priority and size
    Collections.sort(
        components,
        new Comparator<FGraph>() {
          public int compare(final FGraph graph1, final FGraph graph2) {
            int prio =
                graph2.getProperty(Properties.PRIORITY) - graph1.getProperty(Properties.PRIORITY);
            if (prio == 0) {
              KVector size1 =
                  graph1
                      .getProperty(Properties.BB_LOWRIGHT)
                      .clone()
                      .sub(graph1.getProperty(Properties.BB_UPLEFT));
              KVector size2 =
                  graph2
                      .getProperty(Properties.BB_LOWRIGHT)
                      .clone()
                      .sub(graph2.getProperty(Properties.BB_UPLEFT));
              return Double.compare(size1.x * size1.y, size2.x * size2.y);
            }
            return prio;
          }
        });

    FGraph result = new FGraph();
    result.copyProperties(components.get(0));

    // determine the maximal row width by the maximal box width and the total area
    double maxRowWidth = 0.0f;
    double totalArea = 0.0f;
    for (FGraph graph : components) {
      KVector size =
          graph
              .getProperty(Properties.BB_LOWRIGHT)
              .clone()
              .sub(graph.getProperty(Properties.BB_UPLEFT));
      maxRowWidth = Math.max(maxRowWidth, size.x);
      totalArea += size.x * size.y;
    }
    maxRowWidth =
        Math.max(
            maxRowWidth,
            (float) Math.sqrt(totalArea) * result.getProperty(Properties.ASPECT_RATIO));
    double spacing = result.getProperty(Properties.SPACING).doubleValue();

    // place nodes iteratively into rows
    double xpos = 0, ypos = 0, highestBox = 0, broadestRow = spacing;
    for (FGraph graph : components) {
      KVector size =
          graph
              .getProperty(Properties.BB_LOWRIGHT)
              .clone()
              .sub(graph.getProperty(Properties.BB_UPLEFT));
      if (xpos + size.x > maxRowWidth) {
        // place the graph into the next row
        xpos = 0;
        ypos += highestBox + spacing;
        highestBox = 0;
      }
      moveGraph(result, graph, xpos, ypos);
      broadestRow = Math.max(broadestRow, xpos + size.x);
      highestBox = Math.max(highestBox, size.y);
      xpos += size.x + spacing;
    }

    return result;
  }