/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.configuration2.tree;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;

/**
 * <p>
 * An internal helper class for a atomic updates of an {@link InMemoryNodeModel}.
 * </p>
 * <p>
 * This class performs updates on the node structure of a node model consisting of {@link ImmutableNode} objects.
 * Because the nodes themselves cannot be changed updates are achieved by replacing parts of the structure with new
 * nodes; the new nodes are copies of original nodes with the corresponding manipulations applied. Therefore, each
 * update of a node in the structure results in a new structure in which the affected node is replaced by a new one, and
 * this change bubbles up to the root node (because all parent nodes have to be replaced by instances with an updated
 * child reference).
 * </p>
 * <p>
 * A single update of a model may consist of multiple changes on nodes. For instance, a remove property operation can
 * include many nodes. There are some reasons why such updates should be handled in a single "transaction" rather than
 * executing them on altered node structures one by one:
 * <ul>
 * <li>An operation is typically executed on a set of source nodes from the original node hierarchy. While manipulating
 * nodes, nodes of this set may be replaced by new ones. The handling of these replacements complicates things a
 * lot.</li>
 * <li>Performing all updates one after the other may cause more updates of nodes than necessary. Nodes near to the root
 * node always have to be replaced when a child of them gets manipulated. If all these updates are deferred and handled
 * in a single transaction, the resulting operation is more efficient.</li>
 * </ul>
 * </p>
 */
final class ModelTransaction {

    /**
     * A specialized operation class for adding an attribute to a target node.
     */
    private static final class AddAttributeOperation extends Operation {
        /** The attribute name. */
        private final String attributeName;

        /** The attribute value. */
        private final Object attributeValue;

        /**
         * Creates a new instance of {@code AddAttributeOperation}.
         *
         * @param name the name of the attribute
         * @param value the value of the attribute
         */
        public AddAttributeOperation(final String name, final Object value) {
            attributeName = name;
            attributeValue = value;
        }

        @Override
        protected ImmutableNode apply(final ImmutableNode target, final Operations operations) {
            return target.setAttribute(attributeName, attributeValue);
        }
    }

    /**
     * A specialized operation class for adding multiple attributes to a target node.
     */
    private static final class AddAttributesOperation extends Operation {
        /** The map with attributes. */
        private final Map<String, Object> attributes;

        /**
         * Creates a new instance of {@code AddAttributesOperation}.
         *
         * @param attrs the map with attributes
         */
        public AddAttributesOperation(final Map<String, Object> attrs) {
            attributes = attrs;
        }

        @Override
        protected ImmutableNode apply(final ImmutableNode target, final Operations operations) {
            return target.setAttributes(attributes);
        }
    }

    /**
     * A specialized operation class which changes the name of a node.
     */
    private static final class ChangeNodeNameOperation extends Operation {
        /** The new node name. */
        private final String newName;

        /**
         * Creates a new instance of {@code ChangeNodeNameOperation} and sets the new node name.
         *
         * @param name the new node name
         */
        public ChangeNodeNameOperation(final String name) {
            newName = name;
        }

        @Override
        protected ImmutableNode apply(final ImmutableNode target, final Operations operations) {
            return target.setName(newName);
        }
    }

    /**
     * A specialized operation class which changes the value of a node.
     */
    private static final class ChangeNodeValueOperation extends Operation {
        /** The new value for the affected node. */
        private final Object newValue;

        /**
         * Creates a new instance of {@code ChangeNodeValueOperation} and initializes it with the new value to set for the node.
         *
         * @param value the new node value
         */
        public ChangeNodeValueOperation(final Object value) {
            newValue = value;
        }

        @Override
        protected ImmutableNode apply(final ImmutableNode target, final Operations operations) {
            return target.setValue(newValue);
        }
    }

    /**
     * A specialized {@code Operation} implementation for replacing the children of a target node. All other properties are
     * not touched. With this operation single children of a node can be altered or removed; new children can be added. This
     * operation is frequently used because each update of a node causes updates of the children of all parent nodes.
     * Therefore, it is treated in a special way and allows adding further sub operations dynamically.
     */
    private final class ChildrenUpdateOperation extends Operation {
        /** A collection with new nodes to be added. */
        private Collection<ImmutableNode> newNodes;

