/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module iModels
 */
import { DbResult, Id64, IModelStatus, ITwinError, OpenMode } from "@itwin/core-bentley";
import { EcefLocation, EditTxnError, IModelError } from "@itwin/core-common";
import { Range3d } from "@itwin/core-geometry";
import { _activeTxn, _cache, _instanceKeyCache, _nativeDb } from "./internal/Symbols";
/**
 * Status of a bulk element delete operation, mirroring the C++ `BulkDeleteStatus` enum.
 * @beta
 */
export var BulkDeleteElementsStatus;
(function (BulkDeleteElementsStatus) {
    /** All supplied elements were deleted successfully. */
    BulkDeleteElementsStatus[BulkDeleteElementsStatus["Success"] = 0] = "Success";
    /** Some elements were deleted but others could not be, typically due to foreign key constraints on the elements not being deleted. */
    BulkDeleteElementsStatus[BulkDeleteElementsStatus["PartialSuccess"] = 1] = "PartialSuccess";
    /** No elements were deleted. This occurs when the SQL DELETE statement itself fails, e.g. due to a FK constraint violation that prevents the entire batch from being processed. */
    BulkDeleteElementsStatus[BulkDeleteElementsStatus["DeletionFailed"] = 2] = "DeletionFailed";
})(BulkDeleteElementsStatus || (BulkDeleteElementsStatus = {}));
/**
 * Represents an explicit editing transaction for an iModel.
 *
 * An explicit EditTxn lets callers define a deliberate unit of work by choosing when editing
 * starts (`start`) and how it ends (`end()` / `end("save")` or `end("abandon")`). This avoids mixing
 * unrelated edits into one implicit unit of work and makes save/rollback boundaries explicit.
 *
 * Explicit EditTxn instances must be active before mutating operations are performed, regardless of enforcement level.
 * In other words, explicit transaction behavior is independent of `implicitWriteEnforcement`.
 *
 * @see [EditTxn transaction model and migration guidance]($docs/learning/backend/EditTxn.md)
 *
 * *During indirect changes (commit processing):* Use callback args (`indirectEditTxn`) in callbacks like
 * [[Relationship.onRootChangedArg]] and [[Relationship.onDeletedDependencyArg]] that fire during indirect processing.
 *
 * @beta
 */
