var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
var __asyncValues = (this && this.__asyncValues) || function (o) {
    if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
    var m = o[Symbol.asyncIterator], i;
    return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
    function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
    function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
};
import { flatten } from '../utils/object.js';
import { createLevelDatabase, LevelWrapper } from './level-wrapper.js';
/**
 * A LevelDB implementation for indexing the messages stored in the DWN.
 */
export class IndexLevel {
    constructor(config) {
        this.config = Object.assign({ createLevelDatabase }, config);
        this.db = new LevelWrapper(Object.assign(Object.assign({}, this.config), { valueEncoding: 'utf8' }));
    }
    open() {
        return __awaiter(this, void 0, void 0, function* () {
            return this.db.open();
        });
    }
    close() {
        return __awaiter(this, void 0, void 0, function* () {
            return this.db.close();
        });
    }
    /**
     * Adds indexes for a specific data/object/content.
     * @param dataId ID of the data/object/content being indexed.
     */
    put(dataId, indexes, options) {
        return __awaiter(this, void 0, void 0, function* () {
            indexes = flatten(indexes);
            const operations = [];
            // create an index entry for each property in the `indexes`
            for (const propertyName in indexes) {
                const propertyValue = indexes[propertyName];
                // NOTE: appending data ID after (property + value) serves two purposes:
                // 1. creates a unique entry of the property-value pair per data/object
                // 2. when we need to delete all indexes of a given data ID (`delete()`), we can reconstruct the index keys and remove the indexes efficiently
                //
                // example keys (\u0000 is just shown for illustration purpose because it is the delimiter used to join the string segments below):
                // 'interface\u0000"Records"\u0000bafyreigs3em7lrclhntzhgvkrf75j2muk6e7ypq3lrw3ffgcpyazyw6pry'
                // 'method\u0000"Write"\u0000bafyreigs3em7lrclhntzhgvkrf75j2muk6e7ypq3lrw3ffgcpyazyw6pry'
                // 'schema\u0000"http://ud4kyzon6ugxn64boz7v"\u0000bafyreigs3em7lrclhntzhgvkrf75j2muk6e7ypq3lrw3ffgcpyazyw6pry'
                // 'dataCid\u0000"bafkreic3ie3cxsblp46vn3ofumdnwiqqk4d5ah7uqgpcn6xps4skfvagze"\u0000bafyreigs3em7lrclhntzhgvkrf75j2muk6e7ypq3lrw3ffgcpyazyw6pry'
                // 'dateCreated\u0000"2023-05-25T18:23:29.425008Z"\u0000bafyreigs3em7lrclhntzhgvkrf75j2muk6e7ypq3lrw3ffgcpyazyw6pry'
                const key = this.join(propertyName, this.encodeValue(propertyValue), dataId);
                operations.push({ type: 'put', key, value: dataId });
            }
            // create a reverse lookup entry for data ID -> its indexes
            // this is for indexes deletion (`delete()`): so that given the data ID, we are able to delete all its indexes
            // we can consider putting this info in a different data partition if this ever becomes more complex/confusing
            operations.push({ type: 'put', key: `__${dataId}__indexes`, value: JSON.stringify(indexes) });
            yield this.db.batch(operations, options);
        });
    }
    query(filter, options) {
        var _a;
        return __awaiter(this, void 0, void 0, function* () {
            // Note: We have an array of Promises in order to support OR (anyOf) matches when given a list of accepted values for a property
            const propertyNameToPromises = {};
            // Do a separate DB query for each property in `filter`
            // We will find the union of these many individual queries later.
            for (const propertyName in filter) {
                const propertyFilter = filter[propertyName];
                if (typeof propertyFilter === 'object') {
                    if (Array.isArray(propertyFilter)) {
                        // `propertyFilter` is a AnyOfFilter
                        // Support OR matches by querying for each values separately,
                        // then adding them to the promises associated with `propertyName`
                        propertyNameToPromises[propertyName] = [];
                        for (const propertyValue of new Set(propertyFilter)) {
                            const exactMatchesPromise = this.findExactMatches(propertyName, propertyValue, options);
                            propertyNameToPromises[propertyName].push(exactMatchesPromise);
                        }
                    }
                    else {
                        // `propertyFilter` is a `RangeFilter`
                        const rangeMatchesPromise = this.findRangeMatches(propertyName, propertyFilter, options);
                        propertyNameToPromises[propertyName] = [rangeMatchesPromise];
                    }
                }
                else {
                    // propertyFilter is an EqualFilter, meaning it is a non-object primitive type
                    const exactMatchesPromise = this.findExactMatches(propertyName, propertyFilter, options);
                    propertyNameToPromises[propertyName] = [exactMatchesPromise];
                }
            }
            // map of ID of all data/object -> list of missing property matches
            // if count of missing property matches is 0, it means the data/object fully matches the filter
            const missingPropertyMatchesForId = {};
            // Resolve promises and find the union of results for each individual propertyName DB query
            const matchedIDs = [];
            for (const [propertyName, promises] of Object.entries(propertyNameToPromises)) {
                // acting as an OR match for the property, any of the promises returning a match will be treated as a property match
                for (const promise of promises) {
                    for (const dataId of yield promise) {
                        // if first time seeing a property matching for the data/object, record all properties needing a match to track progress
                        (_a = missingPropertyMatchesForId[dataId]) !== null && _a !== void 0 ? _a : (missingPropertyMatchesForId[dataId] = new Set([...Object.keys(filter)]));
                        missingPropertyMatchesForId[dataId].delete(propertyName);
                        if (missingPropertyMatchesForId[dataId].size === 0) {
                            // full filter match, add it to return list
                            matchedIDs.push(dataId);
                        }
                    }
                }
            }
            return matchedIDs;
        });
    }
    delete(dataId, options) {
        return __awaiter(this, void 0, void 0, function* () {
            const serializedIndexes = yield this.db.get(`__${dataId}__indexes`, options);
            if (!serializedIndexes) {
                return;
            }
            const indexes = JSON.parse(serializedIndexes);
            // delete all indexes associated with the data of the given ID
            const ops = [];
            for (const propertyName in indexes) {
                const propertyValue = indexes[propertyName];
                const key = this.join(propertyName, this.encodeValue(propertyValue), dataId);
                ops.push({ type: 'del', key });
            }
            ops.push({ type: 'del', key: `__${dataId}__indexes` });
            yield this.db.batch(ops, options);
        });
    }
    clear() {
        return __awaiter(this, void 0, void 0, function* () {
            return this.db.clear();
        });
    }
    /**
     * @returns IDs of data that matches the exact property and value.
     */
    findExactMatches(propertyName, propertyValue, options) {
        var _a, e_1, _b, _c;
        return __awaiter(this, void 0, void 0, function* () {
            const propertyValuePrefix = this.join(propertyName, this.encodeValue(propertyValue));
            const iteratorOptions = {
                gt: propertyValuePrefix
            };
            const matches = [];
            try {
                for (var _d = true, _e = __asyncValues(this.db.iterator(iteratorOptions, options)), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
                    _c = _f.value;
                    _d = false;
                    const [key, dataId] = _c;
                    if (!key.startsWith(propertyValuePrefix)) {
                        break;
                    }
                    matches.push(dataId);
                }
            }
            catch (e_1_1) { e_1 = { error: e_1_1 }; }
            finally {
                try {
                    if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
                }
                finally { if (e_1) throw e_1.error; }
            }
            return matches;
        });
    }
    /**
     * @returns IDs of data that matches the range filter.
     */
    findRangeMatches(propertyName, rangeFilter, options) {
        var _a, e_2, _b, _c;
        return __awaiter(this, void 0, void 0, function* () {
            const iteratorOptions = {};
            for (const comparator in rangeFilter) {
                const comparatorName = comparator;
                iteratorOptions[comparatorName] = this.join(propertyName, this.encodeValue(rangeFilter[comparatorName]));
            }
            // if there is no lower bound specified (`gt` or `gte`), we need to iterate from the upper bound,
            // so that we will iterate over all the matches before hitting mismatches.
            if (iteratorOptions.gt === undefined && iteratorOptions.gte === undefined) {
                iteratorOptions.reverse = true;
            }
            const matches = [];
            try {
                for (var _d = true, _e = __asyncValues(this.db.iterator(iteratorOptions, options)), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
                    _c = _f.value;
                    _d = false;
                    const [key, dataId] = _c;
                    // immediately stop if we arrive at an index entry for a different property
                    if (!key.startsWith(propertyName)) {
                        break;
                    }
                    matches.push(dataId);
                }
            }
            catch (e_2_1) { e_2 = { error: e_2_1 }; }
            finally {
                try {
                    if (!_d && !_a && (_b = _e.return)) yield _b.call(_e);
                }
                finally { if (e_2) throw e_2.error; }
            }
            if ('lte' in rangeFilter) {
                // When `lte` is used, we must also query the exact match explicitly because the exact match will not be included in the iterator above.
                // This is due to the extra data (CID) appended to the (property + value) key prefix, e.g.
                // key = 'dateCreated\u0000"2023-05-25T11:22:33.000000Z"\u0000bafyreigs3em7lrclhntzhgvkrf75j2muk6e7ypq3lrw3ffgcpyazyw6pry'
                // the value would be considered greater than { lte: `dateCreated\u0000"2023-05-25T11:22:33.000000Z"` } used in the iterator options,
                // thus would not be included in the iterator even though we'd like it to be.
                for (const dataId of yield this.findExactMatches(propertyName, rangeFilter.lte, options)) {
                    matches.push(dataId);
                }
            }
            return matches;
        });
    }
    encodeValue(value) {
        if (typeof value === 'string') {
            // We can't just `JSON.stringify` as that'll affect the sort order of strings.
            // For example, `'\x00'` becomes `'\\u0000'`.
            return `"${value}"`;
        }
        return String(value);
    }
    /**
     * Joins the given values using the `\x00` (\u0000) character.
     */
    join(...values) {
        return values.join(`\x00`);
    }
    dump() {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function* () {
            console.group('db');
            yield ((_b = (_a = this.db)['dump']) === null || _b === void 0 ? void 0 : _b.call(_a));
            console.groupEnd();
        });
    }
}
//# sourceMappingURL=index-level.js.map