        /** A collection with nodes to be removed. */
        private Set<ImmutableNode> nodesToRemove;

        /**
         * A map with nodes to be replaced by others. The keys are the nodes to be replaced, the values the replacements.
         */
        private Map<ImmutableNode, ImmutableNode> nodesToReplace;

        /**
         * Adds a node to be added to the target of the operation.
         *
         * @param node the new node to be added
         */
        public void addNewNode(final ImmutableNode node) {
            newNodes = append(newNodes, node);
        }

        /**
         * Adds a collection of nodes to be added to the target of the operation.
         *
         * @param nodes the collection with new nodes
         */
        public void addNewNodes(final Collection<? extends ImmutableNode> nodes) {
            newNodes = concatenate(newNodes, nodes);
        }

        /**
         * Adds a node for a remove operation. This child node is going to be removed from its parent.
         *
         * @param node the child node to be removed
         */
        public void addNodeToRemove(final ImmutableNode node) {
            nodesToRemove = append(nodesToRemove, node);
        }

        /**
         * Adds a node for a replacement operation. The original node is going to be replaced by its replacement.
         *
         * @param org the original node
         * @param replacement the replacement node
         */
        public void addNodeToReplace(final ImmutableNode org, final ImmutableNode replacement) {
            nodesToReplace = append(nodesToReplace, org, replacement);
        }

        /**
         * {@inheritDoc} This implementation applies changes on the children of the passed in target node according to its
         * configuration: new nodes are added, replacements are performed, and nodes no longer needed are removed.
         */
        @Override
        protected ImmutableNode apply(final ImmutableNode target, final Operations operations) {
            final Map<ImmutableNode, ImmutableNode> replacements = fetchReplacementMap();
            final Set<ImmutableNode> removals = fetchRemovalSet();
            final List<ImmutableNode> resultNodes = new LinkedList<>();

            for (final ImmutableNode nd : target) {
                final ImmutableNode repl = replacements.get(nd);
                if (repl != null) {
                    resultNodes.add(repl);
                    replacedNodes.put(nd, repl);
                } else if (removals.contains(nd)) {
                    removedNodes.add(nd);
                } else {
                    resultNodes.add(nd);
                }
            }

            concatenate(resultNodes, newNodes);
            operations.newNodesAdded(newNodes);
            return target.replaceChildren(resultNodes);
        }

        /**
         * Adds all operations defined by the specified object to this instance.
         *
         * @param op the operation to be combined
         */
        public void combine(final ChildrenUpdateOperation op) {
            newNodes = concatenate(newNodes, op.newNodes);
            nodesToReplace = concatenate(nodesToReplace, op.nodesToReplace);
            nodesToRemove = concatenate(nodesToRemove, op.nodesToRemove);
        }

        /**
         * Returns a set with nodes to be removed. If no remove operations are pending, an empty set is returned.
         *
         * @return the set with nodes to be removed
         */
        private Set<ImmutableNode> fetchRemovalSet() {
            return nodesToRemove != null ? nodesToRemove : Collections.<ImmutableNode>emptySet();
        }

        /**
         * Obtains the map with replacement nodes. If no replacements are defined, an empty map is returned.
         *
         * @return the map with replacement nodes
         */
        private Map<ImmutableNode, ImmutableNode> fetchReplacementMap() {
            return nodesToReplace != null ? nodesToReplace : Collections.<ImmutableNode, ImmutableNode>emptyMap();
        }
    }

    /**
     * An abstract base class representing an operation to be performed on a node. Concrete subclasses implement specific
     * update operations.
     */
    private abstract static class Operation {
        /**
         * Executes this operation on the provided target node returning the result.
         *
         * @param target the target node for this operation
         * @param operations the current {@code Operations} instance
         * @return the manipulated node
         */
        protected abstract ImmutableNode apply(ImmutableNode target, Operations operations);
    }

