class TreeUtil {

  @NotNull
  public static <T extends MutableTreeNode, U> List<T> updateChildrenNodes(@NotNull DefaultTreeModel treeModel,
                                                                           @NotNull T parentNode,
                                                                           @NotNull List<U> newEntries,
                                                                           @NotNull UpdateChildrenOps<T, U> ops) {
    if (newEntries.isEmpty()) {
      // Special case: new list is empty, so we remove all children
      // This is more efficient that doing incremental updates
      removeAllChildren(parentNode);
      treeModel.nodeStructureChanged(parentNode);
      return new ArrayList<>();
    }
    else if (parentNode.getChildCount() == 0) {
      // Special case: no existing children, so we map all entries and add them
      // This is more efficient that doing incremental updates
      List<T> nodes = newEntries.stream().map(ops::mapEntry).collect(Collectors.toList());
      removeAllChildren(parentNode);
      setAllowsChildren(parentNode); // Note: Must be done *before* inserting new nodes
      nodes.forEach(x -> parentNode.insert(x, parentNode.getChildCount()));
      treeModel.nodeStructureChanged(parentNode);
      return nodes;
    }
    else {
      // Common case: We go though both list one element at a time, ensuring that the
      // list of children up to the current index is updated to match the new entries
      List<T> addedNodes = new ArrayList<>();

      int childIndex = 0;
      int childCount = parentNode.getChildCount();
      int newEntryIndex = 0;
      int newEntryCount = newEntries.size();
      while (newEntryIndex < newEntries.size() || childIndex < parentNode.getChildCount()) {
        assert childIndex == newEntryIndex;

        // We reached the end of the existing children, just add a node from the new ones
        if (childIndex >= childCount) {
          T newEntryNode = ops.mapEntry(newEntries.get(newEntryIndex));
          addedNodes.add(newEntryNode);
          setAllowsChildren(parentNode); // Note: Must be done *before* inserting new nodes
          treeModel.insertNodeInto(newEntryNode, parentNode, childIndex);
          newEntryIndex++;
          childIndex++;
          childCount++;
          continue;
        }

        // We reached the end of the new children, delete existing children from parent
        if (newEntryIndex >= newEntryCount) {
          treeModel.removeNodeFromParent((MutableTreeNode)parentNode.getChildAt(childIndex));
          childCount--;
          continue;
        }

        // Both sides have an entry.
        T childNode = ops.getChildNode(parentNode, childIndex);
        if (childNode == null) {
          // Existing tree node is not of type "T", remove it.
          treeModel.removeNodeFromParent((MutableTreeNode)parentNode.getChildAt(childIndex));
          childCount--;
          continue;
        }

        int compareResult = ops.compareNodeWithEntry(childNode, newEntries.get(newEntryIndex));
        if (compareResult == 0) {
          ops.updateNode(childNode, newEntries.get(newEntryIndex));
          treeModel.nodeChanged(childNode);
          childIndex++;
          newEntryIndex++;
          continue;
        }

        if (compareResult < 0) {
          treeModel.removeNodeFromParent((MutableTreeNode)parentNode.getChildAt(childIndex));
          childCount--;
          continue;
        }

        // There is at least one node to insert in the existing list of children
        // For example:
        //   Existing children: [a, b, e, f]
        //   Entries          : [a, b, c, d, g, h]
        //                       0  1  2  3  4  5
        //   We need to insert 2 new tree nodes [c, d] starting at index 2 in the parent node
        //   So we compute "nextIndex" as equal to "4"  (first entry >= current child node),
        //   and we insert nodes mapped from entries at index 2 to 4 (excluded)
        assert compareResult > 0;
        int nextIndex = findIndexOfNextEntry(newEntries, newEntryIndex + 1, childNode, ops);
        assert nextIndex >= newEntryIndex + 1;
        assert nextIndex <= newEntries.size();
        for (; newEntryIndex < nextIndex; newEntryIndex++) {
          T newChildNode = ops.mapEntry(newEntries.get(newEntryIndex));
          addedNodes.add(newChildNode);
          treeModel.insertNodeInto(newChildNode, parentNode, childIndex);
          childIndex++;
          childCount++;
        }
      }
      assert childIndex == newEntryIndex;
      assert childCount == newEntryCount;
      return addedNodes;
    }
  }

}