/*!
 * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
 * Licensed under the MIT License.
 */
import { assert } from "@fluidframework/core-utils/internal";
import { topDownPath, } from "../core/index.js";
import { brand, getOrCreate } from "../util/index.js";
/**
 * Index symbol for syntax tree
 *
 * @internal
 */
export const indexSymbol = Symbol("editable-tree-binder:index");
/**
 * Enumeration of binding categories
 *
 * @internal
 */
export const BindingType = {
    Remove: "remove",
    Insert: "insert",
    Invalidation: "invalidation",
    Batch: "batch",
};
/**
 * A generic implementation of a {@link PathVisitor} enabling the registration of listeners
 * categorized by {@link BindingContextType} and {@link BindPolicy}.
 */
class AbstractPathVisitor {
    constructor(options) {
        this.options = options;
        this.registeredListeners = new Map();
    }
    // TODO: make these methods abstract and make AbstractPathVisitor implementations implement them
    afterCreate(content) { }
    beforeDestroy(content) { }
    beforeAttach(source, destination) { }
    afterAttach(source, destination) {
        this.onInsert({
            parent: destination.parent,
            parentField: destination.field,
            parentIndex: destination.start,
        }, this.getContent(destination));
    }
    beforeDetach(source, destination) { }
    afterDetach(source, destination) {
        this.onRemove({
            parent: source.parent,
            parentField: source.field,
            parentIndex: source.index,
        }, destination.end - destination.start);
    }
    beforeReplace(newContent, oldContent, oldContentDestination) { }
    afterReplace(newContentSource, newContent, oldContent) {
        this.onRemove({
            parent: newContent.parent,
            parentField: newContent.field,
            parentIndex: newContent.start,
        }, oldContent.end - oldContent.start);
        this.onInsert({
            parent: newContent.parent,
            parentField: newContent.field,
            parentIndex: newContent.start,
        }, this.getContent(newContent));
    }
    getContent(range) {
        // TODO: either lookup the content in the forest or stop providing the content in the events
        return [];
    }
    registerListener(contextType, policies, listener) {
        const contextRoots = getOrCreate(this.registeredListeners, contextType, () => new Map());
        policies.forEach((policy) => {
            const tree = policy.bindTree;
            const currentRoot = this.findRoot(contextType, tree.field);
            if (currentRoot === undefined) {
                const newRoot = {
                    field: tree.field,
                    index: tree.index,
                    listeners: new Set(),
                    children: new Map(),
                    matchPolicy: policy.matchPolicy,
                };
                assert(contextRoots !== undefined, 0x6da /* expected contextRoots to be defined */);
                contextRoots.set(tree.field, newRoot);
                this.bindTree(contextType, tree, listener, newRoot);
            }
            else {
                this.bindTree(contextType, tree, listener, currentRoot);
            }
        });
        return () => {
            policies.forEach((policy) => this.unregisterListener(contextType, policy.bindTree, listener));
        };
    }
    bindTree(contextType, tree, listener, callTree) {
        if (tree.children.size === 0) {
            callTree.listeners.add(listener);
        }
        else {
            tree.children.forEach((childTree, fieldKey) => {
                const childCallTree = getOrCreate(callTree.children, fieldKey, () => {
                    const newChildCallTree = {
                        field: fieldKey,
                        index: childTree.index,
                        listeners: new Set(),
                        children: new Map(),
                    };
                    callTree.children.set(fieldKey, newChildCallTree);
                    return newChildCallTree;
                });
                this.bindTree(contextType, childTree, listener, childCallTree);
            });
        }
    }
    findRoot(contextType, field) {
        return this.registeredListeners.get(contextType)?.get(field);
    }
    unregisterListener(contextType, tree, listener, callTree) {
        const foundTree = callTree ?? this.findRoot(contextType, tree.field);
        if (foundTree !== undefined) {
            if (tree.children.size === 0) {
                foundTree.listeners.delete(listener);
            }
            else {
                tree.children.forEach((childTree, fieldKey) => {
                    assert(foundTree !== undefined, 0x6db /* expected foundTree to be defined */);
                    const childCallTree = foundTree.children.get(fieldKey);
                    if (childCallTree !== undefined) {
                        this.unregisterListener(contextType, childTree, listener, childCallTree);
                    }
                });
            }
        }
    }
    getListeners(contextType, downPath) {
        const foundRoot = this.findRoot(contextType, downPath[0].field);
        if (foundRoot === undefined) {
            return undefined;
        }
        else {
            const subtreeMatch = (subtreePolicy, depth) => {
                if (subtreePolicy?.maxDepth !== undefined && depth > subtreePolicy.maxDepth) {
                    return false;
                }
                return true;
            };
            const accumulateMatching = (treeNode, index, onMatch) => {
                const step = downPath[index];
                if (step === undefined ||
                    treeNode.field !== step.field ||
                    (treeNode.index !== undefined && step.index !== treeNode.index)) {
                    return undefined;
                }
                for (const child of treeNode.children.values()) {
                    accumulateMatching(child, index + 1, onMatch);
                }
                onMatch(index, treeNode);
            };
            const matchedNodes = new Set();
            if (foundRoot.matchPolicy === "path") {
                accumulateMatching(foundRoot, 0, (index, treeNode) => {
                    if (index === downPath.length - 1) {
                        treeNode.listeners.forEach((listener) => matchedNodes.add(listener));
                    }
                });
            }
            else if (foundRoot.matchPolicy === "subtree") {
                accumulateMatching(foundRoot, 0, (index, treeNode) => {
                    treeNode.listeners.forEach((listener) => matchedNodes.add(listener));
                });
            }
            else {
                const matchPolicy = foundRoot.matchPolicy;
                accumulateMatching(foundRoot, 0, (index, treeNode) => {
                    if (subtreeMatch(matchPolicy, downPath.length - 1)) {
                        treeNode.listeners.forEach((listener) => matchedNodes.add(listener));
                    }
                });
            }
            return matchedNodes.size > 0 ? matchedNodes : undefined;
        }
    }
    hasRegisteredContextType(contextType) {
        return this.registeredListeners.has(contextType);
    }
    dispose() {
        this.registeredListeners.clear();
    }
}
/**
 * A visitor that invokes listeners immediately when a path is traversed.
 */