    /**
     * A helper class which collects multiple update operations to be executed on a single node.
     */
    private final class Operations {
        /** An operation for manipulating child nodes. */
        private ChildrenUpdateOperation childrenOperation;

        /**
         * A collection for the other operations to be performed on the target node.
         */
        private Collection<Operation> operations;

        /** A collection with nodes added by an operation. */
        private Collection<ImmutableNode> addedNodesInOperation;

        /**
         * Adds an operation which manipulates children.
         *
         * @param co the operation
         */
        public void addChildrenOperation(final ChildrenUpdateOperation co) {
            if (childrenOperation == null) {
                childrenOperation = co;
            } else {
                childrenOperation.combine(co);
            }
        }

        /**
         * Adds an operation.
         *
         * @param op the operation
         */
        public void addOperation(final Operation op) {
            operations = append(operations, op);
        }

        /**
         * Executes all operations stored in this object on the given target node. The resulting node then has to be integrated
         * in the current node hierarchy. Unless the root node is already reached, this causes another updated operation to be
         * created which replaces the manipulated child in the parent node.
         *
         * @param target the target node for this operation
         * @param level the level of the target node
         */
        public void apply(final ImmutableNode target, final int level) {
            ImmutableNode node = target;
            if (childrenOperation != null) {
                node = childrenOperation.apply(node, this);
            }

            if (operations != null) {
                for (final Operation op : operations) {
                    node = op.apply(node, this);
                }
            }

            handleAddedNodes(node);
            if (level == 0) {
                // reached the root node
                newRoot = node;
                replacedNodes.put(target, node);
            } else {
                // propagate change
                propagateChange(target, node, level);
            }
        }

        /**
         * Checks whether new nodes have been added during operation execution. If so, the parent mapping has to be updated.
         *
         * @param node the resulting node after applying all operations
         */
        private void handleAddedNodes(final ImmutableNode node) {
            if (addedNodesInOperation != null) {
                addedNodesInOperation.forEach(child -> {
                    parentMapping.put(child, node);
                    addedNodes.add(child);
                });
            }
        }

        /**
         * Notifies this object that new nodes have been added by a sub operation. It has to be ensured that these nodes are
         * added to the parent mapping.
         *
         * @param newNodes the collection of newly added nodes
         */
        public void newNodesAdded(final Collection<ImmutableNode> newNodes) {
            addedNodesInOperation = concatenate(addedNodesInOperation, newNodes);
        }

        /**
         * Propagates the changes on the target node to the next level above of the hierarchy. If the updated node is no longer
         * defined, it can even be removed from its parent. Otherwise, it is just replaced.
         *
         * @param target the target node for this operation
         * @param node the resulting node after applying all operations
         * @param level the level of the target node
         */
        private void propagateChange(final ImmutableNode target, final ImmutableNode node, final int level) {
            final ImmutableNode parent = getParent(target);
            final ChildrenUpdateOperation co = new ChildrenUpdateOperation();
            if (InMemoryNodeModel.checkIfNodeDefined(node)) {
                co.addNodeToReplace(target, node);
            } else {
                co.addNodeToRemove(target);
            }
            fetchOperations(parent, level - 1).addChildrenOperation(co);
        }
    }

    /**
     * A specialized operation class for removing an attribute from a target node.
     */
    private static final class RemoveAttributeOperation extends Operation {
        /** The attribute name. */
        private final String attributeName;

        /**
         * Creates a new instance of {@code RemoveAttributeOperation}.
         *
         * @param name the name of the attribute
         */
        public RemoveAttributeOperation(final String name) {
            attributeName = name;
        }

        @Override
        protected ImmutableNode apply(final ImmutableNode target, final Operations operations) {
            return target.removeAttribute(attributeName);
        }
    }

