/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module IModelConnection
 */
import { assert, BeEvent, CompressedId64Set, Guid, IModelStatus, OpenMode } from "@itwin/core-bentley";
import { getPullChangesIpcChannel, IModelError, } from "@itwin/core-common";
import { BriefcaseTxns } from "./BriefcaseTxns";
import { GraphicalEditingScope } from "./GraphicalEditingScope";
import { IModelApp } from "./IModelApp";
import { IModelConnection } from "./IModelConnection";
import { IpcApp } from "./IpcApp";
import { disposeTileTreesForGeometricModels } from "./tile/internal";
import { Viewport } from "./Viewport";
/** Keeps track of changes to models, buffering them until synchronization points.
 * While a GraphicalEditingScope is open, the changes are buffered until the scope exits, at which point they are processed.
 * Otherwise, the buffered changes are processed after undo/redo, commit, or pull+merge changes.
 */
class ModelChangeMonitor {
    constructor(briefcase) {
        this._deletedModels = new Set();
        this._modelIdToGuid = new Map();
        this._removals = [];
        this._briefcase = briefcase;
        // Buffer updated geometry guids.
        this._removals.push(briefcase.txns.onModelGeometryChanged.addListener((changes) => {
            for (const change of changes) {
                this._deletedModels.delete(change.id);
                this._modelIdToGuid.set(change.id, change.guid);
            }
        }));
        // Buffer deletions of models.
        this._removals.push(briefcase.txns.onModelsChanged.addListener((changes) => {
            if (changes.deleted) {
                for (const id of CompressedId64Set.iterable(changes.deleted)) {
                    this._modelIdToGuid.delete(id);
                    this._deletedModels.add(id);
                }
            }
        }));
        // Outside of an editing scope, we want to update viewport contents after commit, undo/redo, or merging changes.
        const maybeProcess = async () => {
            if (this.editingScope)
                return;
            const modelIds = Array.from(this._modelIdToGuid.keys());
            if (modelIds.length > 0)
                await IModelApp.tileAdmin.purgeTileTrees(this._briefcase, modelIds);
            this.processBuffered();
        };
        this._removals.push(briefcase.txns.onCommitted.addListener(maybeProcess));
        this._removals.push(briefcase.txns.onReplayedExternalTxns.addListener(maybeProcess));
        this._removals.push(briefcase.txns.onAfterUndoRedo.addListener(maybeProcess));
        this._removals.push(briefcase.txns.onChangesPulled.addListener(maybeProcess));
    }
    async close() {
        for (const removal of this._removals)
            removal();
        this._removals.length = 0;
        if (this._editingScope) {
            await this._editingScope.exit();
            this._editingScope = undefined;
        }
    }
    get editingScope() {
        return this._editingScope;
    }
    async enterEditingScope() {
        if (this._editingScope)
            throw new Error("Cannot create an editing scope for an iModel that already has one");
        this._editingScope = await GraphicalEditingScope.enter(this._briefcase);
        const removeGeomListener = this._editingScope.onGeometryChanges.addListener((changes) => {
            const modelIds = [];
            for (const change of changes)
                modelIds.push(change.id);
            this.invalidateScenes(modelIds);
        });
        this._editingScope.onExited.addOnce((scope) => {
            assert(scope === this._editingScope);
            this._editingScope = undefined;
            removeGeomListener();
            this.processBuffered();
        });
        return this._editingScope;
    }
    processBuffered() {
        const models = this._briefcase.models;
        for (const [id, guid] of this._modelIdToGuid) {
            const model = models.getLoaded(id)?.asGeometricModel;
            if (model)
                model.geometryGuid = guid;
        }
        const modelIds = new Set(this._modelIdToGuid.keys());
        for (const deleted of this._deletedModels) {
            modelIds.add(deleted);
            models.unload(deleted);
        }
        this.invalidateScenes(modelIds);
        disposeTileTreesForGeometricModels(modelIds, this._briefcase);
        this._briefcase.onBufferedModelChanges.raiseEvent(modelIds);
        this._modelIdToGuid.clear();
        this._deletedModels.clear();
    }
    invalidateScenes(changedModels) {
        for (const user of IModelApp.tileAdmin.tileUsers) {
            if (user instanceof Viewport && user.iModel === this._briefcase) {
                for (const modelId of changedModels) {
                    if (user.view.viewsModel(modelId)) {
                        user.invalidateScene();
                        user.setFeatureOverrideProviderChanged();
                        break;
                    }
                }
            }
        }
    }
}
/** Settings that can be used to control the behavior of [[Tool]]s that modify a [[BriefcaseConnection]].
 * For example, tools that want to create new elements can consult the briefcase's editor tool settings to
 * determine into which model and category to insert the elements.
 * Specialized tools are free to ignore these settings.
 * @see [[BriefcaseConnection.editorToolSettings]] to query or modify the current settings for a briefcase.
 * @see [CreateElementTool]($editor-frontend) for an example of a tool that uses these settings.
 * @beta
 */
