var __assign = (this && this.__assign) || function () {
    __assign = Object.assign || function(t) {
        for (var s, i = 1, n = arguments.length; i < n; i++) {
            s = arguments[i];
            for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
                t[p] = s[p];
        }
        return t;
    };
    return __assign.apply(this, arguments);
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
    if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
        if (ar || !(i in from)) {
            if (!ar) ar = Array.prototype.slice.call(from, 0, i);
            ar[i] = from[i];
        }
    }
    return to.concat(ar || Array.prototype.slice.call(from));
};
/**
 * Performs a depth-first traversal over all of the node descendants,
 * incrementing currentIndex by 1 for each
 */
function getNodeDataAtTreeIndexOrNextIndex(_a) {
    var targetIndex = _a.targetIndex, node = _a.node, currentIndex = _a.currentIndex, getNodeKey = _a.getNodeKey, _b = _a.path, path = _b === void 0 ? [] : _b, _c = _a.lowerSiblingCounts, lowerSiblingCounts = _c === void 0 ? [] : _c, _d = _a.ignoreCollapsed, ignoreCollapsed = _d === void 0 ? true : _d, _e = _a.isPseudoRoot, isPseudoRoot = _e === void 0 ? false : _e;
    // The pseudo-root is not considered in the path
    var selfPath = !isPseudoRoot ? __spreadArray(__spreadArray([], path, true), [getNodeKey({ node: node, treeIndex: currentIndex })], false) : [];
    // Return target node when found
    if (currentIndex === targetIndex) {
        return {
            node: node,
            lowerSiblingCounts: lowerSiblingCounts,
            path: selfPath
        };
    }
    // Add one and continue for nodes with no children or hidden children
    if (!node.children || (ignoreCollapsed && node.expanded !== true)) {
        return { nextIndex: currentIndex + 1 };
    }
    // Iterate over each child and their descendants and return the
    // target node if childIndex reaches the targetIndex
    var childIndex = currentIndex + 1;
    var childCount = node.children.length;
    for (var i = 0; i < childCount; i += 1) {
        var result = getNodeDataAtTreeIndexOrNextIndex({
            ignoreCollapsed: ignoreCollapsed,
            getNodeKey: getNodeKey,
            targetIndex: targetIndex,
            node: node.children[i],
            currentIndex: childIndex,
            lowerSiblingCounts: __spreadArray(__spreadArray([], lowerSiblingCounts, true), [childCount - i - 1], false),
            path: selfPath
        });
        if (result.node) {
            return result;
        }
        childIndex = result.nextIndex;
    }
    // If the target node is not found, return the farthest traversed index
    return { nextIndex: childIndex };
}
export function getDescendantCount(_a) {
    var node = _a.node, _b = _a.ignoreCollapsed, ignoreCollapsed = _b === void 0 ? true : _b;
    return (getNodeDataAtTreeIndexOrNextIndex({
        getNodeKey: function () { },
        ignoreCollapsed: ignoreCollapsed,
        node: node,
        currentIndex: 0,
        targetIndex: -1
    }).nextIndex - 1);
}
/**
 * Walk all descendants of the given node, depth-first
 *
 * @param {Object} args - Function parameters
 * @param {function} args.callback - Function to call on each node
 * @param {function} args.getNodeKey - Function to get the key from the nodeData and tree index
 * @param {boolean} args.ignoreCollapsed - Ignore children of nodes without `expanded` set to `true`
 * @param {boolean=} args.isPseudoRoot - If true, this node has no real data, and only serves
 *                                        as the parent of all the nodes in the tree
 * @param {Object} args.node - A tree node
 * @param {Object=} args.parentNode - The parent node of `node`
 * @param {number} args.currentIndex - The treeIndex of `node`
 * @param {number[]|string[]} args.path - Array of keys leading up to node to be changed
 * @param {number[]} args.lowerSiblingCounts - An array containing the count of siblings beneath the
 *                                             previous nodes in this path
 *
 * @return {number|false} nextIndex - Index of the next sibling of `node`,
 *                                    or false if the walk should be terminated
 */