    /**
     * Constant for the maximum number of entries in the replacement mapping. If this number is exceeded, the parent mapping
     * is reconstructed. The number is a bit arbitrary. If it is too low, updates - especially on large node structures -
     * are expensive because the parent mapping is often rebuild. If it is too big, read access to the model is slowed down
     * because looking up the parent of a node is more complicated.
     */
    private static final int MAX_REPLACEMENTS = 200;

    /** Constant for an unknown level. */
    private static final int LEVEL_UNKNOWN = -1;

    /**
     * Appends a single element to a collection. The collection may be null, then it is created.
     *
     * @param col the collection
     * @param node the element to be added
     * @param <E> the type of elements involved
     * @return the resulting collection
     */
    private static <E> Collection<E> append(final Collection<E> col, final E node) {
        final Collection<E> result = col != null ? col : new LinkedList<>();
        result.add(node);
        return result;
    }

    /**
     * Adds a single key-value pair to a map. The map may be null, then it is created.
     *
     * @param map the map
     * @param key the key
     * @param value the value
     * @param <K> the type of the key
     * @param <V> the type of the value
     * @return the resulting map
     */
    private static <K, V> Map<K, V> append(final Map<K, V> map, final K key, final V value) {
        final Map<K, V> result = map != null ? map : new HashMap<>();
        result.put(key, value);
        return result;
    }

    /**
     * Appends a single element to a set. The set may be null then it is created.
     *
     * @param col the set
     * @param elem the element to be added
     * @param <E> the type of the elements involved
     * @return the resulting set
     */
    private static <E> Set<E> append(final Set<E> col, final E elem) {
        final Set<E> result = col != null ? col : new HashSet<>();
        result.add(elem);
        return result;
    }

    /**
     * Constructs the concatenation of two collections. Both can be null.
     *
     * @param col1 the first collection
     * @param col2 the second collection
     * @param <E> the type of the elements involved
     * @return the resulting collection
     */
    private static <E> Collection<E> concatenate(final Collection<E> col1, final Collection<? extends E> col2) {
        if (col2 == null) {
            return col1;
        }

        final Collection<E> result = col1 != null ? col1 : new ArrayList<>(col2.size());
        result.addAll(col2);
        return result;
    }

    /**
     * Constructs the concatenation of two maps. Both can be null.
     *
     * @param map1 the first map
     * @param map2 the second map
     * @param <K> the type of the keys
     * @param <V> the type of the values
     * @return the resulting map
     */
    private static <K, V> Map<K, V> concatenate(final Map<K, V> map1, final Map<? extends K, ? extends V> map2) {
        if (map2 == null) {
            return map1;
        }

        final Map<K, V> result = map1 != null ? map1 : new HashMap<>();
        result.putAll(map2);
        return result;
    }

    /**
     * Constructs the concatenation of two sets. Both can be null.
     *
     * @param set1 the first set
     * @param set2 the second set
     * @param <E> the type of the elements involved
     * @return the resulting set
     */
    private static <E> Set<E> concatenate(final Set<E> set1, final Set<? extends E> set2) {
        if (set2 == null) {
            return set1;
        }

        final Set<E> result = set1 != null ? set1 : new HashSet<>();
        result.addAll(set2);
        return result;
    }

    /** Stores the current tree data of the calling node model. */
    private final TreeData currentData;

    /** The root node for query operations. */
    private final ImmutableNode queryRoot;

    /** The selector to the root node of this transaction. */
    private final NodeSelector rootNodeSelector;

    /** The {@code NodeKeyResolver} to be used for this transaction. */
    private final NodeKeyResolver<ImmutableNode> resolver;

    /** A new replacement mapping. */
    private final Map<ImmutableNode, ImmutableNode> replacementMapping;

    /** The nodes replaced in this transaction. */
    private final Map<ImmutableNode, ImmutableNode> replacedNodes;

    /** A new parent mapping. */
    private final Map<ImmutableNode, ImmutableNode> parentMapping;

    /** A collection with nodes which have been added. */
    private final Collection<ImmutableNode> addedNodes;

    /** A collection with nodes which have been removed. */
    private final Collection<ImmutableNode> removedNodes;

