"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PartialChangeUnifier = exports.ChangeUnifierCache = void 0;
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module ECDb
 */
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const SQLiteDb_1 = require("./SQLiteDb");
/** @beta */
var ChangeUnifierCache;
(function (ChangeUnifierCache) {
    /**
     * Creates an in-memory cache backed by a `Map`.
     * Fast, but may exhaust memory for very large changesets.
     * @returns An [[ChangeCache]] backed by an in-memory `Map`.
     * @beta
     */
    function createInMemoryCache() {
        return new InMemoryCache();
    }
    ChangeUnifierCache.createInMemoryCache = createInMemoryCache;
    /**
     * Creates a SQLite-backed cache stored in a temporary SQlite database.
     * Slower than in-memory but useful in handling large changesets.
     * Temporary SQlite database is first created in memory and
     * parts of a temporary database might be flushed to disk if the database becomes large or
     * if SQLite comes under memory pressure.
     * @param bufferedReadInstanceSizeInBytes Read-batch size in bytes (default 10 MB).
     * @returns An [[ChangeCache]] backed by a SQLite temp table.
     * @beta
     */
    function createSqliteBackedCache(bufferedReadInstanceSizeInBytes = 1024 * 1024 * 10) {
        return new SqliteBackedCache(bufferedReadInstanceSizeInBytes);
    }
    ChangeUnifierCache.createSqliteBackedCache = createSqliteBackedCache;
})(ChangeUnifierCache || (exports.ChangeUnifierCache = ChangeUnifierCache = {}));
// ---------------------------------------------------------------------------
// Private: InMemoryCache
// ---------------------------------------------------------------------------
class InMemoryCache {
    _cache = new Map();
    get(key) {
        return this._cache.get(key);
    }
    set(key, value) {
        // Remove undefined meta keys to keep serialised form compact.
        const meta = value.$meta;
        if (meta) {
            Object.keys(meta).forEach((k) => meta[k] === undefined && delete meta[k]);
        }
        this._cache.set(key, value);
    }
    *all() {
        for (const key of Array.from(this._cache.keys()).sort()) {
            const instance = this._cache.get(key);
            if (instance)
                yield instance;
        }
    }
    count() {
        return this._cache.size;
    }
    [Symbol.dispose]() {
        this._cache.clear();
    }
}
// ---------------------------------------------------------------------------
// Private: NativeSqliteBackedInstanceCache
// ---------------------------------------------------------------------------
class SqliteBackedCache {
    bufferedReadInstanceSizeInBytes;
    _cacheTable = `[${core_bentley_1.Guid.createValue()}]`;
    static defaultBufferSize = 1024 * 1024 * 10; // 10 MB
    _db;
    constructor(bufferedReadInstanceSizeInBytes = SqliteBackedCache.defaultBufferSize) {
        this.bufferedReadInstanceSizeInBytes = bufferedReadInstanceSizeInBytes;
        this._db = new SQLiteDb_1.SQLiteDb();
        this._db.openDb("", { skipFileCheck: true, rawSQLite: true, openMode: core_bentley_1.OpenMode.ReadWrite }); // creating temp sqlite db https://sqlite.org/inmemorydb.html#:~:text=Temporary%20Databases,under%20the%20default%20SQLite%20configuration.
        if (bufferedReadInstanceSizeInBytes <= 0)
            throw new Error("bufferedReadInstanceSizeInBytes must be greater than 0");
        this.createTempTable();
    }
    createTempTable() {
        this._db.withSqliteStatement(`CREATE TABLE ${this._cacheTable} ([key] text primary key, [value] text)`, (stmt) => {
            if (core_bentley_1.DbResult.BE_SQLITE_DONE !== stmt.step())
                throw new Error("unable to create temp cache table");
        });
    }
    get(key) {
        return this._db.withPreparedSqliteStatement(`SELECT [value] FROM ${this._cacheTable} WHERE [key]=?`, (stmt) => {
            stmt.reset();
            stmt.clearBindings();
            stmt.bindString(1, key);
            if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
                return JSON.parse(stmt.getValueString(0), core_common_1.Base64EncodedString.reviver);
            return undefined;
        });
    }
    set(key, value) {
        const shallowCopy = Object.assign({}, value);
        this._db.withPreparedSqliteStatement(`INSERT INTO ${this._cacheTable} ([key], [value]) VALUES (?, ?) ON CONFLICT ([key]) DO UPDATE SET [value] = [excluded].[value]`, (stmt) => {
            stmt.reset();
            stmt.clearBindings();
            stmt.bindString(1, key);
            stmt.bindString(2, JSON.stringify(shallowCopy, core_common_1.Base64EncodedString.replacer));
            stmt.step();
        });
    }
    *all() {
        const sql = `
      SELECT JSON_GROUP_ARRAY(JSON([value]))
      FROM (
        SELECT [value],
               SUM(LENGTH([value])) OVER (ORDER BY [key] ROWS UNBOUNDED PRECEDING) / ${this.bufferedReadInstanceSizeInBytes} AS [bucket]
        FROM ${this._cacheTable}
      )
      GROUP BY [bucket]`;
        const stmt = this._db.prepareSqliteStatement(sql);
        try {
            while (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW) {
                const bucket = JSON.parse(stmt.getValueString(0), core_common_1.Base64EncodedString.reviver);
                for (const instance of bucket)
                    yield instance;
            }
        }
        finally {
            stmt[Symbol.dispose]();
        }
    }
    count() {
        return this._db.withPreparedSqliteStatement(`SELECT COUNT(*) FROM ${this._cacheTable}`, (stmt) => {
            stmt.reset();
            if (stmt.step() === core_bentley_1.DbResult.BE_SQLITE_ROW)
                return stmt.getValue(0).getInteger();
            return 0;
        });
    }
    [Symbol.dispose]() {
        this._db.closeDb();
    }
}
// ---------------------------------------------------------------------------
// PartialChangeUnifier
// ---------------------------------------------------------------------------
/**
 * Combines partial EC change instances (one per SQLite table row) into complete
 * instances that span all tables mapping to a single EC entity.
 *
 * The merge key is derived from the `instanceKey` and `stage` stored in `$meta.instanceKey` and `$meta.stage`.
 *
 * **Usage:**
 * ```ts
 * using reader = ChangesetReader.openFile({ fileName, db });
 * using unifier = new PartialChangeUnifier();
 * while (reader.step()) {
 *   unifier.appendFrom(reader);
 * }
 * for (const instance of unifier.instances) { ... }
 * ```
 * @beta
 */