function walkDescendants(_a) {
    var callback = _a.callback, getNodeKey = _a.getNodeKey, ignoreCollapsed = _a.ignoreCollapsed, _b = _a.isPseudoRoot, isPseudoRoot = _b === void 0 ? false : _b, node = _a.node, _c = _a.parentNode, parentNode = _c === void 0 ? null : _c, currentIndex = _a.currentIndex, _d = _a.path, path = _d === void 0 ? [] : _d, _e = _a.lowerSiblingCounts, lowerSiblingCounts = _e === void 0 ? [] : _e;
    // The pseudo-root is not considered in the path
    var selfPath = isPseudoRoot ? [] : __spreadArray(__spreadArray([], path, true), [getNodeKey({ node: node, treeIndex: currentIndex })], false);
    var selfInfo = isPseudoRoot
        ? null
        : {
            node: node,
            parentNode: parentNode,
            path: selfPath,
            lowerSiblingCounts: lowerSiblingCounts,
            treeIndex: currentIndex
        };
    if (!isPseudoRoot) {
        var callbackResult = callback(selfInfo);
        // Cut walk short if the callback returned false
        if (callbackResult === false) {
            return false;
        }
    }
    // Return self on nodes with no children or hidden children
    if (!node.children || (node.expanded !== true && ignoreCollapsed && !isPseudoRoot)) {
        return currentIndex;
    }
    // Get all descendants
    var childIndex = currentIndex;
    var childCount = node.children.length;
    if (typeof node.children !== 'function') {
        for (var i = 0; i < childCount; i += 1) {
            childIndex = walkDescendants({
                callback: callback,
                getNodeKey: getNodeKey,
                ignoreCollapsed: ignoreCollapsed,
                node: node.children[i],
                parentNode: isPseudoRoot ? null : node,
                currentIndex: childIndex + 1,
                lowerSiblingCounts: __spreadArray(__spreadArray([], lowerSiblingCounts, true), [childCount - i - 1], false),
                path: selfPath
            });
            // Cut walk short if the callback returned false
            if (childIndex === false) {
                return false;
            }
        }
    }
    return childIndex;
}
/**
 * Perform a change on the given node and all its descendants, traversing the tree depth-first
 *
 * @param {Object} args - Function parameters
 * @param {function} args.callback - Function to call on each node
 * @param {function} args.getNodeKey - Function to get the key from the nodeData and tree index
 * @param {boolean} args.ignoreCollapsed - Ignore children of nodes without `expanded` set to `true`
 * @param {boolean=} args.isPseudoRoot - If true, this node has no real data, and only serves
 *                                        as the parent of all the nodes in the tree
 * @param {Object} args.node - A tree node
 * @param {Object=} args.parentNode - The parent node of `node`
 * @param {number} args.currentIndex - The treeIndex of `node`
 * @param {number[]|string[]} args.path - Array of keys leading up to node to be changed
 * @param {number[]} args.lowerSiblingCounts - An array containing the count of siblings beneath the
 *                                             previous nodes in this path
 *
 * @return {number|false} nextIndex - Index of the next sibling of `node`,
 *                                    or false if the walk should be terminated
 */
function mapDescendants(_a) {
    var callback = _a.callback, getNodeKey = _a.getNodeKey, ignoreCollapsed = _a.ignoreCollapsed, _b = _a.isPseudoRoot, isPseudoRoot = _b === void 0 ? false : _b, node = _a.node, _c = _a.parentNode, parentNode = _c === void 0 ? null : _c, currentIndex = _a.currentIndex, _d = _a.path, path = _d === void 0 ? [] : _d, _e = _a.lowerSiblingCounts, lowerSiblingCounts = _e === void 0 ? [] : _e;
    var nextNode = __assign({}, node);
    // The pseudo-root is not considered in the path
    var selfPath = isPseudoRoot ? [] : __spreadArray(__spreadArray([], path, true), [getNodeKey({ node: nextNode, treeIndex: currentIndex })], false);
    var selfInfo = {
        node: nextNode,
        parentNode: parentNode,
        path: selfPath,
        lowerSiblingCounts: lowerSiblingCounts,
        treeIndex: currentIndex
    };
    // Return self on nodes with no children or hidden children
    if (!nextNode.children || (nextNode.expanded !== true && ignoreCollapsed && !isPseudoRoot)) {
        return {
            treeIndex: currentIndex,
            node: callback(selfInfo)
        };
    }
    // Get all descendants
    var childIndex = currentIndex;
    var childCount = nextNode.children.length;
    if (typeof nextNode.children !== 'function') {
        nextNode.children = nextNode.children.map(function (child, i) {
            var mapResult = mapDescendants({
                callback: callback,
                getNodeKey: getNodeKey,
                ignoreCollapsed: ignoreCollapsed,
                node: child,
                parentNode: isPseudoRoot ? null : nextNode,
                currentIndex: childIndex + 1,
                lowerSiblingCounts: __spreadArray(__spreadArray([], lowerSiblingCounts, true), [childCount - i - 1], false),
                path: selfPath
            });
            childIndex = mapResult.treeIndex;
            return mapResult.node;
        });
    }
    return {
        node: callback(selfInfo),
        treeIndex: childIndex
    };
}
/**
 * Count all the visible (expanded) descendants in the tree data.
 *
 * @param {!Object[]} treeData - Tree data
 *
 * @return {number} count
 */
export function getVisibleNodeCount(_a) {
    var treeData = _a.treeData;
    var traverse = function (node) {
        if (!node.children || node.expanded !== true || typeof node.children === 'function') {
            return 1;
        }
        return 1 + node.children.reduce(function (total, currentNode) { return total + traverse(currentNode); }, 0);
    };
    return treeData.reduce(function (total, currentNode) { return total + traverse(currentNode); }, 0);
}
/**
 * Get the <targetIndex>th visible node in the tree data.
 *
 * @param {!Object[]} treeData - Tree data
 * @param {!number} targetIndex - The index of the node to search for
 * @param {!function} getNodeKey - Function to get the key from the nodeData and tree index
 *
 * @return {{
 *      node: Object,
 *      path: []string|[]number,
 *      lowerSiblingCounts: []number
 *  }|null} node - The node at targetIndex, or null if not found
 */
