"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.LocalHub = void 0;
const path_1 = require("path");
const core_bentley_1 = require("@itwin/core-bentley");
const core_common_1 = require("@itwin/core-common");
const BackendHubAccess_1 = require("./BackendHubAccess");
const BriefcaseManager_1 = require("./BriefcaseManager");
const IModelDb_1 = require("./IModelDb");
const IModelJsFs_1 = require("./IModelJsFs");
const SQLiteDb_1 = require("./SQLiteDb");
/**
 * A "local" mock for IModelHub to provide access to a single iModel. Used by HubMock.
 * @internal
 */
class LocalHub {
    rootDir;
    iTwinId;
    iModelId;
    iModelName;
    description;
    _hubDb;
    _nextBriefcaseId = core_common_1.BriefcaseIdValue.FirstValid;
    _latestChangesetIndex = 0;
    get latestChangesetIndex() { return this._latestChangesetIndex; }
    constructor(rootDir, arg) {
        this.rootDir = rootDir;
        this.iTwinId = arg.iTwinId;
        this.iModelId = arg.iModelId;
        this.iModelName = arg.iModelName;
        this.description = arg.description;
        this.cleanup();
        IModelJsFs_1.IModelJsFs.recursiveMkDirSync(this.rootDir);
        IModelJsFs_1.IModelJsFs.mkdirSync(this.changesetDir);
        IModelJsFs_1.IModelJsFs.mkdirSync(this.checkpointDir);
        const db = this._hubDb = new SQLiteDb_1.SQLiteDb();
        db.createDb(this.mockDbName);
        db.executeSQL("CREATE TABLE briefcases(id INTEGER PRIMARY KEY NOT NULL,user TEXT NOT NULL,alias TEXT NOT NULL,assigned INTEGER DEFAULT 1)");
        db.executeSQL("CREATE TABLE timeline(csIndex INTEGER PRIMARY KEY NOT NULL,csId TEXT NOT NULL UNIQUE,description TEXT,user TEXT,size BIGINT,type INTEGER,pushDate TEXT,briefcaseId INTEGER,uncompressedSize BIGINT,\
                   FOREIGN KEY(briefcaseId) REFERENCES briefcases(id))");
        db.executeSQL("CREATE TABLE checkpoints(csIndex INTEGER PRIMARY KEY NOT NULL)");
        db.executeSQL("CREATE TABLE versions(name TEXT PRIMARY KEY NOT NULL,csIndex TEXT,FOREIGN KEY(csIndex) REFERENCES timeline(csIndex))");
        db.executeSQL("CREATE TABLE locks(id INTEGER PRIMARY KEY NOT NULL,level INTEGER NOT NULL,lastExclusiveReleaseChangesetIndex INTEGER,lastSharedReleaseChangesetIndex INTEGER,briefcaseId INTEGER)");
        db.executeSQL("CREATE TABLE sharedLocks(lockId INTEGER NOT NULL,briefcaseId INTEGER NOT NULL,PRIMARY KEY(lockId,briefcaseId))");
        db.executeSQL("CREATE INDEX LockIdx ON locks(briefcaseId)");
        db.executeSQL("CREATE INDEX SharedLockIdx ON sharedLocks(briefcaseId)");
        db.saveChanges();
        const version0Root = `${rootDir}_version0`;
        const version0 = arg.version0 ?? (0, path_1.join)(version0Root, "version0.bim");
        if (!arg.version0) { // if they didn't supply a version0 file, create a blank one.
            IModelJsFs_1.IModelJsFs.recursiveMkDirSync(version0Root);
            IModelJsFs_1.IModelJsFs.removeSync(version0);
            IModelDb_1.SnapshotDb.createEmpty(version0, { rootSubject: { name: arg.description ?? arg.iModelName } }).close();
        }
        const path = this.uploadCheckpoint({ changesetIndex: 0, localFile: version0 });
        if (!arg.version0)
            IModelJsFs_1.IModelJsFs.removeSync(version0);
        const nativeDb = IModelDb_1.IModelDb.openDgnDb({ path }, core_bentley_1.OpenMode.ReadWrite);
        try {
            nativeDb.setITwinId(this.iTwinId);
            nativeDb.setIModelId(this.iModelId);
            nativeDb.saveChanges();
            nativeDb.deleteAllTxns(); // necessary before resetting briefcaseId
            nativeDb.resetBriefcaseId(core_common_1.BriefcaseIdValue.Unassigned);
            nativeDb.saveLocalValue(IModelDb_1.BriefcaseLocalValue.NoLocking, arg.noLocks ? "true" : undefined);
            nativeDb.saveChanges();
        }
        finally {
            nativeDb.closeFile();
        }
    }
    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    get db() { return this._hubDb; } // eslint-disable-line @typescript-eslint/naming-convention
    get changesetDir() { return (0, path_1.join)(this.rootDir, "changesets"); }
    get checkpointDir() { return (0, path_1.join)(this.rootDir, "checkpoints"); }
    get mockDbName() { return (0, path_1.join)(this.rootDir, "localHub.db"); }
    /** Acquire the next available briefcaseId and assign it to the supplied user */
    acquireNewBriefcaseId(user, alias) {
        const db = this.db;
        const newId = this._nextBriefcaseId++;
        db.withSqliteStatement("INSERT INTO briefcases(id,user,alias) VALUES (?,?,?)", (stmt) => {
            stmt.bindInteger(1, newId);
            stmt.bindString(2, user);
            stmt.bindString(3, alias ?? `${user} (${newId})`);
            const rc = stmt.step();
            if (core_bentley_1.DbResult.BE_SQLITE_DONE !== rc)
                throw new core_common_1.IModelError(rc, "can't insert briefcaseId in mock database");
        });
        db.saveChanges();
        return newId;
    }
    /** Release a briefcaseId */
    releaseBriefcaseId(id) {
        const db = this.db;
        this.releaseAllLocks({ briefcaseId: id, changesetIndex: 0 });
        db.withSqliteStatement("UPDATE briefcases SET assigned=0 WHERE id=?", (stmt) => {
            stmt.bindInteger(1, id);
            const rc = stmt.step();
            if (core_bentley_1.DbResult.BE_SQLITE_DONE !== rc)
                throw new core_common_1.IModelError(rc, `briefcaseId ${id} was not reserved`);
        });
        db.saveChanges();
    }
    /** Get an array of all of the currently assigned Briefcases */
    getBriefcases(onlyAssigned = true) {
        const briefcases = [];
        let sql = "SELECT id,user,alias,assigned FROM briefcases";
        if (onlyAssigned)
            sql += " WHERE assigned=1";
        this.db.withSqliteStatement(sql, (stmt) => {
            while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step()) {
                briefcases.push({
                    id: stmt.getValueInteger(0),
                    user: stmt.getValueString(1),
                    alias: stmt.getValueString(2),
                    assigned: stmt.getValueInteger(3) !== 0,
                });
            }
        });
        return briefcases;
    }
    /** Get an array of all of the currently assigned BriefcaseIds for a user */
    getBriefcaseIds(user) {
        const briefcases = [];
        this.db.withSqliteStatement("SELECT id FROM briefcases WHERE user=? AND assigned=1", (stmt) => {
            stmt.bindString(1, user);
            while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step())
                briefcases.push(stmt.getValueInteger(0));
        });
        return briefcases;
    }
    getBriefcase(id) {
        return this.db.withSqliteStatement("SELECT user,alias,assigned FROM briefcases WHERE id=?", (stmt) => {
            stmt.bindInteger(1, id);
            if (core_bentley_1.DbResult.BE_SQLITE_ROW !== stmt.step())
                throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, "no briefcase with that id");
            return { id, user: stmt.getValueString(0), alias: stmt.getValueString(1), assigned: stmt.getValueInteger(2) !== 0 };
        });
    }
    getChangesetFileName(index) {
        return (0, path_1.join)(this.changesetDir, `changeset-${index}`);
    }
    /** Add a changeset to the timeline
     * @return the changesetIndex of the added changeset
     */
    addChangeset(changeset) {
        const stats = IModelJsFs_1.IModelJsFs.lstatSync(changeset.pathname);
        if (!stats)
            throw new Error(`cannot read changeset file ${changeset.pathname}`);
        const parentIndex = this.getChangesetById(changeset.parentId).index;
        if (parentIndex !== this.latestChangesetIndex)
            throw new core_common_1.IModelError(core_bentley_1.IModelStatus.InvalidParent, "changeset parent is not latest changeset");
        this.getBriefcase(changeset.briefcaseId); // throws if invalid id
        const db = this.db;
        changeset.index = this._latestChangesetIndex + 1;
        db.withSqliteStatement("INSERT INTO timeline(csIndex,csId,description,size,type,pushDate,user,briefcaseId,uncompressedSize) VALUES (?,?,?,?,?,?,?,?,?)", (stmt) => {
            stmt.bindInteger(1, changeset.index);
            stmt.bindString(2, changeset.id);
            stmt.bindString(3, changeset.description);
            stmt.bindInteger(4, stats.size);
            stmt.bindInteger(5, changeset.changesType ?? 0);
            stmt.bindString(6, changeset.pushDate ?? new Date().toISOString());
            stmt.bindString(7, changeset.userCreated ?? "");
            stmt.bindInteger(8, changeset.briefcaseId);
            stmt.bindInteger(9, changeset.uncompressedSize ?? 0);
            const rc = stmt.step();
            if (core_bentley_1.DbResult.BE_SQLITE_DONE !== rc)
                throw new core_common_1.IModelError(rc, "can't insert changeset into mock db");
        });
        this._latestChangesetIndex = changeset.index; // only change this after insert succeeds
        db.saveChanges();
        IModelJsFs_1.IModelJsFs.copySync(changeset.pathname, this.getChangesetFileName(changeset.index));
        return changeset.index;
    }
    getIndexFromChangeset(changeset) {
        return changeset.index ?? this.getChangesetIndex(changeset.id);
    }
    /** Get the index of a changeset by its Id */
    getChangesetIndex(id) {
        if (id === "")
            return 0;
        return this.db.withPreparedSqliteStatement("SELECT csIndex FROM timeline WHERE csId=?", (stmt) => {
            stmt.bindString(1, id);
            const rc = stmt.step();
            if (core_bentley_1.DbResult.BE_SQLITE_ROW !== rc)
                throw new core_common_1.IModelError(rc, `changeset ${id} not found`);
            return stmt.getValueInteger(0);
        });
    }
    /** Get the properties of a changeset by its Id */
    getChangesetById(id) {
        return this.getChangesetByIndex(this.getChangesetIndex(id));
    }
    getPreviousIndex(index) {
        return this.db.withPreparedSqliteStatement("SELECT max(csIndex) FROM timeline WHERE csIndex<?", (stmt) => {
            stmt.bindInteger(1, index);
            const rc = stmt.step();
            if (core_bentley_1.DbResult.BE_SQLITE_ROW !== rc)
                throw new core_common_1.IModelError(rc, `cannot get previous index`);
            return stmt.getValueInteger(0);
        });
    }
    getParentId(index) {
        if (index === 0)
            return "";
        return this.db.withPreparedSqliteStatement("SELECT csId FROM timeline WHERE csIndex=?", (stmt) => {
            stmt.bindInteger(1, this.getPreviousIndex(index));
            stmt.step();
            return stmt.getValueString(0);
        });
    }
    /** Get the properties of a changeset by its index */
    getChangesetByIndex(index) {
        if (index <= 0)
            return { id: "", changesType: 0, description: "version0", parentId: "", briefcaseId: 0, pushDate: "", userCreated: "", index: 0, size: 0, uncompressedSize: 0 };
        return this.db.withPreparedSqliteStatement("SELECT description,size,type,pushDate,user,csId,briefcaseId,uncompressedSize FROM timeline WHERE csIndex=?", (stmt) => {
            stmt.bindInteger(1, index);
            const rc = stmt.step();
            if (core_bentley_1.DbResult.BE_SQLITE_ROW !== rc)
                throw new core_common_1.IModelError(rc, `changeset at index ${index} not found`);
            return {
                description: stmt.getValueString(0),
                size: stmt.getValueDouble(1),
                changesType: stmt.getValueInteger(2),
                pushDate: stmt.getValueString(3),
                userCreated: stmt.getValueString(4),
                id: stmt.getValueString(5),
                briefcaseId: stmt.getValueInteger(6),
                index,
                parentId: this.getParentId(index),
                uncompressedSize: stmt.getValueInteger(7),
            };
        });
    }
    getLatestChangeset() {
        return this.getChangesetByIndex(this.latestChangesetIndex);
    }
    getChangesetId(index) {
        return this.getChangesetByIndex(index).id;
    }
    /** Get an array of changesets starting with first to last, by index */
    queryChangesets(range) {
        const changesets = [];
        const first = range?.first ?? 0;
        const last = range?.end ?? this.latestChangesetIndex;
        this.db.withPreparedSqliteStatement("SELECT csIndex FROM timeline WHERE csIndex>=? AND csIndex<=? ORDER BY csIndex", (stmt) => {
            stmt.bindInteger(1, first);
            stmt.bindInteger(2, last);
            while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step())
                changesets.push(this.getChangesetByIndex(stmt.getValueInteger(0)));
        });
        return changesets;
    }
    /** Name a version */
    addNamedVersion(arg) {
        const db = this.db;
        db.withSqliteStatement("INSERT INTO versions(name,csIndex) VALUES (?,?)", (stmt) => {
            stmt.bindString(1, arg.versionName);
            stmt.bindInteger(2, arg.csIndex);
            const rc = stmt.step();
            if (core_bentley_1.DbResult.BE_SQLITE_DONE !== rc)
                throw new core_common_1.IModelError(rc, "can't insert named version");
        });
        db.saveChanges();
    }
    /** Delete a named version */
    deleteNamedVersion(versionName) {
        const db = this.db;
        db.withSqliteStatement("DELETE FROM versions WHERE name=?", (stmt) => {
            stmt.bindString(1, versionName);
            const rc = stmt.step();
            if (core_bentley_1.DbResult.BE_SQLITE_DONE !== rc)
                throw new core_common_1.IModelError(rc, "can't delete named version");
        });
        db.saveChanges();
    }
    /** find the changeset for a named version */
    findNamedVersion(versionName) {
        const index = this.db.withSqliteStatement("SELECT csIndex FROM versions WHERE name=?", (stmt) => {
            stmt.bindString(1, versionName);
            const rc = stmt.step();
            if (core_bentley_1.DbResult.BE_SQLITE_ROW !== rc)
                throw new core_common_1.IModelError(core_bentley_1.IModelStatus.NotFound, `Named version ${versionName} not found`);
            return stmt.getValueInteger(0);
        });
        return this.getChangesetByIndex(index);
    }
    checkpointNameFromIndex(csIndex) {
        return `checkpoint-${csIndex}`;
    }
    /** "upload" a checkpoint */
    uploadCheckpoint(arg) {
        const db = this.db;
        db.withSqliteStatement("INSERT INTO checkpoints(csIndex) VALUES (?)", (stmt) => {
            stmt.bindInteger(1, arg.changesetIndex);
            const res = stmt.step();
            if (core_bentley_1.DbResult.BE_SQLITE_DONE !== res)
                throw new core_common_1.IModelError(res, "can't insert checkpoint into mock db");
        });
        db.saveChanges();
        const outName = (0, path_1.join)(this.checkpointDir, this.checkpointNameFromIndex(arg.changesetIndex));
        IModelJsFs_1.IModelJsFs.copySync(arg.localFile, outName);
        return outName;
    }
    /** Get an array of the indexes for a range of checkpoints */
    getCheckpoints(range) {
        const first = range?.first ?? 0;
        const last = range?.end ?? this.latestChangesetIndex;
        const checkpoints = [];
        this.db.withSqliteStatement("SELECT csIndex FROM checkpoints WHERE csIndex>=? AND csIndex<=? ORDER BY csIndex", (stmt) => {
            stmt.bindInteger(1, first);
            stmt.bindInteger(2, last);
            while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step())
                checkpoints.push(stmt.getValueInteger(0));
        });
        return checkpoints;
    }
    /** Find the checkpoint that is no newer than a changesetIndex */
    queryPreviousCheckpoint(changesetIndex) {
        if (changesetIndex <= 0)
            return 0;
        return this.db.withSqliteStatement("SELECT max(csIndex) FROM checkpoints WHERE csIndex <= ? ", (stmt) => {
            stmt.bindInteger(1, changesetIndex);
            const res = stmt.step();
            if (core_bentley_1.DbResult.BE_SQLITE_ROW !== res)
                throw new core_common_1.IModelError(res, "can't get previous checkpoint");
            return stmt.getValueInteger(0);
        });
    }
    /** "download" a checkpoint */
    downloadCheckpoint(arg) {
        const index = this.getIndexFromChangeset(arg.changeset);
        const prev = this.queryPreviousCheckpoint(index);
        IModelJsFs_1.IModelJsFs.copySync((0, path_1.join)(this.checkpointDir, this.checkpointNameFromIndex(prev)), arg.targetFile);
        const changeset = this.getChangesetByIndex(index);
        return { index, id: changeset.id };
    }
    copyChangeset(arg) {
        IModelJsFs_1.IModelJsFs.copySync(this.getChangesetFileName(arg.index), arg.pathname);
        return arg;
    }
    /** "download" a changeset */
    downloadChangeset(arg) {
        const cs = this.getChangesetByIndex(arg.index);
        const csProps = { ...cs, pathname: (0, path_1.join)(arg.targetDir, cs.id), index: arg.index };
        return this.copyChangeset(csProps);
    }
    /** "download" all the changesets in a given range */
    downloadChangesets(arg) {
        const cSets = this.queryChangesets(arg.range);
        for (const cs of cSets) {
            cs.pathname = (0, path_1.join)(arg.targetDir, cs.id);
            this.copyChangeset(cs);
        }
        return cSets;
    }
    querySharedLockHolders(elementId) {
        return this.db.withPreparedSqliteStatement("SELECT briefcaseId FROM sharedLocks WHERE lockId=?", (stmt) => {
            stmt.bindId(1, elementId);
            const briefcases = new Set();
            while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step())
                briefcases.add(stmt.getValueInteger(0));
            return briefcases;
        });
    }
    queryAllLocks(briefcaseId) {
        this.getBriefcase(briefcaseId); // throws if briefcaseId invalid.
        const locks = [];
        this.db.withPreparedSqliteStatement("SELECT id FROM locks WHERE briefcaseId=?", (stmt) => {
            stmt.bindInteger(1, briefcaseId);
            while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step())
                locks.push({ id: stmt.getValueId(0), state: core_common_1.LockState.Exclusive });
        });
        this.db.withPreparedSqliteStatement("SELECT lockId FROM sharedLocks WHERE briefcaseId=?", (stmt) => {
            stmt.bindInteger(1, briefcaseId);
            while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step())
                locks.push({ id: stmt.getValueId(0), state: core_common_1.LockState.Shared });
        });
        return locks;
    }
    queryLockStatus(elementId) {
        return this.db.withPreparedSqliteStatement("SELECT lastSharedReleaseChangesetIndex,lastExclusiveReleaseChangesetIndex,level,briefcaseId FROM locks WHERE id=?", (stmt) => {
            stmt.bindId(1, elementId);
            const rc = stmt.step();
            if (core_bentley_1.DbResult.BE_SQLITE_ROW !== rc)
                return { state: core_common_1.LockState.None };
            const lastSharedCsVal = stmt.getValue(0);
            const lastExclusiveCsVal = stmt.getValue(1);
            const lock = {
                lastSharedReleaseChangesetIndex: lastSharedCsVal.isNull ? undefined : lastSharedCsVal.getInteger(),
                lastExclusiveReleaseChangesetIndex: lastExclusiveCsVal.isNull ? undefined : lastExclusiveCsVal.getInteger(),
                state: stmt.getValueInteger(2),
            };
            switch (lock.state) {
                case core_common_1.LockState.None:
                    return lock;
                case core_common_1.LockState.Exclusive:
                    return { ...lock, briefcaseId: stmt.getValueInteger(3) };
                case core_common_1.LockState.Shared:
                    return { ...lock, sharedBy: this.querySharedLockHolders(elementId) };
                default:
                    throw new Error("illegal lock state");
            }
        });
    }
    doesBriefcaseRequirePullBeforeLock(currStatus, props, briefcase) {
        if (props.state === core_common_1.LockState.None)
            return false;
        const briefcaseChangesetIndex = this.getIndexFromChangeset(briefcase.changeset);
        const exclusiveIndexIsNewer = currStatus.lastExclusiveReleaseChangesetIndex !== undefined &&
            currStatus.lastExclusiveReleaseChangesetIndex > briefcaseChangesetIndex;
        if (props.state === core_common_1.LockState.Shared) {
            // To acquire a shared lock, the briefcase must at least at the latest exclusive changeset index
            return exclusiveIndexIsNewer;
        }
        else {
            (0, core_bentley_1.assert)(props.state === core_common_1.LockState.Exclusive);
            const sharedIndexIsNewer = currStatus.lastSharedReleaseChangesetIndex !== undefined &&
                currStatus.lastSharedReleaseChangesetIndex > briefcaseChangesetIndex;
            // To acquire the exclusive lock the briefcase must be at least at the greater of the two last-indexes
            return exclusiveIndexIsNewer || sharedIndexIsNewer;
        }
    }
    addSharedLockRecord(lockId, briefcaseId) {
        this.db.withPreparedSqliteStatement("INSERT INTO sharedLocks(lockId,briefcaseId) VALUES(?,?) ON CONFLICT(lockId,briefcaseId) DO NOTHING", (stmt) => {
            stmt.bindId(1, lockId);
            stmt.bindInteger(2, briefcaseId);
            const rc = stmt.step();
            if (rc !== core_bentley_1.DbResult.BE_SQLITE_DONE)
                throw new core_common_1.IModelError(rc, "cannot insert shared lock");
        });
    }
    reserveLock(currStatus, props, briefcase) {
        if (this.doesBriefcaseRequirePullBeforeLock(currStatus, props, briefcase)) {
            throw new core_common_1.IModelError(core_bentley_1.IModelHubStatus.PullIsRequired, "pull is required to obtain lock");
        }
        const wantShared = props.state === core_common_1.LockState.Shared;
        if (wantShared && (currStatus.state === core_common_1.LockState.Exclusive))
            throw new Error("cannot acquire shared lock because an exclusive lock is already held");
        this.db.withPreparedSqliteStatement("INSERT INTO locks(id,level,briefcaseId) VALUES(?,?,?) ON CONFLICT(id) DO UPDATE SET briefcaseId=excluded.briefcaseId,level=excluded.level", (stmt) => {
            stmt.bindId(1, props.id);
            stmt.bindInteger(2, props.state);
            stmt.bindValue(3, wantShared ? undefined : briefcase.briefcaseId);
            const rc = stmt.step();
            if (rc !== core_bentley_1.DbResult.BE_SQLITE_DONE)
                throw new core_common_1.IModelError(rc, "cannot insert lock");
        });
        if (wantShared) {
            this.addSharedLockRecord(props.id, briefcase.briefcaseId);
        }
    }
    downgradeExclusiveLockToShared(lockId, briefcaseId) {
        this.db.withPreparedSqliteStatement("UPDATE locks SET level=1,briefcaseId=NULL WHERE id=? AND briefcaseId=? AND level=2", (stmt) => {
            stmt.bindId(1, lockId);
            stmt.bindInteger(2, briefcaseId);
            const rc = stmt.step();
            if (rc !== core_bentley_1.DbResult.BE_SQLITE_DONE)
                throw new core_common_1.IModelError(rc, "can't downgrade lock");
        });
        // Verify that the above actually updated a row. If it didn't, something has gone wrong with the downgrade.
        // This shouldn't happen.
        this.db.withPreparedSqliteStatement("SELECT CHANGES()", (stmt) => {
            stmt.step();
            if (stmt.getValueInteger(0) !== 1)
                throw new core_common_1.IModelError(core_bentley_1.IModelHubStatus.LockOwnedByAnotherBriefcase, "lock not held by this briefcase or lock is not exclusive");
        });
        this.addSharedLockRecord(lockId, briefcaseId);
    }
    clearLock(id) {
        this.db.withPreparedSqliteStatement("UPDATE locks SET level=0,briefcaseId=NULL WHERE id=?", (stmt) => {
            stmt.bindId(1, id);
            const rc = stmt.step();
            if (rc !== core_bentley_1.DbResult.BE_SQLITE_DONE)
                throw new core_common_1.IModelError(rc, "can't release lock");
        });
    }
    updateLockExclusiveChangeset(id, index) {
        if (index <= 0)
            return;
        this.db.withPreparedSqliteStatement("UPDATE locks SET lastExclusiveReleaseChangesetIndex=? WHERE id=?", (stmt) => {
            stmt.bindInteger(1, index);
            stmt.bindId(2, id);
            const rc = stmt.step();
            if (rc !== core_bentley_1.DbResult.BE_SQLITE_DONE)
                throw new core_common_1.IModelError(rc, "can't update lock exclusive changeSetId");
        });
    }
    updateLockSharedChangeset(id, index) {
        if (index <= 0)
            return;
        this.db.withPreparedSqliteStatement("UPDATE locks SET lastSharedReleaseChangesetIndex=? WHERE id=?", (stmt) => {
            stmt.bindInteger(1, index);
            stmt.bindId(2, id);
            const rc = stmt.step();
            if (rc !== core_bentley_1.DbResult.BE_SQLITE_DONE)
                throw new core_common_1.IModelError(rc, "can't update lock shared changeSetId");
        });
    }
    requestLock(props, briefcase) {
        if (props.state === core_common_1.LockState.None)
            throw new Error("cannot request lock for LockState.None");
        this.getBriefcase(briefcase.briefcaseId); // throws if briefcaseId invalid.
        const lockStatus = this.queryLockStatus(props.id);
        switch (lockStatus.state) {
            case core_common_1.LockState.None:
                return this.reserveLock(lockStatus, props, briefcase);
            case core_common_1.LockState.Shared:
                if (props.state === core_common_1.LockState.Shared) {
                    if (!lockStatus.sharedBy.has(briefcase.briefcaseId))
                        this.reserveLock(lockStatus, props, briefcase);
                }
                else {
                    // if requester is the only one holding a shared lock, "upgrade" the lock from shared to exclusive
                    if (lockStatus.sharedBy.size > 1 || !lockStatus.sharedBy.has(briefcase.briefcaseId)) {
                        const id = lockStatus.sharedBy.values().next().value; // eslint-disable-line @typescript-eslint/no-non-null-assertion
                        throw new BackendHubAccess_1.LockConflict(id, this.getBriefcase(id).alias, "shared lock is held");
                    }
                    this.removeSharedLock(props.id, briefcase.briefcaseId);
                    this.reserveLock(this.queryLockStatus(props.id), props, briefcase);
                }
                return;
            case core_common_1.LockState.Exclusive:
                if (lockStatus.briefcaseId !== briefcase.briefcaseId)
                    throw new BackendHubAccess_1.LockConflict(lockStatus.briefcaseId, this.getBriefcase(lockStatus.briefcaseId).alias, "exclusive lock is already held");
        }
    }
    removeSharedLock(lockId, briefcaseId) {
        this.db.withPreparedSqliteStatement("DELETE FROM sharedLocks WHERE lockId=? AND briefcaseId=?", (stmt) => {
            stmt.bindId(1, lockId);
            stmt.bindInteger(2, briefcaseId);
            const rc = stmt.step();
            if (rc !== core_bentley_1.DbResult.BE_SQLITE_DONE)
                throw new core_common_1.IModelError(rc, "can't remove shared lock");
        });
    }
    abandonLock(props, briefcase) {
        // When abandoning (but not when releasing), props.state indicates which
        // state we're abandoning _to_. Specifically, an Exclusive lock can be
        // abandoned to either Shared or None.
        // It makes no sense to abandon a lock _to_ the Exclusive state.
        if (props.state === core_common_1.LockState.Exclusive)
            throw new core_common_1.IModelError(core_bentley_1.IModelHubStatus.InvalidArgumentError, "must specify Shared or None when abandoning a lock");
        const lockId = props.id;
        const previousLockStatus = this.queryLockStatus(lockId);
        switch (previousLockStatus.state) {
            case core_common_1.LockState.None:
                throw new core_common_1.IModelError(core_bentley_1.IModelHubStatus.LockDoesNotExist, "lock not held");
            case core_common_1.LockState.Exclusive:
                if (previousLockStatus.briefcaseId !== briefcase.briefcaseId)
                    throw new core_common_1.IModelError(core_bentley_1.IModelHubStatus.LockOwnedByAnotherBriefcase, "lock not held by this briefcase");
                if (props.state === core_common_1.LockState.Shared) {
                    // Exclusive -> Shared
                    this.downgradeExclusiveLockToShared(lockId, briefcase.briefcaseId);
                }
                else {
                    // Exclusive -> None
                    this.clearLock(lockId);
                }
                break;
            case core_common_1.LockState.Shared:
                if (!previousLockStatus.sharedBy.has(briefcase.briefcaseId))
                    throw new core_common_1.IModelError(core_bentley_1.IModelHubStatus.LockDoesNotExist, "shared lock not held by this briefcase");
                this.removeSharedLock(lockId, briefcase.briefcaseId);
                if (previousLockStatus.sharedBy.size === 1)
                    this.clearLock(lockId);
        }
    }
    releaseLock(props, briefcase) {
        // Unlike abandonLock above, release always releases the lock all the way back to the None state.
        // So we ignore props.state. There's no such thing as a "downgrade" here.
        const lockId = props.id;
        const previousLockStatus = this.queryLockStatus(lockId);
        switch (previousLockStatus.state) {
            case core_common_1.LockState.None:
                throw new core_common_1.IModelError(core_bentley_1.IModelHubStatus.LockDoesNotExist, "lock not held");
            case core_common_1.LockState.Exclusive:
                if (previousLockStatus.briefcaseId !== briefcase.briefcaseId)
                    throw new core_common_1.IModelError(core_bentley_1.IModelHubStatus.LockOwnedByAnotherBriefcase, "lock not held by this briefcase");
                this.updateLockExclusiveChangeset(lockId, briefcase.changesetIndex);
                this.clearLock(lockId);
                break;
            case core_common_1.LockState.Shared:
                if (!previousLockStatus.sharedBy.has(briefcase.briefcaseId))
                    throw new core_common_1.IModelError(core_bentley_1.IModelHubStatus.LockDoesNotExist, "shared lock not held by this briefcase");
                this.updateLockSharedChangeset(lockId, briefcase.changesetIndex);
                this.removeSharedLock(lockId, briefcase.briefcaseId);
                if (previousLockStatus.sharedBy.size === 1)
                    this.clearLock(lockId);
        }
    }
    /** Acquire a set of locks. If any lock cannot be acquired, no locks are acquired  */
    acquireLocks(locks, briefcase) {
        try {
            for (const lock of locks)
                this.requestLock({ id: lock[0], state: lock[1] }, briefcase);
            this.db.saveChanges(); // only after all locks have been acquired
        }
        catch (err) {
            this.db.abandonChanges(); // abandon all locks that may have been acquired
            throw err;
        }
    }
    acquireLock(props, briefcase) {
        const locks = new Map();
        locks.set(props.id, props.state);
        this.acquireLocks(locks, briefcase);
    }
    releaseLocks(locks, arg) {
        for (const props of locks)
            this.releaseLock(props, arg);
        this.db.saveChanges();
    }
    abandonLocks(locks, arg) {
        for (const props of locks)
            this.abandonLock({ id: props[0], state: props[1] }, arg);
        this.db.saveChanges();
    }
    releaseAllLocks(arg) {
        const locks = this.queryAllLocks(arg.briefcaseId);
        this.releaseLocks(locks, arg);
    }
    abandonAllLocks(arg) {
        const locks = new Map();
        this.queryAllLocks(arg.briefcaseId).forEach(lock => locks.set(lock.id, core_common_1.LockState.None));
        this.abandonLocks(locks, arg);
    }
    countTable(tableName) {
        return this.db.withSqliteStatement(`SELECT count(*) from ${tableName}`, (stmt) => {
            stmt.step();
            return stmt.getValueInteger(0);
        });
    }
    // for debugging
    countSharedLocks() { return this.countTable("sharedLocks"); }
    // for debugging
    countLocks() { return this.countTable("locks"); }
    // for debugging
    queryAllSharedLocks() {
        const locks = [];
        this.db.withPreparedSqliteStatement("SELECT lockId,briefcaseId FROM sharedLocks", (stmt) => {
            while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step())
                locks.push({ id: stmt.getValueId(0), briefcaseId: stmt.getValueInteger(1) });
        });
        return locks;
    }
    // for debugging
    queryLocks() {
        const locks = [];
        this.db.withPreparedSqliteStatement("SELECT id,level,lastExclusiveReleaseChangesetIndex,lastSharedReleaseChangesetIndex,briefcaseId FROM locks", (stmt) => {
            while (core_bentley_1.DbResult.BE_SQLITE_ROW === stmt.step())
                locks.push({
                    id: stmt.getValueId(0),
                    level: stmt.getValueInteger(1),
                    lastExclusiveReleaseChangesetIndex: stmt.getValue(2).isNull ? undefined : stmt.getValueInteger(2),
                    lastSharedReleaseChangesetIndex: stmt.getValue(3).isNull ? undefined : stmt.getValueInteger(3),
                    briefcaseId: stmt.getValue(4).isNull ? undefined : stmt.getValueInteger(4),
                });
        });
        return locks;
    }
    removeDir(dirName) {
        if (IModelJsFs_1.IModelJsFs.existsSync(dirName)) {
            IModelJsFs_1.IModelJsFs.purgeDirSync(dirName);
            IModelJsFs_1.IModelJsFs.rmdirSync(dirName);
        }
    }
    cleanup() {
        if (this._hubDb) {
            this._hubDb.closeDb();
            this._hubDb = undefined;
        }
        try {
            this.removeDir(BriefcaseManager_1.BriefcaseManager.getIModelPath(this.iModelId));
            this.removeDir(this.rootDir);
        }
        catch {
            // eslint-disable-next-line no-console
            console.log(`ERROR: test left an iModel open for [${this.iModelName}]. LocalMock cannot clean up - make sure you call imodel.close() in your test`);
        }
    }
}
exports.LocalHub = LocalHub;
//# sourceMappingURL=LocalHub.js.map