export class BriefcaseEditorToolSettings {
    constructor() {
        /** An event raised just after the default [[category]] is changed. */
        this.onCategoryChanged = new BeEvent();
        /** An event raised just after the default [[model]] is changed. */
        this.onModelChanged = new BeEvent();
    }
    /** The [Category]($backend) into which new elements should be inserted by default.
     * Specialized tools are free to ignore this setting and instead use their own logic to select an appropriate category.
     * @see [[onCategoryChanged]] to be notified when this property is modified.
     * @see [CreateElementTool.targetCategory]($editor-frontend) for an example of a tool that uses this setting.
     */
    get category() {
        return this._category;
    }
    set category(category) {
        const previousCategory = this.category;
        if (category !== this.category) {
            this._category = category;
            this.onCategoryChanged.raiseEvent(previousCategory);
        }
    }
    /** The [Model]($backend) into which new elements should be inserted by default.
     * Specialized tools are free to ignore this setting and instead use their own logic to select an appropriate model.
     * @see [[onModelChanged]] to be notified when this property is modified.
     * @see [CreateElementTool.targetModelId]($editor-frontend) for an example of a tool that uses this setting.
     */
    get model() {
        return this._model;
    }
    set model(model) {
        const previousModel = this.model;
        if (model !== this.model) {
            this._model = model;
            this.onModelChanged.raiseEvent(previousModel);
        }
    }
}
/** A connection to an editable briefcase on the backend. This class uses [Ipc]($docs/learning/IpcInterface.md) to communicate
 * to the backend and may only be used by [[IpcApp]]s.
 * @public
 */