    /**
     * Stores all nodes which have been removed in this transaction (not only the root nodes of removed trees).
     */
    private final Collection<ImmutableNode> allRemovedNodes;

    /**
     * Stores the operations to be executed during this transaction. The map is sorted by the levels of the nodes to be
     * manipulated: Operations on nodes down in the hierarchy are executed first because they affect the nodes closer to the
     * root.
     */
    private final SortedMap<Integer, Map<ImmutableNode, Operations>> operations;

    /** A map with reference objects to be added during this transaction. */
    private Map<ImmutableNode, Object> newReferences;

    /** The new root node. */
    private ImmutableNode newRoot;

    /**
     * Creates a new instance of {@code ModelTransaction} for the current tree data.
     *
     * @param treeData the current {@code TreeData} structure to operate on
     * @param selector an optional {@code NodeSelector} defining the target root node for this transaction; this can be used
     *        to perform operations on tracked nodes
     * @param resolver the {@code NodeKeyResolver}
     */
    public ModelTransaction(final TreeData treeData, final NodeSelector selector, final NodeKeyResolver<ImmutableNode> resolver) {
        currentData = treeData;
        this.resolver = resolver;
        replacementMapping = getCurrentData().copyReplacementMapping();
        replacedNodes = new HashMap<>();
        parentMapping = getCurrentData().copyParentMapping();
        operations = new TreeMap<>();
        addedNodes = new LinkedList<>();
        removedNodes = new LinkedList<>();
        allRemovedNodes = new LinkedList<>();
        queryRoot = initQueryRoot(treeData, selector);
        rootNodeSelector = selector;
    }

    /**
     * Adds an operation for adding a new child to a given parent node.
     *
     * @param parent the parent node
     * @param newChild the new child to be added
     */
    public void addAddNodeOperation(final ImmutableNode parent, final ImmutableNode newChild) {
        final ChildrenUpdateOperation op = new ChildrenUpdateOperation();
        op.addNewNode(newChild);
        fetchOperations(parent, LEVEL_UNKNOWN).addChildrenOperation(op);
    }

    /**
     * Adds an operation for adding a number of new children to a given parent node.
     *
     * @param parent the parent node
     * @param newNodes the collection of new child nodes
     */
    public void addAddNodesOperation(final ImmutableNode parent, final Collection<? extends ImmutableNode> newNodes) {
        final ChildrenUpdateOperation op = new ChildrenUpdateOperation();
        op.addNewNodes(newNodes);
        fetchOperations(parent, LEVEL_UNKNOWN).addChildrenOperation(op);
    }

    /**
     * Adds an operation for adding an attribute to a target node.
     *
     * @param target the target node
     * @param name the name of the attribute
     * @param value the value of the attribute
     */
    public void addAttributeOperation(final ImmutableNode target, final String name, final Object value) {
        fetchOperations(target, LEVEL_UNKNOWN).addOperation(new AddAttributeOperation(name, value));
    }

    /**
     * Adds an operation for adding multiple attributes to a target node.
     *
     * @param target the target node
     * @param attributes the map with attributes to be set
     */
    public void addAttributesOperation(final ImmutableNode target, final Map<String, Object> attributes) {
        fetchOperations(target, LEVEL_UNKNOWN).addOperation(new AddAttributesOperation(attributes));
    }

    /**
     * Adds an operation for changing the name of a target node.
     *
     * @param target the target node
     * @param newName the new name for this node
     */
    public void addChangeNodeNameOperation(final ImmutableNode target, final String newName) {
        fetchOperations(target, LEVEL_UNKNOWN).addOperation(new ChangeNodeNameOperation(newName));
    }

    /**
     * Adds an operation for changing the value of a target node.
     *
     * @param target the target node
     * @param newValue the new value for this node
     */
    public void addChangeNodeValueOperation(final ImmutableNode target, final Object newValue) {
        fetchOperations(target, LEVEL_UNKNOWN).addOperation(new ChangeNodeValueOperation(newValue));
    }