export function getVisibleNodeInfoAtIndex(_a) {
    var treeData = _a.treeData, targetIndex = _a.index, getNodeKey = _a.getNodeKey;
    if (!treeData || treeData.length < 1) {
        return null;
    }
    // Call the tree traversal with a pseudo-root node
    var result = getNodeDataAtTreeIndexOrNextIndex({
        targetIndex: targetIndex,
        getNodeKey: getNodeKey,
        node: {
            children: treeData,
            expanded: true
        },
        currentIndex: -1,
        path: [],
        lowerSiblingCounts: [],
        isPseudoRoot: true
    });
    if (result.node) {
        return result;
    }
    return null;
}
/**
 * Walk descendants depth-first and call a callback on each
 *
 * @param {!Object[]} treeData - Tree data
 * @param {!function} getNodeKey - Function to get the key from the nodeData and tree index
 * @param {function} callback - Function to call on each node
 * @param {boolean=} ignoreCollapsed - Ignore children of nodes without `expanded` set to `true`
 *
 * @return void
 */
export function walk(_a) {
    var treeData = _a.treeData, getNodeKey = _a.getNodeKey, callback = _a.callback, _b = _a.ignoreCollapsed, ignoreCollapsed = _b === void 0 ? true : _b;
    if (!treeData || treeData.length < 1) {
        return;
    }
    walkDescendants({
        callback: callback,
        getNodeKey: getNodeKey,
        ignoreCollapsed: ignoreCollapsed,
        isPseudoRoot: true,
        node: { children: treeData },
        currentIndex: -1,
        path: [],
        lowerSiblingCounts: []
    });
}
/**
 * Perform a depth-first transversal of the descendants and
 *  make a change to every node in the tree
 *
 * @param {!Object[]} treeData - Tree data
 * @param {!function} getNodeKey - Function to get the key from the nodeData and tree index
 * @param {function} callback - Function to call on each node
 * @param {boolean=} ignoreCollapsed - Ignore children of nodes without `expanded` set to `true`
 *
 * @return {Object[]} changedTreeData - The changed tree data
 */
export function map(_a) {
    var treeData = _a.treeData, getNodeKey = _a.getNodeKey, callback = _a.callback, _b = _a.ignoreCollapsed, ignoreCollapsed = _b === void 0 ? true : _b;
    if (!treeData || treeData.length < 1) {
        return [];
    }
    return mapDescendants({
        callback: callback,
        getNodeKey: getNodeKey,
        ignoreCollapsed: ignoreCollapsed,
        isPseudoRoot: true,
        node: { children: treeData },
        currentIndex: -1,
        path: [],
        lowerSiblingCounts: []
    }).node.children;
}
/**
 * Expand or close every node in the tree
 *
 * @param {!Object[]} treeData - Tree data
 * @param {?boolean} expanded - Whether the node is expanded or not
 *
 * @return {Object[]} changedTreeData - The changed tree data
 */
export function toggleExpandedForAll(_a) {
    var treeData = _a.treeData, _b = _a.expanded, expanded = _b === void 0 ? true : _b;
    return map({
        treeData: treeData,
        callback: function (_a) {
            var node = _a.node;
            return (__assign(__assign({}, node), { expanded: expanded }));
        },
        getNodeKey: function (_a) {
            var treeIndex = _a.treeIndex;
            return treeIndex;
        },
        ignoreCollapsed: false
    });
}
export var RESULT_MISS_ERROR_MESSAGE = 'No node found at the given path.';
/**
 * Replaces node at path with object, or callback-defined object
 *
 * @param {!Object[]} treeData
 * @param {number[]|string[]} path - Array of keys leading up to node to be changed
 * @param {function|any} newNode - Node to replace the node at the path with, or a function producing the new node
 * @param {!function} getNodeKey - Function to get the key from the nodeData and tree index
 * @param {boolean=} ignoreCollapsed - Ignore children of nodes without `expanded` set to `true`
 *
 * @return {Object[]} changedTreeData - The changed tree data
 */