class DirectPathVisitor extends AbstractPathVisitor {
    processListeners(path, listeners, otherArgs) {
        for (const listener of listeners) {
            listener({
                path,
                ...otherArgs,
            });
        }
    }
    processRegisteredPaths(path, type, otherArgs) {
        const current = toDownPath(path);
        const listeners = this.getListeners(type, current);
        if (listeners !== undefined) {
            this.processListeners(path, listeners, otherArgs);
        }
    }
    onRemove(path, count) {
        this.processRegisteredPaths(path, BindingType.Remove, {
            count,
            type: BindingType.Remove,
        });
    }
    onInsert(path, content) {
        this.processRegisteredPaths(path, BindingType.Insert, {
            content,
            type: BindingType.Insert,
        });
    }
}
/**
 * A visitor that invokes listeners only once when flushed if any modifications detected on the registered paths.
 */
class InvalidatingPathVisitor extends AbstractPathVisitor {
    constructor() {
        super(...arguments);
        this.listeners = new Set();
    }
    processRegisteredPaths(path) {
        const current = toDownPath(path);
        const listeners = this.getListeners(BindingType.Invalidation, current);
        if (listeners !== undefined) {
            for (const listener of listeners) {
                this.listeners.add(listener);
            }
        }
    }
    onRemove(path, count) {
        this.processRegisteredPaths(path);
    }
    onInsert(path, content) {
        this.processRegisteredPaths(path);
    }
    flush() {
        for (const listener of this.listeners) {
            listener({
                type: BindingType.Invalidation,
            });
        }
        this.listeners.clear();
        return this;
    }
    dispose() {
        this.flush();
        super.dispose();
    }
}
/**
 * A visitor that buffers all events which match the registered event categories and corresponding paths.
 * Listeners are invoked when flushed. Flushing has also the ability to sort and batch the events.
 */
