import { assert, unreachable } from "../../../shared/src/asserts.js";
import { BTreeSet } from "../../../shared/src/btree-set.js";
import { hasOwn } from "../../../shared/src/has-own.js";
import { once } from "../../../shared/src/iterables.js";
import { assertOrderingIncludesPK } from "../builder/builder.js";
import { createPredicate, transformFilters, } from "../builder/filter.js";
import { constraintMatchesPrimaryKey, constraintMatchesRow, primaryKeyConstraintFromFilters, } from "./constraint.js";
import { compareValues, makeComparator, valuesEqual, } from "./data.js";
import { filterPush } from "./filter-push.js";
import {} from "./operator.js";
/**
 * A `MemorySource` is a source that provides data to the pipeline from an
 * in-memory data source.
 *
 * This data is kept in sorted order as downstream pipelines will always expect
 * the data they receive from `pull` to be in sorted order.
 */
export class MemorySource {
    #tableName;
    #columns;
    #primaryKey;
    #primaryIndexSort;
    #indexes = new Map();
    #connections = [];
    #overlay;
    constructor(tableName, columns, primaryKey, primaryIndexData) {
        this.#tableName = tableName;
        this.#columns = columns;
        this.#primaryKey = primaryKey;
        this.#primaryIndexSort = primaryKey.map(k => [k, 'asc']);
        const comparator = makeBoundComparator(this.#primaryIndexSort);
        this.#indexes.set(JSON.stringify(this.#primaryIndexSort), {
            comparator,
            data: primaryIndexData ?? new BTreeSet(comparator),
            usedBy: new Set(),
        });
        assertOrderingIncludesPK(this.#primaryIndexSort, this.#primaryKey);
    }
    // Mainly for tests.
    getSchemaInfo() {
        return {
            tableName: this.#tableName,
            columns: this.#columns,
            primaryKey: this.#primaryKey,
        };
    }
    fork() {
        const primaryIndex = this.#getPrimaryIndex();
        return new MemorySource(this.#tableName, this.#columns, this.#primaryKey, primaryIndex.data.clone());
    }
    get data() {
        return this.#getPrimaryIndex().data;
    }
    #getSchema(connection) {
        return {
            tableName: this.#tableName,
            columns: this.#columns,
            primaryKey: this.#primaryKey,
            sort: connection.sort,
            system: 'client',
            relationships: {},
            isHidden: false,
            compareRows: connection.compareRows,
        };
    }
    connect(sort, filters, splitEditKeys) {
        const transformedFilters = transformFilters(filters);
        const input = {
            getSchema: () => schema,
            fetch: req => this.#fetch(req, connection),
            cleanup: req => this.#cleanup(req, connection),
            setOutput: output => {
                connection.output = output;
            },
            destroy: () => {
                this.#disconnect(input);
            },
            fullyAppliedFilters: !transformedFilters.conditionsRemoved,
        };
        const connection = {
            input,
            output: undefined,
            sort,
            splitEditKeys,
            compareRows: makeComparator(sort),
            filters: transformedFilters.filters
                ? {
                    condition: transformedFilters.filters,
                    predicate: createPredicate(transformedFilters.filters),
                }
                : undefined,
        };
        const schema = this.#getSchema(connection);
        assertOrderingIncludesPK(sort, this.#primaryKey);
        this.#connections.push(connection);
        return input;
    }
    #disconnect(input) {
        const idx = this.#connections.findIndex(c => c.input === input);
        assert(idx !== -1, 'Connection not found');
        this.#connections.splice(idx, 1);
        // TODO: We used to delete unused indexes here. But in common cases like
        // navigating into issue detail pages it caused a ton of constantly
        // building and destroying indexes.
        //
        // Perhaps some intelligent LRU or something is needed here but for now,
        // the opposite extreme of keeping all indexes for the lifetime of the
        // page seems better.
    }
    #getPrimaryIndex() {
        const index = this.#indexes.get(JSON.stringify(this.#primaryIndexSort));
        assert(index, 'Primary index not found');
        return index;
    }
    #getOrCreateIndex(sort, usedBy) {
        const key = JSON.stringify(sort);
        const index = this.#indexes.get(key);
        // Future optimization could use existing index if it's the same just sorted
        // in reverse of needed.
        if (index) {
            index.usedBy.add(usedBy);
            return index;
        }
        const comparator = makeBoundComparator(sort);
        // When creating these synchronously becomes a problem, a few options:
        // 1. Allow users to specify needed indexes up front
        // 2. Create indexes in a different thread asynchronously (this would require
        // modifying the BTree to be able to be passed over structured-clone, or using
        // a different library.)
        // 3. We could even theoretically do (2) on multiple threads and then merge the
        // results!
        const data = new BTreeSet(comparator);
        // I checked, there's no special path for adding data in bulk faster.
        // The constructor takes an array, but it just calls add/set over and over.
        for (const row of this.#getPrimaryIndex().data) {
            data.add(row);
        }
        const newIndex = { comparator, data, usedBy: new Set([usedBy]) };
        this.#indexes.set(key, newIndex);
        return newIndex;
    }
    // For unit testing that we correctly clean up indexes.
    getIndexKeys() {
        return [...this.#indexes.keys()];
    }
    *#fetch(req, from) {
        const callingConnectionIndex = this.#connections.indexOf(from);
        assert(callingConnectionIndex !== -1, 'Output not found');
        const conn = this.#connections[callingConnectionIndex];
        const { sort: requestedSort, compareRows } = conn;
        const connectionComparator = (r1, r2) => compareRows(r1, r2) * (req.reverse ? -1 : 1);
        const pkConstraint = primaryKeyConstraintFromFilters(conn.filters?.condition, this.#primaryKey);
        // The primary key constraint will be more limiting than the constraint
        // so swap out to that if it exists.
        const fetchOrPkConstraint = pkConstraint ?? req.constraint;
        // If there is a constraint, we need an index sorted by it first.
        const indexSort = [];
        if (fetchOrPkConstraint) {
            for (const key of Object.keys(fetchOrPkConstraint)) {
                indexSort.push([key, 'asc']);
            }
        }
        // For the special case of constraining by PK, we don't need to worry about
        // any requested sort since there can only be one result. Otherwise we also
        // need the index sorted by the requested sort.
        if (this.#primaryKey.length > 1 ||
            !fetchOrPkConstraint ||
            !constraintMatchesPrimaryKey(fetchOrPkConstraint, this.#primaryKey)) {
            indexSort.push(...requestedSort);
        }
        const index = this.#getOrCreateIndex(indexSort, from);
        const { data, comparator: compare } = index;
        const indexComparator = (r1, r2) => compare(r1, r2) * (req.reverse ? -1 : 1);
        const startAt = req.start?.row;
        // If there is a constraint, we want to start our scan at the first row that
        // matches the constraint. But because the next OrderPart can be `desc`,
        // it's not true that {[constraintKey]: constraintValue} is the first
        // matching row. Because in that case, the other fields will all be
        // `undefined`, and in Zero `undefined` is always less than any other value.
        // So if the second OrderPart is descending then `undefined` values will
        // actually be the *last* row. We need a way to stay "start at the first row
        // with this constraint value". RowBound with the corresponding compareBound
        // comparator accomplishes this. The right thing is probably to teach the
        // btree library to support this concept.
        let scanStart;
        if (fetchOrPkConstraint) {
            scanStart = {};
            for (const [key, dir] of indexSort) {
                if (hasOwn(fetchOrPkConstraint, key)) {
                    scanStart[key] = fetchOrPkConstraint[key];
                }
                else {
                    if (req.reverse) {
                        scanStart[key] = dir === 'asc' ? maxValue : minValue;
                    }
                    else {
                        scanStart[key] = dir === 'asc' ? minValue : maxValue;
                    }
                }
            }
        }
        else {
            scanStart = startAt;
        }
        const rowsIterable = generateRows(data, scanStart, req.reverse);
        const withOverlay = generateWithOverlay(startAt, pkConstraint ? once(rowsIterable) : rowsIterable, 
        // use `req.constraint` here and not `fetchOrPkConstraint` since `fetchOrPkConstraint` could be the
        // primary key constraint. The primary key constraint comes from filters and is acting as a filter
        // rather than as the fetch constraint.
        req.constraint, this.#overlay, callingConnectionIndex, 
        // Use indexComparator, generateWithOverlayInner has a subtle dependency
        // on this.  Since generateWithConstraint is done after
        // generateWithOverlay, the generator consumed by generateWithOverlayInner
        // does not end when the constraint stops matching and so the final
        // check to yield an add overlay if not yet yielded is not reached.
        // However, using the indexComparator the add overlay will be less than
        // the first row that does not match the constraint, and so any
        // not yet yielded add overlay will be yielded when the first row
        // not matching the constraint is reached.
        indexComparator, conn.filters?.predicate);
        const withConstraint = generateWithConstraint(generateWithStart(withOverlay, req.start, connectionComparator), 
        // we use `req.constraint` and not `fetchOrPkConstraint` here because we need to
        // AND the constraint with what could have been the primary key constraint
        req.constraint);
        yield* conn.filters
            ? generateWithFilter(withConstraint, conn.filters.predicate)
            : withConstraint;
    }
    #cleanup(req, connection) {
        return this.#fetch(req, connection);
    }
    push(change) {
        for (const _ of this.genPush(change)) {
            // Nothing to do.
        }
    }
    *genPush(change) {
        const primaryIndex = this.#getPrimaryIndex();
        const { data } = primaryIndex;
        const exists = (row) => data.has(row);
        const setOverlay = (o) => (this.#overlay = o);
        const writeChange = (c) => this.#writeChange(c);
        if (change.type === 'set') {
            const existing = data.get(change.row);
            if (existing !== undefined) {
                change = {
                    type: 'edit',
                    row: change.row,
                    oldRow: existing,
                };
            }
            else {
                change = {
                    type: 'add',
                    row: change.row,
                };
            }
        }
        yield* genPushAndWriteWithSplitEdit(this.#connections, change, exists, setOverlay, writeChange);
    }
    #writeChange(change) {
        for (const { data } of this.#indexes.values()) {
            switch (change.type) {
                case 'add': {
                    const added = data.add(change.row);
                    // must succeed since we checked has() above.
                    assert(added);
                    break;
                }
                case 'remove': {
                    const removed = data.delete(change.row);
                    // must succeed since we checked has() above.
                    assert(removed);
                    break;
                }
                case 'edit': {
                    // TODO: We could see if the PK (form the index tree's perspective)
                    // changed and if not we could use set.
                    // We cannot just do `set` with the new value since the `oldRow` might
                    // not map to the same entry as the new `row` in the index btree.
                    const removed = data.delete(change.oldRow);
                    // must succeed since we checked has() above.
                    assert(removed);
                    data.add(change.row);
                    break;
                }
                default:
                    unreachable(change);
            }
        }
    }
}
function* generateWithConstraint(it, constraint) {
    for (const node of it) {
        if (constraint && !constraintMatchesRow(constraint, node.row)) {
            break;
        }
        yield node;
    }
}
function* generateWithFilter(it, filter) {
    for (const node of it) {
        if (filter(node.row)) {
            yield node;
        }
    }
}
export function* genPushAndWriteWithSplitEdit(connections, change, exists, setOverlay, writeChange) {
    let shouldSplitEdit = false;
    if (change.type === 'edit') {
        for (const { splitEditKeys } of connections) {
            if (splitEditKeys) {
                for (const key of splitEditKeys) {
                    if (!valuesEqual(change.row[key], change.oldRow[key])) {
                        shouldSplitEdit = true;
                        break;
                    }
                }
            }
        }
    }
    if (change.type === 'edit' && shouldSplitEdit) {
        yield* genPushAndWrite(connections, {
            type: 'remove',
            row: change.oldRow,
        }, exists, setOverlay, writeChange);
        yield* genPushAndWrite(connections, {
            type: 'add',
            row: change.row,
        }, exists, setOverlay, writeChange);
    }
    else {
        yield* genPushAndWrite(connections, change, exists, setOverlay, writeChange);
    }
}
function* genPushAndWrite(connections, change, exists, setOverlay, writeChange) {
    for (const x of genPush(connections, change, exists, setOverlay)) {
        yield x;
    }
    writeChange(change);
}
function* genPush(connections, change, exists, setOverlay) {
    switch (change.type) {
        case 'add':
            assert(!exists(change.row), () => `Row already exists ${stringify(change)}`);
            break;
        case 'remove':
            assert(exists(change.row), () => `Row not found ${stringify(change)}`);
            break;
        case 'edit':
            assert(exists(change.oldRow), () => `Row not found ${stringify(change)}`);
            break;
        default:
            unreachable(change);
    }
    for (const [outputIndex, { output, filters, input }] of connections.entries()) {
        if (output) {
            setOverlay({ outputIndex, change });
            const outputChange = change.type === 'edit'
                ? {
                    type: change.type,
                    oldNode: {
                        row: change.oldRow,
                        relationships: {},
                    },
                    node: {
                        row: change.row,
                        relationships: {},
                    },
                }
                : {
                    type: change.type,
                    node: {
                        row: change.row,
                        relationships: {},
                    },
                };
            filterPush(outputChange, output, input, filters?.predicate);
            yield;
        }
    }
    setOverlay(undefined);
}
export function* generateWithStart(nodes, start, compare) {
    if (!start) {
        yield* nodes;
        return;
    }
    let started = false;
    for (const node of nodes) {
        if (!started) {
            if (start.basis === 'at') {
                if (compare(node.row, start.row) >= 0) {
                    started = true;
                }
            }
            else if (start.basis === 'after') {
                if (compare(node.row, start.row) > 0) {
                    started = true;
                }
            }
        }
        if (started) {
            yield node;
        }
    }
}
/**
 * Takes an iterator and overlay.
 * Splices the overlay into the iterator at the correct position.
 *
 * @param startAt - if there is a lower bound to the stream. If the lower bound of the stream
 * is above the overlay, the overlay will be skipped.
 * @param rows - the stream into which the overlay should be spliced
 * @param constraint - constraint that was applied to the rowIterator and should
 * also be applied to the overlay.
 * @param overlay - the overlay values to splice in
 * @param compare - the comparator to use to find the position for the overlay
 */
export function* generateWithOverlay(startAt, rows, constraint, overlay, connectionIndex, compare, filterPredicate) {
    let overlayToApply = undefined;
    if (overlay && connectionIndex <= overlay.outputIndex) {
        overlayToApply = overlay;
    }
    const overlays = computeOverlays(startAt, constraint, overlayToApply, compare, filterPredicate);
    yield* generateWithOverlayInner(rows, overlays, compare);
}
function computeOverlays(startAt, constraint, overlay, compare, filterPredicate) {
    let overlays = {
        add: undefined,
        remove: undefined,
    };
    switch (overlay?.change.type) {
        case 'add':
            overlays = {
                add: overlay.change.row,
                remove: undefined,
            };
            break;
        case 'remove':
            overlays = {
                add: undefined,
                remove: overlay.change.row,
            };
            break;
        case 'edit':
            overlays = {
                add: overlay.change.row,
                remove: overlay.change.oldRow,
            };
            break;
    }
    if (startAt) {
        overlays = overlaysForStartAt(overlays, startAt, compare);
    }
    if (constraint) {
        overlays = overlaysForConstraint(overlays, constraint);
    }
    if (filterPredicate) {
        overlays = overlaysForFilterPredicate(overlays, filterPredicate);
    }
    return overlays;
}
export { overlaysForStartAt as overlaysForStartAtForTest };
function overlaysForStartAt({ add, remove }, startAt, compare) {
    const undefinedIfBeforeStartAt = (row) => row === undefined || compare(row, startAt) < 0 ? undefined : row;
    return {
        add: undefinedIfBeforeStartAt(add),
        remove: undefinedIfBeforeStartAt(remove),
    };
}
export { overlaysForConstraint as overlaysForConstraintForTest };
function overlaysForConstraint({ add, remove }, constraint) {
    const undefinedIfDoesntMatchConstraint = (row) => row === undefined || !constraintMatchesRow(constraint, row)
        ? undefined
        : row;
    return {
        add: undefinedIfDoesntMatchConstraint(add),
        remove: undefinedIfDoesntMatchConstraint(remove),
    };
}
function overlaysForFilterPredicate({ add, remove }, filterPredicate) {
    const undefinedIfDoesntMatchFilter = (row) => row === undefined || !filterPredicate(row) ? undefined : row;
    return {
        add: undefinedIfDoesntMatchFilter(add),
        remove: undefinedIfDoesntMatchFilter(remove),
    };
}
export function* generateWithOverlayInner(rowIterator, overlays, compare) {
    let addOverlayYielded = false;
    let removeOverlaySkipped = false;
    for (const row of rowIterator) {
        if (!addOverlayYielded && overlays.add) {
            const cmp = compare(overlays.add, row);
            if (cmp < 0) {
                addOverlayYielded = true;
                yield { row: overlays.add, relationships: {} };
            }
        }
        if (!removeOverlaySkipped && overlays.remove) {
            const cmp = compare(overlays.remove, row);
            if (cmp === 0) {
                removeOverlaySkipped = true;
                continue;
            }
        }
        yield { row, relationships: {} };
    }
    if (!addOverlayYielded && overlays.add) {
        yield { row: overlays.add, relationships: {} };
    }
}
const minValue = Symbol('min-value');
const maxValue = Symbol('max-value');
function makeBoundComparator(sort) {
    return (a, b) => {
        // Hot! Do not use destructuring
        for (const entry of sort) {
            const key = entry[0];
            const cmp = compareBounds(a[key], b[key]);
            if (cmp !== 0) {
                return entry[1] === 'asc' ? cmp : -cmp;
            }
        }
        return 0;
    };
}
function compareBounds(a, b) {
    if (a === b) {
        return 0;
    }
    if (a === minValue) {
        return -1;
    }
    if (b === minValue) {
        return 1;
    }
    if (a === maxValue) {
        return 1;
    }
    if (b === maxValue) {
        return -1;
    }
    return compareValues(a, b);
}
function* generateRows(data, scanStart, reverse) {
    yield* data[reverse ? 'valuesFromReversed' : 'valuesFrom'](scanStart);
}
export function stringify(change) {
    return JSON.stringify(change, (_, v) => typeof v === 'bigint' ? v.toString() : v);
}
//# sourceMappingURL=memory-source.js.map