import { createSnapshotPropValue } from "../ImmutableSnapshot.js";
import { decodeEdgeKey } from "../KeyCodec.js";
import { matchGlob } from "../../utils/matchGlob.js";
import NodePropertyProjection from "../NodePropertyProjection.js";
import QueryNodePatternIndex from "./QueryNodePatternIndex.js";
function toFilterSet(list) {
    return Array.isArray(list) && list.length > 0 ? new Set(list) : null;
}
function isKeyVisible(key, redactSet, exposeSet) {
    if (redactSet !== null && redactSet.has(key)) {
        return false;
    }
    if (exposeSet !== null && !exposeSet.has(key)) {
        return false;
    }
    return true;
}
function emptyNodeSnapshot(id) {
    return Object.freeze({
        id,
        props: Object.freeze({}),
        edgesOut: Object.freeze([]),
        edgesIn: Object.freeze([]),
    });
}
function nodeMatches(pattern, nodeId) {
    if (typeof pattern === 'string') {
        return matchGlob(pattern, nodeId);
    }
    return pattern.some((entry) => matchGlob(entry, nodeId));
}
function liveElements(state, kind) {
    const alive = kind === 'node' ? state.nodeAlive : state.edgeAlive;
    return {
        *[Symbol.iterator]() {
            for (const element of alive.entries.keys()) {
                if (alive.contains(element)) {
                    yield element;
                }
            }
        },
    };
}
function providerDirection(direction) {
    return direction === 'outgoing' ? 'out' : 'in';
}
function labelMatches(label, filter) {
    return filter === undefined || label === filter;
}
function nodeEdgeEntry(edge) {
    return Object.freeze({
        nodeId: edge.neighborId,
        label: edge.label,
    });
}
function asyncIterableFrom(source) {
    return {
        [Symbol.asyncIterator]() {
            const iterator = source[Symbol.iterator]();
            return {
                next() {
                    return Promise.resolve(iterator.next());
                },
            };
        },
    };
}
export default class StateQueryReadModel {
    stateHash;
    #state;
    #visibility;
    #neighborProvider;
    #nodePatternIndex;
    constructor(params) {
        this.#state = params.state;
        this.stateHash = params.stateHash;
        this.#visibility = params.visibility;
        this.#neighborProvider = params.neighborProvider ?? null;
        this.#nodePatternIndex = new QueryNodePatternIndex(this.#state.nodeAlive);
    }
    nodes(request) {
        return asyncIterableFrom(this.#visibleNodes(request));
    }
    async *neighbors(nodeId, options) {
        if (!this.#isVisibleNode(nodeId)) {
            return;
        }
        if (this.#neighborProvider !== null) {
            yield* this.#indexedNeighbors(nodeId, options);
            return;
        }
        yield* this.#linearNeighbors(nodeId, options);
    }
    nodeProps(nodeId) {
        if (!this.#isVisibleNode(nodeId)) {
            return Promise.resolve(null);
        }
        const redactSet = toFilterSet(this.#visibility.redact);
        const exposeSet = toFilterSet(this.#visibility.expose);
        const props = {};
        for (const record of NodePropertyProjection.forNode(this.#state, nodeId)) {
            const propKey = record.key.toString();
            if (isKeyVisible(propKey, redactSet, exposeSet)) {
                props[propKey] = createSnapshotPropValue(record.value.toPropValue());
            }
        }
        return Promise.resolve(Object.freeze(props));
    }
    async *#indexedNeighbors(nodeId, options) {
        if (this.#neighborProvider === null) {
            return;
        }
        const neighbors = await this.#neighborProvider.getNeighbors(nodeId, providerDirection(options.direction), options.label === undefined ? undefined : { labels: new Set([options.label]) });
        for (const edge of neighbors) {
            if (this.#isVisibleNode(edge.neighborId)) {
                yield nodeEdgeEntry(edge);
            }
        }
    }
    #linearNeighbors(nodeId, options) {
        return asyncIterableFrom(this.#visibleLinearNeighbors(nodeId, options));
    }
    *#visibleNodes(request) {
        for (const nodeId of this.#nodePatternIndex.liveCandidates(request.pattern)) {
            if (this.#isVisibleNode(nodeId) && nodeMatches(request.pattern, nodeId)) {
                yield emptyNodeSnapshot(nodeId);
            }
        }
    }
    *#visibleLinearNeighbors(nodeId, options) {
        for (const edgeKey of liveElements(this.#state, 'edge')) {
            const entry = this.#linearNeighborForEdge(edgeKey, nodeId, options);
            if (entry !== null) {
                yield entry;
            }
        }
    }
    #linearNeighborForEdge(edgeKey, nodeId, options) {
        const edge = decodeEdgeKey(edgeKey);
        const neighborId = options.direction === 'outgoing'
            ? this.#outgoingNeighbor(nodeId, edge)
            : this.#incomingNeighbor(nodeId, edge);
        if (neighborId === null ||
            !labelMatches(edge.label, options.label) ||
            !this.#isVisibleNode(neighborId)) {
            return null;
        }
        return Object.freeze({ nodeId: neighborId, label: edge.label });
    }
    #outgoingNeighbor(nodeId, edge) {
        return edge.from === nodeId ? edge.to : null;
    }
    #incomingNeighbor(nodeId, edge) {
        return edge.to === nodeId ? edge.from : null;
    }
    #isVisibleNode(nodeId) {
        return this.#state.nodeAlive.contains(nodeId) &&
            nodeMatches(this.#visibility.match, nodeId);
    }
}