    /**
     * Adds an operation for clearing the value of a target node.
     *
     * @param target the target node
     */
    public void addClearNodeValueOperation(final ImmutableNode target) {
        addChangeNodeValueOperation(target, null);
    }

    /**
     * Adds a new reference object for the given node.
     *
     * @param node the affected node
     * @param ref the reference object for this node
     */
    public void addNewReference(final ImmutableNode node, final Object ref) {
        fetchReferenceMap().put(node, ref);
    }

    /**
     * Adds a map with new reference objects. The entries in this map are passed to the {@code ReferenceTracker} during
     * execution of this transaction.
     *
     * @param refs the map with new reference objects
     */
    public void addNewReferences(final Map<ImmutableNode, ?> refs) {
        fetchReferenceMap().putAll(refs);
    }

    /**
     * Adds an operation for removing an attribute from a target node.
     *
     * @param target the target node
     * @param name the name of the attribute
     */
    public void addRemoveAttributeOperation(final ImmutableNode target, final String name) {
        fetchOperations(target, LEVEL_UNKNOWN).addOperation(new RemoveAttributeOperation(name));
    }

    /**
     * Adds an operation for removing a child node of a given node.
     *
     * @param parent the parent node
     * @param node the child node to be removed
     */
    public void addRemoveNodeOperation(final ImmutableNode parent, final ImmutableNode node) {
        final ChildrenUpdateOperation op = new ChildrenUpdateOperation();
        op.addNodeToRemove(node);
        fetchOperations(parent, LEVEL_UNKNOWN).addChildrenOperation(op);
    }

    /**
     * Executes this transaction resulting in a new {@code TreeData} object. The object returned by this method serves as
     * the definition of a new node structure for the calling model.
     *
     * @return the updated {@code TreeData}
     */
    public TreeData execute() {
        executeOperations();
        updateParentMapping();
        return new TreeData(newRoot, parentMapping, replacementMapping,
            currentData.getNodeTracker().update(newRoot, rootNodeSelector, getResolver(), getCurrentData()), updateReferenceTracker());
    }

    /**
     * Executes all operations in this transaction.
     */
    private void executeOperations() {
        while (!operations.isEmpty()) {
            final Integer level = operations.lastKey(); // start down in hierarchy
            operations.remove(level).forEach((k, v) -> v.apply(k, level));
        }
    }

    /**
     * Obtains the {@code Operations} object for manipulating the specified node. If no such object exists yet, it is
     * created. The level can be undefined, then it is determined based on the target node.
     *
     * @param target the target node
     * @param level the level of the target node (may be undefined)
     * @return the {@code Operations} object for this node
     */
    Operations fetchOperations(final ImmutableNode target, final int level) {
        final Integer nodeLevel = Integer.valueOf(level == LEVEL_UNKNOWN ? level(target) : level);
        final Map<ImmutableNode, Operations> levelOperations = operations.computeIfAbsent(nodeLevel, k -> new HashMap<>());
        return levelOperations.computeIfAbsent(target, k -> new Operations());
    }

    /**
     * Returns the map with new reference objects. It is created if necessary.
     *
     * @return the map with reference objects
     */
    private Map<ImmutableNode, Object> fetchReferenceMap() {
        if (newReferences == null) {
            newReferences = new HashMap<>();
        }
        return newReferences;
    }

    /**
     * Gets the current {@code TreeData} object this transaction operates on.
     *
     * @return the associated {@code TreeData} object
     */
    public TreeData getCurrentData() {
        return currentData;
    }

    /**
     * Gets the parent node of the given node.
     *
     * @param node the node in question
     * @return the parent of this node
     */
    ImmutableNode getParent(final ImmutableNode node) {
        return getCurrentData().getParent(node);
    }

    /**
     * Gets the root node to be used within queries. This is not necessarily the current root node of the model. If the
     * operation is executed on a tracked node, this node has to be passed as root nodes to the expression engine.
     *
     * @return the root node for queries and calls to the expression engine
     */
    public ImmutableNode getQueryRoot() {
        return queryRoot;
    }