export function changeNodeAtPath(_a) {
    var treeData = _a.treeData, path = _a.path, newNode = _a.newNode, getNodeKey = _a.getNodeKey, _b = _a.ignoreCollapsed, ignoreCollapsed = _b === void 0 ? true : _b;
    var RESULT_MISS = 'RESULT_MISS';
    var traverse = function (_a) {
        var _b = _a.isPseudoRoot, isPseudoRoot = _b === void 0 ? false : _b, node = _a.node, currentTreeIndex = _a.currentTreeIndex, pathIndex = _a.pathIndex;
        if (!isPseudoRoot && getNodeKey({ node: node, treeIndex: currentTreeIndex }) !== path[pathIndex]) {
            return RESULT_MISS;
        }
        if (pathIndex >= path.length - 1) {
            // If this is the final location in the path, return its changed form
            return typeof newNode === 'function' ? newNode({ node: node, treeIndex: currentTreeIndex }) : newNode;
        }
        if (!node.children) {
            // If this node is part of the path, but has no children, return the unchanged node
            throw new Error('Path referenced children of node with no children.');
        }
        var nextTreeIndex = currentTreeIndex + 1;
        for (var i = 0; i < node.children.length; i += 1) {
            var result_1 = traverse({
                node: node.children[i],
                currentTreeIndex: nextTreeIndex,
                pathIndex: pathIndex + 1
            });
            // If the result went down the correct path
            if (result_1 !== RESULT_MISS) {
                if (result_1) {
                    // If the result was truthy (in this case, an object),
                    //  pass it to the next level of recursion up
                    return __assign(__assign({}, node), { children: __spreadArray(__spreadArray(__spreadArray([], node.children.slice(0, i), true), [result_1], false), node.children.slice(i + 1), true) });
                }
                // If the result was falsy (returned from the newNode function), then
                //  delete the node from the array.
                return __assign(__assign({}, node), { children: __spreadArray(__spreadArray([], node.children.slice(0, i), true), node.children.slice(i + 1), true) });
            }
            nextTreeIndex += 1 + getDescendantCount({ node: node.children[i], ignoreCollapsed: ignoreCollapsed });
        }
        return RESULT_MISS;
    };
    // Use a pseudo-root node in the beginning traversal
    var result = traverse({
        node: { children: treeData },
        currentTreeIndex: -1,
        pathIndex: -1,
        isPseudoRoot: true
    });
    if (result === RESULT_MISS) {
        throw new Error(RESULT_MISS_ERROR_MESSAGE);
    }
    return result.children;
}
/**
 * Removes the node at the specified path and returns the resulting treeData.
 *
 * @param {!Object[]} treeData
 * @param {number[]|string[]} path - Array of keys leading up to node to be deleted
 * @param {!function} getNodeKey - Function to get the key from the nodeData and tree index
 * @param {boolean=} ignoreCollapsed - Ignore children of nodes without `expanded` set to `true`
 *
 * @return {Object[]} changedTreeData - The tree data with the node removed
 */
export function removeNodeAtPath(_a) {
    var treeData = _a.treeData, path = _a.path, getNodeKey = _a.getNodeKey, _b = _a.ignoreCollapsed, ignoreCollapsed = _b === void 0 ? true : _b;
    return changeNodeAtPath({
        treeData: treeData,
        path: path,
        getNodeKey: getNodeKey,
        ignoreCollapsed: ignoreCollapsed,
        newNode: null // Delete the node
    });
}
/**
 * Removes the node at the specified path and returns the resulting treeData.
 *
 * @param {!Object[]} treeData
 * @param {number[]|string[]} path - Array of keys leading up to node to be deleted
 * @param {!function} getNodeKey - Function to get the key from the nodeData and tree index
 * @param {boolean=} ignoreCollapsed - Ignore children of nodes without `expanded` set to `true`
 *
 * @return {Object} result
 * @return {Object[]} result.treeData - The tree data with the node removed
 * @return {Object} result.node - The node that was removed
 * @return {number} result.treeIndex - The previous treeIndex of the removed node
 */
export function removeNode(_a) {
    var treeData = _a.treeData, path = _a.path, getNodeKey = _a.getNodeKey, _b = _a.ignoreCollapsed, ignoreCollapsed = _b === void 0 ? true : _b;
    var removedNode = null;
    var removedTreeIndex = null;
    var nextTreeData = changeNodeAtPath({
        treeData: treeData,
        path: path,
        getNodeKey: getNodeKey,
        ignoreCollapsed: ignoreCollapsed,
        newNode: function (_a) {
            var node = _a.node, treeIndex = _a.treeIndex;
            // Store the target node and delete it from the tree
            removedNode = node;
            removedTreeIndex = treeIndex;
            return null;
        }
    });
    return {
        treeData: nextTreeData,
        node: removedNode,
        treeIndex: removedTreeIndex
    };
}
/**
 * Gets the node at the specified path
 *
 * @param {!Object[]} treeData
 * @param {number[]|string[]} path - Array of keys leading up to node to be deleted
 * @param {!function} getNodeKey - Function to get the key from the nodeData and tree index
 * @param {boolean=} ignoreCollapsed - Ignore children of nodes without `expanded` set to `true`
 *
 * @return {Object|null} nodeInfo - The node info at the given path, or null if not found
 */
export function getNodeAtPath(_a) {
    var treeData = _a.treeData, path = _a.path, getNodeKey = _a.getNodeKey, _b = _a.ignoreCollapsed, ignoreCollapsed = _b === void 0 ? true : _b;
    var foundNodeInfo = null;
    try {
        changeNodeAtPath({
            treeData: treeData,
            path: path,
            getNodeKey: getNodeKey,
            ignoreCollapsed: ignoreCollapsed,
            newNode: function (_a) {
                var node = _a.node, treeIndex = _a.treeIndex;
                foundNodeInfo = { node: node, treeIndex: treeIndex };
                return node;
            }
        });
    }
    catch (_c) {
        // Ignore the error -- the null return will be explanation enough
    }
    return foundNodeInfo;
}
/**
 * Adds the node to the specified parent and returns the resulting treeData.
 *
 * @param {!Object[]} treeData
 * @param {!Object} newNode - The node to insert
 * @param {number|string} parentKey - The key of the to-be parentNode of the node
 * @param {!function} getNodeKey - Function to get the key from the nodeData and tree index
 * @param {boolean=} ignoreCollapsed - Ignore children of nodes without `expanded` set to `true`
 * @param {boolean=} expandParent - If true, expands the parentNode specified by parentPath
 * @param {boolean=} addAsFirstChild - If true, adds new node as first child of tree
 *
 * @return {Object} result
 * @return {Object[]} result.treeData - The updated tree data
 * @return {number} result.treeIndex - The tree index at which the node was inserted
 */
