import { areEqual } from "../../../shared/src/arrays.js";
import { assert, unreachable } from "../../../shared/src/asserts.js";
import {} from "./change.js";
import { normalizeUndefined } from "./data.js";
import { throwFilterOutput, } from "./filter-operators.js";
import {} from "./operator.js";
import { first } from "./stream.js";
/**
 * The Exists operator filters data based on whether or not a relationship is
 * non-empty.
 */
export class Exists {
    #input;
    #relationshipName;
    #storage;
    #not;
    #parentJoinKey;
    #noSizeReuse;
    #output = throwFilterOutput;
    /**
     * This instance variable is `true` when this operator is processing a `push`,
     * and is used to disable reuse of cached sizes across rows with the
     * same parent join key value.
     * This is necessary because during a push relationships can be inconsistent
     * due to push communicating changes (which may change multiple Nodes) one
     * Node at a time.
     */
    #inPush = false;
    constructor(input, storage, relationshipName, parentJoinKey, type) {
        this.#input = input;
        this.#relationshipName = relationshipName;
        this.#input.setFilterOutput(this);
        this.#storage = storage;
        assert(this.#input.getSchema().relationships[relationshipName], `Input schema missing ${relationshipName}`);
        this.#not = type === 'NOT EXISTS';
        this.#parentJoinKey = parentJoinKey;
        // If the parentJoinKey is the primary key, no sense in trying to reuse.
        this.#noSizeReuse = areEqual(parentJoinKey, this.#input.getSchema().primaryKey);
    }
    setFilterOutput(output) {
        this.#output = output;
    }
    filter(node, cleanup) {
        const result = this.#filter(node) && this.#output.filter(node, cleanup);
        if (cleanup) {
            this.#delSize(node);
        }
        return result;
    }
    destroy() {
        this.#input.destroy();
    }
    getSchema() {
        return this.#input.getSchema();
    }
    push(change) {
        assert(!this.#inPush, 'Unexpected re-entrancy');
        this.#inPush = true;
        try {
            switch (change.type) {
                // add, remove and edit cannot change the size of the
                // this.#relationshipName relationship, so simply #pushWithFilter
                case 'add':
                case 'edit': {
                    this.#pushWithFilter(change);
                    return;
                }
                case 'remove': {
                    const size = this.#getSize(change.node);
                    // If size is undefined, this operator has not output
                    // this row before and so it is unnecessary to output a remove for
                    // it.
                    if (size === undefined) {
                        return;
                    }
                    this.#pushWithFilter(change, size);
                    this.#delSize(change.node);
                    return;
                }
                case 'child':
                    // Only add and remove child changes for the
                    // this.#relationshipName relationship, can change the size
                    // of the this.#relationshipName relationship, for other
                    // child changes simply #pushWithFilter
                    if (change.child.relationshipName !== this.#relationshipName ||
                        change.child.change.type === 'edit' ||
                        change.child.change.type === 'child') {
                        this.#pushWithFilter(change);
                        return;
                    }
                    switch (change.child.change.type) {
                        case 'add': {
                            let size = this.#getSize(change.node);
                            if (size !== undefined) {
                                size++;
                                this.#setSize(change.node, size);
                            }
                            else {
                                size = this.#fetchSize(change.node);
                            }
                            if (size === 1) {
                                if (this.#not) {
                                    // Since the add child change currently being processed is not
                                    // pushed to output, the added child needs to be excluded from
                                    // the remove being pushed to output (since the child has
                                    // never been added to the output).
                                    this.#output.push({
                                        type: 'remove',
                                        node: {
                                            row: change.node.row,
                                            relationships: {
                                                ...change.node.relationships,
                                                [this.#relationshipName]: () => [],
                                            },
                                        },
                                    }, this);
                                }
                                else {
                                    this.#output.push({
                                        type: 'add',
                                        node: change.node,
                                    }, this);
                                }
                            }
                            else {
                                this.#pushWithFilter(change, size);
                            }
                            return;
                        }
                        case 'remove': {
                            let size = this.#getSize(change.node);
                            if (size !== undefined) {
                                assert(size > 0);
                                size--;
                                this.#setSize(change.node, size);
                            }
                            else {
                                size = this.#fetchSize(change.node);
                            }
                            if (size === 0) {
                                if (this.#not) {
                                    this.#output.push({
                                        type: 'add',
                                        node: change.node,
                                    }, this);
                                }
                                else {
                                    // Since the remove child change currently being processed is
                                    // not pushed to output, the removed child needs to be added to
                                    // the remove being pushed to output.
                                    this.#output.push({
                                        type: 'remove',
                                        node: {
                                            row: change.node.row,
                                            relationships: {
                                                ...change.node.relationships,
                                                [this.#relationshipName]: () => [
                                                    change.child.change.node,
                                                ],
                                            },
                                        },
                                    }, this);
                                }
                            }
                            else {
                                this.#pushWithFilter(change, size);
                            }
                            return;
                        }
                    }
                    return;
                default:
                    unreachable(change);
            }
        }
        finally {
            this.#inPush = false;
        }
    }
    /**
     * Returns whether or not the node's this.#relationshipName
     * relationship passes the exist/not exists filter condition.
     * If the optional `size` is passed it is used.
     * Otherwise, if there is a stored size for the row it is used.
     * Otherwise the size is computed by streaming the node's
     * relationship with this.#relationshipName (this computed size is also
     * stored).
     */
    #filter(node, size) {
        const exists = (size ?? this.#getOrFetchSize(node)) > 0;
        return this.#not ? !exists : exists;
    }
    /**
     * Pushes a change if this.#filter is true for its row.
     */
    #pushWithFilter(change, size) {
        if (this.#filter(change.node, size)) {
            this.#output.push(change, this);
        }
    }
    #getSize(node) {
        return this.#storage.get(this.#makeSizeStorageKey(node));
    }
    #setSize(node, size) {
        this.#storage.set(this.#makeSizeStorageKey(node), size);
    }
    #delSize(node) {
        this.#storage.del(this.#makeSizeStorageKey(node));
    }
    #getOrFetchSize(node) {
        const size = this.#getSize(node);
        if (size !== undefined) {
            return size;
        }
        return this.#fetchSize(node);
    }
    #fetchSize(node) {
        if (!this.#noSizeReuse && !this.#inPush) {
            const cachedSizeEntry = first(this.#storage.scan({
                prefix: this.#makeSizeStorageKeyPrefix(node),
            }));
            if (cachedSizeEntry !== undefined) {
                this.#setSize(node, cachedSizeEntry[1]);
                return cachedSizeEntry[1];
            }
        }
        const relationship = node.relationships[this.#relationshipName];
        assert(relationship);
        let size = 0;
        for (const _relatedNode of relationship()) {
            size++;
        }
        this.#setSize(node, size);
        return size;
    }
    #makeSizeStorageKeyPrefix(node) {
        return `row/${this.#noSizeReuse
            ? ''
            : JSON.stringify(this.#getKeyValues(node, this.#parentJoinKey))}/`;
    }
    #makeSizeStorageKey(node) {
        return `${this.#makeSizeStorageKeyPrefix(node)}${JSON.stringify(this.#getKeyValues(node, this.#input.getSchema().primaryKey))}`;
    }
    #getKeyValues(node, def) {
        const values = [];
        for (const key of def) {
            values.push(normalizeUndefined(node.row[key]));
        }
        return values;
    }
}
//# sourceMappingURL=exists.js.map