/**
 * Remove the node at `path`.
 *
 * Removing the root yields an empty `all`-group so the tree always has
 * something to render. Returns `rule` unchanged when `path` does not
 * address a removable node.
 * @param rule
 * @param path
 */
export function deleteNode(rule, path) {
    if (path.length === 0) {
        return { type: 'all', rules: [] };
    }
    const parentPath = path.slice(0, -1);
    const key = path.at(-1);
    const parent = getNode(rule, parentPath);
    if ((parent.type === 'all' || parent.type === 'any') &&
        typeof key === 'number') {
        const rules = [...parent.rules];
        rules.splice(key, 1);
        return replaceAt(rule, parentPath, Object.assign(Object.assign({}, parent), { rules }));
    }
    if (parent.type === 'not' && key === 'rule') {
        return replaceAt(rule, parentPath, { type: 'all', rules: [] });
    }
    return rule;
}
/**
 * Replace the `args` of a `ref` node. Returns `rule` unchanged when
 * `path` does not resolve to a `ref`.
 * @param rule
 * @param path
 * @param args
 */
export function updateArgs(rule, path, args) {
    const node = getNode(rule, path);
    if (node.type !== 'ref') {
        return rule;
    }
    return replaceAt(rule, path, Object.assign(Object.assign({}, node), { args }));
}
/**
 * Replace the node at `path` with `replacement`, returning a new rule
 * tree. Used by callers that need to apply a non-atomic change as a
 * single emit (e.g. updating a group's whole `rules` array at once).
 * @param rule
 * @param path
 * @param replacement
 */
export function replaceNode(rule, path, replacement) {
    return replaceAt(rule, path, replacement);
}
/**
 * Wrap `group` in a `not` when `isNegated`, otherwise return it
 * unchanged. Used when re-emitting a group whose negation flag should
 * be preserved.
 * @param group
 * @param isNegated
 */
export function wrapNegated(group, isNegated) {
    return isNegated ? { type: 'not', rule: group } : group;
}
/**
 * Append `child` to a group's `rules`, returning a new group. The
 * input is not mutated.
 * @param group
 * @param child
 */
export function appendChild(group, child) {
    return Object.assign(Object.assign({}, group), { rules: [...group.rules, child] });
}
/**
 * Structural equality on two paths.
 * @param a
 * @param b
 */
export function pathsEqual(a, b) {
    if (a.length !== b.length) {
        return false;
    }
    return a.every((value, index) => value === b[index]);
}
/**
 * Filter validation issues to those whose `path` matches `path`.
 * @param issues
 * @param path
 */
export function issuesAt(issues, path) {
    return issues.filter((issue) => pathsEqual(issue.path, path));
}
function getNode(rule, path) {
    let node = rule;
    for (const key of path) {
        node = stepIntoNode(node, key);
    }
    return node;
}
function replaceAt(root, path, replacement) {
    if (path.length === 0) {
        return replacement;
    }
    const [head, ...tail] = path;
    if (root.type === 'not' && head === 'rule') {
        return Object.assign(Object.assign({}, root), { rule: replaceAt(root.rule, tail, replacement) });
    }
    if ((root.type === 'all' || root.type === 'any') && head === 'rules') {
        if (tail.length === 0) {
            return replacement;
        }
        const [index, ...rest] = tail;
        if (typeof index !== 'number') {
            return root;
        }
        const rules = [...root.rules];
        rules[index] = replaceAt(rules[index], rest, replacement);
        return Object.assign(Object.assign({}, root), { rules });
    }
    return root;
}
function stepIntoNode(node, key) {
    if (node.type === 'not' && key === 'rule') {
        return node.rule;
    }
    if ((node.type === 'all' || node.type === 'any') && key === 'rules') {
        return node;
    }
    if ((node.type === 'all' || node.type === 'any') &&
        typeof key === 'number') {
        return node.rules[key];
    }
    return node;
}