    /**
     * Gets the {@code NodeKeyResolver} used by this transaction.
     *
     * @return the {@code NodeKeyResolver}
     */
    public NodeKeyResolver<ImmutableNode> getResolver() {
        return resolver;
    }

    /**
     * Initializes the root node to be used within queries. If a tracked node selector is provided, this node becomes the
     * root node. Otherwise, the actual root node is used.
     *
     * @param treeData the current data of the model
     * @param selector an optional {@code NodeSelector} defining the target root
     * @return the query root node for this transaction
     */
    private ImmutableNode initQueryRoot(final TreeData treeData, final NodeSelector selector) {
        return selector == null ? treeData.getRootNode() : treeData.getNodeTracker().getTrackedNode(selector);
    }

    /**
     * Determines the level of the specified node in the current hierarchy. The level of the root node is 0, the children of
     * the root have level 1 and so on.
     *
     * @param node the node in question
     * @return the level of this node
     */
    private int level(final ImmutableNode node) {
        ImmutableNode current = getCurrentData().getParent(node);
        int level = 0;
        while (current != null) {
            level++;
            current = getCurrentData().getParent(current);
        }
        return level;
    }

    /**
     * Rebuilds the parent mapping from scratch. This method is called if the replacement mapping exceeds its maximum size.
     * In this case, it is cleared, and a new parent mapping is constructed for the new root node.
     */
    private void rebuildParentMapping() {
        replacementMapping.clear();
        parentMapping.clear();
        InMemoryNodeModel.updateParentMapping(parentMapping, newRoot);
    }

    /**
     * Removes the specified node completely from the replacement mapping. This also includes the nodes that replace the
     * given one.
     *
     * @param node the node to be removed
     */
    private void removeNodeFromReplacementMapping(final ImmutableNode node) {
        ImmutableNode replacement = node;
        do {
            replacement = replacementMapping.remove(replacement);
        } while (replacement != null);
    }

    /**
     * Removes a node and its children (recursively) from the parent and the replacement mappings.
     *
     * @param root the root of the subtree to be removed
     */
    private void removeNodesFromParentAndReplacementMapping(final ImmutableNode root) {
        NodeTreeWalker.INSTANCE.walkBFS(root, new ConfigurationNodeVisitorAdapter<ImmutableNode>() {
            @Override
            public void visitBeforeChildren(final ImmutableNode node, final NodeHandler<ImmutableNode> handler) {
                allRemovedNodes.add(node);
                parentMapping.remove(node);
                removeNodeFromReplacementMapping(node);
            }
        }, getCurrentData());
    }

    /**
     * Updates the parent mapping for the resulting {@code TreeData} instance. This method is called after all update
     * operations have been executed. It ensures that the parent mapping is updated for the changes on the nodes structure.
     */
    private void updateParentMapping() {
        replacementMapping.putAll(replacedNodes);
        if (replacementMapping.size() > MAX_REPLACEMENTS) {
            rebuildParentMapping();
        } else {
            updateParentMappingForAddedNodes();
            updateParentMappingForRemovedNodes();
        }
    }

    /**
     * Adds newly added nodes and their children to the parent mapping.
     */
    private void updateParentMappingForAddedNodes() {
        addedNodes.forEach(node -> InMemoryNodeModel.updateParentMapping(parentMapping, node));
    }

    /**
     * Removes nodes that have been removed during this transaction from the parent and replacement mappings.
     */
    private void updateParentMappingForRemovedNodes() {
        removedNodes.forEach(this::removeNodesFromParentAndReplacementMapping);
    }

    /**
     * Returns an updated {@code ReferenceTracker} instance. The changes performed during this transaction are applied to
     * the tracker.
     *
     * @return the updated tracker instance
     */
    private ReferenceTracker updateReferenceTracker() {
        ReferenceTracker tracker = currentData.getReferenceTracker();
        if (newReferences != null) {
            tracker = tracker.addReferences(newReferences);
        }
        return tracker.updateReferences(replacedNodes, allRemovedNodes);
    }
}