class BufferingPathVisitor extends AbstractPathVisitor {
    constructor() {
        super(...arguments);
        this.eventQueue = [];
    }
    onRemove(path, count) {
        const current = toDownPath(path);
        const listeners = this.getListeners(BindingType.Remove, current);
        if (listeners !== undefined) {
            this.eventQueue.push({
                path,
                count,
                type: BindingType.Remove,
                listeners,
            });
        }
    }
    onInsert(path, content) {
        const current = toDownPath(path);
        const listeners = this.getListeners(BindingType.Insert, current);
        if (listeners !== undefined) {
            this.eventQueue.push({
                path,
                content,
                type: BindingType.Insert,
                listeners,
            });
        }
    }
    flush() {
        const sortedQueue = nativeSort(this.eventQueue, this.options.sortFn ?? (() => 0));
        const batchEventIndices = new Set();
        const batchEvents = [];
        const collected = new Set();
        if (this.hasRegisteredContextType(BindingType.Batch)) {
            for (let i = 0; i < sortedQueue.length; i++) {
                const event = sortedQueue[i];
                const current = toDownPath(event.path);
                const listeners = this.getListeners(BindingType.Batch, current);
                if (listeners !== undefined && listeners.size > 0) {
                    for (const listener of listeners) {
                        collected.add(listener);
                    }
                    batchEvents.push(event);
                    batchEventIndices.add(i);
                }
            }
        }
        for (const listener of collected) {
            listener({
                type: BindingType.Batch,
                events: batchEvents,
            });
        }
        for (let i = 0; i < sortedQueue.length; i++) {
            if (batchEventIndices.has(i)) {
                continue;
            }
            const { listeners, ...context } = sortedQueue[i];
            for (const listener of listeners) {
                listener({ ...context });
            }
        }
        this.eventQueue.length = 0;
        return this;
    }
    dispose() {
        this.flush();
        super.dispose();
    }
}
class AbstractDataBinder {
    constructor(options, visitorFactory) {
        this.options = options;
        this.visitorFactory = visitorFactory;
        this.visitors = new Map();
        this.visitorLocations = new Map();
        this.unregisterHandles = new Set();
    }
    register(anchor, eventType, eventTrees, listener) {
        // TODO: validate BindPath semantics against the schema
        const visitor = getOrCreate(this.visitors, anchor, () => {
            const newVisitor = this.visitorFactory(anchor);
            this.unregisterHandles.add(anchor.on("subtreeChanging", (upPath) => {
                assert(newVisitor !== undefined, 0x6dc /* visitor expected to be defined */);
                if (!this.visitorLocations.has(newVisitor)) {
                    this.visitorLocations.set(newVisitor, upPath);
                }
                return newVisitor;
            }));
            return newVisitor;
        });
        const contextType = eventType;
        this.unregisterHandles.add(visitor.registerListener(contextType, eventTrees, listener));
    }
    unregisterAll() {
        this.unregisterHandles.forEach((h) => h());
        this.unregisterHandles.clear();
        this.visitors.forEach((v) => v.dispose());
        this.visitors.clear();
    }
    extractBindPaths(root) {
        const result = [];
        const depthFirst = (node, path = [root]) => {
            if (node.children.size === 0) {
                result.push(path);
                return;
            }
            for (const [field, childNode] of node.children.entries()) {
                depthFirst(childNode, [...path, { field, index: childNode.index }]);
            }
        };
        depthFirst(root);
        return result;
    }
}
class BufferingDataBinder extends AbstractDataBinder {
    constructor(view, options) {
        super(options, (anchor) => new BufferingPathVisitor(options));
        this.view = view;
        this.autoFlushPolicy = options.autoFlushPolicy;
        if (options.autoFlush) {
            this.enableAutoFlush();
        }
    }
    flush() {
        const unsortedVisitors = Array.from(this.visitorLocations.keys());
        const sortFn = this.options.sortAnchorsFn ?? (() => 0);
        const compareFn = (a, b) => {
            const pathA = this.visitorLocations.get(a);
            const pathB = this.visitorLocations.get(b);
            assert(pathA !== undefined, 0x6dd /* pathA expected to be defined */);
            assert(pathB !== undefined, 0x6de /* pathB expected to be defined */);
            return sortFn(pathA, pathB);
        };
        const sortedVisitors = nativeSort(unsortedVisitors, compareFn);
        for (const visitor of sortedVisitors) {
            visitor.flush();
        }
        return this;
    }
    enableAutoFlush() {
        const callbackFn = (() => {
            this.flush();
        });
        const unregisterFlushing = this.view.on(this.autoFlushPolicy, callbackFn);
        this.unregisterHandles.add(unregisterFlushing);
        return this;
    }
}
class DirectDataBinder extends AbstractDataBinder {
    constructor(view, options) {
        super(options, (anchor) => new DirectPathVisitor(options));
    }
}
class InvalidateDataBinder extends AbstractDataBinder {
    constructor(view, options) {
        super(options, (anchor) => new InvalidatingPathVisitor(options));
        this.view = view;
        this.autoFlushPolicy = options.autoFlushPolicy;
        if (options.autoFlush) {
            this.enableAutoFlush();
        }
    }
    flush() {
        for (const visitor of this.visitors.values()) {
            visitor.flush();
        }
        return this;
    }
    enableAutoFlush() {
        const callbackFn = (() => {
            this.flush();
        });
        const unregisterFlushing = this.view.on(this.autoFlushPolicy, callbackFn);
        this.unregisterHandles.add(unregisterFlushing);
        return this;
    }
}
/**
 * Compute a top-town {@link DownPath} from an {@link UpPath}.
 *
 * @internal
 */