export class BriefcaseConnection extends IModelConnection {
    /** @internal */
    isBriefcaseConnection() { return true; }
    /** The Guid that identifies the iTwin that owns this iModel. */
    get iTwinId() { return super.iTwinId; } // GuidString | undefined for IModelConnection, but required for BriefcaseConnection
    /** The Guid that identifies this iModel. */
    get iModelId() { return super.iModelId; } // GuidString | undefined for IModelConnection, but required for BriefcaseConnection
    constructor(props, openMode) {
        super(props);
        /** Default settings that can be used to control the behavior of [[Tool]]s that modify this briefcase.
         * @beta
         */
        this.editorToolSettings = new BriefcaseEditorToolSettings();
        /** Strictly for tests - dispatched from ModelChangeMonitor.processBuffered.
         * @internal
         */
        this.onBufferedModelChanges = new BeEvent();
        this._openMode = openMode;
        this.txns = new BriefcaseTxns(this);
        this._modelsMonitor = new ModelChangeMonitor(this);
        if (OpenMode.ReadWrite === this._openMode)
            this.txns.onAfterUndoRedo.addListener(async () => { await IModelApp.toolAdmin.restartPrimitiveTool(); });
    }
    /** Open a BriefcaseConnection to a [BriefcaseDb]($backend). */
    static async openFile(briefcaseProps) {
        const iModelProps = await IpcApp.appFunctionIpc.openBriefcase(briefcaseProps);
        const connection = new this({ ...briefcaseProps, ...iModelProps }, briefcaseProps.readonly ? OpenMode.Readonly : OpenMode.ReadWrite);
        IModelConnection.onOpen.raiseEvent(connection);
        return connection;
    }
    /** Open a BriefcaseConnection to a [StandaloneDb]($backend)
     * @note StandaloneDbs, by definition, may not push or pull changes. Attempting to do so will throw exceptions.
     */
    static async openStandalone(filePath, openMode = OpenMode.ReadWrite, opts) {
        const openResponse = await IpcApp.appFunctionIpc.openStandalone(filePath, openMode, opts);
        const connection = new this(openResponse, openMode);
        IModelConnection.onOpen.raiseEvent(connection);
        return connection;
    }
    /** Returns `true` if [[close]] has already been called. */
    get isClosed() { return this._isClosed === true; }
    /**
     * Close this BriefcaseConnection.
     * @note make sure to call [[saveChanges]] before calling this method. Unsaved local changes are abandoned.
     */
    async close() {
        if (this.isClosed)
            return;
        await this._modelsMonitor.close();
        this.beforeClose();
        this.txns.dispose();
        this._isClosed = true;
        await IpcApp.appFunctionIpc.closeIModel(this._fileKey);
    }
    requireTimeline() {
        if (this.iTwinId === Guid.empty)
            throw new IModelError(IModelStatus.WrongIModel, "iModel has no timeline");
    }
    /** Query if there are any pending Txns in this briefcase that are waiting to be pushed. */
    async hasPendingTxns() {
        return this.txns.hasPendingTxns();
    }
    /** Commit pending changes to this briefcase.
     * @param description Optional description of the changes.
     */
    async saveChanges(description) {
        await IpcApp.appFunctionIpc.saveChanges(this.key, description);
    }
    /** Pull (and potentially merge if there are local changes) up to a specified changeset from iModelHub into this briefcase
     * @param toIndex The changeset index to pull changes to. If `undefined`, pull all changes.
     * @param options Options for pulling changes.
     * @see [[BriefcaseTxns.onChangesPulled]] for the event dispatched after changes are pulled.
     */
    async pullChanges(toIndex, options) {
        const removeListeners = [];
        // eslint-disable-next-line deprecation/deprecation
        const shouldReportProgress = !!options?.progressCallback || !!options?.downloadProgressCallback;
        if (shouldReportProgress) {
            const handleProgress = (_evt, data) => {
                // eslint-disable-next-line deprecation/deprecation
                options?.progressCallback?.(data);
                options?.downloadProgressCallback?.(data);
            };
            const removeProgressListener = IpcApp.addListener(getPullChangesIpcChannel(this.iModelId), handleProgress);
            removeListeners.push(removeProgressListener);
        }
        if (options?.abortSignal) {
            const abort = () => void IpcApp.appFunctionIpc.cancelPullChangesRequest(this.key);
            options?.abortSignal.addEventListener("abort", abort);
            removeListeners.push(() => options?.abortSignal?.removeEventListener("abort", abort));
        }
        this.requireTimeline();
        const ipcAppOptions = {
            reportProgress: shouldReportProgress,
            progressInterval: options?.progressInterval,
            enableCancellation: !!options?.abortSignal,
        };
        try {
            this.changeset = await IpcApp.appFunctionIpc.pullChanges(this.key, toIndex, ipcAppOptions);
        }
        finally {
            removeListeners.forEach((remove) => remove());
        }
    }
    /** Create a changeset from local Txns and push to iModelHub. On success, clear Txn table.
     * @param description The description for the changeset
     * @returns the changesetId of the pushed changes
     * @see [[BriefcaseTxns.onChangesPushed]] for the event dispatched after changes are pushed.
     */
    async pushChanges(description) {
        this.requireTimeline();
        return IpcApp.appFunctionIpc.pushChanges(this.key, description);
    }
    /** The current graphical editing scope, if one is in progress.
     * @see [[enterEditingScope]] to begin graphical editing.
     */
    get editingScope() {
        return this._modelsMonitor.editingScope;
    }
    /** Return whether graphical editing is supported for this briefcase. It is not supported if the briefcase is read-only, or the briefcase contains a version of
     * the BisCore ECSchema older than v0.1.11.
     * @see [[enterEditingScope]] to enable graphical editing.
     */
    async supportsGraphicalEditing() {
        return IpcApp.appFunctionIpc.isGraphicalEditingSupported(this.key);
    }
    /** Begin a new graphical editing scope.
     * @throws Error if an editing scope already exists or one could not be created.
     * @see [[GraphicalEditingScope.exit]] to exit the scope.
     * @see [[supportsGraphicalEditing]] to determine whether this method should be expected to succeed.
     * @see [[editingScope]] to obtain the current editing scope, if one is in progress.
     */
    async enterEditingScope() {
        return this._modelsMonitor.enterEditingScope();
    }
}
//# sourceMappingURL=BriefcaseConnection.js.map