export class EditTxn {
    /** Controls how writes through the implicit transaction are handled.
     *
     * This does not relax activation requirements for explicit transactions: explicit EditTxn writes
     * must always come from the active EditTxn.
     *
     * - `allow`: allow implicit writes for backwards compatibility, even while an explicit EditTxn is active.
     * - `log`: allow implicit writes but log `implicit-txn-write-disallowed` errors.
     * - `throw`: reject implicit writes with `implicit-txn-write-disallowed`.
     *
     * This is initialized from [[IModelHostOptions.implicitWriteEnforcement]] during [[IModelHost.startup]].
     *
     * Defaults to `allow` for backwards compatibility.
     * @beta
     */
    static implicitWriteEnforcement = "allow";
    /** The iModel this EditTxn may modify. */
    iModel;
    /** Default description passed to [[saveChanges]] when saving this transaction. */
    description;
    /** True if this transaction currently owns the iModel write surface. */
    get isActive() {
        return this.iModel[_activeTxn] === this;
    }
    constructor(iModel, description) {
        this.iModel = iModel;
        this.description = description;
    }
    verifyWriteable() {
        // Explicit transactions must always be active before writing.
        if (!this.isActive)
            EditTxnError.throwError("not-active", "EditTxn is not active", this.iModel.key);
    }
    /** Start this EditTxn, making it the active transaction for the iModel.
     * @throws EditTxnError if this EditTxn is already active, another EditTxn is already active, or if unsaved changes are present.
     */
    start() {
        if (this.isActive)
            EditTxnError.throwError("already-active", "This EditTxn is already active", this.iModel.key);
        const activeTxn = this.iModel[_activeTxn];
        if (undefined !== activeTxn)
            EditTxnError.throwError("already-active", "Cannot start EditTxn while another EditTxn is active", this.iModel.key, activeTxn.description);
        if (this.iModel[_nativeDb].hasUnsavedChanges())
            EditTxnError.throwError("unsaved-changes", "Cannot start a new EditTxn with unsaved changes", this.iModel.key);
        this.iModel[_activeTxn] = this;
    }
    end(mode = "save", args) {
        if (!this.isActive)
            EditTxnError.throwError("not-active", "EditTxn is not active", this.iModel.key);
        if (mode === "save") {
            this.saveChanges(args);
        }
        else {
            this.abandonChanges();
        }
        this.iModel[_activeTxn] = undefined;
    }
    /** Invoked when the owning iModel is closing.
     * The base implementation commits unsaved changes. Subclasses may override to customize how
     * their changes are handled before the iModel closes.
     * @throws EditTxnError if this EditTxn is not active.
     * @throws IModelError if saving on close fails.
     */
    onClose() {
        if (!this.iModel.isReadonly && this.iModel[_nativeDb].hasUnsavedChanges())
            this.saveChanges();
    }
    /** Abandon database changes while keeping this EditTxn active.
     * @throws EditTxnError if this EditTxn is not active.
     */
    abandonChanges() {
        this.verifyWriteable();
        this.iModel.clearCaches({ instanceCachesOnly: true });
        this.iModel[_nativeDb].abandonChanges();
    }
    /** Save changes with additional arguments.
     * @param args Save changes arguments.
     * @throws EditTxnError if this EditTxn is not active.
     * @throws IModelError if the iModel is readonly, if indirect changes are active, or if the native save fails.
     */
    saveChanges(args) {
        this.verifyWriteable();
        const iModel = this.iModel;
        if (iModel.openMode === OpenMode.Readonly)
            throw new IModelError(IModelStatus.ReadOnly, "IModelDb was opened read-only");
        if (iModel.isBriefcaseDb() && iModel.txns.isIndirectChanges)
            throw new IModelError(IModelStatus.BadRequest, "Cannot save changes while in an indirect change scope");
        args ??= this.description;
        const saveArgs = typeof args === "string" ? { description: args } : args;
        const stat = iModel[_nativeDb].saveChanges(JSON.stringify(saveArgs));
        if (DbResult.BE_SQLITE_ERROR_PropagateChangesFailed === stat)
            throw new IModelError(stat, "Could not save changes due to propagation failure.");
        if (DbResult.BE_SQLITE_OK !== stat)
            throw new IModelError(stat, `Could not save changes (${saveArgs.description})`);
    }
    /** Insert a new element into the iModel.
     * @param elProps The properties of the new element.
     * @returns The newly inserted element's Id.
     * @throws EditTxnError if this EditTxn is not active.
     * @throws [[ITwinError]] if insertion fails.
     */
    insertElement(elProps, options) {
        this.verifyWriteable();
        try {
            this.iModel.elements[_cache].delete({
                id: elProps.id,
                federationGuid: elProps.federationGuid,
                code: elProps.code,
            });
            return elProps.id = this.iModel[_nativeDb].insertElement(elProps, options);
        }
        catch (err) {
            err.message = `Error inserting element [${err.message}]`;
            err.metadata = { elProps };
            throw err;
        }
    }
    /** Update an existing element in the iModel.
     * @param elProps The properties to update.
     * @throws EditTxnError if this EditTxn is not active.
     * @throws [[ITwinError]] if update fails.
     */
    updateElement(elProps) {
        this.verifyWriteable();
        try {
            if (elProps.id) {
                this.iModel.elements[_instanceKeyCache].deleteById(elProps.id);
            }
            else {
                this.iModel.elements[_instanceKeyCache].delete({
                    federationGuid: elProps.federationGuid,
                    code: elProps.code,
                });
            }
            this.iModel.elements[_cache].delete({
                id: elProps.id,
                federationGuid: elProps.federationGuid,
                code: elProps.code,
            });
            this.iModel[_nativeDb].updateElement(elProps);
        }
        catch (err) {
            err.message = `Error updating element [${err.message}], id: ${elProps.id}`;
            err.metadata = { elProps };
            throw err;
        }
    }
    /** Delete elements from the iModel.
     * @param ids The Ids of the elements to delete.
     * @throws EditTxnError if this EditTxn is not active.
     * @throws [[ITwinError]] if deletion fails.
     */
    deleteElement(ids) {
        this.verifyWriteable();
        const iModel = this.iModel;
        Id64.toIdSet(ids).forEach((id) => {
            try {
                this.iModel.elements[_cache].delete({ id });
                this.iModel.elements[_instanceKeyCache].deleteById(id);
                iModel[_nativeDb].deleteElement(id);
            }
            catch (err) {
                err.message = `Error deleting element [${err.message}], id: ${id}`;
                err.metadata = { elementId: id };
                throw err;
            }
        });
    }
    /**
     * Delete multiple elements from the iModel.
     * @param ids The ids of the elements to delete. All ids must be well-formed and valid [[Id64String]]s.
     * @param deleteOptions Options for the delete operation.
     * @returns A result object containing information about the deletion operation success and the element ids that failed to delete (if any).
     * @throws [[ITwinError]] if any of the supplied ids are not well-formed/valid [[Id64String]]s.
     * @beta
     */
    deleteElements(ids, deleteOptions) {
        this.verifyWriteable();
        const invalidIds = new Set();
        for (const id of ids) {
            if (!Id64.isValidId64(id))
                invalidIds.add(id);
        }
        if (invalidIds.size > 0)
            ITwinError.throwError({ message: `Invalid element ids: ${Array.from(invalidIds).join(", ")}`, iTwinErrorId: { scope: "imodel", key: "invalid-arguments" } });
        const bulkDeletionResult = this.iModel[_nativeDb].deleteElements(ids, deleteOptions);
        const finalResult = { ...bulkDeletionResult, failedIds: Id64.toIdSet(bulkDeletionResult.failedIds) };
        if (finalResult.status === BulkDeleteElementsStatus.DeletionFailed)
            return finalResult;
        for (const id of ids) {
            if (!finalResult.failedIds.has(id)) {
                this.iModel.elements[_cache].delete({ id });
                this.iModel.elements[_instanceKeyCache].deleteById(id);
            }
        }
        return finalResult;
    }
    /** Insert a new aspect into the iModel.
     * @param aspectProps The properties of the new aspect.
     * @returns The newly inserted aspect Id.
     * @throws EditTxnError if this EditTxn is not active.
     * @throws IModelError if insertion fails.
     */
    insertAspect(aspectProps) {
        this.verifyWriteable();
        try {
            return this.iModel[_nativeDb].insertElementAspect(aspectProps);
        }
        catch (err) {
            const error = new IModelError(err.errorNumber, `Error inserting ElementAspect [${err.message}], class: ${aspectProps.classFullName}`, aspectProps);
            error.cause = err;
            throw error;
        }
    }
    /** Update an existing aspect in the iModel.
     * @param aspectProps The properties of the aspect to update.
     * @throws EditTxnError if this EditTxn is not active.
     * @throws IModelError if update fails.
     */
    updateAspect(aspectProps) {
        this.verifyWriteable();
        try {
            this.iModel[_nativeDb].updateElementAspect(aspectProps);
        }
        catch (err) {
            const error = new IModelError(err.errorNumber, `Error updating ElementAspect [${err.message}], id: ${aspectProps.id}`, aspectProps);
            error.cause = err;
            throw error;
        }
    }
    /** Delete one or more aspects from the iModel.
     * @param aspectInstanceIds The Ids of the aspects to delete.
     * @throws EditTxnError if this EditTxn is not active.
     * @throws IModelError if deletion fails.
     */
    deleteAspect(aspectInstanceIds) {
        this.verifyWriteable();
        Id64.toIdSet(aspectInstanceIds).forEach((aspectInstanceId) => {
            try {
                this.iModel[_nativeDb].deleteElementAspect(aspectInstanceId);
            }
            catch (err) {
                const error = new IModelError(err.errorNumber, `Error deleting ElementAspect [${err.message}], id: ${aspectInstanceId}`);
                error.cause = err;
                throw error;
            }
        });
    }
    /** Delete definition elements from the iModel when they are not referenced.
     * @param definitionElementIds The Ids of the definition elements to attempt to delete.
     * @returns The set of definition elements that were still in use and therefore not deleted.
     * @throws EditTxnError if this EditTxn is not active.
     * @throws IModelError if usage queries fail.
     */
    deleteDefinitionElements(definitionElementIds) {
        this.verifyWriteable();
        const usageInfo = this.iModel[_nativeDb].queryDefinitionElementUsage(definitionElementIds);
        if (!usageInfo)
            throw new IModelError(IModelStatus.BadRequest, "Error querying for DefinitionElement usage");
        const usedIdSet = usageInfo.usedIds ? Id64.toIdSet(usageInfo.usedIds) : new Set();
        const deleteIfUnused = (ids, used) => {
            ids?.forEach((id) => {
                if (!used.has(id))
                    this.deleteElement(id);
            });
        };
        try {
            this.iModel[_nativeDb].beginPurgeOperation();
            deleteIfUnused(usageInfo.spatialCategoryIds, usedIdSet);
            deleteIfUnused(usageInfo.drawingCategoryIds, usedIdSet);
            deleteIfUnused(usageInfo.viewDefinitionIds, usedIdSet);
            deleteIfUnused(usageInfo.geometryPartIds, usedIdSet);
            deleteIfUnused(usageInfo.lineStyleIds, usedIdSet);
            deleteIfUnused(usageInfo.renderMaterialIds, usedIdSet);
            deleteIfUnused(usageInfo.subCategoryIds, usedIdSet);
            deleteIfUnused(usageInfo.textureIds, usedIdSet);
            deleteIfUnused(usageInfo.displayStyleIds, usedIdSet);
            deleteIfUnused(usageInfo.categorySelectorIds, usedIdSet);
            deleteIfUnused(usageInfo.modelSelectorIds, usedIdSet);
            if (usageInfo.otherDefinitionElementIds)
                this.deleteElement(usageInfo.otherDefinitionElementIds);
        }
        finally {
            this.iModel[_nativeDb].endPurgeOperation();
        }
        if (usageInfo.viewDefinitionIds) {
            // Recheck view-related definitions after deleting view definitions that may have been their last reference.
            let viewRelatedIds = [];
            if (usageInfo.displayStyleIds)
                viewRelatedIds = viewRelatedIds.concat(usageInfo.displayStyleIds.filter((id) => usedIdSet.has(id)));
            if (usageInfo.categorySelectorIds)
                viewRelatedIds = viewRelatedIds.concat(usageInfo.categorySelectorIds.filter((id) => usedIdSet.has(id)));
            if (usageInfo.modelSelectorIds)
                viewRelatedIds = viewRelatedIds.concat(usageInfo.modelSelectorIds.filter((id) => usedIdSet.has(id)));
            if (viewRelatedIds.length > 0) {
                const viewRelatedUsageInfo = this.iModel[_nativeDb].queryDefinitionElementUsage(viewRelatedIds);
                if (viewRelatedUsageInfo) {
                    const usedViewRelatedIdSet = viewRelatedUsageInfo.usedIds ? Id64.toIdSet(viewRelatedUsageInfo.usedIds) : new Set();
                    try {
                        this.iModel[_nativeDb].beginPurgeOperation();
                        deleteIfUnused(viewRelatedUsageInfo.displayStyleIds, usedViewRelatedIdSet);
                        deleteIfUnused(viewRelatedUsageInfo.categorySelectorIds, usedViewRelatedIdSet);
                        deleteIfUnused(viewRelatedUsageInfo.modelSelectorIds, usedViewRelatedIdSet);
                    }
                    finally {
                        this.iModel[_nativeDb].endPurgeOperation();
                    }
                    viewRelatedIds.forEach((id) => {
                        if (!usedViewRelatedIdSet.has(id))
                            usedIdSet.delete(id);
                    });
                }
            }
        }
        return usedIdSet;
    }
    /** Insert a new model into the iModel.
     * @param props The data for the new model.
     * @returns The newly inserted model's Id.
     * @throws EditTxnError if this EditTxn is not active.
     * @throws IModelError if insertion fails.
     */
    insertModel(props) {
        this.verifyWriteable();
        try {
            return props.id = this.iModel[_nativeDb].insertModel(props);
        }
        catch (err) {
            const error = new IModelError(err.errorNumber, `Error inserting model [${err.message}], class=${props.classFullName}`);
            error.cause = err;
            throw error;
        }
    }
    /** Update an existing model in the iModel.
     * @param props the properties of the model to change
     * @throws EditTxnError if this EditTxn is not active.
     * @throws IModelError if update fails.
     */
    updateModel(props) {
        this.verifyWriteable();
        try {
            if (props.id)
                this.iModel.models[_cache].delete(props.id);
            this.iModel[_nativeDb].updateModel(props);
        }
        catch (err) {
            const error = new IModelError(err.errorNumber, `Error updating model [${err.message}], id: ${props.id}`);
            error.cause = err;
            throw error;
        }
    }
    /** Update the geometry guid of a model.
     * @param modelId The Id of the model to update.
     * @throws EditTxnError if this EditTxn is not active.
     * @throws IModelError if the update fails.
     */
    updateGeometryGuid(modelId) {
        this.verifyWriteable();
        this.iModel.models[_cache].delete(modelId);
        const error = this.iModel[_nativeDb].updateModelGeometryGuid(modelId);
        if (error !== IModelStatus.Success)
            throw new IModelError(error, `Error updating geometry guid for model ${modelId}`);
    }
    /** Delete models from the iModel.
     * @param ids The Ids of the models to delete.
     * @throws EditTxnError if this EditTxn is not active.
     * @throws IModelError if deletion fails.
     */
    deleteModel(ids) {
        this.verifyWriteable();
        Id64.toIdSet(ids).forEach((id) => {
            try {
                this.iModel.models[_cache].delete(id);
                this.iModel.models[_instanceKeyCache].deleteById(id);
                this.iModel[_nativeDb].deleteModel(id);
            }
            catch (err) {
                const error = new IModelError(err.errorNumber, `Error deleting model [${err.message}], id: ${id}`);
                error.cause = err;
                throw error;
            }
        });
    }
    /** Insert a new relationship into the iModel.
     * @param props The properties of the new relationship.
     * @returns The Id of the newly inserted relationship.
     * @throws EditTxnError if this EditTxn is not active.
     * @throws IModelError if the class is invalid for link-table insertion.
     */
    insertRelationship(props) {
        this.verifyWriteable();
        if (!this.iModel[_nativeDb].isLinkTableRelationship(props.classFullName.replace(".", ":")))
            throw new IModelError(DbResult.BE_SQLITE_ERROR, `Class '${props.classFullName}' must be a relationship class and it should be subclass of BisCore:ElementRefersToElements or BisCore:ElementDrivesElement.`);
        return props.id = this.iModel[_nativeDb].insertLinkTableRelationship(props);
    }
    /** Update an existing relationship in the iModel.
     * @param props the properties of the relationship to update.
     * @throws EditTxnError if this EditTxn is not active.
     */
    updateRelationship(props) {
        this.verifyWriteable();
        this.iModel[_nativeDb].updateLinkTableRelationship(props);
    }
    /** Delete a relationship from the iModel.
     * @param props The properties of the relationship to delete.
     * @throws EditTxnError if this EditTxn is not active.
     */
    deleteRelationship(props) {
        this.verifyWriteable();
        this.iModel[_nativeDb].deleteLinkTableRelationship(props);
    }
    /** Delete multiple relationships from the iModel.
     * @param props The properties of the relationships to delete.
     * @throws EditTxnError if this EditTxn is not active.
     */
    deleteRelationships(props) {
        this.verifyWriteable();
        this.iModel[_nativeDb].deleteLinkTableRelationships(props);
    }
    /** Save a file property to the iModel.
     * @param prop The file property to save.
     * @param strValue String value.
     * @param blobVal Blob value.
     * @throws EditTxnError if this EditTxn is not active.
     */
    saveFileProperty(prop, strValue, blobVal) {
        this.verifyWriteable();
        const imodel = this.iModel;
        if (imodel.isBriefcaseDb()) {
            if (imodel.txns.isIndirectChanges) {
                throw new IModelError(IModelStatus.BadRequest, "Cannot save file property while in an indirect change scope");
            }
        }
        imodel[_nativeDb].saveFileProperty(prop, strValue, blobVal);
    }
    /** Delete a file property from the iModel.
     * @param prop The file property to delete.
     * @throws EditTxnError if this EditTxn is not active.
     */
    deleteFileProperty(prop) {
        this.saveFileProperty(prop, undefined, undefined);
    }
    /** Update the project extents of the iModel.
     * @param newExtents The new project extents.
     * @throws EditTxnError if this EditTxn is not active.
     * @throws IModelError if extents are invalid.
     */
    updateProjectExtents(newExtents) {
        this.verifyWriteable();
        const extents = Range3d.fromJSON(newExtents);
        if (extents.isNull)
            throw new IModelError(DbResult.BE_SQLITE_ERROR, "Invalid project extents");
        this.iModel.projectExtents = extents;
        this.updateIModelProps();
    }
    /** Update the ECEF location of the iModel.
     * @param ecef The new ECEF location.
     * @throws EditTxnError if this EditTxn is not active.
     */
    updateEcefLocation(ecef) {
        this.verifyWriteable();
        this.iModel.setEcefLocation(new EcefLocation(ecef));
        this.updateIModelProps();
    }
    /** Update the iModel props in the database from the current in-memory state.
     * @throws EditTxnError if this EditTxn is not active.
     */
    updateIModelProps() {
        this.verifyWriteable();
        this.iModel[_nativeDb].updateIModelProps(this.iModel.toJSON());
    }
    static _settingPropNamespace = "settings";
    static _viewStoreProperty = { namespace: "itwinjs", name: "DefaultViewStore" };
    /** Save a `SettingDictionary` in this iModel.
     * @param name The name for the SettingDictionary. If a dictionary by that name already exists, its value is replaced.
     * @param dict The SettingDictionary object to stringify and save.
     * @throws EditTxnError if this EditTxn is not active.
     * @beta
     */
    saveSettingDictionary(name, dict) {
        this.verifyWriteable();
        this.iModel.withSqliteStatement("REPLACE INTO be_Prop(id,SubId,TxnMode,Namespace,Name,strData) VALUES(0,0,0,?,?,?)", (stmt) => {
            stmt.bindString(1, EditTxn._settingPropNamespace);
            stmt.bindString(2, name);
            stmt.bindString(3, JSON.stringify(dict));
            stmt.stepForWrite();
        });
        this.saveChanges("add settings");
    }
    /** Delete a SettingDictionary from this iModel.
     * @param name The name of the dictionary to delete.
     * @throws EditTxnError if this EditTxn is not active.
     * @beta
     */
    deleteSettingDictionary(name) {
        this.verifyWriteable();
        this.iModel.withSqliteStatement("DELETE FROM be_Prop WHERE Namespace=? AND Name=?", (stmt) => {
            stmt.bindString(1, EditTxn._settingPropNamespace);
            stmt.bindString(2, name);
            stmt.stepForWrite();
        });
        this.saveChanges("delete settings");
    }
    /** Save a default ViewStore container reference in this iModel.
     * @param arg The cloud container properties for the ViewStore.
     * @throws EditTxnError if this EditTxn is not active.
     * @beta
     */
    saveDefaultViewStore(arg) {
        this.verifyWriteable();
        const props = { baseUri: arg.baseUri, containerId: arg.containerId, storageType: arg.storageType }; // sanitize to only known properties
        this.saveFileProperty(EditTxn._viewStoreProperty, JSON.stringify(props));
        this.saveChanges("update default ViewStore");
    }
}
export function withEditTxn(iModel, saveArgsOrFn, maybeFn) {
    const saveArgs = "function" === typeof saveArgsOrFn ? undefined : saveArgsOrFn;
    const fn = "function" === typeof saveArgsOrFn ? saveArgsOrFn : maybeFn;
    if (undefined === fn)
        throw new Error("withEditTxn requires a callback");
    const txn = new EditTxn(iModel, "");
    txn.start();
    try {
        const result = fn(txn);
        if (result instanceof Promise) {
            return result.then((value) => {
                txn.end("save", saveArgs);
                return value;
            }, (err) => {
                if (txn.isActive)
                    txn.end("abandon");
                throw err;
            });
        }
        txn.end("save", saveArgs);
        return result;
    }
    catch (err) {
        if (txn.isActive)
            txn.end("abandon");
        throw err;
    }
}
//# sourceMappingURL=EditTxn.js.map