export function addNodeUnderParent(_a) {
    var treeData = _a.treeData, newNode = _a.newNode, _b = _a.parentKey, parentKey = _b === void 0 ? null : _b, getNodeKey = _a.getNodeKey, _c = _a.ignoreCollapsed, ignoreCollapsed = _c === void 0 ? true : _c, _d = _a.expandParent, expandParent = _d === void 0 ? false : _d, _e = _a.addAsFirstChild, addAsFirstChild = _e === void 0 ? false : _e;
    if (parentKey === null) {
        return addAsFirstChild
            ? {
                treeData: __spreadArray([newNode], (treeData || []), true),
                treeIndex: 0
            }
            : {
                treeData: __spreadArray(__spreadArray([], (treeData || []), true), [newNode], false),
                treeIndex: (treeData || []).length
            };
    }
    var insertedTreeIndex = null;
    var hasBeenAdded = false;
    var changedTreeData = map({
        treeData: treeData,
        getNodeKey: getNodeKey,
        ignoreCollapsed: ignoreCollapsed,
        callback: function (_a) {
            var node = _a.node, treeIndex = _a.treeIndex, path = _a.path;
            var key = path ? path[path.length - 1] : null;
            // Return nodes that are not the parent as-is
            if (hasBeenAdded || key !== parentKey) {
                return node;
            }
            hasBeenAdded = true;
            var parentNode = __assign({}, node);
            if (expandParent) {
                parentNode.expanded = true;
            }
            // If no children exist yet, just add the single newNode
            if (!parentNode.children) {
                insertedTreeIndex = treeIndex + 1;
                return __assign(__assign({}, parentNode), { children: [newNode] });
            }
            if (typeof parentNode.children === 'function') {
                throw new Error('Cannot add to children defined by a function');
            }
            var nextTreeIndex = treeIndex + 1;
            for (var i = 0; i < parentNode.children.length; i += 1) {
                nextTreeIndex += 1 + getDescendantCount({ node: parentNode.children[i], ignoreCollapsed: ignoreCollapsed });
            }
            insertedTreeIndex = nextTreeIndex;
            var children = addAsFirstChild ? __spreadArray([newNode], parentNode.children, true) : __spreadArray(__spreadArray([], parentNode.children, true), [newNode], false);
            return __assign(__assign({}, parentNode), { children: children });
        }
    });
    if (!hasBeenAdded) {
        throw new Error('No node found with the given key.');
    }
    return {
        treeData: changedTreeData,
        treeIndex: insertedTreeIndex
    };
}
function addNodeAtDepthAndIndex(_a) {
    var targetDepth = _a.targetDepth, minimumTreeIndex = _a.minimumTreeIndex, newNode = _a.newNode, ignoreCollapsed = _a.ignoreCollapsed, expandParent = _a.expandParent, _b = _a.isPseudoRoot, isPseudoRoot = _b === void 0 ? false : _b, isLastChild = _a.isLastChild, node = _a.node, currentIndex = _a.currentIndex, currentDepth = _a.currentDepth, getNodeKey = _a.getNodeKey, _c = _a.path, path = _c === void 0 ? [] : _c;
    var selfPath = function (n) { return (isPseudoRoot ? [] : __spreadArray(__spreadArray([], path, true), [getNodeKey({ node: n, treeIndex: currentIndex })], false)); };
    // If the current position is the only possible place to add, add it
    if (currentIndex >= minimumTreeIndex - 1 || (isLastChild && !(node.children && node.children.length))) {
        if (typeof node.children === 'function') {
            throw new Error('Cannot add to children defined by a function');
        }
        else {
            var extraNodeProps = expandParent ? { expanded: true } : {};
            var nextNode_1 = __assign(__assign(__assign({}, node), extraNodeProps), { children: node.children ? __spreadArray([newNode], node.children, true) : [newNode] });
            return {
                node: nextNode_1,
                nextIndex: currentIndex + 2,
                insertedTreeIndex: currentIndex + 1,
                parentPath: selfPath(nextNode_1),
                parentNode: isPseudoRoot ? null : nextNode_1
            };
        }
    }
    // If this is the target depth for the insertion,
    // i.e., where the newNode can be added to the current node's children
    if (currentDepth >= targetDepth - 1) {
        // Skip over nodes with no children or hidden children
        if (!node.children ||
            typeof node.children === 'function' ||
            (node.expanded !== true && ignoreCollapsed && !isPseudoRoot)) {
            return { node: node, nextIndex: currentIndex + 1 };
        }
        // Scan over the children to see if there's a place among them that fulfills
        // the minimumTreeIndex requirement
        var childIndex_1 = currentIndex + 1;
        var insertedTreeIndex_1 = null;
        var insertIndex = null;
        for (var i = 0; i < node.children.length; i += 1) {
            // If a valid location is found, mark it as the insertion location and
            // break out of the loop
            if (childIndex_1 >= minimumTreeIndex) {
                insertedTreeIndex_1 = childIndex_1;
                insertIndex = i;
                break;
            }
            // Increment the index by the child itself plus the number of descendants it has
            childIndex_1 += 1 + getDescendantCount({ node: node.children[i], ignoreCollapsed: ignoreCollapsed });
        }
        // If no valid indices to add the node were found
        if (insertIndex === null) {
            // If the last position in this node's children is less than the minimum index
            // and there are more children on the level of this node, return without insertion
            if (childIndex_1 < minimumTreeIndex && !isLastChild) {
                return { node: node, nextIndex: childIndex_1 };
            }
            // Use the last position in the children array to insert the newNode
            insertedTreeIndex_1 = childIndex_1;
            insertIndex = node.children.length;
        }
        // Insert the newNode at the insertIndex
        var nextNode_2 = __assign(__assign({}, node), { children: __spreadArray(__spreadArray(__spreadArray([], node.children.slice(0, insertIndex), true), [newNode], false), node.children.slice(insertIndex), true) });
        // Return node with successful insert result
        return {
            node: nextNode_2,
            nextIndex: childIndex_1,
            insertedTreeIndex: insertedTreeIndex_1,
            parentPath: selfPath(nextNode_2),
            parentNode: isPseudoRoot ? null : nextNode_2
        };
    }
    // Skip over nodes with no children or hidden children
    if (!node.children ||
        typeof node.children === 'function' ||
        (node.expanded !== true && ignoreCollapsed && !isPseudoRoot)) {
        return { node: node, nextIndex: currentIndex + 1 };
    }
    // Get all descendants
    var insertedTreeIndex = null;
    var pathFragment = null;
    var parentNode = null;
    var childIndex = currentIndex + 1;
    var newChildren = node.children;
    if (typeof newChildren !== 'function') {
        newChildren = newChildren.map(function (child, i) {
            if (insertedTreeIndex !== null) {
                return child;
            }
            var mapResult = addNodeAtDepthAndIndex({
                targetDepth: targetDepth,
                minimumTreeIndex: minimumTreeIndex,
                newNode: newNode,
                ignoreCollapsed: ignoreCollapsed,
                expandParent: expandParent,
                isLastChild: isLastChild && i === newChildren.length - 1,
                node: child,
                currentIndex: childIndex,
                currentDepth: currentDepth + 1,
                getNodeKey: getNodeKey,
                path: [] // Cannot determine the parent path until the children have been processed
            });
            if ('insertedTreeIndex' in mapResult) {
                (insertedTreeIndex = mapResult.insertedTreeIndex, parentNode = mapResult.parentNode, pathFragment = mapResult.parentPath);
            }
            childIndex = mapResult.nextIndex;
            return mapResult.node;
        });
    }
    var nextNode = __assign(__assign({}, node), { children: newChildren });
    var result = {
        node: nextNode,
        nextIndex: childIndex
    };
    if (insertedTreeIndex !== null) {
        result.insertedTreeIndex = insertedTreeIndex;
        result.parentPath = __spreadArray(__spreadArray([], selfPath(nextNode), true), pathFragment, true);
        result.parentNode = parentNode;
    }
    return result;
}
/**
 * Insert a node into the tree at the given depth, after the minimum index
 *
 * @param {!Object[]} treeData - Tree data
 * @param {!number} depth - The depth to insert the node at (the first level of the array being depth 0)
 * @param {!number} minimumTreeIndex - The lowest possible treeIndex to insert the node at
 * @param {!Object} newNode - The node to insert into the tree
 * @param {boolean=} ignoreCollapsed - Ignore children of nodes without `expanded` set to `true`
 * @param {boolean=} expandParent - If true, expands the parent of the inserted node
 * @param {!function} getNodeKey - Function to get the key from the nodeData and tree index
 *
 * @return {Object} result
 * @return {Object[]} result.treeData - The tree data with the node added
 * @return {number} result.treeIndex - The tree index at which the node was inserted
 * @return {number[]|string[]} result.path - Array of keys leading to the node location after insertion
 * @return {Object} result.parentNode - The parent node of the inserted node
 */