class PartialChangeUnifier {
    _cache;
    constructor(_cache = new InMemoryCache()) {
        this._cache = _cache;
    }
    /** Releases the underlying cache. */
    [Symbol.dispose]() {
        this._cache[Symbol.dispose]();
    }
    /** Number of complete (merged) instances currently accumulated. */
    get instanceCount() {
        return this._cache.count();
    }
    /**
     * Append partial changes from the current reader row and merge them into the cache.
     *
     * @param source Any [ChangeSource]($backend) positioned on a valid row.
     * @beta
     */
    appendFrom(source) {
        if (source.op === "Updated") {
            if (source.inserted)
                this.combine(source.inserted);
            if (source.deleted)
                this.combine(source.deleted);
        }
        else if (source.op === "Inserted" && source.inserted) {
            this.combine(source.inserted);
        }
        else if (source.op === "Deleted" && source.deleted) {
            this.combine(source.deleted);
        }
    }
    /**
     * Iterator over all fully-merged EC change instances.
     * @beta
     */
    get instances() {
        return this._cache.all();
    }
    // ---------------------------------------------------------------------------
    // Private helpers
    // ---------------------------------------------------------------------------
    buildKey(instance) {
        const { instanceKey, stage } = instance.$meta;
        return `${instanceKey}-${stage}`.toLowerCase();
    }
    combine(rhs) {
        const key = this.buildKey(rhs);
        const lhs = this._cache.get(key);
        if (lhs) {
            // Merge data fields — rhs wins for any overlapping columns.
            const { $meta: _rhsMeta, ...rhsData } = rhs;
            Object.assign(lhs, rhsData);
            // Accumulate per-table metadata lists.
            lhs.$meta.tables = [...lhs.$meta.tables, ...rhs.$meta.tables];
            lhs.$meta.changeIndexes = [...lhs.$meta.changeIndexes, ...rhs.$meta.changeIndexes];
            // ECInstanceId will be part of changeset fetchedProps for every table, so we should not include multiple of those in the final list
            lhs.$meta.changeFetchedPropNames = [...new Set([...lhs.$meta.changeFetchedPropNames, ...rhs.$meta.changeFetchedPropNames])];
            this._cache.set(key, lhs);
        }
        else {
            this._cache.set(key, rhs);
        }
    }
}
exports.PartialChangeUnifier = PartialChangeUnifier;
//# sourceMappingURL=PartialChangeUnifier.js.map