export function toDownPath(upPath) {
    const downPath = topDownPath(upPath);
    const stepDownPath = downPath.map((u) => {
        return { field: u.parentField, index: u.parentIndex };
    });
    stepDownPath.shift(); // remove last step to the root node
    return stepDownPath;
}
/**
 * Create a buffering data binder.
 *
 * @internal
 */
export function createDataBinderBuffering(view, options) {
    return new BufferingDataBinder(view, options);
}
/**
 * Create a direct data binder.
 *
 * @internal
 */
export function createDataBinderDirect(view, options) {
    return new DirectDataBinder(view, options);
}
/**
 * Create an invalidating data binder.
 *
 * @internal
 */
export function createDataBinderInvalidating(view, options) {
    return new InvalidateDataBinder(view, options);
}
/**
 * Create binder options. If not specified, the default values are:
 * - sortFn: no sorting
 *
 * @internal
 */
export function createBinderOptions({ sortFn }) {
    return { sortFn };
}
/**
 * Create flushable binder options. If not specified, the default values are:
 * - sortFn: no sorting
 * - sortAnchorsFn: no sorting
 * - autoFlush: true
 *
 * @internal
 */
export function createFlushableBinderOptions({ sortFn, sortAnchorsFn, autoFlush = true, autoFlushPolicy, }) {
    return {
        sortFn,
        sortAnchorsFn,
        autoFlush,
        autoFlushPolicy,
    };
}
/**
 * Utility to create a compare function from a list of compare functions.
 *
 * @param fns - a list of compare functions
 * @returns a compare function that can be used for sorting
 * @internal
 */
export function comparePipeline(...fns) {
    return (a, b) => {
        for (const fn of fns) {
            const result = fn(a, b);
            if (result !== 0) {
                return result;
            }
        }
        return 0;
    };
}
/**
 * Native sorting algorithm.
 *
 * @param arr - the array to sort
 * @param compareFn - the compare function
 * @returns the sorted array
 */
function nativeSort(arr, compareFn) {
    return [...arr].sort(compareFn);
}
/**
 * Compiles a (user friendly) syntax tree into the internal binding structure.
 * The syntax tree is a compact representation of related {@link BindPath}s.
 * The match policy can be specified. If not specified, the default value is "path".
 * @returns a {@link BindPolicy} object
 * @internal
 */
export function compileSyntaxTree(syntaxTree, matchPolicy) {
    const entries = Object.entries(syntaxTree);
    if (entries.length === 1) {
        const [fieldName, childNode] = entries[0];
        const fieldKey = brand(fieldName);
        const bindTree = compileSyntaxTreeNode(childNode, fieldKey);
        return { matchPolicy: matchPolicy ?? "path", bindTree };
    }
    else
        throw new Error("Invalid BindSyntaxTree structure");
}
function compileSyntaxTreeNode(node, parentField) {
    if (node === true)
        return { field: parentField, children: new Map() };
    const pathStep = {
        field: parentField,
        index: node[indexSymbol],
    };
    const children = new Map();
    for (const [key, value] of Object.entries(node)) {
        const fieldKey = brand(key);
        children.set(fieldKey, compileSyntaxTreeNode(value, fieldKey));
    }
    return {
        ...pathStep,
        children,
    };
}
//# sourceMappingURL=editableTreeBinder.js.map