export function insertNode(_a) {
    var treeData = _a.treeData, targetDepth = _a.depth, minimumTreeIndex = _a.minimumTreeIndex, newNode = _a.newNode, _b = _a.getNodeKey, getNodeKey = _b === void 0 ? function () { } : _b, _c = _a.ignoreCollapsed, ignoreCollapsed = _c === void 0 ? true : _c, _d = _a.expandParent, expandParent = _d === void 0 ? false : _d;
    if (!treeData && targetDepth === 0) {
        return {
            treeData: [newNode],
            treeIndex: 0,
            path: [getNodeKey({ node: newNode, treeIndex: 0 })],
            parentNode: null
        };
    }
    var insertResult = addNodeAtDepthAndIndex({
        targetDepth: targetDepth,
        minimumTreeIndex: minimumTreeIndex,
        newNode: newNode,
        ignoreCollapsed: ignoreCollapsed,
        expandParent: expandParent,
        getNodeKey: getNodeKey,
        isPseudoRoot: true,
        isLastChild: true,
        node: { children: treeData },
        currentIndex: -1,
        currentDepth: -1
    });
    if (!('insertedTreeIndex' in insertResult)) {
        throw new Error('No suitable position found to insert.');
    }
    var treeIndex = insertResult.insertedTreeIndex;
    return {
        treeData: insertResult.node.children,
        treeIndex: treeIndex,
        path: __spreadArray(__spreadArray([], insertResult.parentPath, true), [getNodeKey({ node: newNode, treeIndex: treeIndex })], false),
        parentNode: insertResult.parentNode
    };
}
/**
 * Get tree data flattened.
 *
 * @param {!Object[]} treeData - Tree data
 * @param {!function} getNodeKey - Function to get the key from the nodeData and tree index
 * @param {boolean=} ignoreCollapsed - Ignore children of nodes without `expanded` set to `true`
 *
 * @return {{
 *      node: Object,
 *      path: []string|[]number,
 *      lowerSiblingCounts: []number
 *  }}[] nodes - The node array
 */
export function getFlatDataFromTree(_a) {
    var treeData = _a.treeData, getNodeKey = _a.getNodeKey, _b = _a.ignoreCollapsed, ignoreCollapsed = _b === void 0 ? true : _b;
    if (!treeData || treeData.length < 1) {
        return [];
    }
    var flattened = [];
    walk({
        treeData: treeData,
        getNodeKey: getNodeKey,
        ignoreCollapsed: ignoreCollapsed,
        callback: function (nodeInfo) {
            flattened.push(nodeInfo);
        }
    });
    return flattened;
}
/**
 * Generate a tree structure from flat data.
 *
 * @param {!Object[]} flatData
 * @param {!function=} getKey - Function to get the key from the nodeData
 * @param {!function=} getParentKey - Function to get the parent key from the nodeData
 * @param {string|number=} rootKey - The value returned by `getParentKey` that corresponds to the root node.
 *                                  For example, if your nodes have id 1-99, you might use rootKey = 0
 *
 * @return {Object[]} treeData - The flat data represented as a tree
 */
export function getTreeFromFlatData(_a) {
    var flatData = _a.flatData, _b = _a.getKey, getKey = _b === void 0 ? function (node) { return node.id; } : _b, _c = _a.getParentKey, getParentKey = _c === void 0 ? function (node) { return node.parentId; } : _c, _d = _a.rootKey, rootKey = _d === void 0 ? '0' : _d;
    if (!flatData) {
        return [];
    }
    var childrenToParents = {};
    flatData.forEach(function (child) {
        var parentKey = getParentKey(child);
        if (parentKey in childrenToParents) {
            childrenToParents[parentKey].push(child);
        }
        else {
            childrenToParents[parentKey] = [child];
        }
    });
    if (!(rootKey in childrenToParents)) {
        return [];
    }
    var trav = function (parent) {
        var parentKey = getKey(parent);
        if (parentKey in childrenToParents) {
            return __assign(__assign({}, parent), { children: childrenToParents[parentKey].map(function (child) { return trav(child); }) });
        }
        return __assign({}, parent);
    };
    return childrenToParents[rootKey].map(function (child) { return trav(child); });
}
/**
 * Check if a node is a descendant of another node.
 *
 * @param {!Object} older - Potential ancestor of younger node
 * @param {!Object} younger - Potential descendant of older node
 *
 * @return {boolean}
 */
export function isDescendant(older, younger) {
    return (!!older.children &&
        typeof older.children !== 'function' &&
        older.children.some(function (child) { return child === younger || isDescendant(child, younger); }));
}
/**
 * Get the maximum depth of the children (the depth of the root node is 0).
 *
 * @param {!Object} node - Node in the tree
 * @param {?number} depth - The current depth
 *
 * @return {number} maxDepth - The deepest depth in the tree
 */
export function getDepth(node, depth) {
    if (depth === void 0) { depth = 0; }
    if (!node.children) {
        return depth;
    }
    if (typeof node.children === 'function') {
        return depth + 1;
    }
    return node.children.reduce(function (deepest, child) { return Math.max(deepest, getDepth(child, depth + 1)); }, depth);
}
/**
 * Find nodes matching a search query in the tree,
 *
 * @param {!function} getNodeKey - Function to get the key from the nodeData and tree index
 * @param {!Object[]} treeData - Tree data
 * @param {?string|number} searchQuery - Function returning a boolean to indicate whether the node is a match or not
 * @param {!function} searchMethod - Function returning a boolean to indicate whether the node is a match or not
 * @param {?number} searchFocusOffset - The offset of the match to focus on
 *                                      (e.g., 0 focuses on the first match, 1 on the second)
 * @param {boolean=} expandAllMatchPaths - If true, expands the paths to any matched node
 * @param {boolean=} expandFocusMatchPaths - If true, expands the path to the focused node
 *
 * @return {Object[]} matches - An array of objects containing the matching `node`s, their `path`s and `treeIndex`s
 * @return {Object[]} treeData - The original tree data with all relevant nodes expanded.
 *                               If expandAllMatchPaths and expandFocusMatchPaths are both false,
 *                               it will be the same as the original tree data.
 */
export function find(_a) {
    var getNodeKey = _a.getNodeKey, treeData = _a.treeData, searchQuery = _a.searchQuery, searchMethod = _a.searchMethod, searchFocusOffset = _a.searchFocusOffset, _b = _a.expandAllMatchPaths, expandAllMatchPaths = _b === void 0 ? false : _b, _c = _a.expandFocusMatchPaths, expandFocusMatchPaths = _c === void 0 ? true : _c;
    var matchCount = 0;
    var trav = function (_a) {
        var _b = _a.isPseudoRoot, isPseudoRoot = _b === void 0 ? false : _b, node = _a.node, currentIndex = _a.currentIndex, _c = _a.path, path = _c === void 0 ? [] : _c;
        var matches = [];
        var isSelfMatch = false;
        var hasFocusMatch = false;
        // The pseudo-root is not considered in the path
        var selfPath = isPseudoRoot ? [] : __spreadArray(__spreadArray([], path, true), [getNodeKey({ node: node, treeIndex: currentIndex })], false);
        var extraInfo = isPseudoRoot
            ? null
            : {
                path: selfPath,
                treeIndex: currentIndex
            };
        // Nodes with with children that aren't lazy
        var hasChildren = node.children && typeof node.children !== 'function' && node.children.length > 0;
        // Examine the current node to see if it is a match
        if (!isPseudoRoot && searchMethod(__assign(__assign({}, extraInfo), { node: node, searchQuery: searchQuery }))) {
            if (matchCount === searchFocusOffset) {
                hasFocusMatch = true;
            }
            // Keep track of the number of matching nodes, so we know when the searchFocusOffset
            //  is reached
            matchCount += 1;
            // We cannot add this node to the matches right away, as it may be changed
            //  during the search of the descendants. The entire node is used in
            //  comparisons between nodes inside the `matches` and `treeData` results
            //  of this method (`find`)
            isSelfMatch = true;
        }
        var childIndex = currentIndex;
        var newNode = __assign({}, node);
        if (hasChildren) {
            // Get all descendants
            newNode.children = newNode.children.map(function (child) {
                var mapResult = trav({
                    node: child,
                    currentIndex: childIndex + 1,
                    path: selfPath
                });
                // Ignore hidden nodes by only advancing the index counter to the returned treeIndex
                // if the child is expanded.
                //
                // The child could have been expanded from the start,
                // or expanded due to a matching node being found in its descendants
                if (mapResult.node.expanded) {
                    childIndex = mapResult.treeIndex;
                }
                else {
                    childIndex += 1;
                }
                if (mapResult.matches.length > 0 || mapResult.hasFocusMatch) {
                    matches = __spreadArray(__spreadArray([], matches, true), mapResult.matches, true);
                    if (mapResult.hasFocusMatch) {
                        hasFocusMatch = true;
                    }
                    // Expand the current node if it has descendants matching the search
                    // and the settings are set to do so.
                    if ((expandAllMatchPaths && mapResult.matches.length > 0) ||
                        ((expandAllMatchPaths || expandFocusMatchPaths) && mapResult.hasFocusMatch)) {
                        newNode.expanded = true;
                    }
                }
                return mapResult.node;
            });
        }
        // Cannot assign a treeIndex to hidden nodes
        if (!isPseudoRoot && !newNode.expanded) {
            matches = matches.map(function (match) { return (__assign(__assign({}, match), { treeIndex: null })); });
        }
        // Add this node to the matches if it fits the search criteria.
        // This is performed at the last minute so newNode can be sent in its final form.
        if (isSelfMatch) {
            matches = __spreadArray([__assign(__assign({}, extraInfo), { node: newNode })], matches, true);
        }
        return {
            node: matches.length > 0 ? newNode : node,
            matches: matches,
            hasFocusMatch: hasFocusMatch,
            treeIndex: childIndex
        };
    };
    var result = trav({
        node: { children: treeData },
        isPseudoRoot: true,
        currentIndex: -1
    });
    return {
        matches: result.matches,
        treeData: result.node.children
    };
}
export default {
    find: find,
    getDepth: getDepth,
    isDescendant: isDescendant,
    getTreeFromFlatData: getTreeFromFlatData,
    getFlatDataFromTree: getFlatDataFromTree,
    insertNode: insertNode,
    addNodeUnderParent: addNodeUnderParent,
    getNodeAtPath: getNodeAtPath,
    removeNode: removeNode,
    removeNodeAtPath: removeNodeAtPath,
    changeNodeAtPath: changeNodeAtPath,
    toggleExpandedForAll: toggleExpandedForAll,
    map: map,
    walk: walk,
    getVisibleNodeInfoAtIndex: getVisibleNodeInfoAtIndex,
    getVisibleNodeCount: getVisibleNodeCount,
    getDescendantCount: getDescendantCount
};
